@moretea-labs/forge 1.5.1 → 1.6.0
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/CHANGELOG.md +20 -0
- package/DESIGN.md +7 -2
- package/README.md +4 -4
- package/README.zh-CN.md +2 -2
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/assets/hooks/pre-edit-guard.sh +8 -39
- package/assets/hooks/prompt-guard.sh +2 -2
- package/assets/plugin-registry.v1.json +54 -3
- package/assets/reference-configs/hook-operations.md +1 -1
- package/assets/reference-configs/sprint-contracts.md +2 -2
- package/assets/skill-version.json +2 -2
- package/assets/templates/helpers/ensure-task-workflow.sh +2 -2
- package/assets/templates/helpers/sprint-backlog.sh +6 -6
- package/docs/architecture/current/automation-and-schedule-engine.md +9 -3
- package/docs/architecture/current/codegraph-context-provider.md +15 -3
- package/docs/architecture/current/dispatch-and-agent-strategy.md +32 -13
- package/docs/architecture/current/entity-model.md +20 -0
- package/docs/architecture/current/external-plugin-governance.md +3 -3
- package/docs/architecture/current/system-overview.md +26 -1
- package/docs/architecture/current/thin-harness-v1.md +10 -0
- package/docs/operations/chatgpt-personal-assistant-runtime.md +12 -2
- package/docs/operations/controller-browser-plugin.md +12 -16
- package/docs/operations/features.md +1 -1
- package/docs/operations/homebrew.md +2 -2
- package/docs/tutorials/01-install-and-start.md +1 -1
- package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
- package/docs/wiki/Installation.md +1 -1
- package/docs/wiki/Quick-Start.md +1 -1
- package/package.json +1 -1
- package/scripts/check-public-docs.sh +14 -1
- package/scripts/migrate-chatgpt-automation-schedules.ts +64 -0
- package/scripts/sprint-backlog.sh +1 -1
- package/src/cli/commands/chatgpt.ts +31 -2
- package/src/cli/commands/plugin.ts +36 -9
- package/src/cli/controller/context-pack.ts +158 -6
- package/src/cli/hook/prompt-guard-decision.ts +17 -18
- package/src/cli/hook/prompt-intents.ts +1 -1
- package/src/cli/local-bridge/console-automations.ts +205 -23
- package/src/cli/local-bridge/console-view-models.ts +2 -0
- package/src/cli/local-bridge/facade-api.ts +2 -0
- package/src/cli/local-bridge/frontend/src/App.tsx +1 -1
- package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +143 -3
- package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +1 -1
- package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +181 -3
- package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +1 -1
- package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +4 -3
- package/src/cli/local-bridge/frontend/src/lib/api.ts +18 -6
- package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +1 -1
- package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +2 -1
- package/src/cli/local-bridge/frontend/src/styles.css +27 -11
- package/src/cli/local-bridge/frontend/src/types.ts +6 -2
- package/src/cli/local-bridge/server.ts +29 -19
- package/src/cli/local-bridge/ui-dist/app.css +1 -1
- package/src/cli/local-bridge/ui-dist/app.js +5003 -4815
- package/src/cli/mcp/paths.ts +8 -2
- package/src/cli/mcp/repository-tools.ts +124 -9
- package/src/cli/mcp/server.ts +164 -19
- package/src/cli/mcp/transports/http.ts +25 -2
- package/src/cli/mcp/transports/session-registry.ts +24 -0
- package/src/cli/repositories/command-classifier.ts +113 -0
- package/src/cli/repositories/safe-patch.ts +6 -4
- package/src/cli/repositories/structured-git.ts +49 -7
- package/src/cli/repository/inspector.ts +16 -3
- package/src/runtime/assistant/intent.ts +3 -3
- package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +157 -2
- package/src/runtime/control-plane/execution/execution-identity.ts +65 -14
- package/src/runtime/control-plane/execution/validation.ts +15 -2
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +32 -0
- package/src/runtime/control-plane/execution/work-validation-reconciler.ts +6 -7
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +2 -1
- package/src/runtime/control-plane/facade/capability-registry.ts +1 -1
- package/src/runtime/control-plane/facade/controller-session-store.ts +12 -13
- package/src/runtime/control-plane/facade/goal-workloop-access.ts +7 -2
- package/src/runtime/control-plane/facade/goal-workloop.ts +231 -8
- package/src/runtime/control-plane/facade/plan-contract-store.ts +32 -12
- package/src/runtime/control-plane/facade/requirement-board.ts +24 -8
- package/src/runtime/control-plane/facade/types.ts +33 -6
- package/src/runtime/control-plane/facade/work-contract-store.ts +31 -2
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +375 -23
- package/src/runtime/control-plane/persistence/requirement-store.ts +15 -11
- package/src/runtime/diagnostics/mcp-timing.ts +6 -0
- package/src/runtime/execution/process-runtime/check-facade.ts +1 -10
- package/src/runtime/execution/process-runtime/check-result.ts +10 -2
- package/src/runtime/execution/process-runtime/command-facade.ts +1 -0
- package/src/runtime/execution/process-runtime/gc.ts +64 -10
- package/src/runtime/execution/process-runtime/persisted-check.ts +10 -2
- package/src/runtime/execution/process-runtime/resource-claims.ts +87 -2
- package/src/runtime/execution/process-runtime/runtime.ts +16 -1
- package/src/runtime/execution/process-runtime/store.ts +6 -0
- package/src/runtime/gateway/mcp/execution-tools.ts +27 -12
- package/src/runtime/gateway/mcp/router.ts +1 -1
- package/src/runtime/gateway/mcp/runtime-tools.ts +482 -178
- package/src/runtime/model-clients/deepseek-adapter.ts +0 -32
- package/src/runtime/plugins/browser-adapter.ts +19 -72
- package/src/runtime/plugins/browser-handoff-host.ts +6 -31
- package/src/runtime/plugins/browser-handoff.ts +0 -3
- package/src/runtime/plugins/browser-macos-bridge.ts +8 -0
- package/src/runtime/plugins/desktop-operator-registration.ts +8 -11
- package/src/runtime/plugins/external-registration.ts +84 -5
- package/src/runtime/plugins/first-party-registry.ts +2 -2
- package/src/runtime/plugins/gmail-adapter.ts +28 -2
- package/src/runtime/plugins/local-system-adapter.ts +2 -2
- package/src/runtime/plugins/plugin-management-adapter.ts +140 -0
- package/src/runtime/plugins/store.ts +107 -5
- package/src/runtime/plugins/types.ts +4 -0
- package/src/runtime/recovery/actions.ts +0 -10
- package/src/runtime/recovery/classifier.ts +0 -9
- package/src/runtime/recovery/maintenance-executor.ts +43 -11
- package/src/runtime/recovery/probe.ts +0 -1
- package/src/runtime/recovery/types.ts +0 -1
- package/src/runtime/root/release-materialize.ts +143 -19
- package/src/runtime/root/service.ts +6 -4
- package/src/runtime/safe-tooling/index.ts +0 -1
- package/src/runtime/safe-tooling/types.ts +0 -30
- package/src/runtime/standalone-recovery/core.ts +2 -2
- package/src/runtime/workflow/schedules/browser-probe.ts +180 -0
- package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +40 -0
- package/src/runtime/workflow/schedules/engine.ts +357 -64
- package/src/runtime/workflow/schedules/types.ts +5 -0
- package/src/runtime/workflow/schedules/work-continuation.ts +100 -27
- package/bin/forge-desktop-helper.mjs +0 -204
- package/docs/operations/controller-desktop-plugin.md +0 -112
- package/src/runtime/plugins/desktop-adapter.ts +0 -374
- package/src/runtime/safe-tooling/browser-safe.ts +0 -133
|
@@ -13,6 +13,7 @@ import { listCandidateFindings } from '../findings/store';
|
|
|
13
13
|
import { getControllerSession, getWorkContract, isTerminalWorkContractStatus, listHandoffItems } from '../../control-plane/facade';
|
|
14
14
|
import { launchSuperController } from '../../control-plane/launcher/thin-launcher';
|
|
15
15
|
import { getExternalControllerLaunchReservation } from '../../control-plane/launcher/launch-reservation-store';
|
|
16
|
+
import { runScheduledChatgptPrompt, runWorkChatgptContinuation } from '../../control-plane/launcher/chatgpt-work-continuation';
|
|
16
17
|
import {
|
|
17
18
|
getOccurrence,
|
|
18
19
|
getSchedule,
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
saveScheduleDecision,
|
|
26
27
|
} from './store';
|
|
27
28
|
import { applyScheduleFailure } from './settlement';
|
|
29
|
+
import { classifyScheduledBrowserObservation, executeScheduledBrowserProbe } from './browser-probe';
|
|
28
30
|
import type {
|
|
29
31
|
RepositorySchedule,
|
|
30
32
|
ScheduleDecisionType,
|
|
@@ -35,12 +37,16 @@ import type {
|
|
|
35
37
|
const execFileAsync = promisify(execFile);
|
|
36
38
|
const LIVE_MAINTENANCE_OPERATION = 'runtime_maintenance_apply';
|
|
37
39
|
const EXTERNAL_CONTROLLER_WAKE_OPERATION = 'external_controller_wake';
|
|
40
|
+
const CHATGPT_BROWSER_PROMPT_OPERATION = 'chatgpt_browser_prompt';
|
|
41
|
+
const BROWSER_PROBE_OPERATION = 'browser_probe';
|
|
38
42
|
|
|
39
43
|
function isDeterministicSchedule(schedule: RepositorySchedule): boolean {
|
|
40
|
-
// Kernel may execute only
|
|
41
|
-
//
|
|
44
|
+
// Kernel may execute only allowlisted, deterministic dispatch operations.
|
|
45
|
+
// ChatGPT owns semantic work; the Scheduler only dispatches the configured prompt.
|
|
42
46
|
return schedule.action.operation === LIVE_MAINTENANCE_OPERATION
|
|
43
|
-
|| schedule.action.operation === EXTERNAL_CONTROLLER_WAKE_OPERATION
|
|
47
|
+
|| schedule.action.operation === EXTERNAL_CONTROLLER_WAKE_OPERATION
|
|
48
|
+
|| schedule.action.operation === CHATGPT_BROWSER_PROMPT_OPERATION
|
|
49
|
+
|| schedule.action.operation === BROWSER_PROBE_OPERATION;
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
function normalizedWindow(minutes: number, at = Date.now()): string {
|
|
@@ -234,8 +240,9 @@ async function triggerDue(
|
|
|
234
240
|
}
|
|
235
241
|
}
|
|
236
242
|
|
|
237
|
-
function
|
|
238
|
-
if (schedule.action.operation !== EXTERNAL_CONTROLLER_WAKE_OPERATION
|
|
243
|
+
function workBoundScheduleWorkId(schedule: RepositorySchedule): string | undefined {
|
|
244
|
+
if (schedule.action.operation !== EXTERNAL_CONTROLLER_WAKE_OPERATION
|
|
245
|
+
&& schedule.action.operation !== BROWSER_PROBE_OPERATION) return undefined;
|
|
239
246
|
const value = schedule.action.arguments?.work_id;
|
|
240
247
|
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
241
248
|
}
|
|
@@ -251,7 +258,7 @@ function executionJobBelongsToWork(job: ReturnType<typeof listExecutionJobs>[num
|
|
|
251
258
|
|
|
252
259
|
async function stopReason(controllerHome: string, schedule: RepositorySchedule): Promise<string | undefined> {
|
|
253
260
|
const projection = readRepositoryProjection(controllerHome, schedule.repoId);
|
|
254
|
-
const workId =
|
|
261
|
+
const workId = workBoundScheduleWorkId(schedule);
|
|
255
262
|
const work = workId ? getWorkContract({ controllerHome, repoId: schedule.repoId }, workId) : undefined;
|
|
256
263
|
if (schedule.stopConditions.includes('human_review_required')) {
|
|
257
264
|
if (workId) {
|
|
@@ -344,6 +351,251 @@ function decideOccurrence(
|
|
|
344
351
|
return saveOccurrence(controllerHome, { ...occurrence, decision, decisionId, status, reason });
|
|
345
352
|
}
|
|
346
353
|
|
|
354
|
+
function externalControllerLaunchArgs(args: Record<string, unknown>, controllerType: string): string[] {
|
|
355
|
+
const launchArgs = Array.isArray(args.launch_args) ? args.launch_args.map(String) : [];
|
|
356
|
+
if (controllerType !== 'chatgpt') return launchArgs;
|
|
357
|
+
const explicit = new Set(launchArgs.filter((value) => value.startsWith('--')).map((value) => value.split('=', 1)[0]));
|
|
358
|
+
const add = (flag: string, value: unknown) => {
|
|
359
|
+
if (explicit.has(flag) || typeof value !== 'string' || !value.trim()) return;
|
|
360
|
+
launchArgs.push(flag, value.trim());
|
|
361
|
+
};
|
|
362
|
+
add('--model', args.model);
|
|
363
|
+
add('--reasoning', args.reasoning);
|
|
364
|
+
add('--tab-policy', args.tab_policy);
|
|
365
|
+
return launchArgs;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function executeChatgptBrowserPrompt(
|
|
369
|
+
controllerHome: string,
|
|
370
|
+
schedule: RepositorySchedule,
|
|
371
|
+
occurrence: ScheduleOccurrence,
|
|
372
|
+
timestamp: string,
|
|
373
|
+
args: Record<string, unknown>,
|
|
374
|
+
): Promise<ScheduleOccurrence> {
|
|
375
|
+
const prompt = typeof args.prompt === 'string' ? args.prompt.trim() : '';
|
|
376
|
+
if (!prompt) {
|
|
377
|
+
return decideOccurrence(controllerHome, schedule, occurrence, 'operation_blocked', 'skipped', 'CHATGPT_BROWSER_PROMPT_REQUIRED');
|
|
378
|
+
}
|
|
379
|
+
const model = typeof args.model === 'string' ? args.model : 'gpt-5.6';
|
|
380
|
+
const reasoning = args.reasoning === 'medium' || args.reasoning === 'xhigh' ? args.reasoning : 'high';
|
|
381
|
+
const tabPolicy = args.tab_policy === 'reuse' || args.tab_policy === 'new' ? args.tab_policy : 'auto';
|
|
382
|
+
const dispatching = decideOccurrence(
|
|
383
|
+
controllerHome,
|
|
384
|
+
schedule,
|
|
385
|
+
occurrence,
|
|
386
|
+
'execute',
|
|
387
|
+
'running',
|
|
388
|
+
'Forge Scheduler is dispatching the configured prompt through the controller-owned ChatGPT browser session.',
|
|
389
|
+
occurrenceDecisionEvidence({ operation: CHATGPT_BROWSER_PROMPT_OPERATION, model, reasoning, tabPolicy }),
|
|
390
|
+
);
|
|
391
|
+
const result = await runScheduledChatgptPrompt({
|
|
392
|
+
controllerHome,
|
|
393
|
+
repoId: schedule.repoId,
|
|
394
|
+
automationId: schedule.scheduleId,
|
|
395
|
+
prompt,
|
|
396
|
+
title: schedule.name,
|
|
397
|
+
browserSessionId: typeof args.browser_session_id === 'string' ? args.browser_session_id : undefined,
|
|
398
|
+
conversationUrl: typeof args.conversation_url === 'string' ? args.conversation_url : undefined,
|
|
399
|
+
model,
|
|
400
|
+
reasoning,
|
|
401
|
+
tabPolicy,
|
|
402
|
+
timeoutMs: typeof args.timeout_ms === 'number' ? args.timeout_ms : undefined,
|
|
403
|
+
});
|
|
404
|
+
if (result.status === 'failed') {
|
|
405
|
+
const reason = result.error?.message ?? 'ChatGPT browser prompt dispatch failed.';
|
|
406
|
+
const failed = applyScheduleFailure(controllerHome, schedule.scheduleId, schedule.repoId, occurrence.occurrenceId, {
|
|
407
|
+
outcome: 'failed',
|
|
408
|
+
decision: 'execute',
|
|
409
|
+
reason,
|
|
410
|
+
handoff: {
|
|
411
|
+
title: `ChatGPT scheduled prompt ${occurrence.occurrenceId} failed`,
|
|
412
|
+
summary: 'Forge recorded the trigger but could not dispatch the configured ChatGPT prompt.',
|
|
413
|
+
reason,
|
|
414
|
+
creationReason: 'repeated_infrastructure_failure',
|
|
415
|
+
blockingDecision: 'Repair ChatGPT/browser readiness before unattended prompt dispatch resumes.',
|
|
416
|
+
recommendedDecision: 'Inspect browser attachment and ChatGPT execution preference verification, then retrigger this automation.',
|
|
417
|
+
recommendedPrompt: `Inspect failed ChatGPT prompt occurrence ${occurrence.occurrenceId} for schedule ${schedule.scheduleId}.`,
|
|
418
|
+
statusSummary: 'Scheduled ChatGPT prompt dispatch failed.',
|
|
419
|
+
blockedBy: ['chatgpt_browser_prompt_failed'],
|
|
420
|
+
attemptedActions: [`schedule:${schedule.scheduleId}`, `browser-session:${result.browserSessionId}`],
|
|
421
|
+
},
|
|
422
|
+
});
|
|
423
|
+
const latest = getSchedule(controllerHome, schedule.repoId, schedule.scheduleId);
|
|
424
|
+
saveSchedule(controllerHome, { ...latest, lastTriggeredAt: timestamp, lastOccurrenceId: occurrence.occurrenceId });
|
|
425
|
+
return failed.occurrence ?? saveOccurrence(controllerHome, { ...dispatching, status: 'failed', reason });
|
|
426
|
+
}
|
|
427
|
+
const latest = getSchedule(controllerHome, schedule.repoId, schedule.scheduleId);
|
|
428
|
+
const durableConversationUrl = result.conversationUrl && /\/c\/[^/?#]+/.test(result.conversationUrl)
|
|
429
|
+
? result.conversationUrl
|
|
430
|
+
: undefined;
|
|
431
|
+
saveSchedule(controllerHome, {
|
|
432
|
+
...latest,
|
|
433
|
+
action: durableConversationUrl && tabPolicy !== 'new'
|
|
434
|
+
? {
|
|
435
|
+
...latest.action,
|
|
436
|
+
arguments: { ...latest.action.arguments, conversation_url: durableConversationUrl },
|
|
437
|
+
}
|
|
438
|
+
: latest.action,
|
|
439
|
+
lastTriggeredAt: timestamp,
|
|
440
|
+
lastOccurrenceId: occurrence.occurrenceId,
|
|
441
|
+
consecutiveFailures: 0,
|
|
442
|
+
nextEligibleAt: undefined,
|
|
443
|
+
pausedReason: undefined,
|
|
444
|
+
});
|
|
445
|
+
return saveOccurrence(controllerHome, {
|
|
446
|
+
...dispatching,
|
|
447
|
+
status: 'succeeded',
|
|
448
|
+
reason: `ChatGPT prompt dispatched via ${result.browserSessionId}${result.reusedSession ? ' (reused)' : ' (new session)'}; execution preference ${result.executionPreferenceVerified ? 'verified' : 'not verified'}.`,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async function executeExternalControllerWake(
|
|
453
|
+
controllerHome: string,
|
|
454
|
+
schedule: RepositorySchedule,
|
|
455
|
+
occurrence: ScheduleOccurrence,
|
|
456
|
+
timestamp: string,
|
|
457
|
+
args: Record<string, unknown>,
|
|
458
|
+
extraEvidence: Record<string, unknown> = {},
|
|
459
|
+
): Promise<ScheduleOccurrence> {
|
|
460
|
+
const workId = typeof args.work_id === 'string' ? args.work_id.trim() : '';
|
|
461
|
+
const controllerType = typeof args.controller_type === 'string' ? args.controller_type.trim() : 'chatgpt';
|
|
462
|
+
if (!workId) {
|
|
463
|
+
return decideOccurrence(controllerHome, schedule, occurrence, 'operation_blocked', 'skipped', 'EXTERNAL_CONTROLLER_WAKE_WORK_ID_REQUIRED');
|
|
464
|
+
}
|
|
465
|
+
if (!['chatgpt', 'codex', 'claude', 'grok'].includes(controllerType)) {
|
|
466
|
+
return decideOccurrence(controllerHome, schedule, occurrence, 'operation_blocked', 'skipped', 'EXTERNAL_CONTROLLER_WAKE_TYPE_INVALID');
|
|
467
|
+
}
|
|
468
|
+
const workStore = { controllerHome, repoId: schedule.repoId };
|
|
469
|
+
const work = getWorkContract(workStore, workId);
|
|
470
|
+
if (!work) {
|
|
471
|
+
saveSchedule(controllerHome, { ...schedule, enabled: false, pausedReason: `Work ${workId} no longer exists.`, lastTriggeredAt: timestamp, lastOccurrenceId: occurrence.occurrenceId });
|
|
472
|
+
return decideOccurrence(controllerHome, schedule, occurrence, 'operation_blocked', 'skipped', `EXTERNAL_CONTROLLER_WAKE_WORK_NOT_FOUND:${workId}`);
|
|
473
|
+
}
|
|
474
|
+
if (isTerminalWorkContractStatus(work.status)) {
|
|
475
|
+
saveSchedule(controllerHome, { ...schedule, enabled: false, pausedReason: `Work ${workId} is terminal (${work.status}).`, lastTriggeredAt: timestamp, lastOccurrenceId: occurrence.occurrenceId });
|
|
476
|
+
return decideOccurrence(controllerHome, schedule, occurrence, 'nothing_to_do', 'skipped', `Work ${workId} is terminal (${work.status}); automatic continuation stopped.`);
|
|
477
|
+
}
|
|
478
|
+
const existingOwner = getControllerSession(workStore, workId);
|
|
479
|
+
if (existingOwner) {
|
|
480
|
+
saveSchedule(controllerHome, { ...schedule, lastTriggeredAt: timestamp, lastOccurrenceId: occurrence.occurrenceId });
|
|
481
|
+
return decideOccurrence(controllerHome, schedule, occurrence, 'nothing_to_do', 'skipped', `Work ${workId} already has an active Controller ${existingOwner.controllerId}.`);
|
|
482
|
+
}
|
|
483
|
+
const launchReservation = getExternalControllerLaunchReservation(workStore, workId);
|
|
484
|
+
if (launchReservation) {
|
|
485
|
+
saveSchedule(controllerHome, { ...schedule, lastTriggeredAt: timestamp, lastOccurrenceId: occurrence.occurrenceId });
|
|
486
|
+
return decideOccurrence(controllerHome, schedule, occurrence, 'nothing_to_do', 'skipped', `Work ${workId} already has a pending external Controller launch ${launchReservation.reservationId}.`);
|
|
487
|
+
}
|
|
488
|
+
const wakeDecision = decideOccurrence(
|
|
489
|
+
controllerHome,
|
|
490
|
+
schedule,
|
|
491
|
+
occurrence,
|
|
492
|
+
'execute',
|
|
493
|
+
'running',
|
|
494
|
+
'Deterministic external Controller wake is starting through Thin Launcher.',
|
|
495
|
+
occurrenceDecisionEvidence({
|
|
496
|
+
operation: schedule.action.operation,
|
|
497
|
+
workId,
|
|
498
|
+
controllerType,
|
|
499
|
+
...(controllerType === 'chatgpt' ? {
|
|
500
|
+
model: typeof args.model === 'string' ? args.model : 'gpt-5.6',
|
|
501
|
+
reasoning: typeof args.reasoning === 'string' ? args.reasoning : 'high',
|
|
502
|
+
tabPolicy: typeof args.tab_policy === 'string' ? args.tab_policy : 'auto',
|
|
503
|
+
} : {}),
|
|
504
|
+
...extraEvidence,
|
|
505
|
+
}),
|
|
506
|
+
);
|
|
507
|
+
try {
|
|
508
|
+
const repository = getRepository(schedule.repoId, controllerHome, { includeRemoved: true });
|
|
509
|
+
const continuationPrompt = typeof args.continuation_prompt === 'string'
|
|
510
|
+
? args.continuation_prompt
|
|
511
|
+
: `Scheduled continuation ${occurrence.occurrenceId} from ${schedule.scheduleId}. Read current Forge state and continue only the bounded Work objective.`;
|
|
512
|
+
if (controllerType === 'chatgpt') {
|
|
513
|
+
const reasoning = args.reasoning === 'medium' || args.reasoning === 'xhigh' ? args.reasoning : 'high';
|
|
514
|
+
const tabPolicy = args.tab_policy === 'reuse' || args.tab_policy === 'new' ? args.tab_policy : 'auto';
|
|
515
|
+
const dispatched = await runWorkChatgptContinuation({
|
|
516
|
+
controllerHome,
|
|
517
|
+
repoId: schedule.repoId,
|
|
518
|
+
repoRoot: repository.canonicalRoot,
|
|
519
|
+
workId,
|
|
520
|
+
prompt: continuationPrompt,
|
|
521
|
+
title: schedule.name,
|
|
522
|
+
browserSessionId: typeof args.browser_session_id === 'string' ? args.browser_session_id : undefined,
|
|
523
|
+
conversationUrl: typeof args.conversation_url === 'string' ? args.conversation_url : undefined,
|
|
524
|
+
model: typeof args.model === 'string' ? args.model : 'gpt-5.6',
|
|
525
|
+
reasoning,
|
|
526
|
+
tabPolicy,
|
|
527
|
+
timeoutMs: typeof args.timeout_ms === 'number' ? args.timeout_ms : undefined,
|
|
528
|
+
});
|
|
529
|
+
if (dispatched.status === 'failed') throw new Error(dispatched.error?.message ?? 'CHATGPT_WORK_CONTINUATION_FAILED');
|
|
530
|
+
const latest = getSchedule(controllerHome, schedule.repoId, schedule.scheduleId);
|
|
531
|
+
saveSchedule(controllerHome, {
|
|
532
|
+
...latest,
|
|
533
|
+
lastTriggeredAt: timestamp,
|
|
534
|
+
lastOccurrenceId: occurrence.occurrenceId,
|
|
535
|
+
consecutiveFailures: 0,
|
|
536
|
+
nextEligibleAt: undefined,
|
|
537
|
+
pausedReason: undefined,
|
|
538
|
+
});
|
|
539
|
+
return saveOccurrence(controllerHome, {
|
|
540
|
+
...wakeDecision,
|
|
541
|
+
status: 'succeeded',
|
|
542
|
+
reason: `ChatGPT Work continuation dispatched via ${dispatched.browserSessionId}; execution preference verified.`,
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
const launched = launchSuperController({
|
|
546
|
+
work: { controllerHome, repoId: schedule.repoId },
|
|
547
|
+
handoff: { controllerHome, repoId: schedule.repoId },
|
|
548
|
+
}, {
|
|
549
|
+
controllerType: controllerType as 'codex' | 'claude' | 'grok',
|
|
550
|
+
executable: typeof args.executable === 'string' ? args.executable : undefined,
|
|
551
|
+
args: externalControllerLaunchArgs(args, controllerType),
|
|
552
|
+
workId,
|
|
553
|
+
handoffId: typeof args.handoff_id === 'string' ? args.handoff_id : undefined,
|
|
554
|
+
browserSessionId: typeof args.browser_session_id === 'string' ? args.browser_session_id : undefined,
|
|
555
|
+
conversationUrl: typeof args.conversation_url === 'string' ? args.conversation_url : undefined,
|
|
556
|
+
launchReservationMs: typeof args.launch_reservation_ms === 'number' ? args.launch_reservation_ms : typeof args.lease_ms === 'number' ? args.lease_ms : undefined,
|
|
557
|
+
continuationPrompt,
|
|
558
|
+
cwd: repository.canonicalRoot,
|
|
559
|
+
});
|
|
560
|
+
const latest = getSchedule(controllerHome, schedule.repoId, schedule.scheduleId);
|
|
561
|
+
saveSchedule(controllerHome, {
|
|
562
|
+
...latest,
|
|
563
|
+
lastTriggeredAt: timestamp,
|
|
564
|
+
lastOccurrenceId: occurrence.occurrenceId,
|
|
565
|
+
consecutiveFailures: 0,
|
|
566
|
+
nextEligibleAt: undefined,
|
|
567
|
+
pausedReason: undefined,
|
|
568
|
+
});
|
|
569
|
+
return saveOccurrence(controllerHome, {
|
|
570
|
+
...wakeDecision,
|
|
571
|
+
status: 'succeeded',
|
|
572
|
+
reason: `External Controller wake started ${launched.controllerType} pid=${String(launched.pid ?? 'unknown')} for Work ${workId}.`,
|
|
573
|
+
});
|
|
574
|
+
} catch (error) {
|
|
575
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
576
|
+
const failed = applyScheduleFailure(controllerHome, schedule.scheduleId, schedule.repoId, occurrence.occurrenceId, {
|
|
577
|
+
outcome: 'failed',
|
|
578
|
+
decision: 'execute',
|
|
579
|
+
reason,
|
|
580
|
+
handoff: {
|
|
581
|
+
title: `External Controller wake ${occurrence.occurrenceId} failed`,
|
|
582
|
+
summary: 'Forge recorded the schedule trigger but could not wake the configured external Controller.',
|
|
583
|
+
reason,
|
|
584
|
+
creationReason: 'repeated_infrastructure_failure',
|
|
585
|
+
blockingDecision: 'Repair the external Controller/browser launch path or update the saved session reference.',
|
|
586
|
+
recommendedDecision: 'Inspect launcher/browser readiness, then retrigger this bounded continuation.',
|
|
587
|
+
recommendedPrompt: `Resume Work ${workId} manually, inspect failed wake occurrence ${occurrence.occurrenceId}, and repair the configured external Controller launch path before unattended continuation resumes.`,
|
|
588
|
+
statusSummary: 'Scheduled external Controller wake failed.',
|
|
589
|
+
blockedBy: ['external_controller_wake_failed'],
|
|
590
|
+
attemptedActions: [`schedule:${schedule.scheduleId}`, `work:${workId}`, `controller:${controllerType}`],
|
|
591
|
+
},
|
|
592
|
+
});
|
|
593
|
+
const latest = getSchedule(controllerHome, schedule.repoId, schedule.scheduleId);
|
|
594
|
+
saveSchedule(controllerHome, { ...latest, lastTriggeredAt: timestamp, lastOccurrenceId: occurrence.occurrenceId });
|
|
595
|
+
return failed.occurrence ?? saveOccurrence(controllerHome, { ...wakeDecision, status: 'failed', reason });
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
347
599
|
export async function evaluateSchedule(
|
|
348
600
|
controllerHome: string,
|
|
349
601
|
schedule: RepositorySchedule,
|
|
@@ -548,76 +800,117 @@ export async function evaluateSchedule(
|
|
|
548
800
|
|
|
549
801
|
const args = schedule.action.arguments ?? {};
|
|
550
802
|
if (schedule.action.operation === EXTERNAL_CONTROLLER_WAKE_OPERATION) {
|
|
803
|
+
return executeExternalControllerWake(controllerHome, schedule, occurrence, timestamp, args);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
if (schedule.action.operation === CHATGPT_BROWSER_PROMPT_OPERATION) {
|
|
807
|
+
return executeChatgptBrowserPrompt(controllerHome, schedule, occurrence, timestamp, args);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
if (schedule.action.operation === BROWSER_PROBE_OPERATION) {
|
|
551
811
|
const workId = typeof args.work_id === 'string' ? args.work_id.trim() : '';
|
|
552
|
-
const controllerType = typeof args.controller_type === 'string' ? args.controller_type.trim() : 'chatgpt';
|
|
553
812
|
if (!workId) {
|
|
554
|
-
return decideOccurrence(controllerHome, schedule, occurrence, 'operation_blocked', 'skipped', '
|
|
555
|
-
}
|
|
556
|
-
if (!['chatgpt', 'codex', 'claude', 'grok'].includes(controllerType)) {
|
|
557
|
-
return decideOccurrence(controllerHome, schedule, occurrence, 'operation_blocked', 'skipped', 'EXTERNAL_CONTROLLER_WAKE_TYPE_INVALID');
|
|
813
|
+
return decideOccurrence(controllerHome, schedule, occurrence, 'operation_blocked', 'skipped', 'SCHEDULE_BROWSER_PROBE_WORK_ID_REQUIRED');
|
|
558
814
|
}
|
|
559
815
|
const workStore = { controllerHome, repoId: schedule.repoId };
|
|
560
816
|
const work = getWorkContract(workStore, workId);
|
|
561
817
|
if (!work) {
|
|
562
818
|
saveSchedule(controllerHome, { ...schedule, enabled: false, pausedReason: `Work ${workId} no longer exists.`, lastTriggeredAt: timestamp, lastOccurrenceId: occurrenceId });
|
|
563
|
-
return decideOccurrence(controllerHome, schedule, occurrence, 'operation_blocked', 'skipped', `
|
|
819
|
+
return decideOccurrence(controllerHome, schedule, occurrence, 'operation_blocked', 'skipped', `SCHEDULE_BROWSER_PROBE_WORK_NOT_FOUND:${workId}`);
|
|
564
820
|
}
|
|
565
821
|
if (isTerminalWorkContractStatus(work.status)) {
|
|
566
822
|
saveSchedule(controllerHome, { ...schedule, enabled: false, pausedReason: `Work ${workId} is terminal (${work.status}).`, lastTriggeredAt: timestamp, lastOccurrenceId: occurrenceId });
|
|
567
|
-
return decideOccurrence(controllerHome, schedule, occurrence, 'nothing_to_do', 'skipped', `Work ${workId} is terminal (${work.status});
|
|
568
|
-
}
|
|
569
|
-
const existingOwner = getControllerSession(workStore, workId);
|
|
570
|
-
if (existingOwner) {
|
|
571
|
-
saveSchedule(controllerHome, { ...schedule, lastTriggeredAt: timestamp, lastOccurrenceId: occurrenceId });
|
|
572
|
-
return decideOccurrence(controllerHome, schedule, occurrence, 'nothing_to_do', 'skipped', `Work ${workId} already has an active Controller ${existingOwner.controllerId}.`);
|
|
573
|
-
}
|
|
574
|
-
const launchReservation = getExternalControllerLaunchReservation(workStore, workId);
|
|
575
|
-
if (launchReservation) {
|
|
576
|
-
saveSchedule(controllerHome, { ...schedule, lastTriggeredAt: timestamp, lastOccurrenceId: occurrenceId });
|
|
577
|
-
return decideOccurrence(controllerHome, schedule, occurrence, 'nothing_to_do', 'skipped', `Work ${workId} already has a pending external Controller launch ${launchReservation.reservationId}.`);
|
|
823
|
+
return decideOccurrence(controllerHome, schedule, occurrence, 'nothing_to_do', 'skipped', `Work ${workId} is terminal (${work.status}); browser watcher stopped before probing.`);
|
|
578
824
|
}
|
|
579
|
-
|
|
580
|
-
controllerHome,
|
|
581
|
-
schedule,
|
|
582
|
-
occurrence,
|
|
583
|
-
'execute',
|
|
584
|
-
'running',
|
|
585
|
-
'Deterministic external Controller wake is starting through Thin Launcher.',
|
|
586
|
-
occurrenceDecisionEvidence({ operation: schedule.action.operation, workId, controllerType }),
|
|
587
|
-
);
|
|
825
|
+
|
|
588
826
|
try {
|
|
589
827
|
const repository = getRepository(schedule.repoId, controllerHome, { includeRemoved: true });
|
|
590
|
-
const
|
|
591
|
-
work: { controllerHome, repoId: schedule.repoId },
|
|
592
|
-
handoff: { controllerHome, repoId: schedule.repoId },
|
|
593
|
-
}, {
|
|
594
|
-
controllerType: controllerType as 'chatgpt' | 'codex' | 'claude' | 'grok',
|
|
595
|
-
executable: typeof args.executable === 'string' ? args.executable : undefined,
|
|
596
|
-
args: Array.isArray(args.launch_args) ? args.launch_args.map(String) : [],
|
|
597
|
-
workId,
|
|
598
|
-
handoffId: typeof args.handoff_id === 'string' ? args.handoff_id : undefined,
|
|
599
|
-
browserSessionId: typeof args.browser_session_id === 'string' ? args.browser_session_id : undefined,
|
|
600
|
-
conversationUrl: typeof args.conversation_url === 'string' ? args.conversation_url : undefined,
|
|
601
|
-
launchReservationMs: typeof args.launch_reservation_ms === 'number' ? args.launch_reservation_ms : typeof args.lease_ms === 'number' ? args.lease_ms : undefined,
|
|
602
|
-
continuationPrompt: typeof args.continuation_prompt === 'string'
|
|
603
|
-
? args.continuation_prompt
|
|
604
|
-
: `Scheduled continuation ${occurrence.occurrenceId} from ${schedule.scheduleId}. Read current Forge state and continue only the bounded Work objective.`,
|
|
605
|
-
cwd: repository.canonicalRoot,
|
|
606
|
-
});
|
|
828
|
+
const probe = await executeScheduledBrowserProbe({ controllerHome, repository, occurrenceId, args });
|
|
607
829
|
const latest = getSchedule(controllerHome, schedule.repoId, schedule.scheduleId);
|
|
608
|
-
|
|
830
|
+
const observationEvidence = occurrenceDecisionEvidence({
|
|
831
|
+
probeStatus: probe.status,
|
|
832
|
+
url: probe.url,
|
|
833
|
+
projectedLineCount: probe.projectedLineCount,
|
|
834
|
+
observedChars: probe.observedChars,
|
|
835
|
+
truncated: probe.truncated,
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
if (probe.status === 'auth_required') {
|
|
839
|
+
const observedSchedule = saveSchedule(controllerHome, {
|
|
840
|
+
...latest,
|
|
841
|
+
lastTriggeredAt: timestamp,
|
|
842
|
+
lastOccurrenceId: occurrenceId,
|
|
843
|
+
lastObservationAt: timestamp,
|
|
844
|
+
lastObservationStatus: 'auth_required',
|
|
845
|
+
consecutiveFailures: 0,
|
|
846
|
+
nextEligibleAt: undefined,
|
|
847
|
+
pausedReason: undefined,
|
|
848
|
+
});
|
|
849
|
+
if (args.wake_on_auth_required !== false) {
|
|
850
|
+
const wakeArgs = {
|
|
851
|
+
...args,
|
|
852
|
+
continuation_prompt: typeof args.auth_required_prompt === 'string'
|
|
853
|
+
? args.auth_required_prompt
|
|
854
|
+
: typeof args.continuation_prompt === 'string'
|
|
855
|
+
? args.continuation_prompt
|
|
856
|
+
: `Scheduled browser watcher ${schedule.scheduleId} detected that authentication is required (${probe.authReason ?? 'login marker'}). Inspect the bound external dependency and request user login only if it cannot be restored safely.`,
|
|
857
|
+
};
|
|
858
|
+
return executeExternalControllerWake(controllerHome, observedSchedule, occurrence, timestamp, wakeArgs, { ...observationEvidence, authReason: probe.authReason });
|
|
859
|
+
}
|
|
860
|
+
return decideOccurrence(controllerHome, observedSchedule, occurrence, 'operation_blocked', 'skipped', `Browser watcher requires authentication: ${probe.authReason ?? 'login marker matched'}`, observationEvidence);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
if (probe.status === 'keepalive') {
|
|
864
|
+
const observedSchedule = saveSchedule(controllerHome, {
|
|
865
|
+
...latest,
|
|
866
|
+
lastTriggeredAt: timestamp,
|
|
867
|
+
lastOccurrenceId: occurrenceId,
|
|
868
|
+
lastObservationAt: timestamp,
|
|
869
|
+
lastObservationStatus: 'keepalive',
|
|
870
|
+
consecutiveFailures: 0,
|
|
871
|
+
consecutiveNoops: 0,
|
|
872
|
+
nextEligibleAt: undefined,
|
|
873
|
+
pausedReason: undefined,
|
|
874
|
+
});
|
|
875
|
+
return decideOccurrence(controllerHome, observedSchedule, occurrence, 'execute', 'succeeded', 'Browser session keepalive refreshed successfully.', observationEvidence);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const observation = classifyScheduledBrowserObservation(
|
|
879
|
+
latest.lastObservationFingerprint,
|
|
880
|
+
probe.fingerprint,
|
|
881
|
+
args.wake_on_first_observation === true,
|
|
882
|
+
);
|
|
883
|
+
const changed = observation.status === 'changed';
|
|
884
|
+
const shouldWake = args.wake_on_change !== false && observation.shouldWake;
|
|
885
|
+
const observationStatus: RepositorySchedule['lastObservationStatus'] = observation.status;
|
|
886
|
+
const observedSchedule = saveSchedule(controllerHome, {
|
|
609
887
|
...latest,
|
|
610
888
|
lastTriggeredAt: timestamp,
|
|
611
889
|
lastOccurrenceId: occurrenceId,
|
|
890
|
+
lastObservationAt: timestamp,
|
|
891
|
+
lastObservationFingerprint: probe.fingerprint,
|
|
892
|
+
lastObservationChangedAt: changed ? timestamp : latest.lastObservationChangedAt,
|
|
893
|
+
lastObservationStatus: observationStatus,
|
|
612
894
|
consecutiveFailures: 0,
|
|
895
|
+
consecutiveNoops: shouldWake ? 0 : (latest.consecutiveNoops ?? 0) + 1,
|
|
613
896
|
nextEligibleAt: undefined,
|
|
614
897
|
pausedReason: undefined,
|
|
615
898
|
});
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
status
|
|
619
|
-
|
|
620
|
-
|
|
899
|
+
|
|
900
|
+
if (!shouldWake) {
|
|
901
|
+
const reason = observation.status === 'baseline'
|
|
902
|
+
? 'Browser watcher baseline recorded; no Controller wake was emitted.'
|
|
903
|
+
: 'Browser watcher observation is unchanged; no Controller wake was emitted.';
|
|
904
|
+
return decideOccurrence(controllerHome, observedSchedule, occurrence, 'nothing_to_do', 'skipped', reason, { ...observationEvidence, observationStatus });
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const wakeArgs = {
|
|
908
|
+
...args,
|
|
909
|
+
continuation_prompt: typeof args.continuation_prompt === 'string'
|
|
910
|
+
? args.continuation_prompt
|
|
911
|
+
: `Scheduled browser watcher ${schedule.scheduleId} detected a changed external observation. Read the current external state, correlate it with Work ${workId}, and continue only the bounded Work objective.`,
|
|
912
|
+
};
|
|
913
|
+
return executeExternalControllerWake(controllerHome, observedSchedule, occurrence, timestamp, wakeArgs, { ...observationEvidence, observationStatus });
|
|
621
914
|
} catch (error) {
|
|
622
915
|
const reason = error instanceof Error ? error.message : String(error);
|
|
623
916
|
const failed = applyScheduleFailure(controllerHome, schedule.scheduleId, schedule.repoId, occurrence.occurrenceId, {
|
|
@@ -625,21 +918,21 @@ export async function evaluateSchedule(
|
|
|
625
918
|
decision: 'execute',
|
|
626
919
|
reason,
|
|
627
920
|
handoff: {
|
|
628
|
-
title: `
|
|
629
|
-
summary: '
|
|
921
|
+
title: `Browser watcher occurrence ${occurrence.occurrenceId} failed`,
|
|
922
|
+
summary: 'A bounded browser probe failed before Forge could compare the external observation.',
|
|
630
923
|
reason,
|
|
631
924
|
creationReason: 'repeated_infrastructure_failure',
|
|
632
|
-
blockingDecision: 'Repair
|
|
633
|
-
recommendedDecision: 'Inspect
|
|
634
|
-
recommendedPrompt: `
|
|
635
|
-
statusSummary: 'Scheduled
|
|
636
|
-
blockedBy: ['
|
|
637
|
-
attemptedActions: [`schedule:${schedule.scheduleId}`, `work:${workId}`,
|
|
925
|
+
blockingDecision: 'Repair browser/session readiness or update the watcher target before unattended probing resumes.',
|
|
926
|
+
recommendedDecision: 'Inspect the browser plugin/session and retrigger the watcher after the target is readable.',
|
|
927
|
+
recommendedPrompt: `Inspect browser watcher schedule ${schedule.scheduleId} for Work ${workId}, repair the failed browser probe, then retrigger one bounded occurrence.`,
|
|
928
|
+
statusSummary: 'Scheduled browser watcher failed.',
|
|
929
|
+
blockedBy: ['browser_probe_failed'],
|
|
930
|
+
attemptedActions: [`schedule:${schedule.scheduleId}`, `work:${workId}`, 'operation:browser_probe'],
|
|
638
931
|
},
|
|
639
932
|
});
|
|
640
933
|
const latest = getSchedule(controllerHome, schedule.repoId, schedule.scheduleId);
|
|
641
934
|
saveSchedule(controllerHome, { ...latest, lastTriggeredAt: timestamp, lastOccurrenceId: occurrenceId });
|
|
642
|
-
return failed.occurrence ?? saveOccurrence(controllerHome, { ...
|
|
935
|
+
return failed.occurrence ?? saveOccurrence(controllerHome, { ...occurrence, status: 'failed', decision: 'execute', reason });
|
|
643
936
|
}
|
|
644
937
|
}
|
|
645
938
|
|
|
@@ -73,6 +73,11 @@ export interface RepositorySchedule {
|
|
|
73
73
|
consecutiveNoops?: number;
|
|
74
74
|
nextEligibleAt?: string;
|
|
75
75
|
pausedReason?: string;
|
|
76
|
+
/** Last deterministic external observation for browser_watch/browser_keepalive. */
|
|
77
|
+
lastObservationAt?: string;
|
|
78
|
+
lastObservationFingerprint?: string;
|
|
79
|
+
lastObservationChangedAt?: string;
|
|
80
|
+
lastObservationStatus?: 'baseline' | 'unchanged' | 'changed' | 'keepalive' | 'auth_required';
|
|
76
81
|
}
|
|
77
82
|
|
|
78
83
|
export type ScheduleDecisionType =
|