@moretea-labs/forge 1.5.0 → 1.6.0
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/CHANGELOG.md +28 -1
- package/DESIGN.md +7 -2
- package/README.md +7 -7
- package/README.zh-CN.md +5 -3
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/assets/hooks/pre-edit-guard.sh +8 -39
- package/assets/hooks/prompt-guard.sh +2 -2
- package/assets/plugin-registry.v1.json +54 -3
- package/assets/reference-configs/hook-operations.md +1 -1
- package/assets/reference-configs/sprint-contracts.md +2 -2
- package/assets/skill-version.json +2 -2
- package/assets/templates/helpers/ensure-task-workflow.sh +2 -2
- package/assets/templates/helpers/sprint-backlog.sh +6 -6
- package/docs/architecture/current/automation-and-schedule-engine.md +9 -3
- package/docs/architecture/current/codegraph-context-provider.md +15 -3
- package/docs/architecture/current/dispatch-and-agent-strategy.md +32 -13
- package/docs/architecture/current/entity-model.md +20 -0
- package/docs/architecture/current/external-plugin-governance.md +3 -3
- package/docs/architecture/current/system-overview.md +26 -1
- package/docs/architecture/current/thin-harness-v1.md +10 -0
- package/docs/forge-chatgpt-mcp-setup.md +27 -3
- package/docs/operations/chatgpt-personal-assistant-runtime.md +12 -2
- package/docs/operations/controller-browser-plugin.md +12 -16
- package/docs/operations/features.md +1 -1
- package/docs/operations/homebrew.md +2 -2
- package/docs/public-usage-guide.md +1 -1
- package/docs/public-usage-guide.zh-CN.md +1 -1
- package/docs/tutorials/01-install-and-start.md +6 -4
- package/docs/tutorials/01-install-and-start.zh-CN.md +3 -1
- package/docs/tutorials/02-connect-chatgpt.md +8 -2
- package/docs/tutorials/02-connect-chatgpt.zh-CN.md +8 -2
- package/docs/wiki/Installation.md +1 -1
- package/docs/wiki/Quick-Start.md +1 -1
- package/package.json +1 -1
- package/scripts/check-public-docs.sh +14 -1
- package/scripts/migrate-chatgpt-automation-schedules.ts +64 -0
- package/scripts/sprint-backlog.sh +1 -1
- package/src/cli/commands/chatgpt.ts +31 -2
- package/src/cli/commands/init-hook.ts +3 -1
- package/src/cli/commands/mcp.ts +2 -0
- package/src/cli/commands/plugin.ts +36 -9
- package/src/cli/commands/runtime.ts +1 -0
- package/src/cli/commands/setup-profile.ts +49 -18
- package/src/cli/controller/context-pack.ts +158 -6
- package/src/cli/hook/prompt-guard-decision.ts +17 -18
- package/src/cli/hook/prompt-intents.ts +1 -1
- package/src/cli/local-bridge/console-assets.ts +17 -0
- package/src/cli/local-bridge/console-automations.ts +205 -23
- package/src/cli/local-bridge/console-view-models.ts +2 -0
- package/src/cli/local-bridge/facade-api.ts +2 -0
- package/src/cli/local-bridge/frontend/src/App.tsx +1 -1
- package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +143 -3
- package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +1 -1
- package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +181 -3
- package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +1 -1
- package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +4 -3
- package/src/cli/local-bridge/frontend/src/lib/api.ts +18 -6
- package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +1 -1
- package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +2 -1
- package/src/cli/local-bridge/frontend/src/styles.css +27 -11
- package/src/cli/local-bridge/frontend/src/types.ts +6 -2
- package/src/cli/local-bridge/server.ts +31 -20
- package/src/cli/local-bridge/ui-dist/app.css +1 -1
- package/src/cli/local-bridge/ui-dist/app.js +5003 -4815
- package/src/cli/mcp/auth.ts +2 -0
- package/src/cli/mcp/paths.ts +8 -2
- package/src/cli/mcp/repository-tools.ts +124 -9
- package/src/cli/mcp/server.ts +164 -19
- package/src/cli/mcp/setup.ts +9 -1
- package/src/cli/mcp/transports/http.ts +52 -2
- package/src/cli/mcp/transports/session-registry.ts +24 -0
- package/src/cli/repositories/command-classifier.ts +113 -0
- package/src/cli/repositories/safe-patch.ts +6 -4
- package/src/cli/repositories/structured-git.ts +49 -7
- package/src/cli/repository/inspector.ts +16 -3
- package/src/runtime/assistant/intent.ts +3 -3
- package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +157 -2
- package/src/runtime/control-plane/execution/execution-identity.ts +65 -14
- package/src/runtime/control-plane/execution/validation.ts +15 -2
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +32 -0
- package/src/runtime/control-plane/execution/work-validation-reconciler.ts +6 -7
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +2 -1
- package/src/runtime/control-plane/facade/capability-registry.ts +1 -1
- package/src/runtime/control-plane/facade/controller-session-store.ts +12 -13
- package/src/runtime/control-plane/facade/goal-workloop-access.ts +7 -2
- package/src/runtime/control-plane/facade/goal-workloop.ts +231 -8
- package/src/runtime/control-plane/facade/plan-contract-store.ts +32 -12
- package/src/runtime/control-plane/facade/requirement-board.ts +24 -8
- package/src/runtime/control-plane/facade/types.ts +33 -6
- package/src/runtime/control-plane/facade/work-contract-store.ts +31 -2
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +375 -23
- package/src/runtime/control-plane/persistence/requirement-store.ts +15 -11
- package/src/runtime/diagnostics/mcp-timing.ts +6 -0
- package/src/runtime/execution/process-runtime/check-facade.ts +1 -10
- package/src/runtime/execution/process-runtime/check-result.ts +10 -2
- package/src/runtime/execution/process-runtime/command-facade.ts +1 -0
- package/src/runtime/execution/process-runtime/gc.ts +64 -10
- package/src/runtime/execution/process-runtime/persisted-check.ts +10 -2
- package/src/runtime/execution/process-runtime/resource-claims.ts +87 -2
- package/src/runtime/execution/process-runtime/runtime.ts +16 -1
- package/src/runtime/execution/process-runtime/store.ts +6 -0
- package/src/runtime/gateway/mcp/execution-tools.ts +27 -12
- package/src/runtime/gateway/mcp/router.ts +1 -1
- package/src/runtime/gateway/mcp/runtime-tools.ts +482 -178
- package/src/runtime/model-clients/deepseek-adapter.ts +0 -32
- package/src/runtime/plugins/browser-adapter.ts +19 -72
- package/src/runtime/plugins/browser-handoff-host.ts +6 -31
- package/src/runtime/plugins/browser-handoff.ts +0 -3
- package/src/runtime/plugins/browser-macos-bridge.ts +8 -0
- package/src/runtime/plugins/desktop-operator-registration.ts +8 -11
- package/src/runtime/plugins/external-registration.ts +84 -5
- package/src/runtime/plugins/first-party-registry.ts +2 -2
- package/src/runtime/plugins/gmail-adapter.ts +28 -2
- package/src/runtime/plugins/local-system-adapter.ts +2 -2
- package/src/runtime/plugins/plugin-management-adapter.ts +140 -0
- package/src/runtime/plugins/store.ts +107 -5
- package/src/runtime/plugins/types.ts +4 -0
- package/src/runtime/recovery/actions.ts +0 -10
- package/src/runtime/recovery/classifier.ts +0 -9
- package/src/runtime/recovery/maintenance-executor.ts +43 -11
- package/src/runtime/recovery/probe.ts +0 -1
- package/src/runtime/recovery/types.ts +0 -1
- package/src/runtime/root/package-connector-service.ts +136 -0
- package/src/runtime/root/package-runtime-service.ts +25 -12
- package/src/runtime/root/release-materialize.ts +161 -18
- package/src/runtime/root/service.ts +6 -4
- package/src/runtime/safe-tooling/index.ts +0 -1
- package/src/runtime/safe-tooling/types.ts +0 -30
- package/src/runtime/standalone-recovery/core.ts +2 -2
- package/src/runtime/workflow/schedules/browser-probe.ts +180 -0
- package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +40 -0
- package/src/runtime/workflow/schedules/engine.ts +357 -64
- package/src/runtime/workflow/schedules/types.ts +5 -0
- package/src/runtime/workflow/schedules/work-continuation.ts +100 -27
- package/bin/forge-desktop-helper.mjs +0 -204
- package/docs/operations/controller-desktop-plugin.md +0 -112
- package/src/runtime/plugins/desktop-adapter.ts +0 -374
- package/src/runtime/safe-tooling/browser-safe.ts +0 -133
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
appendWorkHandoffRef,
|
|
10
10
|
createWorkContract,
|
|
11
11
|
getWorkContract,
|
|
12
|
+
listWorkContracts,
|
|
12
13
|
summarizeWorkContract,
|
|
13
14
|
transitionWorkContractPhase,
|
|
14
15
|
updateWorkContract,
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
import {
|
|
18
19
|
claimPlanStepForWork,
|
|
19
20
|
completePlanStepForWork,
|
|
21
|
+
getPlanContract,
|
|
20
22
|
type PlanContractStoreOptions,
|
|
21
23
|
} from './plan-contract-store';
|
|
22
24
|
import {
|
|
@@ -57,8 +59,11 @@ export interface GoalWorkloopContext {
|
|
|
57
59
|
controllerInstanceId?: string;
|
|
58
60
|
workspaceFingerprint?: string;
|
|
59
61
|
now?: () => string;
|
|
62
|
+
materializeIsolatedWorkspace?: (input: { workId: string; title: string; baseRef?: string }) => { checkoutId: string; root: string; baseRevision?: string | null; managed: true };
|
|
60
63
|
}
|
|
61
64
|
|
|
65
|
+
export type WorkAdmissionRelation = 'continue' | 'extend' | 'parallel' | 'new_goal';
|
|
66
|
+
|
|
62
67
|
export interface GoalWorkloopStartInput {
|
|
63
68
|
objective: string;
|
|
64
69
|
acceptanceCriteria?: string[];
|
|
@@ -68,6 +73,9 @@ export interface GoalWorkloopStartInput {
|
|
|
68
73
|
constraints?: WorkContract['constraints'];
|
|
69
74
|
modeInput: ExecutionModeSelectionInput;
|
|
70
75
|
requestedBy?: WorkContract['requestedBy'];
|
|
76
|
+
relatedWorkId?: string;
|
|
77
|
+
workRelation?: WorkAdmissionRelation;
|
|
78
|
+
requirementId?: string;
|
|
71
79
|
taskId?: string;
|
|
72
80
|
issueId?: string;
|
|
73
81
|
approvalConfirmed?: boolean;
|
|
@@ -280,11 +288,33 @@ export function routeWorkStart(
|
|
|
280
288
|
selectedMode = selectExecutionMode({ ...effectiveModeInput, approvalConfirmed: true });
|
|
281
289
|
policy = evaluateAccessPolicy(selectedMode);
|
|
282
290
|
}
|
|
283
|
-
|
|
291
|
+
let mode = applyForcedMode(selectedMode);
|
|
292
|
+
|
|
293
|
+
// Route Policy is the sole execution-depth authority. Existing Work is
|
|
294
|
+
// ownership context, not a reason to upgrade an otherwise Direct operation
|
|
295
|
+
// into Durable Work. Explicit Work/Plan ownership is resolved only after a
|
|
296
|
+
// durable route has already been selected; checkout writer conflicts affect
|
|
297
|
+
// placement, not semantic task identity.
|
|
284
298
|
|
|
285
299
|
if (mode.mode === 'direct_control') {
|
|
286
300
|
const available = ctx.availableChecks ?? [];
|
|
287
301
|
const normalized = normalizeCheckIds(input.checks ?? [], available);
|
|
302
|
+
// Explicit ownership may annotate a Direct operation, but it never changes
|
|
303
|
+
// the RouteDecision. The actual repository mutation can pass this Work id
|
|
304
|
+
// to repository_safe_patch_apply / repository_command_execute for evidence
|
|
305
|
+
// attribution while staying on the Direct/Process path.
|
|
306
|
+
const directOwnershipCandidates = input.relatedWorkId || (input.planId && input.planStepId)
|
|
307
|
+
? listWorkContracts({ ...ctx.workStore, status: 'active', limit: 100 })
|
|
308
|
+
.filter((candidate) => (candidate.lifecycleRole ?? 'primary') === 'primary')
|
|
309
|
+
: [];
|
|
310
|
+
const directPlan = input.planId && ctx.planStore ? getPlanContract(ctx.planStore, input.planId) : undefined;
|
|
311
|
+
const directPlanStep = directPlan && input.planStepId ? directPlan.steps.find((step) => step.id === input.planStepId) : undefined;
|
|
312
|
+
const directOwner = (input.relatedWorkId
|
|
313
|
+
? directOwnershipCandidates.find((candidate) => candidate.workId === input.relatedWorkId)
|
|
314
|
+
: undefined)
|
|
315
|
+
?? (directPlanStep?.workId
|
|
316
|
+
? directOwnershipCandidates.find((candidate) => candidate.workId === directPlanStep.workId)
|
|
317
|
+
: undefined);
|
|
288
318
|
const suggested = validateSuggestedNextActions([
|
|
289
319
|
{
|
|
290
320
|
label: 'Apply bounded direct edit',
|
|
@@ -315,6 +345,13 @@ export function routeWorkStart(
|
|
|
315
345
|
directControlPreserved: true,
|
|
316
346
|
objective: input.objective.slice(0, 1_000),
|
|
317
347
|
normalizedChecks: normalized,
|
|
348
|
+
...(directOwner ? {
|
|
349
|
+
ownership: {
|
|
350
|
+
workId: directOwner.workId,
|
|
351
|
+
relation: input.workRelation ?? 'continue',
|
|
352
|
+
executionDepthPreserved: true,
|
|
353
|
+
},
|
|
354
|
+
} : {}),
|
|
318
355
|
},
|
|
319
356
|
warnings: [...policy.warnings, ...normalized.warnings],
|
|
320
357
|
suggestedNextActions: suggested,
|
|
@@ -437,12 +474,189 @@ export function startGoalWorkloop(
|
|
|
437
474
|
): FacadeResult {
|
|
438
475
|
const at = nowIso(ctx);
|
|
439
476
|
const available = ctx.availableChecks ?? [];
|
|
440
|
-
const normalized = normalizeCheckIds(input.checks ?? [], available);
|
|
441
477
|
const workspaceMode = input.constraints?.workspaceMode ?? 'auto';
|
|
478
|
+
const activeWorks = listWorkContracts({ ...ctx.workStore, status: 'active', limit: 100 })
|
|
479
|
+
.filter((candidate) => (candidate.lifecycleRole ?? 'primary') === 'primary');
|
|
480
|
+
const workspaceOwner = ctx.checkoutId
|
|
481
|
+
? activeWorks.find((candidate) => candidate.checkoutId === ctx.checkoutId
|
|
482
|
+
&& candidate.worktreePolicy.required !== true
|
|
483
|
+
&& !candidate.worktreeRef)
|
|
484
|
+
: undefined;
|
|
485
|
+
const explicitRelatedWork = input.relatedWorkId
|
|
486
|
+
? activeWorks.find((candidate) => candidate.workId === input.relatedWorkId)
|
|
487
|
+
: undefined;
|
|
488
|
+
const plan = input.planId && ctx.planStore ? getPlanContract(ctx.planStore, input.planId) : undefined;
|
|
489
|
+
const planStep = plan && input.planStepId ? plan.steps.find((step) => step.id === input.planStepId) : undefined;
|
|
490
|
+
const sameStringSet = (provided: string[] | undefined, frozen: string[]): boolean => {
|
|
491
|
+
if (provided === undefined) return true;
|
|
492
|
+
const actual = [...new Set(provided)].sort();
|
|
493
|
+
const expected = [...new Set(frozen)].sort();
|
|
494
|
+
return actual.length === expected.length && actual.every((value, index) => value === expected[index]);
|
|
495
|
+
};
|
|
496
|
+
if (planStep) {
|
|
497
|
+
const mismatches = [
|
|
498
|
+
!sameStringSet(input.acceptanceCriteria, planStep.acceptanceCriteria) && 'acceptance_criteria',
|
|
499
|
+
!sameStringSet(input.allowedPaths, planStep.allowedPaths) && 'allowed_paths',
|
|
500
|
+
!sameStringSet(input.forbiddenPaths, planStep.forbiddenPaths) && 'forbidden_paths',
|
|
501
|
+
!sameStringSet(input.checks, planStep.checks) && 'check_ids',
|
|
502
|
+
].filter((value): value is string => Boolean(value));
|
|
503
|
+
if (mismatches.length > 0) {
|
|
504
|
+
return buildFacadeResult({
|
|
505
|
+
status: 'blocked',
|
|
506
|
+
summary: `PLAN_STEP_WORK_CONTRACT_MISMATCH: ${input.planId}/${input.planStepId} conflicts with frozen Plan step field(s): ${mismatches.join(', ')}. Replan instead of widening or narrowing Work admission.`,
|
|
507
|
+
data: { executionStarted: false, workContractCreated: false, planId: input.planId, planStepId: input.planStepId, mismatches },
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
const effectiveAcceptanceCriteria = planStep?.acceptanceCriteria ?? input.acceptanceCriteria ?? [];
|
|
512
|
+
const effectiveAllowedPaths = planStep?.allowedPaths ?? input.allowedPaths ?? [];
|
|
513
|
+
const effectiveForbiddenPaths = planStep?.forbiddenPaths ?? input.forbiddenPaths ?? [];
|
|
514
|
+
const effectiveChecks = planStep?.checks ?? input.checks ?? [];
|
|
515
|
+
const normalized = normalizeCheckIds(effectiveChecks, available);
|
|
516
|
+
const planStepWork = planStep?.workId ? activeWorks.find((candidate) => candidate.workId === planStep.workId) : undefined;
|
|
517
|
+
const requirementWorks = input.requirementId
|
|
518
|
+
? activeWorks.filter((candidate) => candidate.requirementId === input.requirementId)
|
|
519
|
+
: [];
|
|
520
|
+
const deterministicTarget = explicitRelatedWork ?? planStepWork ?? (requirementWorks.length === 1 ? requirementWorks[0] : undefined);
|
|
521
|
+
const requestedRelation = input.workRelation ?? (input.modeInput.requiresParallelism === true ? 'parallel' : undefined);
|
|
522
|
+
// Only strong semantic bindings participate in ownership resolution. An
|
|
523
|
+
// unrelated active Work or a checkout writer is a placement fact, not a
|
|
524
|
+
// semantic candidate for continue/extend/parallel/new_goal.
|
|
525
|
+
const candidateWorks = [...new Map(
|
|
526
|
+
[explicitRelatedWork, planStepWork, ...requirementWorks]
|
|
527
|
+
.filter((candidate): candidate is WorkContract => Boolean(candidate))
|
|
528
|
+
.map((candidate) => [candidate.workId, candidate]),
|
|
529
|
+
).values()].slice(0, 8);
|
|
530
|
+
|
|
531
|
+
const resolutionRequired = (reason: string, target = deterministicTarget): FacadeResult => buildFacadeResult({
|
|
532
|
+
status: 'ok',
|
|
533
|
+
summary: reason,
|
|
534
|
+
data: {
|
|
535
|
+
executionStarted: false,
|
|
536
|
+
workContractCreated: false,
|
|
537
|
+
admissionDecision: 'resolution_required',
|
|
538
|
+
resolutionRequired: true,
|
|
539
|
+
requestedBy: input.requestedBy ?? 'chatgpt',
|
|
540
|
+
...(target ? { recommendedWork: summarizeWorkContract(target) } : {}),
|
|
541
|
+
candidates: candidateWorks.map(summarizeWorkContract),
|
|
542
|
+
allowedRelations: ['continue', 'extend', 'parallel', 'new_goal'],
|
|
543
|
+
},
|
|
544
|
+
evidenceRefs: target?.evidenceRefs ?? [],
|
|
545
|
+
suggestedNextActions: target
|
|
546
|
+
? [{
|
|
547
|
+
label: 'Continue existing Work',
|
|
548
|
+
tool: 'rh_work',
|
|
549
|
+
operation: 'continue',
|
|
550
|
+
payload: { work_id: target.workId },
|
|
551
|
+
risk: 'readonly',
|
|
552
|
+
confidence: planStepWork || explicitRelatedWork ? 'high' : 'medium',
|
|
553
|
+
reason: 'Resolve intent before execution; do not create a second Work until the relationship is explicit.',
|
|
554
|
+
}]
|
|
555
|
+
: [],
|
|
556
|
+
rawAvailable: false,
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
if ((input.requestedBy ?? 'chatgpt') === 'scheduler') {
|
|
560
|
+
return resolutionRequired(
|
|
561
|
+
deterministicTarget
|
|
562
|
+
? `SCHEDULER_CONTINUATION_REQUIRED: scheduler-triggered execution must continue its bound Work ${deterministicTarget.workId}; it must not create a new durable Work.`
|
|
563
|
+
: 'SCHEDULER_WORK_BINDING_REQUIRED: scheduler-triggered execution must bind and wake an existing durable Work; it must not invent a new Work from a prompt.',
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (planStepWork && !requestedRelation) {
|
|
568
|
+
return buildFacadeResult({
|
|
569
|
+
status: 'ok',
|
|
570
|
+
summary: `PLAN_STEP_REUSES_ACTIVE_WORK: ${input.planId}/${input.planStepId} already executes as ${planStepWork.workId}; reuse that Work instead of creating another.`,
|
|
571
|
+
data: {
|
|
572
|
+
executionStarted: false,
|
|
573
|
+
workContractCreated: false,
|
|
574
|
+
admissionDecision: 'reuse_existing',
|
|
575
|
+
resolutionRequired: false,
|
|
576
|
+
work: summarizeWorkContract(planStepWork),
|
|
577
|
+
},
|
|
578
|
+
evidenceRefs: planStepWork.evidenceRefs,
|
|
579
|
+
suggestedNextActions: [{ label: 'Continue existing Work', tool: 'rh_work', operation: 'continue', payload: { work_id: planStepWork.workId }, risk: 'readonly', confidence: 'high' }],
|
|
580
|
+
rawAvailable: false,
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (requestedRelation === 'continue' || requestedRelation === 'extend') {
|
|
585
|
+
if (!deterministicTarget) {
|
|
586
|
+
return resolutionRequired(`${requestedRelation.toUpperCase()}_TARGET_REQUIRED: select related_work_id or bind the request to an active Plan/Requirement before execution.`);
|
|
587
|
+
}
|
|
588
|
+
if (requestedRelation === 'extend' && deterministicTarget.planId) {
|
|
589
|
+
return resolutionRequired(
|
|
590
|
+
`PLAN_EXTENSION_REQUIRES_REPLAN: Work ${deterministicTarget.workId} is governed by Plan ${deterministicTarget.planId}; extend the authoritative Plan/Requirement before continuing rather than silently widening Work scope.`,
|
|
591
|
+
deterministicTarget,
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
const selected = requestedRelation === 'extend'
|
|
595
|
+
? updateWorkContract(ctx.workStore, deterministicTarget.workId, {
|
|
596
|
+
acceptanceCriteria: [...new Set([...deterministicTarget.acceptanceCriteria, ...(input.acceptanceCriteria ?? [])])].slice(0, 30),
|
|
597
|
+
allowedPaths: [...new Set([...deterministicTarget.allowedPaths, ...(input.allowedPaths ?? [])])].slice(0, 50),
|
|
598
|
+
forbiddenPaths: [...new Set([...deterministicTarget.forbiddenPaths, ...(input.forbiddenPaths ?? [])])].slice(0, 50),
|
|
599
|
+
checks: [...new Set([...deterministicTarget.checks, ...normalized.validCheckIds])].slice(0, 30),
|
|
600
|
+
scopeSummary: `Extended serially: ${input.objective}`.slice(0, 500),
|
|
601
|
+
continuationPrompt: `Continue work ${ctx.repoId}: ${deterministicTarget.objective.slice(0, 160)}. Additional requested scope: ${input.objective.slice(0, 500)}`,
|
|
602
|
+
})
|
|
603
|
+
: deterministicTarget;
|
|
604
|
+
return buildFacadeResult({
|
|
605
|
+
status: 'ok',
|
|
606
|
+
summary: `${requestedRelation === 'extend' ? 'Existing Work extended' : 'Existing Work selected'}: ${selected.workId}. No new WorkContract was created.`,
|
|
607
|
+
data: {
|
|
608
|
+
executionStarted: false,
|
|
609
|
+
workContractCreated: false,
|
|
610
|
+
admissionDecision: requestedRelation === 'extend' ? 'extend_existing' : 'reuse_existing',
|
|
611
|
+
resolutionRequired: false,
|
|
612
|
+
work: summarizeWorkContract(selected),
|
|
613
|
+
},
|
|
614
|
+
evidenceRefs: selected.evidenceRefs,
|
|
615
|
+
suggestedNextActions: [{ label: 'Continue existing Work', tool: 'rh_work', operation: 'continue', payload: { work_id: selected.workId }, risk: 'readonly', confidence: 'high' }],
|
|
616
|
+
rawAvailable: false,
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (!requestedRelation && candidateWorks.length > 0) {
|
|
621
|
+
return resolutionRequired(
|
|
622
|
+
`WORK_ADMISSION_RESOLUTION_REQUIRED: ${candidateWorks.length} active Work candidate(s) exist. Classify this request as continue, extend, parallel, or new_goal before any durable Work is created.`,
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const placementConflict = Boolean(workspaceOwner);
|
|
442
627
|
const needsWorktree = executionMode === 'goal_workloop' && (input.constraints?.requireWorktree
|
|
443
628
|
?? (workspaceMode === 'isolated'
|
|
629
|
+
|| placementConflict
|
|
444
630
|
|| (workspaceMode === 'auto' && routeDecision.requiresIsolation === true)));
|
|
631
|
+
if (!needsWorktree && workspaceOwner) {
|
|
632
|
+
return buildFacadeResult({
|
|
633
|
+
status: 'blocked',
|
|
634
|
+
summary: `WORKSPACE_OWNERSHIP_INVARIANT: unresolved admission reached checkout ${ctx.checkoutId} while active Work ${workspaceOwner.workId} owns it. This is an internal routing invariant, not a normal fallback path.`,
|
|
635
|
+
data: {
|
|
636
|
+
executionStarted: false,
|
|
637
|
+
workContractCreated: false,
|
|
638
|
+
conflictType: 'workspace_single_writer_invariant',
|
|
639
|
+
existingWork: summarizeWorkContract(workspaceOwner),
|
|
640
|
+
},
|
|
641
|
+
evidenceRefs: workspaceOwner.evidenceRefs,
|
|
642
|
+
rawAvailable: false,
|
|
643
|
+
});
|
|
644
|
+
}
|
|
445
645
|
const generatedWorkId = workIdFor(input.objective);
|
|
646
|
+
let isolatedWorkspace: ReturnType<NonNullable<GoalWorkloopContext['materializeIsolatedWorkspace']>> | undefined;
|
|
647
|
+
if (needsWorktree) {
|
|
648
|
+
if (!ctx.materializeIsolatedWorkspace) {
|
|
649
|
+
return buildFacadeResult({ status: 'blocked', summary: 'ISOLATED_WORKSPACE_MATERIALIZER_REQUIRED: isolated Work cannot enter running state without a concrete managed checkout.', data: { executionStarted: false, workContractCreated: false, worktreeRequired: true } });
|
|
650
|
+
}
|
|
651
|
+
try {
|
|
652
|
+
isolatedWorkspace = ctx.materializeIsolatedWorkspace({ workId: generatedWorkId, title: input.objective, baseRef: ctx.sourceRevision });
|
|
653
|
+
if (!isolatedWorkspace.checkoutId || !isolatedWorkspace.root || isolatedWorkspace.checkoutId === ctx.checkoutId) {
|
|
654
|
+
throw new Error('ISOLATED_WORKSPACE_NOT_DISTINCT');
|
|
655
|
+
}
|
|
656
|
+
} catch (error) {
|
|
657
|
+
return buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? `ISOLATED_WORKSPACE_MATERIALIZATION_FAILED: ${error.message}` : 'ISOLATED_WORKSPACE_MATERIALIZATION_FAILED', data: { executionStarted: false, workContractCreated: false, worktreeRequired: true } });
|
|
658
|
+
}
|
|
659
|
+
}
|
|
446
660
|
if (input.planId || input.planStepId) {
|
|
447
661
|
if (!input.planId || !input.planStepId || !ctx.planStore || !ctx.sourceRevision) {
|
|
448
662
|
return buildFacadeResult({ status: 'blocked', summary: 'PLAN_CONTEXT_REQUIRED: plan_id, plan_step_id and a current source revision are required.', data: { executionStarted: false } });
|
|
@@ -473,28 +687,32 @@ export function startGoalWorkloop(
|
|
|
473
687
|
const work = createWorkContract(ctx.workStore, {
|
|
474
688
|
workId: generatedWorkId,
|
|
475
689
|
repoId: ctx.repoId,
|
|
476
|
-
checkoutId: ctx.checkoutId,
|
|
690
|
+
checkoutId: isolatedWorkspace?.checkoutId ?? ctx.checkoutId,
|
|
477
691
|
principalId: ctx.principalId,
|
|
478
692
|
controllerInstanceId: ctx.controllerInstanceId,
|
|
479
|
-
baseRevision: ctx.sourceRevision,
|
|
480
|
-
workspaceFingerprint: ctx.workspaceFingerprint,
|
|
693
|
+
baseRevision: isolatedWorkspace?.baseRevision ?? ctx.sourceRevision,
|
|
694
|
+
workspaceFingerprint: isolatedWorkspace ? undefined : ctx.workspaceFingerprint,
|
|
481
695
|
routeDecisionFingerprint: routeDecision.inputFingerprint,
|
|
482
696
|
routeDecision,
|
|
483
697
|
mode: executionMode,
|
|
484
698
|
objective: input.objective,
|
|
485
|
-
acceptanceCriteria:
|
|
699
|
+
acceptanceCriteria: effectiveAcceptanceCriteria,
|
|
486
700
|
constraints: input.constraints ?? { requireHandoffOnAmbiguity: true },
|
|
487
701
|
risk: workRiskFor(input),
|
|
702
|
+
workKind: input.modeInput.requiresExternalEffect === true && input.modeInput.remoteWrite === true
|
|
703
|
+
? 'remote_effect'
|
|
704
|
+
: undefined,
|
|
488
705
|
status: 'running',
|
|
489
706
|
phase: 'implementation',
|
|
490
707
|
issueId: input.issueId,
|
|
491
708
|
taskId: input.taskId,
|
|
709
|
+
requirementId: input.requirementId,
|
|
492
710
|
planId: input.planId,
|
|
493
711
|
planStepId: input.planStepId,
|
|
494
712
|
planSourceRevision: input.planId ? ctx.sourceRevision : undefined,
|
|
495
713
|
scopeSummary: input.modeInput.scopeClear ? 'scope declared at start' : 'scope incomplete',
|
|
496
|
-
allowedPaths:
|
|
497
|
-
forbiddenPaths:
|
|
714
|
+
allowedPaths: effectiveAllowedPaths,
|
|
715
|
+
forbiddenPaths: effectiveForbiddenPaths,
|
|
498
716
|
checks: normalized.validCheckIds,
|
|
499
717
|
driver: {
|
|
500
718
|
preferred: executionMode === 'direct_control'
|
|
@@ -509,6 +727,7 @@ export function startGoalWorkloop(
|
|
|
509
727
|
? 'Isolation was explicitly requested or required for parallel execution.'
|
|
510
728
|
: 'Current workspace is the stability-first default; isolation remains opt-in.',
|
|
511
729
|
},
|
|
730
|
+
worktreeRef: isolatedWorkspace?.root,
|
|
512
731
|
evidencePolicy: {
|
|
513
732
|
defaultDetailLevel: 'summary',
|
|
514
733
|
allowRawOptIn: true,
|
|
@@ -1115,6 +1334,9 @@ export function stopGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorkloopSt
|
|
|
1115
1334
|
// cleanup handler verifies ownership, cleanliness and successful removal.
|
|
1116
1335
|
worktreeRef: work.worktreeRef,
|
|
1117
1336
|
});
|
|
1337
|
+
const plan = updated.planId && updated.planStepId && ctx.planStore
|
|
1338
|
+
? completePlanStepForWork(ctx.planStore, { planId: updated.planId, stepId: updated.planStepId, work: updated })
|
|
1339
|
+
: undefined;
|
|
1118
1340
|
|
|
1119
1341
|
return buildFacadeResult({
|
|
1120
1342
|
status: 'ok',
|
|
@@ -1126,6 +1348,7 @@ export function stopGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorkloopSt
|
|
|
1126
1348
|
worktreeDeleted: false,
|
|
1127
1349
|
cleanupPending: destructiveCleanup && Boolean(work.worktreeRef),
|
|
1128
1350
|
destructiveCleanupAuthorized: destructiveCleanup,
|
|
1351
|
+
...(plan ? { plan } : {}),
|
|
1129
1352
|
},
|
|
1130
1353
|
evidenceRefs: work.evidenceRefs.slice(0, 5),
|
|
1131
1354
|
suggestedNextActions: [
|
|
@@ -376,28 +376,48 @@ export function completePlanStepForWork(
|
|
|
376
376
|
if (stepIndex < 0) throw new Error(`PLAN_STEP_NOT_FOUND: ${input.stepId}`);
|
|
377
377
|
const step = current.steps[stepIndex];
|
|
378
378
|
if (step.workId !== input.work.workId) throw new Error(`PLAN_STEP_WORK_MISMATCH: ${input.stepId}`);
|
|
379
|
-
|
|
379
|
+
if (step.status === 'completed') return current;
|
|
380
|
+
const delivered = input.work.status === 'completed'
|
|
380
381
|
&& input.work.phase === 'cleanup'
|
|
381
382
|
&& input.work.evidenceState === 'valid'
|
|
382
383
|
&& Boolean(input.work.completionOutcome && input.work.completionOutcome !== 'superseded')
|
|
383
384
|
&& Boolean(input.work.completionReceipt);
|
|
384
385
|
const failed = input.work.status === 'failed' || input.work.status === 'cancelled';
|
|
385
|
-
if (!
|
|
386
|
+
if (!delivered && !failed) throw new Error(`PLAN_STEP_WORK_NOT_TERMINAL: ${input.work.workId}`);
|
|
386
387
|
const at = nowIso(options);
|
|
387
388
|
const steps = [...current.steps];
|
|
388
389
|
steps[stepIndex] = {
|
|
389
390
|
...step,
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
391
|
+
// Work completion proves machine delivery only. The Plan step remains in
|
|
392
|
+
// semantic validation until an explicit Controller acceptance records it.
|
|
393
|
+
// Failed/cancelled Work returns the slice to ready for replanning.
|
|
394
|
+
status: delivered ? 'validating' : 'ready',
|
|
395
|
+
evidenceRefs: input.work.evidenceRefs.length > 0 ? input.work.evidenceRefs.slice(0, 20) : step.evidenceRefs,
|
|
394
396
|
};
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
397
|
+
return { ...current, status: delivered ? 'verifying' : 'replanning', steps, updatedAt: at };
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export function acceptPlanStepEvidence(
|
|
402
|
+
options: PlanContractStoreOptions,
|
|
403
|
+
input: { planId: string; stepId: string; reviewer: string; rationale: string },
|
|
404
|
+
): PlanContract {
|
|
405
|
+
const reviewer = input.reviewer.trim();
|
|
406
|
+
const rationale = input.rationale.trim();
|
|
407
|
+
if (!reviewer || !rationale) throw new Error('PLAN_STEP_SEMANTIC_ACCEPTANCE_METADATA_REQUIRED');
|
|
408
|
+
return updatePlanContract(options, input.planId, (current) => {
|
|
409
|
+
const stepIndex = current.steps.findIndex((step) => step.id === sanitizeFileComponent(input.stepId));
|
|
410
|
+
if (stepIndex < 0) throw new Error(`PLAN_STEP_NOT_FOUND: ${input.stepId}`);
|
|
411
|
+
const step = current.steps[stepIndex];
|
|
412
|
+
if (step.status === 'completed') return current;
|
|
413
|
+
if (step.status !== 'validating') throw new Error(`PLAN_STEP_NOT_READY_FOR_SEMANTIC_ACCEPTANCE: ${step.id} is ${step.status}`);
|
|
414
|
+
const steps = [...current.steps];
|
|
415
|
+
steps[stepIndex] = {
|
|
416
|
+
...step,
|
|
417
|
+
status: 'completed',
|
|
418
|
+
evidenceRefs: [{ title: 'semantic acceptance', summary: `${reviewer}: ${rationale}`, detailLevel: 'summary' as const }, ...step.evidenceRefs].slice(0, 20),
|
|
401
419
|
};
|
|
420
|
+
const allCompleted = steps.every((candidate) => candidate.status === 'completed');
|
|
421
|
+
return { ...current, status: allCompleted ? 'finalized' : 'executing', steps, updatedAt: nowIso(options) };
|
|
402
422
|
});
|
|
403
423
|
}
|
|
@@ -28,7 +28,9 @@ export interface RequirementBoardItem {
|
|
|
28
28
|
maintenanceSummary?: string;
|
|
29
29
|
blocker?: string;
|
|
30
30
|
requiredUserDecision?: string;
|
|
31
|
+
/** Compatibility projection for older UI clients; derived from activePlanIds. */
|
|
31
32
|
activePlanId?: string;
|
|
33
|
+
activePlanIds: string[];
|
|
32
34
|
latestDelivery?: {
|
|
33
35
|
workId: string;
|
|
34
36
|
receiptId: string;
|
|
@@ -55,10 +57,14 @@ function explicitUserDecision(requirement: Requirement): string | undefined {
|
|
|
55
57
|
return decision || undefined;
|
|
56
58
|
}
|
|
57
59
|
|
|
58
|
-
function
|
|
60
|
+
function activePlansForRequirement(requirementId: string, plans: readonly PlanContract[]): PlanContract[] {
|
|
61
|
+
return plans.filter((plan) => plan.requirementId === requirementId);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function projectedRequirementState(requirement: Requirement, activePlans: readonly PlanContract[]): RequirementState {
|
|
59
65
|
if (requirement.state !== 'waiting_for_user') return requirement.state;
|
|
60
66
|
if (explicitUserDecision(requirement)) return 'waiting_for_user';
|
|
61
|
-
return
|
|
67
|
+
return activePlans.length > 0 ? 'active' : 'planned';
|
|
62
68
|
}
|
|
63
69
|
|
|
64
70
|
function loadScopedWork(options: RequirementBoardOptions): WorkContract[] {
|
|
@@ -92,8 +98,10 @@ function latestDelivery(requirementId: string, work: readonly WorkContract[]): R
|
|
|
92
98
|
};
|
|
93
99
|
}
|
|
94
100
|
|
|
95
|
-
function projectRequirement(requirement: Requirement, work: readonly WorkContract[]): RequirementBoardItem {
|
|
96
|
-
const
|
|
101
|
+
function projectRequirement(requirement: Requirement, work: readonly WorkContract[], plans: readonly PlanContract[] = []): RequirementBoardItem {
|
|
102
|
+
const activePlans = activePlansForRequirement(requirement.requirementId, plans);
|
|
103
|
+
const activePlanIds = activePlans.map((plan) => plan.planId);
|
|
104
|
+
const state = projectedRequirementState(requirement, activePlans);
|
|
97
105
|
const requiredUserDecision = explicitUserDecision(requirement);
|
|
98
106
|
const maintenanceSummary = requirement.needsAttention
|
|
99
107
|
? boundedText(requirement.attentionSummary, 1_000) || 'This Requirement has a maintenance finding.'
|
|
@@ -108,7 +116,8 @@ function projectRequirement(requirement: Requirement, work: readonly WorkContrac
|
|
|
108
116
|
maintenanceSummary,
|
|
109
117
|
blocker: requirement.needsAttention && state !== 'waiting_for_user' ? maintenanceSummary : undefined,
|
|
110
118
|
requiredUserDecision,
|
|
111
|
-
activePlanId:
|
|
119
|
+
activePlanId: activePlanIds[0],
|
|
120
|
+
activePlanIds,
|
|
112
121
|
latestDelivery: latestDelivery(requirement.requirementId, work),
|
|
113
122
|
updatedAt: requirement.updatedAt,
|
|
114
123
|
detailPointer: {
|
|
@@ -132,7 +141,10 @@ function requirementCounts(requirements: readonly RequirementBoardItem[]): Recor
|
|
|
132
141
|
|
|
133
142
|
export function buildRequirementBoard(options: RequirementBoardOptions): Record<string, unknown> {
|
|
134
143
|
const work = loadScopedWork(options);
|
|
135
|
-
const
|
|
144
|
+
const activePlans = options.repoId?.trim()
|
|
145
|
+
? listPlanContracts({ controllerHome: options.controllerHome, repoId: options.repoId, status: 'active', limit: 100 })
|
|
146
|
+
: [];
|
|
147
|
+
const allRequirements = loadRequirements(options).map((requirement) => projectRequirement(requirement, work, activePlans));
|
|
136
148
|
const requirements = allRequirements.slice(0, DEFAULT_REQUIREMENT_LIMIT);
|
|
137
149
|
const counts = requirementCounts(allRequirements);
|
|
138
150
|
return {
|
|
@@ -229,9 +241,13 @@ export function buildExecutionDiagnostics(
|
|
|
229
241
|
: [];
|
|
230
242
|
const scopedPlans = options.requirementId
|
|
231
243
|
? allPlans.filter((plan) => plan.requirementId === options.requirementId
|
|
232
|
-
|
|
244
|
+
// Legacy migration compatibility only: current Plans own the forward
|
|
245
|
+
// requirementId relation, so the persisted Requirement pointer is not an
|
|
246
|
+
// execution/lifecycle authority.
|
|
247
|
+
|| (!plan.requirementId && selectedRequirements.some((requirement) => requirement.activePlanId === plan.planId)))
|
|
233
248
|
: allPlans;
|
|
234
|
-
const
|
|
249
|
+
const activePlans = allPlans.filter((plan) => !['finalized', 'superseded', 'cancelled', 'invalidated_by_drift'].includes(plan.status));
|
|
250
|
+
const requirements = selectedRequirements.map((requirement) => projectRequirement(requirement, allWork, activePlans));
|
|
235
251
|
const works = scopedWork.slice(0, limit).map(summarizeDiagnosticWork);
|
|
236
252
|
const plans = scopedPlans.slice(0, limit).map((plan) => summarizeDiagnosticPlan(plan, options.detailLevel));
|
|
237
253
|
const maintenanceFindings = requirements
|
|
@@ -156,18 +156,35 @@ export interface LocalEffectCompletionReceipt {
|
|
|
156
156
|
recordedAt: string;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
/** Durable completion authority for one typed remote plugin effect. */
|
|
160
|
+
export interface RemoteEffectCompletionReceipt {
|
|
161
|
+
schemaVersion: 1;
|
|
162
|
+
/** Reuse the authoritative PLG-* receipt id rather than manufacturing parallel evidence. */
|
|
163
|
+
receiptId: string;
|
|
164
|
+
source: 'remote_effect';
|
|
165
|
+
workId: string;
|
|
166
|
+
pluginId: string;
|
|
167
|
+
actionId: string;
|
|
168
|
+
requestId: string;
|
|
169
|
+
semanticKey: string;
|
|
170
|
+
resultDigest: string;
|
|
171
|
+
recordedAt: string;
|
|
172
|
+
}
|
|
173
|
+
|
|
159
174
|
/**
|
|
160
175
|
* Completion authority for a standalone Direct Edit that is not bound to the
|
|
161
|
-
* retired Issue/Task completion model.
|
|
162
|
-
* edit session
|
|
163
|
-
*
|
|
176
|
+
* retired Issue/Task completion model. Normal completion proves one finalized
|
|
177
|
+
* edit session at an exact reachable Git revision. Historical completion may
|
|
178
|
+
* instead reference one explicit reviewed reconciliation after exact revision,
|
|
179
|
+
* path-scope, verification, reachability, and cleanup proof checks.
|
|
164
180
|
*/
|
|
165
181
|
export interface DirectEditWorkCompletionReceipt {
|
|
166
182
|
schemaVersion: 1;
|
|
167
183
|
receiptId: string;
|
|
168
184
|
source: 'direct_edit_work';
|
|
169
185
|
workId: string;
|
|
170
|
-
editSessionId
|
|
186
|
+
editSessionId?: string;
|
|
187
|
+
reconciliationId?: string;
|
|
171
188
|
targetBranch: string;
|
|
172
189
|
targetRevision: string;
|
|
173
190
|
sourceRevision?: string;
|
|
@@ -179,12 +196,18 @@ export interface DirectEditWorkCompletionReceipt {
|
|
|
179
196
|
recordedAt: string;
|
|
180
197
|
}
|
|
181
198
|
|
|
182
|
-
export type WorkCompletionReceipt = RepositoryCompletionReceipt | DirectEditWorkCompletionReceipt | LocalEffectCompletionReceipt;
|
|
199
|
+
export type WorkCompletionReceipt = RepositoryCompletionReceipt | DirectEditWorkCompletionReceipt | LocalEffectCompletionReceipt | RemoteEffectCompletionReceipt;
|
|
183
200
|
|
|
184
201
|
export function isRepositoryCompletionReceipt(
|
|
185
202
|
receipt: WorkCompletionReceipt,
|
|
186
203
|
): receipt is RepositoryCompletionReceipt {
|
|
187
|
-
return receipt.source !== 'local_effect' && receipt.source !== 'direct_edit_work';
|
|
204
|
+
return receipt.source !== 'local_effect' && receipt.source !== 'remote_effect' && receipt.source !== 'direct_edit_work';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function isRemoteEffectCompletionReceipt(
|
|
208
|
+
receipt: WorkCompletionReceipt,
|
|
209
|
+
): receipt is RemoteEffectCompletionReceipt {
|
|
210
|
+
return receipt.source === 'remote_effect';
|
|
188
211
|
}
|
|
189
212
|
|
|
190
213
|
export function isDirectEditWorkCompletionReceipt(
|
|
@@ -431,6 +454,10 @@ export interface WorkContract {
|
|
|
431
454
|
phaseEvidence: WorkPhaseEvidenceMap;
|
|
432
455
|
/** Explicit v2 execution semantics. `status` is retained for compatibility. */
|
|
433
456
|
workKind: WorkKind;
|
|
457
|
+
/** Primary is an objective-level business execution lane; execution_child is a resumable low-level operation handle owned by a primary Work or standalone caller. */
|
|
458
|
+
lifecycleRole?: 'primary' | 'execution_child';
|
|
459
|
+
/** Optional objective-level parent when this Work is only an execution child. */
|
|
460
|
+
parentWorkId?: string;
|
|
434
461
|
dispatchState: DispatchState;
|
|
435
462
|
evidenceState: EvidenceState;
|
|
436
463
|
completionOutcome?: CompletionOutcome;
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
type WorkPhaseEvidenceState,
|
|
33
33
|
type WorkContractStore,
|
|
34
34
|
isDirectEditWorkCompletionReceipt,
|
|
35
|
+
isRemoteEffectCompletionReceipt,
|
|
35
36
|
isRepositoryCompletionReceipt,
|
|
36
37
|
isTerminalWorkContractStatus,
|
|
37
38
|
} from './types';
|
|
@@ -195,7 +196,7 @@ function legacyPhaseEvidence(
|
|
|
195
196
|
state,
|
|
196
197
|
source: completedByReceipt ? 'recorded' : source,
|
|
197
198
|
summary: completedByReceipt
|
|
198
|
-
? `Phase ${phase}
|
|
199
|
+
? `Phase ${phase} satisfied by Work completion receipt ${contract.completionReceipt!.receiptId}.`
|
|
199
200
|
: index < currentIndex
|
|
200
201
|
? `Legacy Work advanced beyond ${phase}.`
|
|
201
202
|
: `Legacy Work phase ${phase} is ${state}.`,
|
|
@@ -285,13 +286,20 @@ function validateWorkSemantics(contract: WorkContract): WorkContract {
|
|
|
285
286
|
if (receipt.delivery.status !== 'integrated' || !receipt.delivery.reachable) throw new Error('WORK_COMPLETION_RECEIPT_DELIVERY_NOT_PROVEN');
|
|
286
287
|
if (!['complete', 'maintenance_warning'].includes(receipt.cleanup.status) || receipt.cleanup.blockers.length > 0) throw new Error('WORK_COMPLETION_RECEIPT_CLEANUP_NOT_PROVEN');
|
|
287
288
|
} else if (isDirectEditWorkCompletionReceipt(receipt)) {
|
|
288
|
-
|
|
289
|
+
const directEditAuthorityCount = Number(Boolean(receipt.editSessionId?.trim())) + Number(Boolean(receipt.reconciliationId?.trim()));
|
|
290
|
+
if (directEditAuthorityCount !== 1) throw new Error('WORK_COMPLETION_RECEIPT_DIRECT_EDIT_AUTHORITY_REQUIRED');
|
|
289
291
|
if (!receipt.targetBranch.trim() || !receipt.targetRevision.trim()) throw new Error('WORK_COMPLETION_RECEIPT_TARGET_REQUIRED');
|
|
290
292
|
if (receipt.delivery.status !== 'integrated' || !receipt.delivery.reachable) throw new Error('WORK_COMPLETION_RECEIPT_DELIVERY_NOT_PROVEN');
|
|
291
293
|
if (!['complete', 'maintenance_warning'].includes(receipt.cleanup.status) || receipt.cleanup.blockers.length > 0) throw new Error('WORK_COMPLETION_RECEIPT_CLEANUP_NOT_PROVEN');
|
|
292
294
|
if (!['repository_change', 'completed_no_change'].includes(contract.workKind)) throw new Error('WORK_COMPLETION_RECEIPT_DIRECT_EDIT_KIND_REQUIRED');
|
|
293
295
|
const expectedOutcome = contract.workKind === 'completed_no_change' ? 'completed_no_change' : 'completed_changed';
|
|
294
296
|
if (contract.completionOutcome !== expectedOutcome) throw new Error('WORK_COMPLETION_RECEIPT_DIRECT_EDIT_OUTCOME_REQUIRED');
|
|
297
|
+
} else if (isRemoteEffectCompletionReceipt(receipt)) {
|
|
298
|
+
if (contract.workKind !== 'remote_effect') throw new Error('WORK_COMPLETION_RECEIPT_REMOTE_EFFECT_KIND_REQUIRED');
|
|
299
|
+
if (contract.completionOutcome !== 'completed_remote') throw new Error('WORK_COMPLETION_RECEIPT_REMOTE_EFFECT_OUTCOME_REQUIRED');
|
|
300
|
+
if (!receipt.pluginId.trim() || !receipt.actionId.trim() || !receipt.requestId.trim() || !receipt.semanticKey.trim() || !receipt.resultDigest.trim()) {
|
|
301
|
+
throw new Error('WORK_COMPLETION_RECEIPT_REMOTE_EFFECT_IDENTITY_REQUIRED');
|
|
302
|
+
}
|
|
295
303
|
} else {
|
|
296
304
|
if (contract.workKind !== 'local_effect') throw new Error('WORK_COMPLETION_RECEIPT_LOCAL_EFFECT_KIND_REQUIRED');
|
|
297
305
|
if (contract.completionOutcome !== 'completed_local') throw new Error('WORK_COMPLETION_RECEIPT_LOCAL_EFFECT_OUTCOME_REQUIRED');
|
|
@@ -335,6 +343,9 @@ function validateWorkSemantics(contract: WorkContract): WorkContract {
|
|
|
335
343
|
if (outcome === 'completed_remote' && contract.workKind !== 'remote_effect') {
|
|
336
344
|
throw new Error('WORK_SEMANTICS_INVALID: completed_remote requires remote_effect WorkKind');
|
|
337
345
|
}
|
|
346
|
+
if (outcome === 'completed_remote' && contract.completionReceipt?.source !== 'remote_effect') {
|
|
347
|
+
throw new Error('WORK_SEMANTICS_INVALID: completed_remote requires a remote_effect completion receipt');
|
|
348
|
+
}
|
|
338
349
|
if (outcome === 'superseded' && contract.workKind !== 'superseded') {
|
|
339
350
|
throw new Error('WORK_SEMANTICS_INVALID: superseded requires superseded WorkKind');
|
|
340
351
|
}
|
|
@@ -372,6 +383,12 @@ function validateWorkSemanticTransition(current: WorkContract, next: WorkContrac
|
|
|
372
383
|
if (current.completionOutcome && current.completionOutcome !== next.completionOutcome) {
|
|
373
384
|
throw new Error(`WORK_SEMANTICS_TRANSITION_INVALID: completion outcome ${current.completionOutcome} is immutable`);
|
|
374
385
|
}
|
|
386
|
+
if ((current.lifecycleRole ?? 'primary') !== (next.lifecycleRole ?? 'primary')) {
|
|
387
|
+
throw new Error('WORK_SEMANTICS_TRANSITION_INVALID: lifecycleRole is immutable');
|
|
388
|
+
}
|
|
389
|
+
if (current.parentWorkId !== next.parentWorkId) {
|
|
390
|
+
throw new Error('WORK_SEMANTICS_TRANSITION_INVALID: parentWorkId is immutable');
|
|
391
|
+
}
|
|
375
392
|
return next;
|
|
376
393
|
}
|
|
377
394
|
|
|
@@ -544,6 +561,8 @@ export function createWorkContract(options: WorkContractStoreOptions, input: Cre
|
|
|
544
561
|
constraints: input.constraints ?? { requireHandoffOnAmbiguity: true },
|
|
545
562
|
risk: input.risk ?? 'medium',
|
|
546
563
|
workKind: input.workKind ?? 'repository_change',
|
|
564
|
+
lifecycleRole: input.lifecycleRole ?? 'primary',
|
|
565
|
+
parentWorkId: input.parentWorkId?.trim() || undefined,
|
|
547
566
|
dispatchState: input.dispatchState ?? inferredDispatchState(input.status ?? 'open'),
|
|
548
567
|
evidenceState: input.evidenceState ?? inferredEvidenceState(input.status ?? 'open'),
|
|
549
568
|
completionOutcome: input.completionOutcome,
|
|
@@ -631,6 +650,7 @@ function workRequestIndexPath(controllerHome: string, requestId: string): string
|
|
|
631
650
|
export interface AcceptSubmittedWorkInput {
|
|
632
651
|
requestId: string;
|
|
633
652
|
repoId: string;
|
|
653
|
+
parentWorkId?: string;
|
|
634
654
|
semanticKey: string;
|
|
635
655
|
operation: SubmittedWorkOperation;
|
|
636
656
|
objective?: string;
|
|
@@ -690,6 +710,13 @@ export function acceptSubmittedWorkContract(
|
|
|
690
710
|
if (!input.operation?.name?.trim()) {
|
|
691
711
|
throw new Error('INVALID_ARGUMENT: work_submit is missing required argument(s): operation');
|
|
692
712
|
}
|
|
713
|
+
const parentWorkId = input.parentWorkId?.trim() || undefined;
|
|
714
|
+
if (parentWorkId) {
|
|
715
|
+
const parent = getWorkContract({ controllerHome: home, repoId: input.repoId, now: options.now }, parentWorkId);
|
|
716
|
+
if (!parent) throw new Error(`PARENT_WORK_NOT_FOUND: ${parentWorkId}`);
|
|
717
|
+
if (isTerminalWorkContractStatus(parent.status)) throw new Error(`PARENT_WORK_TERMINAL: ${parentWorkId}:${parent.status}`);
|
|
718
|
+
if ((parent.lifecycleRole ?? 'primary') !== 'primary') throw new Error(`PARENT_WORK_NOT_PRIMARY: ${parentWorkId}`);
|
|
719
|
+
}
|
|
693
720
|
|
|
694
721
|
const requestLockId = createHash('sha256').update(normalizedRequestId).digest('hex').slice(0, 24);
|
|
695
722
|
return withControllerLock(home, { scope: 'global', resource: `work-request-${requestLockId}` }, `accept-work:${normalizedRequestId}`, () => {
|
|
@@ -715,6 +742,8 @@ export function acceptSubmittedWorkContract(
|
|
|
715
742
|
principalId: input.principalId,
|
|
716
743
|
controllerInstanceId: input.controllerInstanceId,
|
|
717
744
|
mode: input.mode ?? 'direct_control',
|
|
745
|
+
lifecycleRole: 'execution_child',
|
|
746
|
+
parentWorkId,
|
|
718
747
|
objective: (input.objective ?? `Accepted operation ${input.operation.name}`).slice(0, 2_000),
|
|
719
748
|
acceptanceCriteria: input.acceptanceCriteria ?? [],
|
|
720
749
|
workKind: input.workKind,
|