@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
|
@@ -274,7 +274,43 @@ export function repositoryGitCommit(controllerHome: string, repository: Reposito
|
|
|
274
274
|
if (diffCheck.status === 0 && input.allowEmpty !== true) {
|
|
275
275
|
return { repoId: repository.repoId, checkoutId: repository.activeCheckoutId, before, stage, after: repositoryGitStatus(repository), committed: false, error: { code: 'GIT_NOTHING_STAGED', message: 'No staged changes to commit. Pass paths to stage, or allow_empty=true for an empty commit.' } };
|
|
276
276
|
}
|
|
277
|
-
const
|
|
277
|
+
const mergeInProgress = runGit(repository, ['rev-parse', '--verify', 'MERGE_HEAD'], 64 * 1024).ok;
|
|
278
|
+
if (mergeInProgress) {
|
|
279
|
+
const unresolved = runGit(repository, ['diff', '--name-only', '--diff-filter=U', '-z'], 128 * 1024);
|
|
280
|
+
const unresolvedPaths = unresolved.stdout.split('\0').map((path) => path.trim()).filter(Boolean);
|
|
281
|
+
if (!unresolved.ok || unresolvedPaths.length > 0) {
|
|
282
|
+
return {
|
|
283
|
+
repoId: repository.repoId,
|
|
284
|
+
checkoutId: repository.activeCheckoutId,
|
|
285
|
+
before,
|
|
286
|
+
stage,
|
|
287
|
+
after: repositoryGitStatus(repository),
|
|
288
|
+
committed: false,
|
|
289
|
+
error: { code: 'GIT_MERGE_UNRESOLVED', message: unresolvedPaths.length > 0 ? `Merge still has unresolved path(s): ${unresolvedPaths.join(', ')}` : unresolved.stderr || 'Unable to inspect unresolved merge paths.' },
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
if (paths.length > 0) {
|
|
293
|
+
const stagedNames = runGit(repository, ['diff', '--cached', '--name-only', '-z'], 128 * 1024);
|
|
294
|
+
if (!stagedNames.ok) {
|
|
295
|
+
return { repoId: repository.repoId, checkoutId: repository.activeCheckoutId, before, stage, after: repositoryGitStatus(repository), committed: false, error: { code: 'GIT_MERGE_STAGED_SCOPE_INSPECTION_FAILED', message: stagedNames.stderr || 'Unable to inspect the merge index.' } };
|
|
296
|
+
}
|
|
297
|
+
const allowed = new Set(paths);
|
|
298
|
+
const stagedPaths = stagedNames.stdout.split('\0').map((path) => path.trim()).filter(Boolean);
|
|
299
|
+
const outsideScope = stagedPaths.filter((path) => !allowed.has(path));
|
|
300
|
+
if (outsideScope.length > 0) {
|
|
301
|
+
return {
|
|
302
|
+
repoId: repository.repoId,
|
|
303
|
+
checkoutId: repository.activeCheckoutId,
|
|
304
|
+
before,
|
|
305
|
+
stage,
|
|
306
|
+
after: repositoryGitStatus(repository),
|
|
307
|
+
committed: false,
|
|
308
|
+
error: { code: 'GIT_MERGE_STAGED_SCOPE_MISMATCH', message: `Resolved merge index contains staged path(s) outside the requested commit scope: ${outsideScope.join(', ')}` },
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const commitArgs = ['commit', '-m', message, ...(input.allowEmpty === true ? ['--allow-empty'] : []), ...(!mergeInProgress && paths.length > 0 ? ['--only', '--', ...paths] : [])];
|
|
278
314
|
const commit = executeRepositoryGitCommand(controllerHome, repository, { args: commitArgs, authorization: 'explicit_user_request', ...input });
|
|
279
315
|
const ok = commit.status === 'executed' && commit.ok === true;
|
|
280
316
|
return {
|
|
@@ -289,12 +325,30 @@ export function repositoryGitCommit(controllerHome: string, repository: Reposito
|
|
|
289
325
|
};
|
|
290
326
|
}
|
|
291
327
|
|
|
292
|
-
export function repositoryGitFinishWorkflow(controllerHome: string, repository: RepositoryRecord, input: { targetBranch?: unknown; featureBranch?: unknown; deleteBranch?: unknown; noFf?: unknown; authorizationDecision?: AuthorizationDecision; sessionId?: string; principalId?: string; workId?: string; goalId?: string }): RepositoryGitFinishResult {
|
|
328
|
+
export function repositoryGitFinishWorkflow(controllerHome: string, repository: RepositoryRecord, input: { targetBranch?: unknown; featureBranch?: unknown; deleteBranch?: unknown; noFf?: unknown; preserveDirtyTargetPaths?: unknown; authorizationDecision?: AuthorizationDecision; sessionId?: string; principalId?: string; workId?: string; goalId?: string }): RepositoryGitFinishResult {
|
|
293
329
|
const before = repositoryGitStatus(repository);
|
|
294
330
|
const featureBranch = assertSafeBranchName(input.featureBranch ?? before.branch);
|
|
295
331
|
const targetBranch = assertSafeBranchName(input.targetBranch ?? repository.defaultBranch ?? 'main');
|
|
296
332
|
const steps: RepositoryGitFinishResult['steps'] = [];
|
|
297
333
|
const alreadyOnTarget = before.branch === targetBranch;
|
|
334
|
+
const requestedPreservedDirtyPaths = normalizePaths(input.preserveDirtyTargetPaths);
|
|
335
|
+
const currentDirtyPaths = [...new Set([...before.staged, ...before.unstaged, ...before.untracked])].sort();
|
|
336
|
+
const preserveDirtyTargetInPlace = alreadyOnTarget && requestedPreservedDirtyPaths.length > 0;
|
|
337
|
+
if (preserveDirtyTargetInPlace) {
|
|
338
|
+
const requested = new Set(requestedPreservedDirtyPaths);
|
|
339
|
+
if (requested.size !== currentDirtyPaths.length || currentDirtyPaths.some((path) => !requested.has(path))) {
|
|
340
|
+
return { repoId: repository.repoId, checkoutId: repository.activeCheckoutId, featureBranch, targetBranch, before, steps, after: before, completed: false, error: { code: 'GIT_DIRTY_TARGET_OWNERSHIP_MISMATCH', message: 'The exact current dirty target paths no longer match the Work-owned preservation set; retry ownership inspection before integration.' } };
|
|
341
|
+
}
|
|
342
|
+
const candidateDiff = runGit(repository, ['diff', '--name-only', `${targetBranch}..${featureBranch}`], 128 * 1024);
|
|
343
|
+
if (!candidateDiff.ok) {
|
|
344
|
+
return { repoId: repository.repoId, checkoutId: repository.activeCheckoutId, featureBranch, targetBranch, before, steps, after: before, completed: false, error: { code: 'GIT_DIRTY_TARGET_PREFLIGHT_FAILED', message: candidateDiff.stderr || 'Unable to inspect candidate paths before dirty-target integration.' } };
|
|
345
|
+
}
|
|
346
|
+
const candidatePaths = new Set(candidateDiff.stdout.split(/\r?\n/).map((path) => path.trim()).filter(Boolean));
|
|
347
|
+
const overlapping = currentDirtyPaths.filter((path) => candidatePaths.has(path));
|
|
348
|
+
if (overlapping.length > 0) {
|
|
349
|
+
return { repoId: repository.repoId, checkoutId: repository.activeCheckoutId, featureBranch, targetBranch, before, steps, after: before, completed: false, error: { code: 'GIT_DIRTY_TARGET_PATH_CONFLICT', message: `Work-owned dirty target path(s) overlap the integration candidate: ${overlapping.join(', ')}` } };
|
|
350
|
+
}
|
|
351
|
+
}
|
|
298
352
|
if (!before.clean && !alreadyOnTarget) {
|
|
299
353
|
return { repoId: repository.repoId, checkoutId: repository.activeCheckoutId, featureBranch, targetBranch, before, steps, after: before, completed: false, error: { code: 'GIT_WORKTREE_NOT_CLEAN', message: 'Commit or revert changes before switching branches to finish a workflow.' } };
|
|
300
354
|
}
|
|
@@ -306,7 +360,7 @@ export function repositoryGitFinishWorkflow(controllerHome: string, repository:
|
|
|
306
360
|
steps.push({ name: 'switch_target', execution: switchTarget });
|
|
307
361
|
if (switchTarget.status !== 'executed' || switchTarget.ok !== true) return { repoId: repository.repoId, checkoutId: repository.activeCheckoutId, featureBranch, targetBranch, before, steps, after: repositoryGitStatus(repository), completed: false, error: { code: 'GIT_SWITCH_TARGET_FAILED', message: switchTarget.stderr || 'git switch failed' } };
|
|
308
362
|
}
|
|
309
|
-
const protectTrackedChanges = alreadyOnTarget && (before.staged.length > 0 || before.unstaged.length > 0);
|
|
363
|
+
const protectTrackedChanges = alreadyOnTarget && !preserveDirtyTargetInPlace && (before.staged.length > 0 || before.unstaged.length > 0);
|
|
310
364
|
const finishAuthorization = 'explicit_user_request' as const;
|
|
311
365
|
let stashRef: string | null = null;
|
|
312
366
|
if (protectTrackedChanges) {
|
|
@@ -733,7 +733,9 @@ export function searchRepositoryMany(
|
|
|
733
733
|
policyDeniedFiles += 1;
|
|
734
734
|
continue;
|
|
735
735
|
}
|
|
736
|
-
const
|
|
736
|
+
const info = statSync(decision.absolutePath);
|
|
737
|
+
if (!info.isFile()) continue;
|
|
738
|
+
const size = info.size;
|
|
737
739
|
if (size > policy.maxFileBytes) {
|
|
738
740
|
skippedLargeFiles += 1;
|
|
739
741
|
continue;
|
|
@@ -780,6 +782,7 @@ type AsyncSearchCandidate =
|
|
|
780
782
|
| { path: string; kind: 'denied' }
|
|
781
783
|
| { path: string; kind: 'large' }
|
|
782
784
|
| { path: string; kind: 'binary' }
|
|
785
|
+
| { path: string; kind: 'nonfile' }
|
|
783
786
|
| { path: string; kind: 'source'; bytes: Buffer }
|
|
784
787
|
| { path: string; kind: 'error'; error: unknown };
|
|
785
788
|
|
|
@@ -792,7 +795,9 @@ async function readSearchCandidateAsync(
|
|
|
792
795
|
try {
|
|
793
796
|
const decision = resolveMcpPath(repoRoot, path, policy, 'read', canonicalRepoRoot);
|
|
794
797
|
if (!decision.ok || !decision.absolutePath) return { path, kind: 'denied' };
|
|
795
|
-
const
|
|
798
|
+
const info = await statAsync(decision.absolutePath);
|
|
799
|
+
if (!info.isFile()) return { path, kind: 'nonfile' };
|
|
800
|
+
const size = info.size;
|
|
796
801
|
if (size > policy.maxFileBytes) return { path, kind: 'large' };
|
|
797
802
|
const bytes = await readFileAsync(decision.absolutePath);
|
|
798
803
|
if (binary(bytes)) return { path, kind: 'binary' };
|
|
@@ -872,6 +877,7 @@ export async function searchRepositoryManyAsync(
|
|
|
872
877
|
skippedBinaryFiles += 1;
|
|
873
878
|
continue;
|
|
874
879
|
}
|
|
880
|
+
if (candidate.kind === 'nonfile') continue;
|
|
875
881
|
if (candidate.kind === 'error') throw candidate.error;
|
|
876
882
|
scannedFiles += 1;
|
|
877
883
|
matchSearchManySource(matchState, candidate.path, candidate.bytes);
|
|
@@ -950,7 +956,9 @@ export function searchRepository(repoRoot: string, policy: McpPolicy, opts: Sear
|
|
|
950
956
|
policyDeniedFiles += 1;
|
|
951
957
|
continue;
|
|
952
958
|
}
|
|
953
|
-
const
|
|
959
|
+
const info = statSync(decision.absolutePath);
|
|
960
|
+
if (!info.isFile()) continue;
|
|
961
|
+
const size = info.size;
|
|
954
962
|
if (size > policy.maxFileBytes) {
|
|
955
963
|
skippedLargeFiles += 1;
|
|
956
964
|
continue;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export type SemanticNavigationKind = 'definition' | 'references' | 'implementations';
|
|
2
|
+
|
|
3
|
+
export interface SemanticNavigationRequest {
|
|
4
|
+
navigation: SemanticNavigationKind;
|
|
5
|
+
path: string;
|
|
6
|
+
line: number;
|
|
7
|
+
column: number;
|
|
8
|
+
tsconfigPath?: string;
|
|
9
|
+
language?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface SemanticNavigationLocation {
|
|
13
|
+
path: string;
|
|
14
|
+
line: number;
|
|
15
|
+
column: number;
|
|
16
|
+
name?: string;
|
|
17
|
+
kind?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SemanticNavigationResult {
|
|
21
|
+
providerId: string;
|
|
22
|
+
providerIdentity?: string;
|
|
23
|
+
language: string;
|
|
24
|
+
navigation: SemanticNavigationKind;
|
|
25
|
+
target: { path: string; line: number; column: number };
|
|
26
|
+
locations: SemanticNavigationLocation[];
|
|
27
|
+
policyDeniedReads?: number;
|
|
28
|
+
details?: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type SemanticNavigationOutcome =
|
|
32
|
+
| { ok: true; result: SemanticNavigationResult }
|
|
33
|
+
| { ok: false; code: string; message: string };
|
|
34
|
+
|
|
35
|
+
export interface SemanticNavigationAccess {
|
|
36
|
+
cacheScope: string;
|
|
37
|
+
sourceIdentity?: string;
|
|
38
|
+
profile: string;
|
|
39
|
+
allowRepositoryPath(relativePath: string): boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface SemanticNavigationProvider {
|
|
43
|
+
id: string;
|
|
44
|
+
languages: readonly string[];
|
|
45
|
+
supports(request: SemanticNavigationRequest): boolean;
|
|
46
|
+
navigate(
|
|
47
|
+
repoRoot: string,
|
|
48
|
+
requests: SemanticNavigationRequest[],
|
|
49
|
+
access: SemanticNavigationAccess,
|
|
50
|
+
): Promise<SemanticNavigationOutcome[]>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface IndexedSemanticNavigationRequest {
|
|
54
|
+
index: number;
|
|
55
|
+
request: SemanticNavigationRequest;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface IndexedSemanticNavigationOutcome {
|
|
59
|
+
index: number;
|
|
60
|
+
outcome: SemanticNavigationOutcome;
|
|
61
|
+
}
|
|
@@ -3,67 +3,15 @@ import { navigateTypeScriptSymbol } from './typescript-navigation';
|
|
|
3
3
|
import { navigateSwiftSymbols } from './swift-navigation';
|
|
4
4
|
import { GenericLspSemanticProvider, type GenericLspProviderDescriptor } from './generic-lsp-provider';
|
|
5
5
|
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface SemanticNavigationLocation {
|
|
18
|
-
path: string;
|
|
19
|
-
line: number;
|
|
20
|
-
column: number;
|
|
21
|
-
name?: string;
|
|
22
|
-
kind?: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface SemanticNavigationResult {
|
|
26
|
-
providerId: string;
|
|
27
|
-
providerIdentity?: string;
|
|
28
|
-
language: string;
|
|
29
|
-
navigation: SemanticNavigationKind;
|
|
30
|
-
target: { path: string; line: number; column: number };
|
|
31
|
-
locations: SemanticNavigationLocation[];
|
|
32
|
-
policyDeniedReads?: number;
|
|
33
|
-
details?: Record<string, unknown>;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export type SemanticNavigationOutcome =
|
|
37
|
-
| { ok: true; result: SemanticNavigationResult }
|
|
38
|
-
| { ok: false; code: string; message: string };
|
|
39
|
-
|
|
40
|
-
export interface SemanticNavigationAccess {
|
|
41
|
-
cacheScope: string;
|
|
42
|
-
sourceIdentity?: string;
|
|
43
|
-
profile: string;
|
|
44
|
-
allowRepositoryPath(relativePath: string): boolean;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export interface SemanticNavigationProvider {
|
|
48
|
-
id: string;
|
|
49
|
-
languages: readonly string[];
|
|
50
|
-
supports(request: SemanticNavigationRequest): boolean;
|
|
51
|
-
navigate(
|
|
52
|
-
repoRoot: string,
|
|
53
|
-
requests: SemanticNavigationRequest[],
|
|
54
|
-
access: SemanticNavigationAccess,
|
|
55
|
-
): Promise<SemanticNavigationOutcome[]>;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export interface IndexedSemanticNavigationRequest {
|
|
59
|
-
index: number;
|
|
60
|
-
request: SemanticNavigationRequest;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export interface IndexedSemanticNavigationOutcome {
|
|
64
|
-
index: number;
|
|
65
|
-
outcome: SemanticNavigationOutcome;
|
|
66
|
-
}
|
|
6
|
+
export * from './semantic-navigation-contract';
|
|
7
|
+
import type {
|
|
8
|
+
IndexedSemanticNavigationOutcome,
|
|
9
|
+
IndexedSemanticNavigationRequest,
|
|
10
|
+
SemanticNavigationAccess,
|
|
11
|
+
SemanticNavigationOutcome,
|
|
12
|
+
SemanticNavigationProvider,
|
|
13
|
+
SemanticNavigationRequest,
|
|
14
|
+
} from './semantic-navigation-contract';
|
|
67
15
|
|
|
68
16
|
function extensionMatches(path: string, values: readonly string[]): boolean {
|
|
69
17
|
return values.includes(extname(path).toLowerCase());
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { resolve } from 'path';
|
|
2
4
|
import { getEditSession, listEditSessions, type EditSession } from '../../../cli/editing/edit-session';
|
|
3
|
-
import { globMatches } from '../../../cli/mcp/paths';
|
|
4
5
|
import { runProcess } from '../../../effects/process-runner';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
6
|
+
import { getWorkContract, updateWorkContract } from '../facade/work-contract-store';
|
|
7
|
+
import { completeWorkWithReceipt } from './work-completion-authority';
|
|
7
8
|
import { isDirectEditWorkCompletionReceipt, isTerminalWorkContractStatus, type DirectEditWorkCompletionReceipt, type WorkReconciliationRecord } from '../facade/types';
|
|
8
9
|
import { historicalVerificationEvidenceAtRevision } from './verification-evidence';
|
|
9
10
|
import { readWorkHandle } from './work-handle-store';
|
|
11
|
+
import { assertWorkPathsWithinScope, findWorkPathScopeViolation } from './work-path-scope';
|
|
10
12
|
|
|
11
13
|
export interface DirectEditWorkCompletionReconciliation {
|
|
12
14
|
completedWorkIds: string[];
|
|
@@ -102,6 +104,15 @@ export function reconcileFinalizedDirectEditWorksAfterCommit(input: {
|
|
|
102
104
|
skipped.push({ sessionId: session.sessionId, workId: session.workId, reason: 'configured_checks_not_passed' });
|
|
103
105
|
continue;
|
|
104
106
|
}
|
|
107
|
+
const scopeViolation = findWorkPathScopeViolation(work, paths);
|
|
108
|
+
if (scopeViolation) {
|
|
109
|
+
skipped.push({
|
|
110
|
+
sessionId: session.sessionId,
|
|
111
|
+
workId: session.workId,
|
|
112
|
+
reason: `${scopeViolation.kind === 'forbidden' ? 'work_scope_forbidden' : 'work_scope_out_of_scope'}:${scopeViolation.path}`,
|
|
113
|
+
});
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
105
116
|
const status = git(input.repoRoot, ['status', '--porcelain=v1', '--', ...paths]);
|
|
106
117
|
if (!status.ok || status.stdout.trim()) {
|
|
107
118
|
skipped.push({ sessionId: session.sessionId, workId: session.workId, reason: 'owned_paths_not_clean' });
|
|
@@ -141,7 +152,7 @@ export function reconcileFinalizedDirectEditWorksAfterCommit(input: {
|
|
|
141
152
|
verifiedAt: session.verifiedAt ?? session.finalizedAt ?? recordedAt,
|
|
142
153
|
recordedAt,
|
|
143
154
|
};
|
|
144
|
-
|
|
155
|
+
completeWorkWithReceipt(
|
|
145
156
|
{ controllerHome: input.controllerHome, repoId: input.repoId },
|
|
146
157
|
session.workId,
|
|
147
158
|
receipt,
|
|
@@ -175,6 +186,21 @@ function exactCommit(repoRoot: string, revision: string, label: string): string
|
|
|
175
186
|
return result.stdout.trim();
|
|
176
187
|
}
|
|
177
188
|
|
|
189
|
+
function assertRemoteContainmentIfApplicable(repoRoot: string, targetBranch: string, targetRevision: string): void {
|
|
190
|
+
const remoteResult = git(repoRoot, ['config', '--get', `branch.${targetBranch}.remote`]);
|
|
191
|
+
const remote = remoteResult.ok ? remoteResult.stdout.trim() : '';
|
|
192
|
+
if (!remote || remote === '.') return;
|
|
193
|
+
const mergeResult = git(repoRoot, ['config', '--get', `branch.${targetBranch}.merge`]);
|
|
194
|
+
const mergeRef = mergeResult.ok ? mergeResult.stdout.trim() : '';
|
|
195
|
+
if (!mergeRef.startsWith('refs/heads/')) throw new Error('DIRECT_EDIT_WORK_RECONCILIATION_REMOTE_TRACKING_REQUIRED');
|
|
196
|
+
const remoteRef = `refs/remotes/${remote}/${mergeRef.slice('refs/heads/'.length)}`;
|
|
197
|
+
const remoteRevision = git(repoRoot, ['rev-parse', '--verify', `${remoteRef}^{commit}`]);
|
|
198
|
+
if (!remoteRevision.ok || !remoteRevision.stdout.trim()) throw new Error('DIRECT_EDIT_WORK_RECONCILIATION_REMOTE_TRACKING_REQUIRED');
|
|
199
|
+
if (!git(repoRoot, ['merge-base', '--is-ancestor', targetRevision, remoteRevision.stdout.trim()]).ok) {
|
|
200
|
+
throw new Error('DIRECT_EDIT_WORK_RECONCILIATION_REMOTE_CONTAINMENT_REQUIRED');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
178
204
|
function comparedRevisionPaths(repoRoot: string, baseRevision: string, targetRevision: string): string[] {
|
|
179
205
|
const result = git(repoRoot, ['diff', '--name-only', '-z', baseRevision, targetRevision]);
|
|
180
206
|
if (!result.ok) throw new Error('DIRECT_EDIT_WORK_RECONCILIATION_CHANGED_PATHS_UNAVAILABLE');
|
|
@@ -189,6 +215,20 @@ function normalizedComparedPaths(paths: string[]): string[] {
|
|
|
189
215
|
return normalized;
|
|
190
216
|
}
|
|
191
217
|
|
|
218
|
+
function unresolvedRepositorySourcePaths(repoRoot: string): string[] {
|
|
219
|
+
const tracked = [
|
|
220
|
+
git(repoRoot, ['diff', '--name-only', '-z']),
|
|
221
|
+
git(repoRoot, ['diff', '--cached', '--name-only', '-z']),
|
|
222
|
+
];
|
|
223
|
+
if (tracked.some((result) => !result.ok)) throw new Error('DIRECT_EDIT_WORK_RECONCILIATION_EFFECT_SOURCE_STATUS_UNAVAILABLE');
|
|
224
|
+
const untracked = git(repoRoot, ['ls-files', '--others', '--exclude-standard', '-z']);
|
|
225
|
+
if (!untracked.ok) throw new Error('DIRECT_EDIT_WORK_RECONCILIATION_EFFECT_SOURCE_STATUS_UNAVAILABLE');
|
|
226
|
+
return [...new Set([
|
|
227
|
+
...tracked.flatMap((result) => result.stdout.split('\0').filter(Boolean)),
|
|
228
|
+
...untracked.stdout.split('\0').filter((path) => path && !path.startsWith('.ai/harness/')),
|
|
229
|
+
])].sort((left, right) => left.localeCompare(right));
|
|
230
|
+
}
|
|
231
|
+
|
|
192
232
|
/**
|
|
193
233
|
* Explicitly close a historically delivered Direct Edit Work whose original
|
|
194
234
|
* edit-session binding was missed. This is never inferred: the caller must
|
|
@@ -210,18 +250,24 @@ export function acceptReviewedDirectEditWorkReconciliation(input: ReviewedDirect
|
|
|
210
250
|
}
|
|
211
251
|
throw new Error(`DIRECT_EDIT_WORK_RECONCILIATION_WORK_TERMINAL: ${input.workId}`);
|
|
212
252
|
}
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
: undefined;
|
|
253
|
+
const currentHandle = readWorkHandle(input.controllerHome, input.repoId, input.workId);
|
|
254
|
+
const failedHandle = work.status === 'failed' ? currentHandle : undefined;
|
|
216
255
|
const failedReviewedRecovery = work.workKind === 'repository_change'
|
|
217
256
|
&& work.status === 'failed'
|
|
218
257
|
&& failedHandle?.managedWorktree === false
|
|
219
258
|
&& failedHandle.state === 'failed'
|
|
220
259
|
&& failedHandle.finalization.validation === 'failed'
|
|
221
260
|
&& String(failedHandle.finalization.lastError ?? failedHandle.failureReason ?? '').includes('WORK_HANDLE_HEAD_CHANGED');
|
|
222
|
-
|
|
261
|
+
const historicalEffectRecovery = !currentHandle
|
|
262
|
+
&& !isTerminalWorkContractStatus(work.status)
|
|
263
|
+
&& (work.workKind === 'local_effect' || work.workKind === 'remote_effect');
|
|
264
|
+
if ((isTerminalWorkContractStatus(work.status) && !failedReviewedRecovery)
|
|
265
|
+
|| (work.workKind !== 'repository_change' && !historicalEffectRecovery)) {
|
|
223
266
|
throw new Error(`DIRECT_EDIT_WORK_RECONCILIATION_WORK_NOT_ELIGIBLE: ${input.workId}`);
|
|
224
267
|
}
|
|
268
|
+
if (historicalEffectRecovery && work.checks.length === 0) {
|
|
269
|
+
throw new Error('DIRECT_EDIT_WORK_RECONCILIATION_CHECK_EVIDENCE_REQUIRED');
|
|
270
|
+
}
|
|
225
271
|
if (!work.baseRevision?.trim()) throw new Error('DIRECT_EDIT_WORK_RECONCILIATION_BASE_REVISION_MISSING');
|
|
226
272
|
|
|
227
273
|
const baseRevision = exactCommit(input.repoRoot, work.baseRevision, 'BASE_REVISION');
|
|
@@ -234,6 +280,7 @@ export function acceptReviewedDirectEditWorkReconciliation(input: ReviewedDirect
|
|
|
234
280
|
if (!git(input.repoRoot, ['merge-base', '--is-ancestor', targetRevision, targetBranch]).ok) {
|
|
235
281
|
throw new Error('DIRECT_EDIT_WORK_RECONCILIATION_TARGET_UNREACHABLE');
|
|
236
282
|
}
|
|
283
|
+
assertRemoteContainmentIfApplicable(input.repoRoot, targetBranch, targetRevision);
|
|
237
284
|
|
|
238
285
|
const comparedPaths = normalizedComparedPaths(input.comparedPaths);
|
|
239
286
|
let comparisonBaseRevision = baseRevision;
|
|
@@ -248,12 +295,21 @@ export function acceptReviewedDirectEditWorkReconciliation(input: ReviewedDirect
|
|
|
248
295
|
if (comparedPaths.length !== actualPaths.length || comparedPaths.some((path, index) => path !== actualPaths[index])) {
|
|
249
296
|
throw new Error('DIRECT_EDIT_WORK_RECONCILIATION_PATH_COMPARISON_MISMATCH');
|
|
250
297
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
298
|
+
assertWorkPathsWithinScope(work, comparedPaths, {
|
|
299
|
+
forbidden: 'DIRECT_EDIT_WORK_RECONCILIATION_FORBIDDEN_PATH',
|
|
300
|
+
outOfScope: 'DIRECT_EDIT_WORK_RECONCILIATION_PATH_OUT_OF_SCOPE',
|
|
301
|
+
});
|
|
255
302
|
const dirty = git(input.repoRoot, ['status', '--porcelain=v1', '--', ...comparedPaths]);
|
|
256
303
|
if (!dirty.ok || dirty.stdout.trim()) throw new Error('DIRECT_EDIT_WORK_RECONCILIATION_OWNED_PATHS_DIRTY');
|
|
304
|
+
if (historicalEffectRecovery) {
|
|
305
|
+
const separateOwnedWorktree = Boolean(work.worktreeRef?.trim())
|
|
306
|
+
&& resolve(work.worktreeRef!) !== resolve(input.repoRoot)
|
|
307
|
+
&& existsSync(work.worktreeRef!);
|
|
308
|
+
if (separateOwnedWorktree) throw new Error('DIRECT_EDIT_WORK_RECONCILIATION_EFFECT_CLEANUP_REQUIRED');
|
|
309
|
+
if (unresolvedRepositorySourcePaths(input.repoRoot).length > 0) {
|
|
310
|
+
throw new Error('DIRECT_EDIT_WORK_RECONCILIATION_EFFECT_SOURCE_DELTA_UNRESOLVED');
|
|
311
|
+
}
|
|
312
|
+
}
|
|
257
313
|
|
|
258
314
|
let verifiedAt = new Date().toISOString();
|
|
259
315
|
for (const checkId of work.checks) {
|
|
@@ -318,28 +374,12 @@ export function acceptReviewedDirectEditWorkReconciliation(input: ReviewedDirect
|
|
|
318
374
|
verifiedAt,
|
|
319
375
|
recordedAt,
|
|
320
376
|
};
|
|
321
|
-
|
|
377
|
+
completeWorkWithReceipt(
|
|
322
378
|
{ controllerHome: input.controllerHome, repoId: input.repoId },
|
|
323
379
|
input.workId,
|
|
324
380
|
receipt,
|
|
325
381
|
'completed_changed',
|
|
326
382
|
'repository_change',
|
|
327
383
|
);
|
|
328
|
-
if (recorded.requirementId) {
|
|
329
|
-
try {
|
|
330
|
-
completeRequirementFromWork({ controllerHome: input.controllerHome }, { requirementId: recorded.requirementId, work: recorded });
|
|
331
|
-
} catch (error) {
|
|
332
|
-
try {
|
|
333
|
-
appendWorkEvidence({ controllerHome: input.controllerHome, repoId: input.repoId }, input.workId, {
|
|
334
|
-
title: 'requirement completion projection pending',
|
|
335
|
-
summary: `Work completion remains authoritative; Requirement projection could not be applied: ${error instanceof Error ? error.message : String(error)}`.slice(0, 2_000),
|
|
336
|
-
detailLevel: 'summary',
|
|
337
|
-
});
|
|
338
|
-
} catch {
|
|
339
|
-
// Reconciliation already recorded the terminal receipt. A diagnostic
|
|
340
|
-
// write failure cannot make that completed Work non-terminal again.
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
384
|
return { workId: input.workId, reconciliation, receipt };
|
|
345
385
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { readExecutionSession } from './session-store';
|
|
2
|
+
import { controllerSessionPrincipalId, getControllerSession, listControllerSessions } from '../facade/controller-session-store';
|
|
3
|
+
import { getWorkContract } from '../facade/work-contract-store';
|
|
4
|
+
import { isTerminalWorkContractStatus } from '../facade/types';
|
|
5
|
+
|
|
6
|
+
export interface RepositoryWorkAttributionCaller {
|
|
7
|
+
sessionId?: string;
|
|
8
|
+
principalId?: string;
|
|
9
|
+
controllerInstanceId?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface RepositoryWorkAttributionTarget {
|
|
13
|
+
repoId: string;
|
|
14
|
+
activeCheckoutId: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function resolveExplicitClaimedRepositoryWork(
|
|
18
|
+
controllerHome: string,
|
|
19
|
+
target: RepositoryWorkAttributionTarget,
|
|
20
|
+
caller: RepositoryWorkAttributionCaller | undefined,
|
|
21
|
+
workId: string,
|
|
22
|
+
) {
|
|
23
|
+
if (!caller?.principalId?.trim()) return undefined;
|
|
24
|
+
const work = getWorkContract({ controllerHome, repoId: target.repoId }, workId);
|
|
25
|
+
if (!work || isTerminalWorkContractStatus(work.status)) throw new Error(`WORK_ATTRIBUTION_INVALID: ${workId}`);
|
|
26
|
+
const owner = getControllerSession({ controllerHome, repoId: target.repoId }, workId);
|
|
27
|
+
if (!owner) throw new Error(`WORK_CONTROLLER_CLAIM_REQUIRED: ${workId}`);
|
|
28
|
+
if (controllerSessionPrincipalId(owner) !== caller.principalId.trim()) {
|
|
29
|
+
throw new Error(`WORK_CONTROLLER_OWNERSHIP_MISMATCH: ${workId}`);
|
|
30
|
+
}
|
|
31
|
+
if (work.checkoutId && work.checkoutId !== target.activeCheckoutId) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`WORK_CHECKOUT_MISMATCH: work=${workId}; resolved_checkout=${target.activeCheckoutId}; expected_work_checkout=${work.checkoutId}; retry with checkout_id=${work.checkoutId} and the same work_id`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return work;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function assertNoBoundExecutionSessionMutation(
|
|
40
|
+
controllerHome: string,
|
|
41
|
+
target: RepositoryWorkAttributionTarget,
|
|
42
|
+
caller?: RepositoryWorkAttributionCaller,
|
|
43
|
+
): void {
|
|
44
|
+
if (!caller?.principalId?.trim() || !caller.sessionId?.trim()) return;
|
|
45
|
+
const executionSession = readExecutionSession(controllerHome, {
|
|
46
|
+
sessionId: caller.sessionId,
|
|
47
|
+
principalId: caller.principalId,
|
|
48
|
+
controllerInstanceId: caller.controllerInstanceId,
|
|
49
|
+
});
|
|
50
|
+
const workId = executionSession?.activeWorkId?.trim();
|
|
51
|
+
if (!workId || executionSession?.activeRepositoryId !== target.repoId) return;
|
|
52
|
+
const work = getWorkContract({ controllerHome, repoId: target.repoId }, workId);
|
|
53
|
+
if (!work) throw new Error(`WORK_ATTRIBUTION_INVALID: ${workId}`);
|
|
54
|
+
if (isTerminalWorkContractStatus(work.status)) {
|
|
55
|
+
throw new Error(`WORK_ATTRIBUTION_TERMINAL: ${work.workId}:${work.status}`);
|
|
56
|
+
}
|
|
57
|
+
throw new Error(`WORK_ATTRIBUTION_REQUIRED: ${work.workId}; active execution session mutations must pass work_id explicitly`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function resolveClaimedRepositoryWorkId(
|
|
61
|
+
controllerHome: string,
|
|
62
|
+
target: RepositoryWorkAttributionTarget,
|
|
63
|
+
caller?: RepositoryWorkAttributionCaller,
|
|
64
|
+
explicitWorkId?: unknown,
|
|
65
|
+
): string | undefined {
|
|
66
|
+
const requestedWorkId = typeof explicitWorkId === 'string' ? explicitWorkId.trim() : '';
|
|
67
|
+
if (requestedWorkId) {
|
|
68
|
+
const work = resolveExplicitClaimedRepositoryWork(controllerHome, target, caller, requestedWorkId);
|
|
69
|
+
return work?.workId;
|
|
70
|
+
}
|
|
71
|
+
if (!caller?.principalId?.trim()) return undefined;
|
|
72
|
+
if (caller.sessionId?.trim()) {
|
|
73
|
+
const executionSession = readExecutionSession(controllerHome, {
|
|
74
|
+
sessionId: caller.sessionId,
|
|
75
|
+
principalId: caller.principalId,
|
|
76
|
+
controllerInstanceId: caller.controllerInstanceId,
|
|
77
|
+
});
|
|
78
|
+
const workId = executionSession?.activeWorkId?.trim();
|
|
79
|
+
if (workId
|
|
80
|
+
&& executionSession?.activeRepositoryId === target.repoId
|
|
81
|
+
&& (!executionSession.activeCheckoutId || executionSession.activeCheckoutId === target.activeCheckoutId)) {
|
|
82
|
+
const work = getWorkContract({ controllerHome, repoId: target.repoId }, workId);
|
|
83
|
+
if (work && isTerminalWorkContractStatus(work.status)) {
|
|
84
|
+
throw new Error(`WORK_ATTRIBUTION_TERMINAL: ${work.workId}:${work.status}`);
|
|
85
|
+
}
|
|
86
|
+
const owner = getControllerSession({ controllerHome, repoId: target.repoId }, workId);
|
|
87
|
+
if (work && owner?.sessionId === caller.sessionId && controllerSessionPrincipalId(owner) === caller.principalId.trim()) {
|
|
88
|
+
return workId;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const principal = caller.principalId.trim();
|
|
93
|
+
const candidates = listControllerSessions({ controllerHome, repoId: target.repoId })
|
|
94
|
+
.filter((owner) => controllerSessionPrincipalId(owner) === principal)
|
|
95
|
+
.map((owner) => ({ owner, work: getWorkContract({ controllerHome, repoId: target.repoId }, owner.workId) }))
|
|
96
|
+
.filter((entry): entry is { owner: ReturnType<typeof listControllerSessions>[number]; work: NonNullable<ReturnType<typeof getWorkContract>> } => Boolean(
|
|
97
|
+
entry.work
|
|
98
|
+
&& !isTerminalWorkContractStatus(entry.work.status)
|
|
99
|
+
&& (!entry.work.checkoutId || entry.work.checkoutId === target.activeCheckoutId),
|
|
100
|
+
));
|
|
101
|
+
if (candidates.length === 1) return candidates[0].work.workId;
|
|
102
|
+
if (candidates.length > 1) {
|
|
103
|
+
throw new Error(`WORK_ATTRIBUTION_AMBIGUOUS: principal ${principal} owns ${candidates.length} active Works on checkout ${target.activeCheckoutId}`);
|
|
104
|
+
}
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|