@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
|
@@ -67,6 +67,8 @@ export interface ControllerContextPackOptions {
|
|
|
67
67
|
maxSnippets?: number;
|
|
68
68
|
maxCharsPerSnippet?: number;
|
|
69
69
|
structuralContext?: StructuralContextMode;
|
|
70
|
+
/** Optional repository-level CodeGraph root used as structural baseline when the selected checkout has no local index. */
|
|
71
|
+
structuralIndexRoot?: string;
|
|
70
72
|
retrievalMode?: ControllerContextRetrievalMode;
|
|
71
73
|
/** GPT-selected cross-cutting evidence dimensions. Forge expands mechanically and never treats them as semantic completeness proof. */
|
|
72
74
|
impactDomains?: ControllerContextImpactDomain[];
|
|
@@ -138,6 +140,9 @@ export interface ControllerContextPackProjection {
|
|
|
138
140
|
requestedMode: StructuralContextMode;
|
|
139
141
|
status: "disabled" | "ready" | "stale" | "unavailable" | "degraded";
|
|
140
142
|
requiredSatisfied: boolean;
|
|
143
|
+
indexSource: "current_checkout" | "repository_baseline";
|
|
144
|
+
overlayChangedFiles: string[];
|
|
145
|
+
baselineRevisionMatches: boolean;
|
|
141
146
|
query?: string;
|
|
142
147
|
entryPoints: Array<Pick<CodeGraphNodeSummary, "id" | "kind" | "name" | "filePath" | "startLine" | "endLine">>;
|
|
143
148
|
relatedFiles: string[];
|
|
@@ -146,6 +151,26 @@ export interface ControllerContextPackProjection {
|
|
|
146
151
|
timingsMs?: CodeGraphReadProviderResponse["timingsMs"];
|
|
147
152
|
truncated: boolean;
|
|
148
153
|
};
|
|
154
|
+
impactContext: {
|
|
155
|
+
status: "not_requested" | "ready" | "degraded";
|
|
156
|
+
confidence: "low" | "medium" | "high";
|
|
157
|
+
primaryTargets: string[];
|
|
158
|
+
mustInspect: string[];
|
|
159
|
+
likelyAffected: string[];
|
|
160
|
+
relevantTests: string[];
|
|
161
|
+
relevantChecks: string[];
|
|
162
|
+
architectureContracts: string[];
|
|
163
|
+
coverageGaps: string[];
|
|
164
|
+
relationSources: Array<"codegraph" | "lexical" | "forge_checks">;
|
|
165
|
+
freshness: {
|
|
166
|
+
structuralStatus: "disabled" | "ready" | "stale" | "unavailable" | "degraded";
|
|
167
|
+
lastIndexedAt?: number | null;
|
|
168
|
+
changedFileCount: number;
|
|
169
|
+
indexSource: "current_checkout" | "repository_baseline";
|
|
170
|
+
overlayChangedFileCount: number;
|
|
171
|
+
baselineRevisionMatches: boolean;
|
|
172
|
+
};
|
|
173
|
+
};
|
|
149
174
|
files: ControllerContextPackFile[];
|
|
150
175
|
deniedPaths: Array<{ path: string; reason: string }>;
|
|
151
176
|
omitted: Array<{ path: string; reason: string }>;
|
|
@@ -273,6 +298,18 @@ function coveredByGlob(path: string, globs: string[]): boolean {
|
|
|
273
298
|
return globs.length === 0 || globs.some((glob) => globMatches(glob, path));
|
|
274
299
|
}
|
|
275
300
|
|
|
301
|
+
function gitStatusChangedPaths(status: string): string[] {
|
|
302
|
+
const paths: string[] = [];
|
|
303
|
+
for (const line of status.split(/\r?\n/)) {
|
|
304
|
+
if (!line.trim() || line.startsWith("##") || line.length < 4) continue;
|
|
305
|
+
let path = line.slice(3).trim();
|
|
306
|
+
if (path.includes(" -> ")) path = path.slice(path.lastIndexOf(" -> ") + 4).trim();
|
|
307
|
+
if (path.startsWith('"') && path.endsWith('"')) path = path.slice(1, -1);
|
|
308
|
+
if (path && !paths.includes(path)) paths.push(path);
|
|
309
|
+
}
|
|
310
|
+
return paths;
|
|
311
|
+
}
|
|
312
|
+
|
|
276
313
|
function addReason(map: Map<string, { reasons: Set<string>; lines: Set<number> }>, path: string, reason: string, line?: number): void {
|
|
277
314
|
const entry = map.get(path) ?? { reasons: new Set<string>(), lines: new Set<number>() };
|
|
278
315
|
entry.reasons.add(reason);
|
|
@@ -453,11 +490,19 @@ export function buildControllerContextPack(
|
|
|
453
490
|
let skippedBinaryFiles = 0;
|
|
454
491
|
let searchTruncated = false;
|
|
455
492
|
const structuralMode = options.structuralContext ?? "off";
|
|
493
|
+
const structuralIndexRoot = options.structuralIndexRoot?.trim() || repoRoot;
|
|
494
|
+
const indexSource = structuralIndexRoot === repoRoot ? "current_checkout" : "repository_baseline";
|
|
495
|
+
const overlayChangedFiles = indexSource === "repository_baseline" ? gitStatusChangedPaths(git.status) : [];
|
|
496
|
+
const baselineRevisionMatches = indexSource === "current_checkout" || gitSnapshot(structuralIndexRoot).head === git.head;
|
|
497
|
+
const graphImpactFiles = new Set<string>();
|
|
456
498
|
let structuralContext: ControllerContextPackProjection["structuralContext"] = {
|
|
457
499
|
provider: "codegraph",
|
|
458
500
|
requestedMode: structuralMode,
|
|
459
501
|
status: "disabled",
|
|
460
502
|
requiredSatisfied: structuralMode !== "required",
|
|
503
|
+
indexSource,
|
|
504
|
+
overlayChangedFiles,
|
|
505
|
+
baselineRevisionMatches,
|
|
461
506
|
entryPoints: [],
|
|
462
507
|
relatedFiles: [],
|
|
463
508
|
truncated: false,
|
|
@@ -474,7 +519,7 @@ export function buildControllerContextPack(
|
|
|
474
519
|
};
|
|
475
520
|
} else {
|
|
476
521
|
const queryCodeGraph = dependencies.queryCodeGraph ?? queryCodeGraphReadProvider;
|
|
477
|
-
const structural = queryCodeGraph(
|
|
522
|
+
const structural = queryCodeGraph(structuralIndexRoot, {
|
|
478
523
|
operation: "context",
|
|
479
524
|
query: structuralQuery,
|
|
480
525
|
limit: Math.min(Math.max(maxFiles, 4), 12),
|
|
@@ -496,8 +541,11 @@ export function buildControllerContextPack(
|
|
|
496
541
|
structuralContext = {
|
|
497
542
|
provider: "codegraph",
|
|
498
543
|
requestedMode: structuralMode,
|
|
499
|
-
status: structural.status,
|
|
500
|
-
requiredSatisfied: structuralMode !== "required" || structural.status === "ready",
|
|
544
|
+
status: indexSource === "repository_baseline" && (!baselineRevisionMatches || overlayChangedFiles.length > 0) && structural.status === "ready" ? "stale" : structural.status,
|
|
545
|
+
requiredSatisfied: structuralMode !== "required" || (structural.status === "ready" && baselineRevisionMatches && overlayChangedFiles.length === 0),
|
|
546
|
+
indexSource,
|
|
547
|
+
overlayChangedFiles,
|
|
548
|
+
baselineRevisionMatches,
|
|
501
549
|
query: structuralQuery,
|
|
502
550
|
entryPoints: graph.entryPoints.slice(0, 12).map((node) => ({
|
|
503
551
|
id: node.id,
|
|
@@ -530,17 +578,50 @@ export function buildControllerContextPack(
|
|
|
530
578
|
addStructuralPath(node.filePath, `codegraph:entry:${node.name}`, node.startLine);
|
|
531
579
|
}
|
|
532
580
|
for (const path of graph.relatedFiles.slice(0, maxFiles * 6)) addStructuralPath(path, "codegraph:related");
|
|
581
|
+
// Context search identifies the structural entry points. For a bounded
|
|
582
|
+
// number of those files, enrich the same Context Plane call with direct
|
|
583
|
+
// file dependencies/dependents. This is impact evidence, not a second
|
|
584
|
+
// routing/tool surface, and remains completely off for ordinary Direct.
|
|
585
|
+
if (structural.status === "ready") {
|
|
586
|
+
const primaryFiles = Array.from(new Set(graph.entryPoints.map((node) => node.filePath).filter(Boolean))).slice(0, 4);
|
|
587
|
+
for (const filePath of primaryFiles) {
|
|
588
|
+
const adjacency = queryCodeGraph(structuralIndexRoot, { operation: "file_dependencies", filePath, limit: 40 });
|
|
589
|
+
if (!adjacency.ok || adjacency.status !== "ready") continue;
|
|
590
|
+
const dependencies = Array.isArray(adjacency.result?.dependencies)
|
|
591
|
+
? adjacency.result.dependencies.filter((value): value is string => typeof value === "string")
|
|
592
|
+
: [];
|
|
593
|
+
const dependents = Array.isArray(adjacency.result?.dependents)
|
|
594
|
+
? adjacency.result.dependents.filter((value): value is string => typeof value === "string")
|
|
595
|
+
: [];
|
|
596
|
+
for (const path of dependencies.slice(0, 40)) {
|
|
597
|
+
graphImpactFiles.add(path);
|
|
598
|
+
addStructuralPath(path, `codegraph:dependency:${filePath}`);
|
|
599
|
+
}
|
|
600
|
+
for (const path of dependents.slice(0, 40)) {
|
|
601
|
+
graphImpactFiles.add(path);
|
|
602
|
+
addStructuralPath(path, `codegraph:dependent:${filePath}`);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
533
606
|
for (const path of metadata?.changedFiles.added ?? []) addStructuralPath(path, "codegraph:changed-file", 1);
|
|
534
607
|
for (const path of metadata?.changedFiles.modified ?? []) addStructuralPath(path, "codegraph:changed-file", 1);
|
|
535
608
|
for (const path of metadata?.changedFiles.removed ?? []) omitted.push({ path, reason: "codegraph reports file removed since index" });
|
|
609
|
+
for (const path of overlayChangedFiles) addStructuralPath(path, "worktree:changed-file", 1);
|
|
536
610
|
}
|
|
537
611
|
}
|
|
538
612
|
|
|
613
|
+
const exactKnownFiles: string[] = [];
|
|
614
|
+
let exactKnownFileScope = explicitKnownPaths.length > 0;
|
|
539
615
|
for (const path of explicitKnownPaths) {
|
|
540
616
|
const expanded = expandKnownPath(repoRoot, policy, path, Math.max(maxFiles * 4, 40));
|
|
541
617
|
for (const file of expanded.files) {
|
|
542
618
|
addReason(candidates, file, expanded.directory ? `explicit-known-directory:${expanded.directory}` : "explicit-known-path", 1);
|
|
543
619
|
}
|
|
620
|
+
if (!expanded.directory && expanded.files.length === 1 && expanded.denied.length === 0 && !expanded.truncated) {
|
|
621
|
+
exactKnownFiles.push(expanded.files[0]!);
|
|
622
|
+
} else {
|
|
623
|
+
exactKnownFileScope = false;
|
|
624
|
+
}
|
|
544
625
|
deniedPaths.push(...expanded.denied);
|
|
545
626
|
scannedFiles += expanded.files.length;
|
|
546
627
|
searchTruncated = searchTruncated || expanded.truncated;
|
|
@@ -550,16 +631,27 @@ export function buildControllerContextPack(
|
|
|
550
631
|
if (!includeGlobs.includes(glob)) includeGlobs.push(glob);
|
|
551
632
|
}
|
|
552
633
|
|
|
634
|
+
// Exact known files are already a caller-selected implementation scope. When
|
|
635
|
+
// no broader impact/structural/include scope was requested, search those files
|
|
636
|
+
// for useful hit lines instead of rereading the whole repository. Planning,
|
|
637
|
+
// debugging, directories and explicit broader scopes retain full discovery.
|
|
638
|
+
const scopedExactKnownFileSearch = exactKnownFileScope
|
|
639
|
+
&& retrievalMode === "implementation"
|
|
640
|
+
&& structuralMode === "off"
|
|
641
|
+
&& impactDomains.length === 0
|
|
642
|
+
&& searchIncludeGlobs.length === 0;
|
|
643
|
+
|
|
553
644
|
// Lexical retrieval is one bounded pass across candidate files for all terms.
|
|
554
645
|
// The old per-term loop reread the same source files up to 14 times even
|
|
555
646
|
// though inventory itself was cached.
|
|
556
647
|
if (searchQueries.length > 0 && candidates.size < maxFiles * 3) {
|
|
557
648
|
const search = searchRepositoryMany(repoRoot, policy, {
|
|
558
649
|
queries: searchQueries,
|
|
559
|
-
|
|
650
|
+
files: scopedExactKnownFileSearch ? exactKnownFiles : undefined,
|
|
651
|
+
includeGlobs: scopedExactKnownFileSearch ? undefined : searchIncludeGlobs,
|
|
560
652
|
excludeGlobs,
|
|
561
653
|
maxResultsPerQuery: Math.max(maxFiles * 4, 12),
|
|
562
|
-
maxFiles: MAX_TOTAL_SEARCHED_FILES,
|
|
654
|
+
maxFiles: scopedExactKnownFileSearch ? exactKnownFiles.length : MAX_TOTAL_SEARCHED_FILES,
|
|
563
655
|
caseSensitive: false,
|
|
564
656
|
cacheKey: JSON.stringify({ head: git.head, status: git.status, diffStat: git.diffStat }),
|
|
565
657
|
});
|
|
@@ -685,6 +777,65 @@ export function buildControllerContextPack(
|
|
|
685
777
|
const rawCodeRequiredForImplementation = retrievalMode !== "implementation"
|
|
686
778
|
|| files.length === 0
|
|
687
779
|
|| rawSnippetTruncated;
|
|
780
|
+
const primaryTargets = Array.from(new Set(structuralContext.entryPoints.map((entry) => entry.filePath).filter(Boolean))).slice(0, 20);
|
|
781
|
+
const structuralUniverse = Array.from(new Set([
|
|
782
|
+
...primaryTargets,
|
|
783
|
+
...graphImpactFiles,
|
|
784
|
+
...structuralContext.relatedFiles,
|
|
785
|
+
...selected.map((entry) => entry.path),
|
|
786
|
+
])).slice(0, 160);
|
|
787
|
+
const relevantTests = structuralUniverse.filter((path) => /(^|\/)(__tests__|tests?|spec)(\/|$)|\.(test|spec)\.[^.]+$/i.test(path)).slice(0, 40);
|
|
788
|
+
const architectureContracts = structuralUniverse.filter((path) =>
|
|
789
|
+
path === "AGENTS.md"
|
|
790
|
+
|| path === "CLAUDE.md"
|
|
791
|
+
|| path.startsWith("docs/architecture/")
|
|
792
|
+
|| path.startsWith(".ai/context/"),
|
|
793
|
+
).slice(0, 40);
|
|
794
|
+
const mustInspect = Array.from(new Set([...primaryTargets, ...graphImpactFiles, ...relevantTests, ...architectureContracts])).slice(0, 80);
|
|
795
|
+
const mustInspectSet = new Set(mustInspect);
|
|
796
|
+
const likelyAffected = structuralUniverse.filter((path) => !mustInspectSet.has(path)).slice(0, 80);
|
|
797
|
+
const changedFileCount = structuralContext.metadata
|
|
798
|
+
? structuralContext.metadata.changedFiles.added.length
|
|
799
|
+
+ structuralContext.metadata.changedFiles.modified.length
|
|
800
|
+
+ structuralContext.metadata.changedFiles.removed.length
|
|
801
|
+
: 0;
|
|
802
|
+
const coverageGaps = [
|
|
803
|
+
...(structuralContext.status === "stale" ? ["structural_index_stale"] : []),
|
|
804
|
+
...(structuralContext.indexSource === "repository_baseline" ? ["structural_repository_baseline_overlay"] : []),
|
|
805
|
+
...(!structuralContext.baselineRevisionMatches ? ["structural_baseline_revision_mismatch"] : []),
|
|
806
|
+
...(structuralContext.status === "unavailable" || structuralContext.status === "degraded" ? ["structural_provider_unavailable"] : []),
|
|
807
|
+
...(structuralContext.truncated ? ["structural_result_truncated"] : []),
|
|
808
|
+
...(changedFileCount > 0 ? ["structural_index_has_unindexed_changes"] : []),
|
|
809
|
+
...impactCoverage.filter((coverage) => coverage.status !== "selected").map((coverage) => `impact_domain_${coverage.status}:${coverage.domain}`),
|
|
810
|
+
];
|
|
811
|
+
const impactContext: ControllerContextPackProjection["impactContext"] = {
|
|
812
|
+
status: structuralMode === "off" ? "not_requested" : structuralContext.status === "ready" ? "ready" : "degraded",
|
|
813
|
+
confidence: structuralContext.status === "ready" && primaryTargets.length > 0 && !structuralContext.truncated
|
|
814
|
+
? "high"
|
|
815
|
+
: structuralContext.status === "ready" || files.length > 0
|
|
816
|
+
? "medium"
|
|
817
|
+
: "low",
|
|
818
|
+
primaryTargets,
|
|
819
|
+
mustInspect,
|
|
820
|
+
likelyAffected,
|
|
821
|
+
relevantTests,
|
|
822
|
+
relevantChecks: taskChecks,
|
|
823
|
+
architectureContracts,
|
|
824
|
+
coverageGaps: Array.from(new Set(coverageGaps)).slice(0, 40),
|
|
825
|
+
relationSources: [
|
|
826
|
+
...(structuralMode !== "off" ? ["codegraph" as const] : []),
|
|
827
|
+
...(files.length > 0 ? ["lexical" as const] : []),
|
|
828
|
+
...(taskChecks.length > 0 ? ["forge_checks" as const] : []),
|
|
829
|
+
],
|
|
830
|
+
freshness: {
|
|
831
|
+
structuralStatus: structuralContext.status,
|
|
832
|
+
lastIndexedAt: structuralContext.metadata?.lastIndexedAt,
|
|
833
|
+
changedFileCount,
|
|
834
|
+
indexSource: structuralContext.indexSource,
|
|
835
|
+
overlayChangedFileCount: structuralContext.overlayChangedFiles.length,
|
|
836
|
+
baselineRevisionMatches: structuralContext.baselineRevisionMatches,
|
|
837
|
+
},
|
|
838
|
+
};
|
|
688
839
|
|
|
689
840
|
return {
|
|
690
841
|
schemaVersion: CONTEXT_PACK_SCHEMA_VERSION,
|
|
@@ -712,6 +863,7 @@ export function buildControllerContextPack(
|
|
|
712
863
|
truncated: searchTruncated,
|
|
713
864
|
},
|
|
714
865
|
structuralContext,
|
|
866
|
+
impactContext,
|
|
715
867
|
files,
|
|
716
868
|
deniedPaths: deniedPaths.slice(0, 20),
|
|
717
869
|
omitted: omitted.slice(0, 30),
|
|
@@ -732,7 +884,7 @@ export function buildControllerContextPack(
|
|
|
732
884
|
retrievalMode === "implementation"
|
|
733
885
|
? "The pack already contains policy-approved current raw source snippets with file SHA identities. Request wider/exact ranges only when ChatGPT judges the impact surface ambiguous or an expansion signal makes the raw evidence mechanically incomplete."
|
|
734
886
|
: "Investigation modes may intentionally expand exact ranges, structural relationships, tests, and neighboring modules before any implementation decision.",
|
|
735
|
-
"Search/CodeGraph ranking is discovery evidence, not a business-semantics authority.
|
|
887
|
+
"Search/CodeGraph ranking is discovery evidence, not a business-semantics authority. impactContext makes the bounded evidence surface and coverage gaps explicit; ChatGPT still decides semantic sufficiency.",
|
|
736
888
|
impactDomains.length > 0 ? `Impact domains were selected by ChatGPT and expanded mechanically in this same retrieval call: ${impactDomains.join(", ")}. Missing or omitted domain evidence is an ambiguity signal, not proof that the domain is irrelevant.` : "No explicit cross-cutting impact domains were requested; ChatGPT may add them when state, scheduling, notifications, events, caching, API, or concurrency could materially change the implementation.",
|
|
737
889
|
"CodeGraph structural evidence is discovery evidence. Raw source access still passes through Forge repository policy and current-file reads.",
|
|
738
890
|
structuralContext.status === "stale" ? "CodeGraph reports stale structural evidence. Treat graph relationships as hints and prefer the returned current raw source for changed files." : structuralContext.status === "unavailable" || structuralContext.status === "degraded" ? "Structural context was unavailable or degraded; bounded text search remains the fallback." : structuralContext.status === "ready" ? "CodeGraph structural context was queried read-only with index sync disabled." : "Structural context was not requested.",
|
|
@@ -112,40 +112,38 @@ function isExecutionIntent(
|
|
|
112
112
|
);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
function explicitPlanExecution(intent: PromptGuardExecutionIntent): boolean {
|
|
116
|
+
return intent === 'embedded_approved_plan' || intent === 'plan_execution_projection';
|
|
117
|
+
}
|
|
118
|
+
|
|
115
119
|
function decideNoActivePlanAction(
|
|
116
120
|
intent: PromptGuardExecutionIntent,
|
|
117
121
|
state: PromptGuardState,
|
|
118
122
|
): PromptGuardAction {
|
|
119
|
-
|
|
120
|
-
|
|
123
|
+
// Host hooks never decide that ordinary implementation requires a Plan.
|
|
124
|
+
// ChatGPT may inspect first and then explicitly commit to Plan execution.
|
|
125
|
+
if (!explicitPlanExecution(intent)) {
|
|
126
|
+
return state.worktree === 'linked_target' ? 'worktree_execution_advice' : 'allow';
|
|
121
127
|
}
|
|
128
|
+
if (state.pending === 'fresh') return 'plan_capture_pending_advice';
|
|
122
129
|
if (state.worktree === 'linked_target') return 'worktree_execution_advice';
|
|
123
|
-
|
|
124
|
-
return 'plan_capture_missing_active_advice';
|
|
125
|
-
}
|
|
126
|
-
return 'plan_status_no_active_block';
|
|
130
|
+
return 'plan_capture_missing_active_advice';
|
|
127
131
|
}
|
|
128
132
|
|
|
129
133
|
function decideDraftPlanAction(
|
|
130
134
|
intent: PromptGuardExecutionIntent,
|
|
131
135
|
): PromptGuardAction {
|
|
132
|
-
|
|
133
|
-
return 'plan_status_not_approved_block';
|
|
136
|
+
return explicitPlanExecution(intent) ? 'plan_capture_draft_advice' : 'allow';
|
|
134
137
|
}
|
|
135
138
|
|
|
136
139
|
function decideApprovedPlanAction(
|
|
137
140
|
intent: PromptGuardExecutionIntent,
|
|
138
141
|
state: PromptGuardState,
|
|
139
142
|
): PromptGuardAction {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
state.contract !== 'present'
|
|
145
|
-
) {
|
|
146
|
-
return 'plan_execution_scaffold_advice';
|
|
147
|
-
}
|
|
148
|
-
if (state.contract !== 'present') return 'contract_missing_block';
|
|
143
|
+
// Plan contracts may guide an explicitly selected Plan execution, but their
|
|
144
|
+
// presence/evidence is not a semantic permission gate for ordinary edits.
|
|
145
|
+
if (!explicitPlanExecution(intent)) return 'allow';
|
|
146
|
+
if (state.contract !== 'present') return 'plan_execution_scaffold_advice';
|
|
149
147
|
return 'allow';
|
|
150
148
|
}
|
|
151
149
|
|
|
@@ -233,6 +231,7 @@ export function decidePromptGuardAction(
|
|
|
233
231
|
): PromptGuardAction {
|
|
234
232
|
if (intent === 'done') return decideDoneAction(state);
|
|
235
233
|
if (!isExecutionIntent(intent)) return 'allow';
|
|
236
|
-
|
|
234
|
+
// Product/spec/Plan state is context for the Controller, not execution
|
|
235
|
+
// authorization. Hard path, permission and resource safety live elsewhere.
|
|
237
236
|
return PROMPT_GUARD_EXECUTION_TABLE[state.plan][intent](state);
|
|
238
237
|
}
|
|
@@ -401,7 +401,7 @@ export function isThinkPlanStartIntent(ctx: PromptIntentContext): boolean {
|
|
|
401
401
|
if (isBugOrHuntIntent(ctx)) return false;
|
|
402
402
|
if (THINK_COMMAND.test(ctx.text)) return true;
|
|
403
403
|
if (isPlanConsultationIntent(ctx)) return false;
|
|
404
|
-
return THINK_PLAN_PHRASE.test(ctx.text)
|
|
404
|
+
return THINK_PLAN_PHRASE.test(ctx.text);
|
|
405
405
|
}
|
|
406
406
|
|
|
407
407
|
export function normalizePlanSlug(value: string): string {
|
|
@@ -2,10 +2,22 @@ import type { RepositoryRecord } from '../repositories/types';
|
|
|
2
2
|
import { listAssistantRoutines } from '../../runtime/assistant/store';
|
|
3
3
|
import { runAssistantRoutineNow } from '../../runtime/assistant/intent';
|
|
4
4
|
import { getAssistantRoutineScheduleBinding, updateAssistantRoutineLifecycle } from '../../runtime/assistant/schedule-binding';
|
|
5
|
+
import { getWorkContract } from '../../runtime/control-plane/facade';
|
|
5
6
|
import { evaluateSchedule } from '../../runtime/workflow/schedules/engine';
|
|
6
7
|
import { getSchedule, listOccurrences, listSchedules, saveSchedule } from '../../runtime/workflow/schedules/store';
|
|
7
8
|
import type { RepositorySchedule, ScheduleOccurrence } from '../../runtime/workflow/schedules/types';
|
|
8
9
|
|
|
10
|
+
export type ConsoleAutomationMode = 'browser_watch' | 'browser_keepalive' | 'continuation' | 'chatgpt_prompt' | 'routine' | 'schedule';
|
|
11
|
+
|
|
12
|
+
export interface ConsoleAutomationHistoryView {
|
|
13
|
+
id: string;
|
|
14
|
+
at: string;
|
|
15
|
+
result: string;
|
|
16
|
+
tone: 'green' | 'amber' | 'red' | 'blue' | 'gray';
|
|
17
|
+
reason?: string;
|
|
18
|
+
trigger?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
9
21
|
export interface ConsoleAutomationView {
|
|
10
22
|
id: string;
|
|
11
23
|
source: 'schedule' | 'routine';
|
|
@@ -17,23 +29,46 @@ export interface ConsoleAutomationView {
|
|
|
17
29
|
schedule: string;
|
|
18
30
|
timezone?: string;
|
|
19
31
|
delivery?: string;
|
|
32
|
+
mode: ConsoleAutomationMode;
|
|
33
|
+
modeLabel: string;
|
|
34
|
+
live?: boolean;
|
|
35
|
+
targetLabel?: string;
|
|
36
|
+
boundWorkId?: string;
|
|
37
|
+
boundWorkObjective?: string;
|
|
38
|
+
observationStatus?: RepositorySchedule['lastObservationStatus'];
|
|
39
|
+
observationAt?: string;
|
|
40
|
+
failureCount?: number;
|
|
41
|
+
policySummary?: string;
|
|
42
|
+
agentModel?: string;
|
|
43
|
+
reasoningLevel?: string;
|
|
44
|
+
tabPolicy?: string;
|
|
20
45
|
lastRunAt?: string;
|
|
21
46
|
lastResult?: string;
|
|
22
47
|
nextRunHint?: string;
|
|
23
48
|
pausedReason?: string;
|
|
49
|
+
history: ConsoleAutomationHistoryView[];
|
|
24
50
|
actions: Array<'run' | 'pause' | 'resume'>;
|
|
25
51
|
}
|
|
26
52
|
|
|
53
|
+
function dailyCronLabel(expression?: string): string | undefined {
|
|
54
|
+
const match = /^(\d{1,2})\s+(\d{1,2})\s+\*\s+\*\s+\*$/.exec(expression?.trim() ?? '');
|
|
55
|
+
if (!match) return undefined;
|
|
56
|
+
const minute = Number(match[1]);
|
|
57
|
+
const hour = Number(match[2]);
|
|
58
|
+
if (minute > 59 || hour > 23) return undefined;
|
|
59
|
+
return `每天 ${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
27
62
|
function triggerLabel(schedule: RepositorySchedule): string {
|
|
28
63
|
const trigger = schedule.trigger;
|
|
29
64
|
switch (trigger.type) {
|
|
30
|
-
case 'interval': return
|
|
31
|
-
case 'cron': return `Cron ${trigger.cronExpression ?? '—'}`;
|
|
32
|
-
case 'calendar': return trigger.calendarAt ?
|
|
33
|
-
case 'condition': return
|
|
34
|
-
case 'repository-event': return
|
|
35
|
-
case 'dependency-checkpoint': return '
|
|
36
|
-
case 'manual': return '
|
|
65
|
+
case 'interval': return `每 ${trigger.everyMinutes ?? '?'} 分钟`;
|
|
66
|
+
case 'cron': return dailyCronLabel(trigger.cronExpression) ?? `Cron ${trigger.cronExpression ?? '—'}`;
|
|
67
|
+
case 'calendar': return trigger.calendarAt ? `单次 · ${trigger.calendarAt}` : '指定时间';
|
|
68
|
+
case 'condition': return `条件满足时 · ${trigger.condition?.kind ?? 'condition'}`;
|
|
69
|
+
case 'repository-event': return `仓库事件 · ${trigger.eventName ?? 'repository event'}`;
|
|
70
|
+
case 'dependency-checkpoint': return '依赖完成时';
|
|
71
|
+
case 'manual': return '仅手动运行';
|
|
37
72
|
}
|
|
38
73
|
}
|
|
39
74
|
|
|
@@ -45,11 +80,11 @@ function nextScheduleHint(schedule: RepositorySchedule): string | undefined {
|
|
|
45
80
|
const base = Date.parse(schedule.lastTriggeredAt ?? schedule.createdAt);
|
|
46
81
|
if (Number.isFinite(base)) {
|
|
47
82
|
const next = base + schedule.trigger.everyMinutes * 60_000;
|
|
48
|
-
return next > Date.now() ? new Date(next).toISOString() : '
|
|
83
|
+
return next > Date.now() ? new Date(next).toISOString() : '下一次调度周期';
|
|
49
84
|
}
|
|
50
85
|
}
|
|
51
|
-
if (schedule.trigger.type === 'cron') return 'Scheduler
|
|
52
|
-
if (schedule.trigger.type === 'condition') return '
|
|
86
|
+
if (schedule.trigger.type === 'cron') return dailyCronLabel(schedule.trigger.cronExpression) ?? '由 Scheduler 计算';
|
|
87
|
+
if (schedule.trigger.type === 'condition') return '条件满足时';
|
|
53
88
|
return undefined;
|
|
54
89
|
}
|
|
55
90
|
|
|
@@ -60,13 +95,135 @@ function safeBoundSchedule(controllerHome: string, repoId: string, scheduleId?:
|
|
|
60
95
|
|
|
61
96
|
function occurrenceResult(occurrence?: ScheduleOccurrence): string | undefined {
|
|
62
97
|
if (!occurrence) return undefined;
|
|
63
|
-
if (occurrence.decision === 'nothing_to_do') return '
|
|
64
|
-
if (occurrence.
|
|
65
|
-
if (occurrence.
|
|
66
|
-
if (occurrence.status === '
|
|
98
|
+
if (occurrence.decision === 'nothing_to_do') return '无变化';
|
|
99
|
+
if (occurrence.decision === 'would_execute') return '预演完成';
|
|
100
|
+
if (occurrence.decision === 'stopped') return '已停止';
|
|
101
|
+
if (occurrence.status === 'failed') return '失败';
|
|
102
|
+
if (occurrence.status === 'succeeded') return '执行成功';
|
|
103
|
+
if (occurrence.status === 'running' || occurrence.status === 'queued' || occurrence.status === 'created') return '运行中';
|
|
104
|
+
if (occurrence.status === 'skipped') return '已跳过';
|
|
67
105
|
return occurrence.status;
|
|
68
106
|
}
|
|
69
107
|
|
|
108
|
+
function occurrenceTone(occurrence: ScheduleOccurrence): ConsoleAutomationHistoryView['tone'] {
|
|
109
|
+
if (occurrence.status === 'failed' || occurrence.decision === 'operation_blocked') return 'red';
|
|
110
|
+
if (occurrence.decision === 'stopped' || occurrence.decision === 'budget_exhausted') return 'amber';
|
|
111
|
+
if (occurrence.status === 'succeeded') return 'green';
|
|
112
|
+
if (occurrence.status === 'running' || occurrence.status === 'queued' || occurrence.status === 'created') return 'blue';
|
|
113
|
+
return 'gray';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function displayOccurrenceReason(reason?: string): string | undefined {
|
|
117
|
+
const value = reason?.trim();
|
|
118
|
+
if (!value) return undefined;
|
|
119
|
+
if (value === 'Browser session keepalive refreshed successfully.') return '登录会话刷新成功,未发现认证问题。';
|
|
120
|
+
if (value === 'Browser watcher baseline recorded; no Controller wake was emitted.') return '已建立观察基线;没有唤醒 ChatGPT。';
|
|
121
|
+
if (value === 'Browser watcher observation is unchanged; no Controller wake was emitted.') return '页面没有变化;保持静默。';
|
|
122
|
+
if (value.startsWith('Browser watcher requires authentication:')) return '目标登录状态已失效,需要重新认证。';
|
|
123
|
+
if (value.startsWith('Work ') && value.includes(' is terminal')) return '绑定的 Work 已完成,自动任务已停止。';
|
|
124
|
+
return value;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function occurrenceHistory(occurrences: ScheduleOccurrence[]): ConsoleAutomationHistoryView[] {
|
|
128
|
+
return occurrences.slice(0, 8).map((occurrence) => ({
|
|
129
|
+
id: occurrence.occurrenceId,
|
|
130
|
+
at: occurrence.updatedAt,
|
|
131
|
+
result: occurrenceResult(occurrence) ?? '已记录',
|
|
132
|
+
tone: occurrenceTone(occurrence),
|
|
133
|
+
reason: displayOccurrenceReason(occurrence.reason),
|
|
134
|
+
trigger: occurrence.triggerContext?.source === 'manual' ? '手动' : occurrence.triggerContext?.source === 'timer' ? '定时' : occurrence.triggerContext?.source,
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function scheduleMode(schedule: RepositorySchedule): ConsoleAutomationMode {
|
|
139
|
+
if (schedule.action.operation === 'external_controller_wake') return 'continuation';
|
|
140
|
+
if (schedule.action.operation === 'chatgpt_browser_prompt') return 'chatgpt_prompt';
|
|
141
|
+
if (schedule.action.operation === 'browser_probe') return schedule.action.arguments?.keepalive_only === true ? 'browser_keepalive' : 'browser_watch';
|
|
142
|
+
return 'schedule';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function modeLabel(mode: ConsoleAutomationMode): string {
|
|
146
|
+
if (mode === 'browser_watch') return '网页变更监听';
|
|
147
|
+
if (mode === 'browser_keepalive') return '登录保活';
|
|
148
|
+
if (mode === 'continuation') return '自动继续 Work';
|
|
149
|
+
if (mode === 'chatgpt_prompt') return 'ChatGPT 自动任务';
|
|
150
|
+
if (mode === 'routine') return '助手例行任务';
|
|
151
|
+
return '自动任务';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function scheduleSummary(mode: ConsoleAutomationMode): string {
|
|
155
|
+
if (mode === 'browser_watch') return '静默观察目标页面;只有内容发生变化时才恢复 ChatGPT。';
|
|
156
|
+
if (mode === 'browser_keepalive') return '静默刷新登录态;正常时不打扰,认证失效时再恢复 ChatGPT。';
|
|
157
|
+
if (mode === 'continuation') return '按计划恢复绑定的 Work,让外部 Controller 继续未完成目标。';
|
|
158
|
+
if (mode === 'chatgpt_prompt') return '由 Forge 定时唤醒 ChatGPT 执行保存的任务提示,不占用 ChatGPT Schedule 名额。';
|
|
159
|
+
return '由 Forge Schedule Engine 管理的持久自动任务。';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function scheduleDelivery(schedule: RepositorySchedule, mode: ConsoleAutomationMode): string | undefined {
|
|
163
|
+
const controller = typeof schedule.action.arguments?.controller_type === 'string' ? schedule.action.arguments.controller_type : 'chatgpt';
|
|
164
|
+
if (mode === 'browser_watch') return `变化时唤醒 ${controller === 'chatgpt' ? 'ChatGPT' : controller}`;
|
|
165
|
+
if (mode === 'browser_keepalive') return `登录失效时唤醒 ${controller === 'chatgpt' ? 'ChatGPT' : controller}`;
|
|
166
|
+
if (mode === 'continuation') return `恢复 ${controller === 'chatgpt' ? 'ChatGPT' : controller}`;
|
|
167
|
+
if (mode === 'chatgpt_prompt') return '发送到 ChatGPT';
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function chatgptExecutionProfile(schedule: RepositorySchedule, mode: ConsoleAutomationMode): { agentModel?: string; reasoningLevel?: string; tabPolicy?: string } {
|
|
172
|
+
if (mode !== 'continuation' && mode !== 'chatgpt_prompt') return {};
|
|
173
|
+
const args = schedule.action.arguments ?? {};
|
|
174
|
+
const controller = typeof args.controller_type === 'string' ? args.controller_type : 'chatgpt';
|
|
175
|
+
if (mode === 'continuation' && controller !== 'chatgpt') return {};
|
|
176
|
+
return {
|
|
177
|
+
agentModel: typeof args.model === 'string' && args.model.trim() ? args.model.trim() : 'gpt-5.6',
|
|
178
|
+
reasoningLevel: typeof args.reasoning === 'string' && args.reasoning.trim() ? args.reasoning.trim() : 'high',
|
|
179
|
+
tabPolicy: typeof args.tab_policy === 'string' && args.tab_policy.trim() ? args.tab_policy.trim() : 'auto',
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function scheduleTarget(schedule: RepositorySchedule, mode: ConsoleAutomationMode): string | undefined {
|
|
184
|
+
const args = schedule.action.arguments ?? {};
|
|
185
|
+
if (typeof args.probe_url === 'string') {
|
|
186
|
+
try { return new URL(args.probe_url).hostname; } catch { return '网页目标'; }
|
|
187
|
+
}
|
|
188
|
+
if (typeof args.probe_session_id === 'string') return mode === 'browser_keepalive' ? '已绑定登录会话' : '已绑定浏览器会话';
|
|
189
|
+
if (typeof args.browser_session_id === 'string') return '已绑定 ChatGPT 会话';
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function observationResult(schedule: RepositorySchedule, fallback?: ScheduleOccurrence): string | undefined {
|
|
194
|
+
switch (schedule.lastObservationStatus) {
|
|
195
|
+
case 'baseline': return '已建立基线';
|
|
196
|
+
case 'unchanged': return '无变化';
|
|
197
|
+
case 'changed': return '检测到变化';
|
|
198
|
+
case 'keepalive': return '登录保持正常';
|
|
199
|
+
case 'auth_required': return '需要重新登录';
|
|
200
|
+
default: return occurrenceResult(fallback);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function scheduleNeedsAttention(schedule: RepositorySchedule): boolean {
|
|
205
|
+
if (schedule.lastObservationStatus === 'auth_required') return true;
|
|
206
|
+
if (schedule.consecutiveFailures >= schedule.policy.maxFailures) return true;
|
|
207
|
+
return Boolean(schedule.pausedReason && /(?:fail|error|auth|login|block|attention|maximum)/i.test(schedule.pausedReason));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function scheduleStatus(schedule: RepositorySchedule): ConsoleAutomationView['status'] {
|
|
211
|
+
if (schedule.enabled) return scheduleNeedsAttention(schedule) ? 'attention' : 'enabled';
|
|
212
|
+
return scheduleNeedsAttention(schedule) ? 'attention' : 'paused';
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function workBinding(controllerHome: string, repoId: string, schedule: RepositorySchedule): { id?: string; objective?: string } {
|
|
216
|
+
const value = schedule.action.arguments?.work_id;
|
|
217
|
+
const workId = typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
218
|
+
if (!workId) return {};
|
|
219
|
+
const work = getWorkContract({ controllerHome, repoId }, workId);
|
|
220
|
+
return { id: workId, objective: work?.objective };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function policySummary(schedule: RepositorySchedule): string {
|
|
224
|
+
return `连续失败 ${schedule.consecutiveFailures}/${schedule.policy.maxFailures} · 冷却 ${schedule.policy.cooldownMinutes} 分钟 · 每日预算 ${schedule.policy.dailyBudgetMinutes} 分钟`;
|
|
225
|
+
}
|
|
226
|
+
|
|
70
227
|
export function listConsoleAutomations(controllerHome: string, repositories: RepositoryRecord[]): ConsoleAutomationView[] {
|
|
71
228
|
const items: ConsoleAutomationView[] = [];
|
|
72
229
|
for (const repository of repositories.filter((entry) => entry.enabled && !entry.removedAt)) {
|
|
@@ -76,22 +233,38 @@ export function listConsoleAutomations(controllerHome: string, repositories: Rep
|
|
|
76
233
|
|
|
77
234
|
for (const schedule of listSchedules(controllerHome, repository.repoId)) {
|
|
78
235
|
if (boundScheduleIds.has(schedule.scheduleId)) continue;
|
|
79
|
-
const
|
|
80
|
-
const
|
|
236
|
+
const occurrences = listOccurrences(controllerHome, repository.repoId, schedule.scheduleId, 8);
|
|
237
|
+
const last = occurrences[0];
|
|
238
|
+
const mode = scheduleMode(schedule);
|
|
239
|
+
const work = workBinding(controllerHome, repository.repoId, schedule);
|
|
240
|
+
const executionProfile = chatgptExecutionProfile(schedule, mode);
|
|
81
241
|
items.push({
|
|
82
242
|
id: schedule.scheduleId,
|
|
83
243
|
source: 'schedule',
|
|
84
244
|
repoId: repository.repoId,
|
|
85
245
|
repositoryName: repository.displayName,
|
|
86
246
|
name: schedule.name,
|
|
87
|
-
summary:
|
|
88
|
-
status,
|
|
247
|
+
summary: scheduleSummary(mode),
|
|
248
|
+
status: scheduleStatus(schedule),
|
|
89
249
|
schedule: triggerLabel(schedule),
|
|
90
250
|
timezone: schedule.trigger.timezone,
|
|
251
|
+
delivery: scheduleDelivery(schedule, mode),
|
|
252
|
+
mode,
|
|
253
|
+
modeLabel: modeLabel(mode),
|
|
254
|
+
live: !schedule.policy.shadowMode,
|
|
255
|
+
targetLabel: scheduleTarget(schedule, mode),
|
|
256
|
+
boundWorkId: work.id,
|
|
257
|
+
boundWorkObjective: work.objective,
|
|
258
|
+
observationStatus: schedule.lastObservationStatus,
|
|
259
|
+
observationAt: schedule.lastObservationAt,
|
|
260
|
+
failureCount: schedule.consecutiveFailures,
|
|
261
|
+
policySummary: policySummary(schedule),
|
|
262
|
+
...executionProfile,
|
|
91
263
|
lastRunAt: last?.updatedAt ?? schedule.lastTriggeredAt,
|
|
92
|
-
lastResult:
|
|
264
|
+
lastResult: observationResult(schedule, last),
|
|
93
265
|
nextRunHint: nextScheduleHint(schedule),
|
|
94
266
|
pausedReason: schedule.pausedReason,
|
|
267
|
+
history: occurrenceHistory(occurrences),
|
|
95
268
|
actions: schedule.enabled ? ['run', 'pause'] : ['resume'],
|
|
96
269
|
});
|
|
97
270
|
}
|
|
@@ -99,8 +272,9 @@ export function listConsoleAutomations(controllerHome: string, repositories: Rep
|
|
|
99
272
|
for (const routine of routines) {
|
|
100
273
|
const binding = bindings.get(routine.routineId);
|
|
101
274
|
const boundSchedule = safeBoundSchedule(controllerHome, repository.repoId, binding?.scheduleId);
|
|
102
|
-
const
|
|
103
|
-
const
|
|
275
|
+
const occurrences = boundSchedule ? listOccurrences(controllerHome, repository.repoId, boundSchedule.scheduleId, 8) : [];
|
|
276
|
+
const last = occurrences[0];
|
|
277
|
+
const scheduleAttention = routine.status === 'enabled' && boundSchedule && scheduleNeedsAttention(boundSchedule);
|
|
104
278
|
const status: ConsoleAutomationView['status'] = scheduleAttention ? 'attention' : routine.status === 'enabled' ? 'enabled' : routine.status === 'paused' ? 'paused' : 'disabled';
|
|
105
279
|
items.push({
|
|
106
280
|
id: routine.routineId,
|
|
@@ -112,11 +286,19 @@ export function listConsoleAutomations(controllerHome: string, repositories: Rep
|
|
|
112
286
|
status,
|
|
113
287
|
schedule: routine.scheduleText,
|
|
114
288
|
timezone: routine.timezone,
|
|
115
|
-
delivery: routine.output === 'assistant_inbox' ? '
|
|
289
|
+
delivery: routine.output === 'assistant_inbox' ? 'ChatGPT 助手收件箱' : routine.output === 'gmail_draft' ? 'Gmail 草稿' : '不外发',
|
|
290
|
+
mode: 'routine',
|
|
291
|
+
modeLabel: modeLabel('routine'),
|
|
292
|
+
live: boundSchedule ? !boundSchedule.policy.shadowMode : undefined,
|
|
293
|
+
observationStatus: boundSchedule?.lastObservationStatus,
|
|
294
|
+
observationAt: boundSchedule?.lastObservationAt,
|
|
295
|
+
failureCount: boundSchedule?.consecutiveFailures,
|
|
296
|
+
policySummary: boundSchedule ? policySummary(boundSchedule) : undefined,
|
|
116
297
|
lastRunAt: last?.updatedAt ?? routine.lastRunAt,
|
|
117
|
-
lastResult: occurrenceResult(last) ?? (routine.lastRunAt ? '
|
|
298
|
+
lastResult: boundSchedule ? observationResult(boundSchedule, last) : occurrenceResult(last) ?? (routine.lastRunAt ? '已触发' : undefined),
|
|
118
299
|
nextRunHint: boundSchedule ? nextScheduleHint(boundSchedule) : routine.nextRunHint,
|
|
119
300
|
pausedReason: boundSchedule?.pausedReason,
|
|
301
|
+
history: occurrenceHistory(occurrences),
|
|
120
302
|
actions: scheduleAttention ? ['resume'] : routine.status === 'enabled' ? ['run', 'pause'] : routine.status === 'paused' ? ['resume'] : [],
|
|
121
303
|
});
|
|
122
304
|
}
|