@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.
Files changed (175) hide show
  1. package/AGENTS.md +2 -0
  2. package/CHANGELOG.md +26 -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 +125 -101
  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 +69 -29
  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 +150 -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 +815 -1015
  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 +3 -2
  166. package/src/runtime/root/package-runtime-service.ts +48 -3
  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,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 { completeRequirementFromWork } from '../persistence/requirement-store';
6
- import { appendWorkEvidence, getWorkContract, recordWorkCompletionReceipt, updateWorkContract } from '../facade/work-contract-store';
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
- recordWorkCompletionReceipt(
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 failedHandle = work.status === 'failed'
214
- ? readWorkHandle(input.controllerHome, input.repoId, input.workId)
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
- if ((isTerminalWorkContractStatus(work.status) && !failedReviewedRecovery) || work.workKind !== 'repository_change') {
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
- 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
- }
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
- const recorded = recordWorkCompletionReceipt(
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
+ }