@moretea-labs/forge 1.7.0 → 1.7.2
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 +26 -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 +125 -101
- 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 +69 -29
- 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 +150 -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 +815 -1015
- 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 +3 -2
- package/src/runtime/root/package-runtime-service.ts +48 -3
- 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
|
@@ -18,6 +18,12 @@ export interface ControllerSessionClaimInput {
|
|
|
18
18
|
controllerInstanceId?: string;
|
|
19
19
|
/** Optional compare-and-swap fence supplied by a recovering caller. */
|
|
20
20
|
expectedClaimGeneration?: number;
|
|
21
|
+
/**
|
|
22
|
+
* Permit a different authenticated controller epoch to recover this Work only
|
|
23
|
+
* after the canonical execution/activity grace has elapsed. Callers must pair
|
|
24
|
+
* this with expectedClaimGeneration from the owner they observed.
|
|
25
|
+
*/
|
|
26
|
+
allowStaleRecovery?: boolean;
|
|
21
27
|
leaseMs?: number;
|
|
22
28
|
}
|
|
23
29
|
|
|
@@ -49,12 +55,39 @@ function write(options: ControllerSessionStoreOptions, store: ControllerSessionS
|
|
|
49
55
|
});
|
|
50
56
|
}
|
|
51
57
|
|
|
52
|
-
const DEFAULT_CONTROLLER_RECOVERY_GRACE_MS =
|
|
58
|
+
const DEFAULT_CONTROLLER_RECOVERY_GRACE_MS = 5 * 60_000;
|
|
53
59
|
const MAX_CONTROLLER_RECOVERY_GRACE_MS = 60 * 60_000;
|
|
60
|
+
const DEFAULT_CONTROLLER_LEASE_MS = 60 * 60_000;
|
|
61
|
+
const MAX_CONTROLLER_LEASE_MS = 60 * 60_000;
|
|
54
62
|
|
|
55
|
-
function
|
|
56
|
-
const
|
|
57
|
-
return
|
|
63
|
+
function optionsNowMs(options: ControllerSessionStoreOptions): number {
|
|
64
|
+
const parsed = Date.parse(now(options));
|
|
65
|
+
return Number.isFinite(parsed) ? parsed : Date.now();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function activeSession(store: ControllerSessionStore, workId: string, atMs: number = Date.now()): ControllerSession | undefined {
|
|
69
|
+
return store.sessions.find((session) => session.workId === workId && Date.parse(session.leaseExpiresAt) > atMs);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function sessionBlocksRecovery(
|
|
73
|
+
options: ControllerSessionStoreOptions,
|
|
74
|
+
owner: ControllerSession,
|
|
75
|
+
input: { nowMs: number; graceMs?: number },
|
|
76
|
+
): boolean {
|
|
77
|
+
const execution = peekExecutionSession(options.controllerHome, owner.sessionId);
|
|
78
|
+
const graceMs = Math.max(60_000, Math.min(input.graceMs ?? DEFAULT_CONTROLLER_RECOVERY_GRACE_MS, MAX_CONTROLLER_RECOVERY_GRACE_MS));
|
|
79
|
+
if (execution?.invalidatedAt) {
|
|
80
|
+
const invalidatedAtMs = Date.parse(execution.invalidatedAt);
|
|
81
|
+
if (!Number.isFinite(invalidatedAtMs)) return true;
|
|
82
|
+
return input.nowMs - invalidatedAtMs < graceMs;
|
|
83
|
+
}
|
|
84
|
+
if (execution?.activeWorkId && execution.activeWorkId !== owner.workId) return false;
|
|
85
|
+
const activityMs = [execution?.lastValidatedAt, execution?.updatedAt, owner.claimedAt]
|
|
86
|
+
.map((value) => value ? Date.parse(value) : Number.NaN)
|
|
87
|
+
.filter(Number.isFinite)
|
|
88
|
+
.reduce((latest, value) => Math.max(latest, value), Number.NEGATIVE_INFINITY);
|
|
89
|
+
if (!Number.isFinite(activityMs)) return false;
|
|
90
|
+
return input.nowMs - activityMs < graceMs;
|
|
58
91
|
}
|
|
59
92
|
|
|
60
93
|
export function controllerSessionBlocksRecovery(
|
|
@@ -62,15 +95,10 @@ export function controllerSessionBlocksRecovery(
|
|
|
62
95
|
workId: string,
|
|
63
96
|
input: { nowMs?: number; graceMs?: number } = {},
|
|
64
97
|
): boolean {
|
|
65
|
-
const owner = activeSession(read(options), workId);
|
|
66
|
-
if (!owner) return false;
|
|
67
|
-
const execution = peekExecutionSession(options.controllerHome, owner.sessionId);
|
|
68
|
-
if (!execution?.invalidatedAt) return true;
|
|
69
|
-
const invalidatedAtMs = Date.parse(execution.invalidatedAt);
|
|
70
|
-
if (!Number.isFinite(invalidatedAtMs)) return true;
|
|
71
98
|
const nowMs = input.nowMs ?? Date.now();
|
|
72
|
-
const
|
|
73
|
-
|
|
99
|
+
const owner = activeSession(read(options), workId, nowMs);
|
|
100
|
+
if (!owner) return false;
|
|
101
|
+
return sessionBlocksRecovery(options, owner, { nowMs, graceMs: input.graceMs });
|
|
74
102
|
}
|
|
75
103
|
|
|
76
104
|
function assertIdentity(input: ControllerSessionClaimInput): void {
|
|
@@ -93,7 +121,7 @@ function claimedSession(
|
|
|
93
121
|
previous?: ControllerSession,
|
|
94
122
|
): ControllerSession {
|
|
95
123
|
const leaseExpiresAt = new Date(
|
|
96
|
-
Date.parse(claimedAt) + Math.max(1_000, Math.min(input.leaseMs ??
|
|
124
|
+
Date.parse(claimedAt) + Math.max(1_000, Math.min(input.leaseMs ?? DEFAULT_CONTROLLER_LEASE_MS, MAX_CONTROLLER_LEASE_MS)),
|
|
97
125
|
).toISOString();
|
|
98
126
|
const previousPrincipal = previous?.principalId?.trim() || previous?.controllerId;
|
|
99
127
|
const inputPrincipal = input.principalId?.trim() || input.controllerId;
|
|
@@ -132,9 +160,10 @@ function persistClaim(
|
|
|
132
160
|
previous?: ControllerSession,
|
|
133
161
|
): ControllerSession {
|
|
134
162
|
const claimedAt = now(options);
|
|
163
|
+
const claimedAtMs = Date.parse(claimedAt);
|
|
135
164
|
const session = claimedSession(options, input, claimedAt, previous);
|
|
136
165
|
const sessions = [
|
|
137
|
-
...store.sessions.filter((entry) => entry.workId !== input.workId && Date.parse(entry.leaseExpiresAt) >
|
|
166
|
+
...store.sessions.filter((entry) => entry.workId !== input.workId && Date.parse(entry.leaseExpiresAt) > claimedAtMs),
|
|
138
167
|
session,
|
|
139
168
|
];
|
|
140
169
|
write(options, { schemaVersion: 1, updatedAt: claimedAt, sessions });
|
|
@@ -142,15 +171,15 @@ function persistClaim(
|
|
|
142
171
|
}
|
|
143
172
|
|
|
144
173
|
export function getControllerSession(options: ControllerSessionStoreOptions, workId: string): ControllerSession | undefined {
|
|
145
|
-
return activeSession(read(options), workId);
|
|
174
|
+
return activeSession(read(options), workId, optionsNowMs(options));
|
|
146
175
|
}
|
|
147
176
|
|
|
148
177
|
export function listControllerSessions(options: ControllerSessionStoreOptions): ControllerSession[] {
|
|
149
|
-
const at =
|
|
178
|
+
const at = optionsNowMs(options);
|
|
150
179
|
return read(options).sessions.filter((session) => Date.parse(session.leaseExpiresAt) > at);
|
|
151
180
|
}
|
|
152
181
|
|
|
153
|
-
export interface
|
|
182
|
+
export interface ControllerOwnershipAuthority {
|
|
154
183
|
controllerId: string;
|
|
155
184
|
controllerType: ControllerType;
|
|
156
185
|
principalId: string;
|
|
@@ -158,6 +187,61 @@ export interface ControllerTerminalizationAuthority {
|
|
|
158
187
|
claimGeneration: number;
|
|
159
188
|
}
|
|
160
189
|
|
|
190
|
+
export type ControllerTerminalizationAuthority = ControllerOwnershipAuthority;
|
|
191
|
+
|
|
192
|
+
export function controllerSessionPrincipalId(
|
|
193
|
+
owner: Pick<ControllerSession, 'principalId' | 'controllerId'>,
|
|
194
|
+
): string {
|
|
195
|
+
return owner.principalId?.trim() || owner.controllerId;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function requireControllerOwnershipAuthority(
|
|
199
|
+
owner: ControllerSession,
|
|
200
|
+
workId: string = owner.workId,
|
|
201
|
+
): ControllerOwnershipAuthority {
|
|
202
|
+
const principalId = controllerSessionPrincipalId(owner);
|
|
203
|
+
const controllerInstanceId = owner.controllerInstanceId?.trim() || '';
|
|
204
|
+
if (!controllerInstanceId) throw new Error(`WORK_CONTROLLER_INSTANCE_MISMATCH: ${workId}`);
|
|
205
|
+
if (typeof owner.claimGeneration !== 'number' || owner.claimGeneration < 1) {
|
|
206
|
+
throw new Error(`WORK_CONTROLLER_CLAIM_GENERATION_REQUIRED: ${workId}`);
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
controllerId: owner.controllerId,
|
|
210
|
+
controllerType: owner.controllerType,
|
|
211
|
+
principalId,
|
|
212
|
+
controllerInstanceId,
|
|
213
|
+
claimGeneration: owner.claimGeneration,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function assertControllerOwnershipAuthority(
|
|
218
|
+
owner: ControllerSession,
|
|
219
|
+
expected: {
|
|
220
|
+
workId?: string;
|
|
221
|
+
controllerId: string;
|
|
222
|
+
principalId: string;
|
|
223
|
+
controllerInstanceId?: string;
|
|
224
|
+
controllerType?: ControllerType;
|
|
225
|
+
},
|
|
226
|
+
): ControllerOwnershipAuthority {
|
|
227
|
+
const workId = expected.workId ?? owner.workId;
|
|
228
|
+
const authority = requireControllerOwnershipAuthority(owner, workId);
|
|
229
|
+
if (authority.controllerId !== expected.controllerId.trim()) {
|
|
230
|
+
throw new Error(`WORK_CONTROLLER_OWNER_MISMATCH: ${workId} is owned by ${owner.controllerId}`);
|
|
231
|
+
}
|
|
232
|
+
if (expected.controllerType && authority.controllerType !== expected.controllerType) {
|
|
233
|
+
throw new Error(`WORK_CONTROLLER_TYPE_MISMATCH: ${workId} is owned by ${owner.controllerType}`);
|
|
234
|
+
}
|
|
235
|
+
if (authority.principalId !== expected.principalId.trim()) {
|
|
236
|
+
throw new Error(`WORK_CONTROLLER_PRINCIPAL_MISMATCH: ${workId}`);
|
|
237
|
+
}
|
|
238
|
+
if (expected.controllerInstanceId !== undefined
|
|
239
|
+
&& authority.controllerInstanceId !== expected.controllerInstanceId.trim()) {
|
|
240
|
+
throw new Error(`WORK_CONTROLLER_INSTANCE_MISMATCH: ${workId}`);
|
|
241
|
+
}
|
|
242
|
+
return authority;
|
|
243
|
+
}
|
|
244
|
+
|
|
161
245
|
export type ControllerTerminalizationFenceReason =
|
|
162
246
|
| 'active_controller_claim'
|
|
163
247
|
| 'stale_controller_authority'
|
|
@@ -167,11 +251,21 @@ export type ControllerTerminalizationFenceResult<T> =
|
|
|
167
251
|
| { allowed: true; value: T; owner?: ControllerSession }
|
|
168
252
|
| { allowed: false; reason: ControllerTerminalizationFenceReason; owner?: ControllerSession };
|
|
169
253
|
|
|
254
|
+
export function controllerTerminalizationAuthorityFromSession(
|
|
255
|
+
owner: ControllerSession,
|
|
256
|
+
): ControllerTerminalizationAuthority | undefined {
|
|
257
|
+
try {
|
|
258
|
+
return requireControllerOwnershipAuthority(owner);
|
|
259
|
+
} catch {
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
170
264
|
function controllerTerminalizationAuthorityMatches(
|
|
171
265
|
owner: ControllerSession,
|
|
172
266
|
authority: ControllerTerminalizationAuthority,
|
|
173
267
|
): boolean {
|
|
174
|
-
const ownerPrincipal = owner
|
|
268
|
+
const ownerPrincipal = controllerSessionPrincipalId(owner);
|
|
175
269
|
const ownerInstanceId = owner.controllerInstanceId?.trim() || '';
|
|
176
270
|
return owner.controllerId === authority.controllerId
|
|
177
271
|
&& owner.controllerType === authority.controllerType
|
|
@@ -206,7 +300,7 @@ export function withControllerSessionTerminalizationFence<T>(
|
|
|
206
300
|
{ scope: 'task', repoId: options.repoId, taskId: `controller-session-${input.workId}` },
|
|
207
301
|
input.actor,
|
|
208
302
|
() => {
|
|
209
|
-
const owner = activeSession(read(options), input.workId);
|
|
303
|
+
const owner = activeSession(read(options), input.workId, optionsNowMs(options));
|
|
210
304
|
if (!owner) {
|
|
211
305
|
if (input.authority) return { allowed: false, reason: 'controller_claim_missing' };
|
|
212
306
|
return { allowed: true, value: operation() };
|
|
@@ -240,7 +334,7 @@ export function releaseControllerSessionWithAuthority(
|
|
|
240
334
|
input.actor,
|
|
241
335
|
() => {
|
|
242
336
|
const store = read(options);
|
|
243
|
-
const owner = activeSession(store, input.workId);
|
|
337
|
+
const owner = activeSession(store, input.workId, optionsNowMs(options));
|
|
244
338
|
if (!owner) return { allowed: false, reason: 'controller_claim_missing' };
|
|
245
339
|
if (!controllerTerminalizationAuthorityMatches(owner, input.authority)) {
|
|
246
340
|
return { allowed: false, reason: 'stale_controller_authority', owner };
|
|
@@ -252,6 +346,19 @@ export function releaseControllerSessionWithAuthority(
|
|
|
252
346
|
);
|
|
253
347
|
}
|
|
254
348
|
|
|
349
|
+
export function releaseObservedControllerSession(
|
|
350
|
+
options: ControllerSessionStoreOptions,
|
|
351
|
+
input: { workId: string; actor: string; owner: ControllerSession },
|
|
352
|
+
): ControllerTerminalizationFenceResult<void> {
|
|
353
|
+
const authority = controllerTerminalizationAuthorityFromSession(input.owner);
|
|
354
|
+
if (!authority) return { allowed: false, reason: 'stale_controller_authority', owner: input.owner };
|
|
355
|
+
return releaseControllerSessionWithAuthority(options, {
|
|
356
|
+
workId: input.workId,
|
|
357
|
+
actor: input.actor,
|
|
358
|
+
authority,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
255
362
|
/**
|
|
256
363
|
* Strict claim used for first ownership acquisition and explicit release/reclaim flows.
|
|
257
364
|
* A live claim with any different controller or transport session remains fenced.
|
|
@@ -268,7 +375,7 @@ export function claimControllerSession(
|
|
|
268
375
|
() => {
|
|
269
376
|
assertWorkClaimable(options, input.workId);
|
|
270
377
|
const store = read(options);
|
|
271
|
-
const current = activeSession(store, input.workId);
|
|
378
|
+
const current = activeSession(store, input.workId, optionsNowMs(options));
|
|
272
379
|
assertExpectedGeneration(input, current);
|
|
273
380
|
if (current && (current.controllerId !== input.controllerId || current.sessionId !== input.sessionId)) {
|
|
274
381
|
throw new Error(`WORK_ALREADY_CLAIMED: ${input.workId} is owned by ${current.controllerId}`);
|
|
@@ -299,15 +406,19 @@ export function resumeControllerSession(
|
|
|
299
406
|
() => {
|
|
300
407
|
assertWorkClaimable(options, input.workId);
|
|
301
408
|
const store = read(options);
|
|
302
|
-
const
|
|
409
|
+
const currentNowMs = optionsNowMs(options);
|
|
410
|
+
const current = activeSession(store, input.workId, currentNowMs);
|
|
303
411
|
assertExpectedGeneration(input, current);
|
|
304
412
|
if (!current) return persistClaim(options, store, input);
|
|
305
413
|
const priorExecution = peekExecutionSession(options.controllerHome, current.sessionId);
|
|
306
414
|
const currentPrincipal = current.principalId?.trim() || priorExecution?.principalId?.trim() || current.controllerId;
|
|
307
|
-
|
|
415
|
+
const staleRecoveryAllowed = input.allowStaleRecovery === true
|
|
416
|
+
&& input.expectedClaimGeneration !== undefined
|
|
417
|
+
&& !sessionBlocksRecovery(options, current, { nowMs: currentNowMs });
|
|
418
|
+
if (currentPrincipal !== input.principalId.trim() && !staleRecoveryAllowed) {
|
|
308
419
|
throw new Error(`WORK_CONTROLLER_PRINCIPAL_MISMATCH: ${input.workId} is owned by another authenticated principal`);
|
|
309
420
|
}
|
|
310
|
-
if (current.controllerId !== input.controllerId) {
|
|
421
|
+
if (current.controllerId !== input.controllerId && !staleRecoveryAllowed) {
|
|
311
422
|
throw new Error(`WORK_ALREADY_CLAIMED: ${input.workId} is owned by ${current.controllerId}`);
|
|
312
423
|
}
|
|
313
424
|
return persistClaim(options, store, input, current);
|
|
@@ -315,6 +426,54 @@ export function resumeControllerSession(
|
|
|
315
426
|
);
|
|
316
427
|
}
|
|
317
428
|
|
|
429
|
+
/**
|
|
430
|
+
* Rebind one active Work to the authenticated Controller transport/Runtime that
|
|
431
|
+
* is serving the current request. Same-instance transport rollover needs only
|
|
432
|
+
* principal/controller proof. A Runtime-instance change additionally requires
|
|
433
|
+
* positive proof that the requested instance is the live canonical Runtime;
|
|
434
|
+
* this prevents a stale old Runtime request from migrating ownership backwards.
|
|
435
|
+
* The observed claim generation is always supplied as a CAS fence.
|
|
436
|
+
*/
|
|
437
|
+
export function bindControllerSessionToCurrentRuntime(
|
|
438
|
+
options: ControllerSessionStoreOptions,
|
|
439
|
+
input: ControllerSessionClaimInput & {
|
|
440
|
+
principalId: string;
|
|
441
|
+
controllerInstanceId: string;
|
|
442
|
+
currentRuntimeInstanceId?: string;
|
|
443
|
+
allowClaimIfMissing?: boolean;
|
|
444
|
+
},
|
|
445
|
+
): ControllerSession {
|
|
446
|
+
assertIdentity(input);
|
|
447
|
+
const current = getControllerSession(options, input.workId);
|
|
448
|
+
if (!current) {
|
|
449
|
+
if (input.allowClaimIfMissing !== true) throw new Error(`WORK_CONTROLLER_OWNER_REQUIRED: ${input.workId}`);
|
|
450
|
+
return claimControllerSession(options, { ...input, expectedClaimGeneration: 0 });
|
|
451
|
+
}
|
|
452
|
+
const currentPrincipal = controllerSessionPrincipalId(current);
|
|
453
|
+
if (current.controllerId !== input.controllerId) {
|
|
454
|
+
throw new Error(`WORK_CONTROLLER_OWNER_MISMATCH: ${input.workId} is owned by ${current.controllerId}`);
|
|
455
|
+
}
|
|
456
|
+
if (current.controllerType !== input.controllerType) {
|
|
457
|
+
throw new Error(`WORK_CONTROLLER_TYPE_MISMATCH: ${input.workId} is owned by ${current.controllerType}`);
|
|
458
|
+
}
|
|
459
|
+
if (currentPrincipal !== input.principalId.trim()) {
|
|
460
|
+
throw new Error(`WORK_CONTROLLER_PRINCIPAL_MISMATCH: ${input.workId}`);
|
|
461
|
+
}
|
|
462
|
+
const ownerInstanceId = current.controllerInstanceId?.trim() || '';
|
|
463
|
+
const requestedInstanceId = input.controllerInstanceId.trim();
|
|
464
|
+
if (ownerInstanceId !== requestedInstanceId) {
|
|
465
|
+
const currentRuntimeInstanceId = input.currentRuntimeInstanceId?.trim() || '';
|
|
466
|
+
if (!currentRuntimeInstanceId || currentRuntimeInstanceId !== requestedInstanceId) {
|
|
467
|
+
throw new Error(`WORK_CONTROLLER_INSTANCE_MISMATCH: ${input.workId}`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if (ownerInstanceId === requestedInstanceId && current.sessionId === input.sessionId) return current;
|
|
471
|
+
return resumeControllerSession(options, {
|
|
472
|
+
...input,
|
|
473
|
+
expectedClaimGeneration: current.claimGeneration,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
318
477
|
export function releaseControllerSession(options: ControllerSessionStoreOptions, workId: string, controllerId: string): void {
|
|
319
478
|
withControllerLock(
|
|
320
479
|
options.controllerHome,
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
type GoalWorkloopStartInput,
|
|
14
14
|
} from './goal-workloop';
|
|
15
15
|
import { getWorkContract } from './work-contract-store';
|
|
16
|
-
import type { CapabilityRisk, FacadeResult, WorkContractConstraints } from './types';
|
|
16
|
+
import type { CapabilityRisk, FacadeResult, WorkContractConstraints, WorkKind } from './types';
|
|
17
17
|
|
|
18
18
|
export {
|
|
19
19
|
continueGoalWorkloop,
|
|
@@ -47,6 +47,18 @@ function accessModeValue(value: unknown): AccessMode | undefined {
|
|
|
47
47
|
return isAccessMode(candidate) ? candidate : undefined;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
function workKindValue(value: unknown): Extract<WorkKind, 'repository_change' | 'completed_no_change' | 'read_only_review' | 'investigation' | 'local_effect' | 'remote_effect' | 'reconciliation'> | undefined {
|
|
51
|
+
return value === 'repository_change'
|
|
52
|
+
|| value === 'completed_no_change'
|
|
53
|
+
|| value === 'read_only_review'
|
|
54
|
+
|| value === 'investigation'
|
|
55
|
+
|| value === 'local_effect'
|
|
56
|
+
|| value === 'remote_effect'
|
|
57
|
+
|| value === 'reconciliation'
|
|
58
|
+
? value
|
|
59
|
+
: undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
50
62
|
function constraintsValue(value: unknown): WorkContractConstraints | undefined {
|
|
51
63
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
|
|
52
64
|
const record = value as Record<string, unknown>;
|
|
@@ -65,6 +77,7 @@ function constraintsValue(value: unknown): WorkContractConstraints | undefined {
|
|
|
65
77
|
? 'isolated'
|
|
66
78
|
: record.workspaceMode === 'auto' || record.workspace_mode === 'auto' ? 'auto' : undefined,
|
|
67
79
|
requireWorktree: booleanValue(record, 'requireWorktree', 'require_worktree'),
|
|
80
|
+
directMainProhibited: booleanValue(record, 'directMainProhibited', 'direct_main_prohibited'),
|
|
68
81
|
};
|
|
69
82
|
return Object.fromEntries(
|
|
70
83
|
Object.entries(constraints).filter(([, entry]) => entry !== undefined),
|
|
@@ -143,6 +156,7 @@ export function runGoalWorkloop(
|
|
|
143
156
|
forbiddenPaths: Array.isArray(args.forbidden_paths) ? args.forbidden_paths.map(String) : undefined,
|
|
144
157
|
checks: Array.isArray(args.check_ids) ? args.check_ids.map(String) : undefined,
|
|
145
158
|
constraints,
|
|
159
|
+
workKind: workKindValue(args.work_kind),
|
|
146
160
|
modeInput: {
|
|
147
161
|
objective: typeof args.objective === 'string' ? args.objective : undefined,
|
|
148
162
|
expectedFiles: typeof args.expected_files === 'number' ? args.expected_files : undefined,
|