@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.
- package/AGENTS.md +2 -0
- package/CHANGELOG.md +13 -0
- package/CLAUDE.md +2 -0
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/agents/openai.yaml +1 -1
- package/assets/partials-agents/02-operating-mode.partial.md +2 -0
- package/assets/plugin-registry.v1.json +3 -3
- package/assets/skill-version.json +2 -2
- package/assets/templates/helpers/check-architecture-sync.sh +17 -0
- package/assets/templates/helpers/inspect-project-state.ts +8 -6
- package/assets/templates/helpers/migrate-workflow-docs.ts +9 -7
- package/assets/templates/helpers/workflow-contract.ts +5 -1
- package/docs/architecture/CURRENT.md +12 -4
- package/docs/forge-plugin-management.md +1 -1
- package/docs/operations/controller-performance-and-502.md +19 -0
- package/docs/operations/homebrew.md +2 -2
- package/package.json +5 -2
- package/scripts/architecture-event.ts +2 -820
- package/scripts/architecture-queue.sh +2 -645
- package/scripts/archive-architecture-request.sh +2 -242
- package/scripts/archive-workflow.sh +1 -291
- package/scripts/benchmark-work-efficiency.ts +0 -0
- package/scripts/capability-config.ts +2 -392
- package/scripts/capability-resolver.ts +2 -503
- package/scripts/capture-plan.sh +1 -332
- package/scripts/check-agent-tooling.sh +2 -1531
- package/scripts/check-architecture-sync.sh +2 -302
- package/scripts/check-brain-manifest.sh +2 -253
- package/scripts/check-context-files.sh +2 -47
- package/scripts/check-deploy-sql-order.sh +2 -115
- package/scripts/check-runtime-architecture.mjs +243 -16
- package/scripts/check-skill-version.ts +3 -191
- package/scripts/check-source-duplication.mjs +61 -0
- package/scripts/check-stable-baseline.ts +0 -0
- package/scripts/check-task-sync.sh +2 -63
- package/scripts/check-task-workflow.sh +2 -1000
- package/scripts/codex-handoff-resume.sh +2 -267
- package/scripts/context-contract-sync.sh +2 -514
- package/scripts/contract-run.ts +2 -463
- package/scripts/contract-worktree.sh +1 -702
- package/scripts/ensure-task-workflow.sh +1 -1192
- package/scripts/inspect-project-state.ts +4 -279
- package/scripts/lib/project-init-lib.sh +7 -2
- package/scripts/lib/run-forge-helper.sh +19 -0
- package/scripts/lib/run-forge-helper.ts +26 -0
- package/scripts/migrate-project-template.sh +1 -33
- package/scripts/migrate-workflow-docs.ts +4 -412
- package/scripts/new-plan.sh +1 -180
- package/scripts/new-spec.sh +1 -47
- package/scripts/new-sprint.sh +1 -46
- package/scripts/plan-to-todo.sh +1 -771
- package/scripts/prepare-codex-handoff.sh +2 -201
- package/scripts/prepare-handoff.sh +1 -78
- package/scripts/refresh-current-status.sh +2 -467
- package/scripts/run-governed-gate.ts +3 -0
- package/scripts/select-agent-context-blocks.sh +2 -64
- package/scripts/ship-worktrees.sh +1 -531
- package/scripts/sprint-backlog.sh +1 -834
- package/scripts/stage-runtime-release.ts +2 -1
- package/scripts/summarize-failures.sh +2 -123
- package/scripts/switch-plan.sh +1 -150
- package/scripts/sync-brain-docs.sh +2 -379
- package/scripts/sync-generated-authority.mjs +175 -0
- package/scripts/verify-contract.sh +2 -593
- package/scripts/verify-sprint.sh +1 -500
- package/scripts/workflow-contract.ts +4 -201
- package/scripts/workstream-sync.sh +2 -261
- package/src/cli/agent-jobs/executor-health.ts +1 -1
- package/src/cli/commands/plugin.ts +12 -3
- package/src/cli/controller/check-runner.ts +7 -2
- package/src/cli/controller/context-pack.ts +46 -21
- package/src/cli/github/contracts.ts +16 -0
- package/src/cli/github/github.ts +2 -16
- package/src/cli/github/session.ts +2 -1
- package/src/cli/local-bridge/facade-api.ts +37 -47
- package/src/cli/local-bridge/server.ts +2 -2
- package/src/cli/mcp/legacy-tool-service.ts +10 -33
- package/src/cli/mcp/post-finalize-work-attribution.ts +150 -0
- package/src/cli/mcp/repository-tools.ts +85 -89
- package/src/cli/mcp/server.ts +57 -13
- package/src/cli/mcp/tool-contract.ts +14 -0
- package/src/cli/mcp/toolset.ts +2 -2
- package/src/cli/mcp/transports/http.ts +23 -0
- package/src/cli/repositories/command-classifier.ts +2 -2
- package/src/cli/repositories/command-executor.ts +24 -6
- package/src/cli/repositories/repository-snapshot.ts +5 -2
- package/src/cli/repositories/structured-git.ts +57 -3
- package/src/cli/repository/inspector.ts +11 -3
- package/src/runtime/context/generic-lsp-provider.ts +1 -1
- package/src/runtime/context/semantic-navigation-contract.ts +61 -0
- package/src/runtime/context/semantic-navigation.ts +9 -61
- package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +18 -25
- package/src/runtime/control-plane/execution/repository-work-attribution.ts +106 -0
- package/src/runtime/control-plane/execution/retained-work-resume.ts +203 -0
- package/src/runtime/control-plane/execution/work-completion-authority.ts +58 -0
- package/src/runtime/control-plane/execution/work-evidence-policy.ts +200 -0
- package/src/runtime/control-plane/execution/work-execution-support.ts +386 -0
- package/src/runtime/control-plane/execution/work-finalization-service.ts +1913 -0
- package/src/runtime/control-plane/execution/work-handle-authority.ts +96 -0
- package/src/runtime/control-plane/execution/work-handle-store.ts +1 -1
- package/src/runtime/control-plane/execution/work-operation-service.ts +468 -0
- package/src/runtime/control-plane/execution/work-path-scope.ts +37 -0
- package/src/runtime/control-plane/execution/work-preparation-service.ts +479 -0
- package/src/runtime/control-plane/execution/work-process-evidence.ts +129 -0
- package/src/runtime/control-plane/execution/work-task-receipt.ts +4 -43
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +13 -3
- package/src/runtime/control-plane/execution/work-verification-context.ts +56 -0
- package/src/runtime/control-plane/execution/work-verification-service.ts +388 -0
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +5 -1
- package/src/runtime/control-plane/facade/controller-round-relay.ts +164 -37
- package/src/runtime/control-plane/facade/controller-session-store.ts +183 -24
- package/src/runtime/control-plane/facade/goal-workloop-access.ts +15 -1
- package/src/runtime/control-plane/facade/goal-workloop.ts +329 -148
- package/src/runtime/control-plane/facade/index.ts +2 -0
- package/src/runtime/control-plane/facade/plan-contract-store.ts +90 -22
- package/src/runtime/control-plane/facade/repository-work-admission.ts +147 -0
- package/src/runtime/control-plane/facade/requirement-authority.ts +83 -0
- package/src/runtime/control-plane/facade/types.ts +68 -5
- package/src/runtime/control-plane/facade/work-contract-store.ts +3 -209
- package/src/runtime/control-plane/facade/work-state-machine.ts +250 -0
- package/src/runtime/control-plane/global-scheduler/maintenance.ts +14 -7
- package/src/runtime/control-plane/global-scheduler/scheduler.ts +5 -1
- package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +13 -0
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +15 -6
- package/src/runtime/control-plane/launcher/launch-reservation-store.ts +54 -5
- package/src/runtime/control-plane/launcher/thin-launcher.ts +56 -7
- package/src/runtime/control-plane/persistence/requirement-store.ts +0 -12
- package/src/runtime/control-plane/repo-actor/registry.ts +4 -2
- package/src/runtime/control-plane/routing/route-policy.ts +18 -6
- package/src/runtime/control-plane/routing/workspace-admission.ts +70 -0
- package/src/runtime/evidence/process-check-execution.ts +18 -0
- package/src/runtime/execution/jobs/child-reference.ts +6 -2
- package/src/runtime/execution/process-runtime/check-result.ts +53 -18
- package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +7 -1
- package/src/runtime/execution/process-runtime/command-facade.ts +39 -17
- package/src/runtime/execution/process-runtime/lightweight-managed.ts +20 -3
- package/src/runtime/execution/process-runtime/persisted-check.ts +4 -1
- package/src/runtime/execution/process-runtime/runtime.ts +10 -0
- package/src/runtime/execution/process-runtime/types.ts +2 -12
- package/src/runtime/execution/work-activity.ts +21 -0
- package/src/runtime/execution/workers/executor.ts +18 -13
- package/src/runtime/gateway/mcp/execution-tools.ts +35 -2930
- package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +61 -0
- package/src/runtime/gateway/mcp/router.ts +2 -2
- package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +21 -2
- package/src/runtime/gateway/mcp/runtime-tools.ts +805 -1009
- package/src/runtime/plugins/app-store-connect-adapter.ts +93 -28
- package/src/runtime/plugins/browser-adapter.ts +256 -1083
- package/src/runtime/plugins/browser-handoff-host.ts +2 -25
- package/src/runtime/plugins/browser-handoff.ts +18 -3
- package/src/runtime/plugins/browser-macos-bridge.ts +121 -16
- package/src/runtime/plugins/browser-manifest-surface.ts +603 -0
- package/src/runtime/plugins/browser-native-session-service.ts +222 -0
- package/src/runtime/plugins/browser-runtime-contract.ts +2 -0
- package/src/runtime/plugins/browser-runtime.ts +10 -0
- package/src/runtime/plugins/browser-session-store.ts +121 -0
- package/src/runtime/plugins/browser-session-types.ts +98 -0
- package/src/runtime/plugins/desktop-operator-registration.ts +4 -4
- package/src/runtime/plugins/external-adapter.ts +92 -58
- package/src/runtime/plugins/macos-capability-broker.ts +0 -8
- package/src/runtime/plugins/store.ts +66 -9
- package/src/runtime/plugins/types.ts +3 -1
- package/src/runtime/recovery/maintenance-executor.ts +61 -25
- package/src/runtime/root/package-connector-service.ts +46 -4
- package/src/runtime/root/package-runtime-release.ts +1 -1
- package/src/runtime/root/package-runtime-service.ts +33 -2
- package/src/runtime/root/release-manifest.ts +137 -16
- package/src/runtime/root/release-materialize.ts +109 -35
- package/src/runtime/root/types.ts +10 -0
- package/src/runtime/standalone-recovery/core.ts +425 -194
- package/src/runtime/standalone-recovery/entry.ts +1 -1
- package/src/runtime/standalone-recovery/installer.ts +1 -1
- package/src/runtime/workflow/schedules/engine.ts +113 -87
- package/src/runtime/workflow/schedules/settlement.ts +26 -23
- package/src/runtime/workflow/schedules/store.ts +79 -10
|
@@ -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
|
|
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
|
|
199
|
+
/** Durable completion authority for one typed remote effect. */
|
|
161
200
|
export interface RemoteEffectCompletionReceipt {
|
|
162
201
|
schemaVersion: 1;
|
|
163
|
-
/**
|
|
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
|
-
|
|
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'
|
|
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,
|