@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
@@ -15,3 +15,5 @@ export * from './goal-workloop-access';
15
15
  export * from './codex-delegation';
16
16
  export * from './self-healing-loop';
17
17
  export * from './operation-digest';
18
+ export * from './requirement-authority';
19
+ export * from './repository-work-admission';
@@ -60,6 +60,11 @@ export interface AdmitPlanContractInput extends CreatePlanContractInput {
60
60
  relatedPlanId?: string;
61
61
  }
62
62
 
63
+ export interface RepairDraftPlanContractInput extends Omit<CreatePlanContractInput, 'planId' | 'repoId' | 'requirementId' | 'evidenceRefs'> {
64
+ /** Source revision observed before entering the admission lock; rejects stale draft writers. */
65
+ expectedSourceRevision?: string;
66
+ }
67
+
63
68
  export type AdmitPlanContractResult = PlanAdmissionResolution;
64
69
 
65
70
  export interface PlanContractSummary {
@@ -254,6 +259,32 @@ function buildPlanContract(input: CreatePlanContractInput, at: string): PlanCont
254
259
  };
255
260
  }
256
261
 
262
+ function draftContentErrors(plan: PlanContract): string[] {
263
+ const errors: string[] = [];
264
+ if (!plan.sourceRevision) errors.push('source_revision is required');
265
+ if (!plan.scopeKey || plan.scopeKey === 'unknown') errors.push('scope_key is required');
266
+ if (!plan.goal) errors.push('goal is required');
267
+ if (plan.steps.length === 0) errors.push('at least one plan step is required');
268
+ const ids = new Set<string>();
269
+ for (const step of plan.steps) {
270
+ if (!step.id || step.id === 'unknown') errors.push('every plan step needs an id');
271
+ else if (ids.has(step.id)) errors.push(`duplicate plan step id: ${step.id}`);
272
+ else ids.add(step.id);
273
+ if (!step.objective) errors.push(`step ${step.id || 'unknown'} needs an objective`);
274
+ if (step.checks.length === 0) errors.push(`step ${step.id || 'unknown'} needs at least one machine-checkable check`);
275
+ if (step.acceptanceCriteria.length === 0) errors.push(`step ${step.id || 'unknown'} needs acceptance criteria`);
276
+ if (step.status !== 'pending') errors.push(`step ${step.id || 'unknown'} must be pending before approval`);
277
+ }
278
+ for (const step of plan.steps) {
279
+ for (const dependency of step.dependencies) {
280
+ const dependencyStep = plan.steps.find((candidate) => candidate.id === dependency);
281
+ if (!dependencyStep) errors.push(`step ${step.id} references unknown dependency ${dependency}`);
282
+ else if (dependencyStep.status !== 'pending') errors.push(`step ${step.id} dependency ${dependency} must be pending before approval`);
283
+ }
284
+ }
285
+ return [...new Set(errors)];
286
+ }
287
+
257
288
  function createPlanContractUnlocked(options: PlanContractStoreOptions, input: CreatePlanContractInput): PlanContract {
258
289
  assertRequirementReference(options, input.requirementId);
259
290
  const at = nowIso(options);
@@ -295,6 +326,64 @@ function replacePlanContractUnlocked(
295
326
  return successor;
296
327
  }
297
328
 
329
+ function repairDraftPlanContractUnlocked(
330
+ options: PlanContractStoreOptions,
331
+ planId: string,
332
+ input: RepairDraftPlanContractInput,
333
+ ): PlanContract {
334
+ const store = readPlanContractStore(options);
335
+ const key = sanitizeFileComponent(planId);
336
+ const index = store.contracts.findIndex((contract) => contract.planId === key);
337
+ if (index < 0) throw new Error(`plan contract not found: ${key}`);
338
+ const current = store.contracts[index]!;
339
+ if (current.status !== 'draft') throw new Error(`PLAN_DRAFT_REPAIR_STATUS_INVALID: ${current.planId}:${current.status}`);
340
+ if (input.expectedSourceRevision !== undefined && current.sourceRevision !== input.expectedSourceRevision) {
341
+ throw new Error(`PLAN_DRAFT_REPAIR_STALE_SOURCE: ${current.planId}:expected=${input.expectedSourceRevision}:actual=${current.sourceRevision}`);
342
+ }
343
+ assertRequirementReference(options, current.requirementId);
344
+ const at = nowIso(options);
345
+ const candidate = buildPlanContract({
346
+ ...input,
347
+ planId: current.planId,
348
+ repoId: current.repoId,
349
+ requirementId: current.requirementId,
350
+ evidenceRefs: current.evidenceRefs,
351
+ }, at);
352
+ const contentErrors = draftContentErrors(candidate);
353
+ if (contentErrors.length > 0) {
354
+ throw new Error(`PLAN_DRAFT_REPAIR_INVALID: ${contentErrors.join('; ')}`);
355
+ }
356
+ const conflictingScope = store.contracts.find((existing, candidateIndex) => candidateIndex !== index
357
+ && !isTerminalPlanContractStatus(existing.status)
358
+ && existing.scopeKey === candidate.scopeKey);
359
+ if (conflictingScope) throw new Error(`PLAN_SCOPE_ALREADY_OWNED: ${candidate.scopeKey}:${conflictingScope.planId}`);
360
+ const repaired: PlanContract = {
361
+ ...candidate,
362
+ createdAt: current.createdAt,
363
+ updatedAt: at,
364
+ };
365
+ const contracts = [...store.contracts];
366
+ contracts[index] = repaired;
367
+ writePlanContractStore(options, { schemaVersion: 1, updatedAt: at, contracts });
368
+ return repaired;
369
+ }
370
+
371
+ export function repairDraftPlanContract(
372
+ options: PlanContractStoreOptions,
373
+ planId: string,
374
+ input: RepairDraftPlanContractInput,
375
+ ): PlanContract {
376
+ return withPlanAdmissionLock(options, () => repairDraftPlanContractUnlocked(options, planId, input));
377
+ }
378
+
379
+ export async function repairDraftPlanContractAsync(
380
+ options: PlanContractStoreOptions,
381
+ planId: string,
382
+ input: RepairDraftPlanContractInput,
383
+ ): Promise<PlanContract> {
384
+ return await withPlanAdmissionLockAsync(options, () => repairDraftPlanContractUnlocked(options, planId, input));
385
+ }
386
+
298
387
  export function createPlanContract(options: PlanContractStoreOptions, input: CreatePlanContractInput): PlanContract {
299
388
  return withPlanAdmissionLock(options, () => {
300
389
  const store = readPlanContractStore(options);
@@ -366,28 +455,7 @@ export function summarizePlanContract(plan: PlanContract): PlanContractSummary {
366
455
  }
367
456
 
368
457
  function approvalErrors(plan: PlanContract, allPlans: readonly PlanContract[]): string[] {
369
- const errors: string[] = [];
370
- if (!plan.sourceRevision) errors.push('source_revision is required before approval');
371
- if (!plan.scopeKey || plan.scopeKey === 'unknown') errors.push('scope_key is required before approval');
372
- if (!plan.goal) errors.push('goal is required before approval');
373
- if (plan.steps.length === 0) errors.push('at least one plan step is required before approval');
374
- const ids = new Set<string>();
375
- for (const step of plan.steps) {
376
- if (!step.id || step.id === 'unknown') errors.push('every plan step needs an id');
377
- else if (ids.has(step.id)) errors.push(`duplicate plan step id: ${step.id}`);
378
- else ids.add(step.id);
379
- if (!step.objective) errors.push(`step ${step.id || 'unknown'} needs an objective`);
380
- if (step.checks.length === 0) errors.push(`step ${step.id || 'unknown'} needs at least one machine-checkable check`);
381
- if (step.acceptanceCriteria.length === 0) errors.push(`step ${step.id || 'unknown'} needs acceptance criteria`);
382
- if (step.status !== 'pending') errors.push(`step ${step.id || 'unknown'} must be pending before approval`);
383
- }
384
- for (const step of plan.steps) {
385
- for (const dependency of step.dependencies) {
386
- const dependencyStep = plan.steps.find((candidate) => candidate.id === dependency);
387
- if (!dependencyStep) errors.push(`step ${step.id} references unknown dependency ${dependency}`);
388
- else if (dependencyStep.status !== 'pending') errors.push(`step ${step.id} dependency ${dependency} must be pending before approval`);
389
- }
390
- }
458
+ const errors = draftContentErrors(plan).map((error) => `${error} before approval`);
391
459
  const scopeIsCommitted = (status: PlanContractStatus): boolean => [
392
460
  'approved',
393
461
  'executing',
@@ -0,0 +1,147 @@
1
+ import type { AccessMode } from '../governance/access-policy';
2
+ import type { RouteDecision } from '../routing/route-policy';
3
+ import { createWorkContract, getWorkContract, updateWorkContract, type WorkContractStoreOptions } from './work-contract-store';
4
+ import type { WorkContract } from './types';
5
+
6
+ export interface PreparedRepositoryWorkAdmissionInput {
7
+ workId: string;
8
+ repoId: string;
9
+ objective: string;
10
+ acceptanceCriteria: string[];
11
+ allowedPaths: string[];
12
+ checks: string[];
13
+ accessMode: AccessMode;
14
+ isolated: boolean;
15
+ requestedBy: WorkContract['requestedBy'];
16
+ requestId: string;
17
+ }
18
+
19
+ /**
20
+ * Canonical WorkContract admission for the compatibility work_prepare surface.
21
+ * Workspace materialization remains an execution concern; this function owns
22
+ * the semantic contract shape so transports cannot invent a parallel policy.
23
+ */
24
+ export function admitPreparedRepositoryWorkContract(
25
+ store: WorkContractStoreOptions,
26
+ input: PreparedRepositoryWorkAdmissionInput,
27
+ ): WorkContract {
28
+ return createWorkContract(store, {
29
+ workId: input.workId,
30
+ repoId: input.repoId,
31
+ mode: input.isolated ? 'goal_workloop' : 'direct_control',
32
+ objective: input.objective,
33
+ acceptanceCriteria: input.acceptanceCriteria,
34
+ allowedPaths: input.allowedPaths,
35
+ forbiddenPaths: [],
36
+ checks: input.checks,
37
+ constraints: {
38
+ accessMode: input.accessMode,
39
+ workspaceMode: input.isolated ? 'isolated' : 'current',
40
+ requireWorktree: input.isolated,
41
+ directMainProhibited: input.isolated,
42
+ allowCommit: true,
43
+ allowMerge: true,
44
+ allowCleanup: true,
45
+ },
46
+ worktreePolicy: {
47
+ required: input.isolated,
48
+ reason: input.isolated ? 'work_prepare selected isolated worktree execution' : 'explicitly reused a registered checkout',
49
+ },
50
+ requestedBy: input.requestedBy,
51
+ requestId: input.requestId,
52
+ });
53
+ }
54
+
55
+ export interface DirectEditWorkAdmissionInput {
56
+ workId: string;
57
+ repoId: string;
58
+ checkoutId?: string;
59
+ principalId?: string;
60
+ controllerInstanceId?: string;
61
+ baseRevision?: string;
62
+ workspaceFingerprint: string;
63
+ routeDecision: RouteDecision;
64
+ objective: string;
65
+ issueId?: string;
66
+ taskId?: string;
67
+ allowedPaths: string[];
68
+ checks: string[];
69
+ requestedBy: WorkContract['requestedBy'];
70
+ }
71
+
72
+ /**
73
+ * Canonical compatibility admission for an Edit Session bound to Direct Work.
74
+ * The caller supplies the canonical Route Policy decision as evidence; this
75
+ * boundary rejects any decision that requires isolation or a non-direct lane.
76
+ */
77
+ export function admitDirectEditWorkContract(
78
+ store: WorkContractStoreOptions,
79
+ input: DirectEditWorkAdmissionInput,
80
+ ): WorkContract {
81
+ if (input.routeDecision.executionMode !== 'direct_control' || input.routeDecision.requiresIsolation) {
82
+ throw new Error('DIRECT_EDIT_WORK_ROUTE_CONFLICT: canonical Route Policy does not permit Direct Control');
83
+ }
84
+ return createWorkContract(store, {
85
+ workId: input.workId,
86
+ repoId: input.repoId,
87
+ checkoutId: input.checkoutId,
88
+ principalId: input.principalId,
89
+ controllerInstanceId: input.controllerInstanceId,
90
+ baseRevision: input.baseRevision,
91
+ workspaceFingerprint: input.workspaceFingerprint,
92
+ routeDecisionFingerprint: input.routeDecision.inputFingerprint,
93
+ routeDecision: input.routeDecision,
94
+ mode: 'direct_control',
95
+ objective: input.objective,
96
+ acceptanceCriteria: [],
97
+ constraints: { workspaceMode: 'current', requireWorktree: false, requireHandoffOnAmbiguity: true },
98
+ risk: 'low',
99
+ status: 'running',
100
+ phase: 'implementation',
101
+ issueId: input.issueId,
102
+ taskId: input.taskId,
103
+ scopeSummary: input.objective,
104
+ allowedPaths: input.allowedPaths,
105
+ forbiddenPaths: [],
106
+ checks: input.checks,
107
+ requestedBy: input.requestedBy,
108
+ evidenceRefs: [],
109
+ handoffRefs: [],
110
+ suggestedNextActions: [],
111
+ policyDecisions: [],
112
+ checkRefs: [],
113
+ reconciliations: [],
114
+ });
115
+ }
116
+
117
+
118
+ export interface MaterializedRepositoryWorkspace {
119
+ managed: boolean;
120
+ checkoutId?: string;
121
+ root?: string;
122
+ baseRevision?: string | null;
123
+ }
124
+
125
+ /**
126
+ * Canonical placement materialization transition for an admitted isolated
127
+ * WorkContract. The concrete worktree creator is injected by the execution
128
+ * boundary; only this authority decides how its evidence updates the Work.
129
+ */
130
+ export function materializeRepositoryWorkPlacement(
131
+ store: WorkContractStoreOptions,
132
+ workId: string,
133
+ materialize: (contract: WorkContract) => MaterializedRepositoryWorkspace,
134
+ ): WorkContract | undefined {
135
+ const contract = getWorkContract(store, workId);
136
+ if (!contract || contract.worktreePolicy.required !== true || contract.worktreeRef) return contract;
137
+ const workspace = materialize(contract);
138
+ if (!workspace.managed || !workspace.checkoutId || !workspace.root) {
139
+ throw new Error('MANAGED_WORKSPACE_NOT_MATERIALIZED');
140
+ }
141
+ return updateWorkContract(store, workId, {
142
+ checkoutId: workspace.checkoutId,
143
+ baseRevision: workspace.baseRevision ?? contract.baseRevision,
144
+ worktreeRef: workspace.root,
145
+ driver: { ...contract.driver, preferred: 'isolated_worktree', allowDirectEdit: false },
146
+ });
147
+ }
@@ -0,0 +1,83 @@
1
+ import {
2
+ createRequirement,
3
+ readRequirement,
4
+ type CreateRequirementInput,
5
+ type Requirement,
6
+ type RequirementStoreOptions,
7
+ } from '../persistence/requirement-store';
8
+
9
+ export type RequirementAdmissionDecision = 'created' | 'reuse_existing' | 'existing_conflict';
10
+
11
+ export interface RequirementAdmissionResult {
12
+ decision: RequirementAdmissionDecision;
13
+ requirement: Requirement;
14
+ created: boolean;
15
+ }
16
+
17
+ function bounded(values: readonly string[] | undefined, limit: number, maxLength = 500): string[] {
18
+ return (values ?? [])
19
+ .map((value) => String(value).trim())
20
+ .filter(Boolean)
21
+ .slice(0, limit)
22
+ .map((value) => value.slice(0, maxLength));
23
+ }
24
+
25
+ /**
26
+ * Canonical Requirement bootstrap request. Persistence performs the same
27
+ * defensive bounds before writing, but create/reuse/conflict admission lives
28
+ * only here so MCP/CLI adapters cannot grow their own Requirement lifecycle.
29
+ */
30
+ export function normalizeRequirementAdmissionInput(input: CreateRequirementInput): CreateRequirementInput {
31
+ const requirementId = String(input.requirementId ?? '').trim();
32
+ if (!requirementId || requirementId.includes('/') || requirementId.includes('\\')) throw new Error('REQUIREMENT_ID_INVALID');
33
+ const normalized: CreateRequirementInput = {
34
+ requirementId: requirementId.slice(0, 160),
35
+ title: String(input.title ?? '').trim().slice(0, 500),
36
+ outcomeStatement: String(input.outcomeStatement ?? '').trim().slice(0, 2_000),
37
+ acceptanceCriteria: bounded(input.acceptanceCriteria, 50),
38
+ requiredDeliveryReferences: bounded(input.requiredDeliveryReferences, 50),
39
+ legacyAliases: bounded(input.legacyAliases, 20, 160),
40
+ };
41
+ if (!normalized.title || !normalized.outcomeStatement) throw new Error('REQUIREMENT_CONTENT_REQUIRED');
42
+ return normalized;
43
+ }
44
+
45
+ function sameBootstrapIdentity(existing: Requirement, requested: CreateRequirementInput): boolean {
46
+ return existing.requirementId === requested.requirementId
47
+ && existing.title === requested.title
48
+ && existing.outcomeStatement === requested.outcomeStatement
49
+ && JSON.stringify(existing.acceptanceCriteria) === JSON.stringify(requested.acceptanceCriteria ?? [])
50
+ && JSON.stringify(existing.requiredDeliveryReferences) === JSON.stringify(requested.requiredDeliveryReferences ?? [])
51
+ && JSON.stringify(existing.legacyAliases) === JSON.stringify(requested.legacyAliases ?? []);
52
+ }
53
+
54
+ function existingDecision(existing: Requirement, requested: CreateRequirementInput): RequirementAdmissionResult {
55
+ return {
56
+ decision: sameBootstrapIdentity(existing, requested) ? 'reuse_existing' : 'existing_conflict',
57
+ requirement: existing,
58
+ created: false,
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Sole bootstrap admission authority for Requirement identity. A create race is
64
+ * closed by rereading the winner and applying the same identity comparison;
65
+ * adapters therefore never need read/compare/create policy of their own.
66
+ */
67
+ export function admitRequirement(
68
+ options: RequirementStoreOptions,
69
+ input: CreateRequirementInput,
70
+ ): RequirementAdmissionResult {
71
+ const requested = normalizeRequirementAdmissionInput(input);
72
+ const existing = readRequirement(options, requested.requirementId)?.value;
73
+ if (existing) return existingDecision(existing, requested);
74
+
75
+ try {
76
+ return { decision: 'created', requirement: createRequirement(options, requested), created: true };
77
+ } catch (error) {
78
+ if (!(error instanceof Error) || !error.message.startsWith('REQUIREMENT_ALREADY_EXISTS:')) throw error;
79
+ const raced = readRequirement(options, requested.requirementId)?.value;
80
+ if (!raced) throw error;
81
+ return existingDecision(raced, requested);
82
+ }
83
+ }
@@ -120,6 +120,7 @@ export const TERMINAL_WORK_CONTRACT_STATUSES: readonly WorkContractStatus[] = [
120
120
  export const WORK_KINDS = [
121
121
  'repository_change',
122
122
  'completed_no_change',
123
+ 'read_only_review',
123
124
  'investigation',
124
125
  'local_effect',
125
126
  'remote_effect',
@@ -137,6 +138,44 @@ export type EvidenceState = (typeof EVIDENCE_STATES)[number];
137
138
  export const COMPLETION_OUTCOMES = ['completed_changed', 'completed_no_change', 'completed_local', 'completed_remote', 'superseded'] as const;
138
139
  export type CompletionOutcome = (typeof COMPLETION_OUTCOMES)[number];
139
140
 
141
+ /** Source-bound semantic evidence accumulated by a recoverable read-only review. */
142
+ export interface ReadOnlyReviewEvidence {
143
+ sourceRevision: string;
144
+ workspaceFingerprint?: string;
145
+ inspectedPaths: string[];
146
+ findings: string[];
147
+ recordedAt: string;
148
+ }
149
+
150
+ /** Controller-reviewed binding from one exact Work acceptance criterion to durable evidence. */
151
+ export interface WorkSemanticAcceptanceEvidence {
152
+ criterion: string;
153
+ evidenceIds: string[];
154
+ rationale: string;
155
+ recordedAt: string;
156
+ }
157
+
158
+ /**
159
+ * Terminal authority for a clean read-only review. This deliberately does not
160
+ * impersonate repository delivery: it proves the reviewed source stayed at the
161
+ * frozen base revision with no workspace delta and records the bounded review
162
+ * scope that justified completed_no_change.
163
+ */
164
+ export interface ReadOnlyReviewCompletionReceipt {
165
+ schemaVersion: 1;
166
+ receiptId: string;
167
+ source: 'read_only_review';
168
+ workId: string;
169
+ baseRevision: string;
170
+ sourceRevision: string;
171
+ workspaceFingerprint?: string;
172
+ /** Explicit unchanged-workspace proof captured by the trusted finalizer. Must be empty. */
173
+ workspaceChangedPaths: string[];
174
+ inspectedPaths: string[];
175
+ findingCount: 0;
176
+ recordedAt: string;
177
+ }
178
+
140
179
  /**
141
180
  * Terminal receipt for a bounded Controller-local side effect that is not a
142
181
  * repository delivery. Repository Task/Issue completion keeps using the
@@ -157,18 +196,21 @@ export interface LocalEffectCompletionReceipt {
157
196
  recordedAt: string;
158
197
  }
159
198
 
160
- /** Durable completion authority for one typed remote plugin effect. */
199
+ /** Durable completion authority for one typed remote effect. */
161
200
  export interface RemoteEffectCompletionReceipt {
162
201
  schemaVersion: 1;
163
- /** Reuse the authoritative PLG-* receipt id rather than manufacturing parallel evidence. */
202
+ /** Plugin receipts retain their PLG-* id; repository Process effects use their canonical Process id. */
164
203
  receiptId: string;
165
204
  source: 'remote_effect';
166
205
  workId: string;
167
- pluginId: string;
206
+ /** Historical receipts omit this field and are interpreted as plugin_action. */
207
+ authority?: 'plugin_action' | 'repository_process';
208
+ pluginId?: string;
168
209
  actionId: string;
169
210
  requestId: string;
170
211
  semanticKey: string;
171
212
  resultDigest: string;
213
+ processId?: string;
172
214
  recordedAt: string;
173
215
  }
174
216
 
@@ -197,12 +239,21 @@ export interface DirectEditWorkCompletionReceipt {
197
239
  recordedAt: string;
198
240
  }
199
241
 
200
- export type WorkCompletionReceipt = RepositoryCompletionReceipt | DirectEditWorkCompletionReceipt | LocalEffectCompletionReceipt | RemoteEffectCompletionReceipt;
242
+ export type WorkCompletionReceipt = RepositoryCompletionReceipt | DirectEditWorkCompletionReceipt | ReadOnlyReviewCompletionReceipt | LocalEffectCompletionReceipt | RemoteEffectCompletionReceipt;
201
243
 
202
244
  export function isRepositoryCompletionReceipt(
203
245
  receipt: WorkCompletionReceipt,
204
246
  ): receipt is RepositoryCompletionReceipt {
205
- return receipt.source !== 'local_effect' && receipt.source !== 'remote_effect' && receipt.source !== 'direct_edit_work';
247
+ return receipt.source !== 'local_effect'
248
+ && receipt.source !== 'remote_effect'
249
+ && receipt.source !== 'direct_edit_work'
250
+ && receipt.source !== 'read_only_review';
251
+ }
252
+
253
+ export function isReadOnlyReviewCompletionReceipt(
254
+ receipt: WorkCompletionReceipt,
255
+ ): receipt is ReadOnlyReviewCompletionReceipt {
256
+ return receipt.source === 'read_only_review';
206
257
  }
207
258
 
208
259
  export function isRemoteEffectCompletionReceipt(
@@ -348,6 +399,8 @@ export interface WorkContractConstraints {
348
399
  /** current is the stability-first default; isolated is opt-in or used for explicit parallelism. */
349
400
  workspaceMode?: 'current' | 'isolated' | 'auto';
350
401
  requireWorktree?: boolean;
402
+ /** Admission fence: repository mutation must not use the Direct Control/current-main lane. */
403
+ directMainProhibited?: boolean;
351
404
  /** True when a change alters the agreed architecture direction rather than only implementation details. */
352
405
  architectureStrategyChange?: boolean;
353
406
  /** True when the proposed change weakens the thin-harness / high-performance execution policy. */
@@ -490,6 +543,10 @@ export interface WorkContract {
490
543
  actualChangedPaths: string[];
491
544
  recordedAt: string;
492
545
  };
546
+ /** Source-bound semantic findings/scope for first-class recoverable read-only review Work. */
547
+ readOnlyReviewEvidence?: ReadOnlyReviewEvidence;
548
+ /** Explicit Controller-reviewed acceptance bindings. Mechanical evidence attribution alone never writes this field. */
549
+ semanticAcceptanceEvidence?: WorkSemanticAcceptanceEvidence[];
493
550
  /** Explicit policy fence. This is not semantic completeness evidence. */
494
551
  allowedPaths: string[];
495
552
  forbiddenPaths: string[];
@@ -679,6 +736,10 @@ export interface ExecutionModeSelectionInput {
679
736
  workspaceDirty?: boolean;
680
737
  workspaceFingerprint?: string;
681
738
  checkoutId?: string;
739
+ /** Canonical typed placement constraint; never inferred from objective text. */
740
+ workspacePlacement?: 'current' | 'isolated' | 'auto';
741
+ /** Admission fence that has precedence over explicit Direct routing. */
742
+ directMainProhibited?: boolean;
682
743
  requiresInvestigation?: boolean;
683
744
  requiresLongRunningChecks?: boolean;
684
745
  requiresParallelism?: boolean;
@@ -742,6 +803,8 @@ export function selectExecutionMode(input: ExecutionModeSelectionInput): Executi
742
803
  dirty: input.workspaceDirty,
743
804
  checkoutId: input.checkoutId,
744
805
  fingerprint: input.workspaceFingerprint,
806
+ placement: input.workspacePlacement,
807
+ directMainProhibited: input.directMainProhibited,
745
808
  },
746
809
  policy: {
747
810
  risk: input.risk,