@moretea-labs/forge 1.7.0 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/AGENTS.md +2 -0
  2. package/CHANGELOG.md +13 -0
  3. package/CLAUDE.md +2 -0
  4. package/THIRD_PARTY_NOTICES.md +1 -1
  5. package/agents/openai.yaml +1 -1
  6. package/assets/partials-agents/02-operating-mode.partial.md +2 -0
  7. package/assets/plugin-registry.v1.json +3 -3
  8. package/assets/skill-version.json +2 -2
  9. package/assets/templates/helpers/check-architecture-sync.sh +17 -0
  10. package/assets/templates/helpers/inspect-project-state.ts +8 -6
  11. package/assets/templates/helpers/migrate-workflow-docs.ts +9 -7
  12. package/assets/templates/helpers/workflow-contract.ts +5 -1
  13. package/docs/architecture/CURRENT.md +12 -4
  14. package/docs/forge-plugin-management.md +1 -1
  15. package/docs/operations/controller-performance-and-502.md +19 -0
  16. package/docs/operations/homebrew.md +2 -2
  17. package/package.json +5 -2
  18. package/scripts/architecture-event.ts +2 -820
  19. package/scripts/architecture-queue.sh +2 -645
  20. package/scripts/archive-architecture-request.sh +2 -242
  21. package/scripts/archive-workflow.sh +1 -291
  22. package/scripts/benchmark-work-efficiency.ts +0 -0
  23. package/scripts/capability-config.ts +2 -392
  24. package/scripts/capability-resolver.ts +2 -503
  25. package/scripts/capture-plan.sh +1 -332
  26. package/scripts/check-agent-tooling.sh +2 -1531
  27. package/scripts/check-architecture-sync.sh +2 -302
  28. package/scripts/check-brain-manifest.sh +2 -253
  29. package/scripts/check-context-files.sh +2 -47
  30. package/scripts/check-deploy-sql-order.sh +2 -115
  31. package/scripts/check-runtime-architecture.mjs +243 -16
  32. package/scripts/check-skill-version.ts +3 -191
  33. package/scripts/check-source-duplication.mjs +61 -0
  34. package/scripts/check-stable-baseline.ts +0 -0
  35. package/scripts/check-task-sync.sh +2 -63
  36. package/scripts/check-task-workflow.sh +2 -1000
  37. package/scripts/codex-handoff-resume.sh +2 -267
  38. package/scripts/context-contract-sync.sh +2 -514
  39. package/scripts/contract-run.ts +2 -463
  40. package/scripts/contract-worktree.sh +1 -702
  41. package/scripts/ensure-task-workflow.sh +1 -1192
  42. package/scripts/inspect-project-state.ts +4 -279
  43. package/scripts/lib/project-init-lib.sh +7 -2
  44. package/scripts/lib/run-forge-helper.sh +19 -0
  45. package/scripts/lib/run-forge-helper.ts +26 -0
  46. package/scripts/migrate-project-template.sh +1 -33
  47. package/scripts/migrate-workflow-docs.ts +4 -412
  48. package/scripts/new-plan.sh +1 -180
  49. package/scripts/new-spec.sh +1 -47
  50. package/scripts/new-sprint.sh +1 -46
  51. package/scripts/plan-to-todo.sh +1 -771
  52. package/scripts/prepare-codex-handoff.sh +2 -201
  53. package/scripts/prepare-handoff.sh +1 -78
  54. package/scripts/refresh-current-status.sh +2 -467
  55. package/scripts/run-governed-gate.ts +3 -0
  56. package/scripts/select-agent-context-blocks.sh +2 -64
  57. package/scripts/ship-worktrees.sh +1 -531
  58. package/scripts/sprint-backlog.sh +1 -834
  59. package/scripts/stage-runtime-release.ts +2 -1
  60. package/scripts/summarize-failures.sh +2 -123
  61. package/scripts/switch-plan.sh +1 -150
  62. package/scripts/sync-brain-docs.sh +2 -379
  63. package/scripts/sync-generated-authority.mjs +175 -0
  64. package/scripts/verify-contract.sh +2 -593
  65. package/scripts/verify-sprint.sh +1 -500
  66. package/scripts/workflow-contract.ts +4 -201
  67. package/scripts/workstream-sync.sh +2 -261
  68. package/src/cli/agent-jobs/executor-health.ts +1 -1
  69. package/src/cli/commands/plugin.ts +12 -3
  70. package/src/cli/controller/check-runner.ts +7 -2
  71. package/src/cli/controller/context-pack.ts +46 -21
  72. package/src/cli/github/contracts.ts +16 -0
  73. package/src/cli/github/github.ts +2 -16
  74. package/src/cli/github/session.ts +2 -1
  75. package/src/cli/local-bridge/facade-api.ts +37 -47
  76. package/src/cli/local-bridge/server.ts +2 -2
  77. package/src/cli/mcp/legacy-tool-service.ts +10 -33
  78. package/src/cli/mcp/post-finalize-work-attribution.ts +150 -0
  79. package/src/cli/mcp/repository-tools.ts +85 -89
  80. package/src/cli/mcp/server.ts +57 -13
  81. package/src/cli/mcp/tool-contract.ts +14 -0
  82. package/src/cli/mcp/toolset.ts +2 -2
  83. package/src/cli/mcp/transports/http.ts +23 -0
  84. package/src/cli/repositories/command-classifier.ts +2 -2
  85. package/src/cli/repositories/command-executor.ts +24 -6
  86. package/src/cli/repositories/repository-snapshot.ts +5 -2
  87. package/src/cli/repositories/structured-git.ts +57 -3
  88. package/src/cli/repository/inspector.ts +11 -3
  89. package/src/runtime/context/generic-lsp-provider.ts +1 -1
  90. package/src/runtime/context/semantic-navigation-contract.ts +61 -0
  91. package/src/runtime/context/semantic-navigation.ts +9 -61
  92. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +18 -25
  93. package/src/runtime/control-plane/execution/repository-work-attribution.ts +106 -0
  94. package/src/runtime/control-plane/execution/retained-work-resume.ts +203 -0
  95. package/src/runtime/control-plane/execution/work-completion-authority.ts +58 -0
  96. package/src/runtime/control-plane/execution/work-evidence-policy.ts +200 -0
  97. package/src/runtime/control-plane/execution/work-execution-support.ts +386 -0
  98. package/src/runtime/control-plane/execution/work-finalization-service.ts +1913 -0
  99. package/src/runtime/control-plane/execution/work-handle-authority.ts +96 -0
  100. package/src/runtime/control-plane/execution/work-handle-store.ts +1 -1
  101. package/src/runtime/control-plane/execution/work-operation-service.ts +468 -0
  102. package/src/runtime/control-plane/execution/work-path-scope.ts +37 -0
  103. package/src/runtime/control-plane/execution/work-preparation-service.ts +479 -0
  104. package/src/runtime/control-plane/execution/work-process-evidence.ts +129 -0
  105. package/src/runtime/control-plane/execution/work-task-receipt.ts +4 -43
  106. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +13 -3
  107. package/src/runtime/control-plane/execution/work-verification-context.ts +56 -0
  108. package/src/runtime/control-plane/execution/work-verification-service.ts +388 -0
  109. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +5 -1
  110. package/src/runtime/control-plane/facade/controller-round-relay.ts +164 -37
  111. package/src/runtime/control-plane/facade/controller-session-store.ts +183 -24
  112. package/src/runtime/control-plane/facade/goal-workloop-access.ts +15 -1
  113. package/src/runtime/control-plane/facade/goal-workloop.ts +329 -148
  114. package/src/runtime/control-plane/facade/index.ts +2 -0
  115. package/src/runtime/control-plane/facade/plan-contract-store.ts +90 -22
  116. package/src/runtime/control-plane/facade/repository-work-admission.ts +147 -0
  117. package/src/runtime/control-plane/facade/requirement-authority.ts +83 -0
  118. package/src/runtime/control-plane/facade/types.ts +68 -5
  119. package/src/runtime/control-plane/facade/work-contract-store.ts +3 -209
  120. package/src/runtime/control-plane/facade/work-state-machine.ts +250 -0
  121. package/src/runtime/control-plane/global-scheduler/maintenance.ts +14 -7
  122. package/src/runtime/control-plane/global-scheduler/scheduler.ts +5 -1
  123. package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +13 -0
  124. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +15 -6
  125. package/src/runtime/control-plane/launcher/launch-reservation-store.ts +54 -5
  126. package/src/runtime/control-plane/launcher/thin-launcher.ts +56 -7
  127. package/src/runtime/control-plane/persistence/requirement-store.ts +0 -12
  128. package/src/runtime/control-plane/repo-actor/registry.ts +4 -2
  129. package/src/runtime/control-plane/routing/route-policy.ts +18 -6
  130. package/src/runtime/control-plane/routing/workspace-admission.ts +70 -0
  131. package/src/runtime/evidence/process-check-execution.ts +18 -0
  132. package/src/runtime/execution/jobs/child-reference.ts +6 -2
  133. package/src/runtime/execution/process-runtime/check-result.ts +53 -18
  134. package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +7 -1
  135. package/src/runtime/execution/process-runtime/command-facade.ts +39 -17
  136. package/src/runtime/execution/process-runtime/lightweight-managed.ts +20 -3
  137. package/src/runtime/execution/process-runtime/persisted-check.ts +4 -1
  138. package/src/runtime/execution/process-runtime/runtime.ts +10 -0
  139. package/src/runtime/execution/process-runtime/types.ts +2 -12
  140. package/src/runtime/execution/work-activity.ts +21 -0
  141. package/src/runtime/execution/workers/executor.ts +18 -13
  142. package/src/runtime/gateway/mcp/execution-tools.ts +35 -2930
  143. package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +61 -0
  144. package/src/runtime/gateway/mcp/router.ts +2 -2
  145. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +21 -2
  146. package/src/runtime/gateway/mcp/runtime-tools.ts +805 -1009
  147. package/src/runtime/plugins/app-store-connect-adapter.ts +93 -28
  148. package/src/runtime/plugins/browser-adapter.ts +256 -1083
  149. package/src/runtime/plugins/browser-handoff-host.ts +2 -25
  150. package/src/runtime/plugins/browser-handoff.ts +18 -3
  151. package/src/runtime/plugins/browser-macos-bridge.ts +121 -16
  152. package/src/runtime/plugins/browser-manifest-surface.ts +603 -0
  153. package/src/runtime/plugins/browser-native-session-service.ts +222 -0
  154. package/src/runtime/plugins/browser-runtime-contract.ts +2 -0
  155. package/src/runtime/plugins/browser-runtime.ts +10 -0
  156. package/src/runtime/plugins/browser-session-store.ts +121 -0
  157. package/src/runtime/plugins/browser-session-types.ts +98 -0
  158. package/src/runtime/plugins/desktop-operator-registration.ts +4 -4
  159. package/src/runtime/plugins/external-adapter.ts +92 -58
  160. package/src/runtime/plugins/macos-capability-broker.ts +0 -8
  161. package/src/runtime/plugins/store.ts +66 -9
  162. package/src/runtime/plugins/types.ts +3 -1
  163. package/src/runtime/recovery/maintenance-executor.ts +61 -25
  164. package/src/runtime/root/package-connector-service.ts +46 -4
  165. package/src/runtime/root/package-runtime-release.ts +1 -1
  166. package/src/runtime/root/package-runtime-service.ts +33 -2
  167. package/src/runtime/root/release-manifest.ts +137 -16
  168. package/src/runtime/root/release-materialize.ts +109 -35
  169. package/src/runtime/root/types.ts +10 -0
  170. package/src/runtime/standalone-recovery/core.ts +425 -194
  171. package/src/runtime/standalone-recovery/entry.ts +1 -1
  172. package/src/runtime/standalone-recovery/installer.ts +1 -1
  173. package/src/runtime/workflow/schedules/engine.ts +113 -87
  174. package/src/runtime/workflow/schedules/settlement.ts +26 -23
  175. 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 commitArgs = ['commit', '-m', message, ...(input.allowEmpty === true ? ['--allow-empty'] : []), ...(paths.length > 0 ? ['--only', '--', ...paths] : [])];
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 size = statSync(decision.absolutePath).size;
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 size = (await statAsync(decision.absolutePath)).size;
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 size = statSync(decision.absolutePath).size;
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;
@@ -7,7 +7,7 @@ import type {
7
7
  SemanticNavigationOutcome,
8
8
  SemanticNavigationProvider,
9
9
  SemanticNavigationRequest,
10
- } from './semantic-navigation';
10
+ } from './semantic-navigation-contract';
11
11
 
12
12
  export interface GenericLspProviderDescriptor {
13
13
  id: string;
@@ -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 type SemanticNavigationKind = 'definition' | 'references' | 'implementations';
7
-
8
- export interface SemanticNavigationRequest {
9
- navigation: SemanticNavigationKind;
10
- path: string;
11
- line: number;
12
- column: number;
13
- tsconfigPath?: string;
14
- language?: string;
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,12 @@
1
1
  import { createHash } from 'crypto';
2
2
  import { getEditSession, listEditSessions, type EditSession } from '../../../cli/editing/edit-session';
3
- import { globMatches } from '../../../cli/mcp/paths';
4
3
  import { runProcess } from '../../../effects/process-runner';
5
- import { completeRequirementFromWork } from '../persistence/requirement-store';
6
- import { appendWorkEvidence, getWorkContract, recordWorkCompletionReceipt, updateWorkContract } from '../facade/work-contract-store';
4
+ import { getWorkContract, updateWorkContract } from '../facade/work-contract-store';
5
+ import { completeWorkWithReceipt } from './work-completion-authority';
7
6
  import { isDirectEditWorkCompletionReceipt, isTerminalWorkContractStatus, type DirectEditWorkCompletionReceipt, type WorkReconciliationRecord } from '../facade/types';
8
7
  import { historicalVerificationEvidenceAtRevision } from './verification-evidence';
9
8
  import { readWorkHandle } from './work-handle-store';
9
+ import { assertWorkPathsWithinScope, findWorkPathScopeViolation } from './work-path-scope';
10
10
 
11
11
  export interface DirectEditWorkCompletionReconciliation {
12
12
  completedWorkIds: string[];
@@ -102,6 +102,15 @@ export function reconcileFinalizedDirectEditWorksAfterCommit(input: {
102
102
  skipped.push({ sessionId: session.sessionId, workId: session.workId, reason: 'configured_checks_not_passed' });
103
103
  continue;
104
104
  }
105
+ const scopeViolation = findWorkPathScopeViolation(work, paths);
106
+ if (scopeViolation) {
107
+ skipped.push({
108
+ sessionId: session.sessionId,
109
+ workId: session.workId,
110
+ reason: `${scopeViolation.kind === 'forbidden' ? 'work_scope_forbidden' : 'work_scope_out_of_scope'}:${scopeViolation.path}`,
111
+ });
112
+ continue;
113
+ }
105
114
  const status = git(input.repoRoot, ['status', '--porcelain=v1', '--', ...paths]);
106
115
  if (!status.ok || status.stdout.trim()) {
107
116
  skipped.push({ sessionId: session.sessionId, workId: session.workId, reason: 'owned_paths_not_clean' });
@@ -141,7 +150,7 @@ export function reconcileFinalizedDirectEditWorksAfterCommit(input: {
141
150
  verifiedAt: session.verifiedAt ?? session.finalizedAt ?? recordedAt,
142
151
  recordedAt,
143
152
  };
144
- recordWorkCompletionReceipt(
153
+ completeWorkWithReceipt(
145
154
  { controllerHome: input.controllerHome, repoId: input.repoId },
146
155
  session.workId,
147
156
  receipt,
@@ -248,10 +257,10 @@ export function acceptReviewedDirectEditWorkReconciliation(input: ReviewedDirect
248
257
  if (comparedPaths.length !== actualPaths.length || comparedPaths.some((path, index) => path !== actualPaths[index])) {
249
258
  throw new Error('DIRECT_EDIT_WORK_RECONCILIATION_PATH_COMPARISON_MISMATCH');
250
259
  }
251
- for (const path of comparedPaths) {
252
- if (work.forbiddenPaths.some((pattern) => globMatches(pattern, path))) throw new Error(`DIRECT_EDIT_WORK_RECONCILIATION_FORBIDDEN_PATH: ${path}`);
253
- if (work.allowedPaths.length > 0 && !work.allowedPaths.some((pattern) => globMatches(pattern, path))) throw new Error(`DIRECT_EDIT_WORK_RECONCILIATION_PATH_OUT_OF_SCOPE: ${path}`);
254
- }
260
+ assertWorkPathsWithinScope(work, comparedPaths, {
261
+ forbidden: 'DIRECT_EDIT_WORK_RECONCILIATION_FORBIDDEN_PATH',
262
+ outOfScope: 'DIRECT_EDIT_WORK_RECONCILIATION_PATH_OUT_OF_SCOPE',
263
+ });
255
264
  const dirty = git(input.repoRoot, ['status', '--porcelain=v1', '--', ...comparedPaths]);
256
265
  if (!dirty.ok || dirty.stdout.trim()) throw new Error('DIRECT_EDIT_WORK_RECONCILIATION_OWNED_PATHS_DIRTY');
257
266
 
@@ -318,28 +327,12 @@ export function acceptReviewedDirectEditWorkReconciliation(input: ReviewedDirect
318
327
  verifiedAt,
319
328
  recordedAt,
320
329
  };
321
- const recorded = recordWorkCompletionReceipt(
330
+ completeWorkWithReceipt(
322
331
  { controllerHome: input.controllerHome, repoId: input.repoId },
323
332
  input.workId,
324
333
  receipt,
325
334
  'completed_changed',
326
335
  'repository_change',
327
336
  );
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
337
  return { workId: input.workId, reconciliation, receipt };
345
338
  }
@@ -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
+ }