@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.
- package/AGENTS.md +2 -0
- package/CHANGELOG.md +26 -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 +125 -101
- 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 +69 -29
- 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 +150 -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 +815 -1015
- 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 +3 -2
- package/src/runtime/root/package-runtime-service.ts +48 -3
- 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
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
createWorkContract,
|
|
13
13
|
getWorkContract,
|
|
14
14
|
listWorkContracts,
|
|
15
|
-
recordWorkCompletionReceipt,
|
|
16
15
|
recordWorkScopeEvidence,
|
|
17
16
|
summarizeWorkContract,
|
|
18
17
|
transitionWorkContractPhase,
|
|
@@ -26,6 +25,8 @@ import {
|
|
|
26
25
|
type PlanContractStoreOptions,
|
|
27
26
|
} from './plan-contract-store';
|
|
28
27
|
import { withPrimaryWorkAdmissionLock } from './semantic-admission';
|
|
28
|
+
import { completeWorkWithReceipt } from '../execution/work-completion-authority';
|
|
29
|
+
import { evaluateReadOnlyReviewSourceIdentity, evaluateWorkCompletionEvidence, evaluateWorkImplementationEvidence, verificationRecordAppliesToCurrentWorkspace } from '../execution/work-evidence-policy';
|
|
29
30
|
import { readRequirement } from '../persistence/requirement-store';
|
|
30
31
|
import {
|
|
31
32
|
classifyVerificationOutcome,
|
|
@@ -51,6 +52,7 @@ import type {
|
|
|
51
52
|
WorkRisk,
|
|
52
53
|
} from './types';
|
|
53
54
|
import { selectExecutionMode } from './types';
|
|
55
|
+
import { resolveWorkspaceAdmissionConstraint } from '../routing/workspace-admission';
|
|
54
56
|
|
|
55
57
|
export type GoalWorkloopOperation = 'start' | 'continue' | 'verify' | 'finalize' | 'stop';
|
|
56
58
|
|
|
@@ -67,6 +69,8 @@ export interface GoalWorkloopContext {
|
|
|
67
69
|
workspaceFingerprint?: string;
|
|
68
70
|
/** Current net repository paths changed from the Work base plus dirty checkout paths. */
|
|
69
71
|
workspaceChangedPaths?: readonly string[];
|
|
72
|
+
/** Durable terminal repository Process ids explicitly bound to this Work + checkout. */
|
|
73
|
+
workBoundProcessEvidenceIds?: readonly string[];
|
|
70
74
|
now?: () => string;
|
|
71
75
|
/** True only while a Gateway caller holds the cross-process primary Work admission lock. */
|
|
72
76
|
semanticAdmissionLocked?: boolean;
|
|
@@ -97,7 +101,13 @@ export interface GoalWorkloopStartInput {
|
|
|
97
101
|
planId?: string;
|
|
98
102
|
planStepId?: string;
|
|
99
103
|
/** Explicit technical evidence shape chosen by the semantic Controller. Never inferred from objective/check text. */
|
|
100
|
-
workKind?: Extract<WorkKind, 'repository_change' | 'completed_no_change' | 'investigation' | 'local_effect' | 'remote_effect' | 'reconciliation'>;
|
|
104
|
+
workKind?: Extract<WorkKind, 'repository_change' | 'completed_no_change' | 'read_only_review' | 'investigation' | 'local_effect' | 'remote_effect' | 'reconciliation'>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface GoalWorkloopAcceptanceEvidenceBinding {
|
|
108
|
+
criterion: string;
|
|
109
|
+
evidenceIds: string[];
|
|
110
|
+
rationale: string;
|
|
101
111
|
}
|
|
102
112
|
|
|
103
113
|
export interface GoalWorkloopContinueInput {
|
|
@@ -108,6 +118,10 @@ export interface GoalWorkloopContinueInput {
|
|
|
108
118
|
checks?: string[];
|
|
109
119
|
additionalLikelyPaths?: string[];
|
|
110
120
|
inspectedPaths?: string[];
|
|
121
|
+
/** Concise semantic findings discovered by a first-class read-only review. */
|
|
122
|
+
reviewFindings?: string[];
|
|
123
|
+
/** Explicit Controller-reviewed bindings from exact declared criteria to durable Work evidence. */
|
|
124
|
+
acceptanceEvidence?: GoalWorkloopAcceptanceEvidenceBinding[];
|
|
111
125
|
/** Explicit Controller decision after deterministic acceptance failure; never inferred from note text. */
|
|
112
126
|
acceptanceFailureDecision?: 'repair' | 'rescope';
|
|
113
127
|
}
|
|
@@ -163,6 +177,7 @@ function handoffIdFor(prefix: string): string {
|
|
|
163
177
|
}
|
|
164
178
|
|
|
165
179
|
function workRiskFor(input: GoalWorkloopStartInput): WorkRisk {
|
|
180
|
+
if (input.workKind === 'read_only_review') return 'readonly';
|
|
166
181
|
const risk = input.modeInput.risk;
|
|
167
182
|
if (input.modeInput.secretAccess === true || input.modeInput.destructive === true) return 'destructive';
|
|
168
183
|
if (input.modeInput.remoteWrite === true) return 'high';
|
|
@@ -217,126 +232,6 @@ function initialEvidence(objective: string): EvidenceRef {
|
|
|
217
232
|
};
|
|
218
233
|
}
|
|
219
234
|
|
|
220
|
-
type ReconciledVerificationHistory = ReturnType<typeof reconcileVerificationHistory>;
|
|
221
|
-
|
|
222
|
-
interface WorkCompletionEvidenceEvaluation {
|
|
223
|
-
status: 'complete' | 'failed' | 'incomplete';
|
|
224
|
-
history: ReconciledVerificationHistory;
|
|
225
|
-
missingChecks: string[];
|
|
226
|
-
durableResultEvidence: boolean;
|
|
227
|
-
reasons: string[];
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function verificationRecordAppliesToCurrentWorkspace(
|
|
231
|
-
record: VerificationRecord,
|
|
232
|
-
currentRevision?: string,
|
|
233
|
-
currentWorkspaceFingerprint?: string,
|
|
234
|
-
): boolean {
|
|
235
|
-
if (currentRevision && record.sourceRevision && record.sourceRevision !== currentRevision) return false;
|
|
236
|
-
if (
|
|
237
|
-
currentWorkspaceFingerprint
|
|
238
|
-
&& record.workspaceFingerprint
|
|
239
|
-
&& record.workspaceFingerprint !== currentWorkspaceFingerprint
|
|
240
|
-
) return false;
|
|
241
|
-
return true;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function isAuthoritativeCurrentWorkVerification(
|
|
245
|
-
work: WorkContract,
|
|
246
|
-
record: VerificationRecord,
|
|
247
|
-
currentRevision?: string,
|
|
248
|
-
): boolean {
|
|
249
|
-
const receipt = record.receipt;
|
|
250
|
-
return Boolean(
|
|
251
|
-
currentRevision
|
|
252
|
-
&& record.outcome === 'valid_pass'
|
|
253
|
-
&& record.sourceRevision === currentRevision
|
|
254
|
-
&& receipt
|
|
255
|
-
&& receipt.repoId === work.repoId
|
|
256
|
-
&& receipt.workId === work.workId
|
|
257
|
-
&& receipt.checkId === record.checkId
|
|
258
|
-
&& receipt.ok === true
|
|
259
|
-
&& receipt.timedOut === false
|
|
260
|
-
&& receipt.cancelled === false,
|
|
261
|
-
);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
interface WorkImplementationEvidenceEvaluation {
|
|
265
|
-
status: 'complete' | 'incomplete';
|
|
266
|
-
changedPaths: string[];
|
|
267
|
-
reasons: string[];
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function evaluateWorkImplementationEvidence(
|
|
271
|
-
work: WorkContract,
|
|
272
|
-
currentWorkspaceChangedPaths: readonly string[] = [],
|
|
273
|
-
): WorkImplementationEvidenceEvaluation {
|
|
274
|
-
if (work.workKind !== 'repository_change') {
|
|
275
|
-
return { status: 'complete', changedPaths: [], reasons: [] };
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
const changedPaths = [...new Set(currentWorkspaceChangedPaths.map((path) => path.trim()).filter(Boolean))].sort();
|
|
279
|
-
if (changedPaths.length > 0) {
|
|
280
|
-
return { status: 'complete', changedPaths, reasons: [] };
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
return {
|
|
284
|
-
status: 'incomplete',
|
|
285
|
-
changedPaths: [],
|
|
286
|
-
reasons: ['Repository-change Work has no current net source changes relative to its base revision. Verification evidence cannot substitute for implementation evidence.'],
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function evaluateWorkCompletionEvidence(
|
|
291
|
-
work: WorkContract,
|
|
292
|
-
currentRevision?: string,
|
|
293
|
-
currentWorkspaceFingerprint?: string,
|
|
294
|
-
): WorkCompletionEvidenceEvaluation {
|
|
295
|
-
const applicableCheckRefs = work.checkRefs.filter((record) =>
|
|
296
|
-
verificationRecordAppliesToCurrentWorkspace(record, currentRevision, currentWorkspaceFingerprint));
|
|
297
|
-
const history = reconcileVerificationHistory(
|
|
298
|
-
applicableCheckRefs.map((record) => ({ checkId: record.checkId, outcome: record.outcome, recordedAt: record.recordedAt })),
|
|
299
|
-
);
|
|
300
|
-
const missingChecks = work.checks.filter((checkId) => !history.validPasses.includes(checkId));
|
|
301
|
-
const durableResultEvidence = work.evidenceRefs.some((evidence) => Boolean(evidence.evidenceId || evidence.artifactId))
|
|
302
|
-
|| applicableCheckRefs.some((record) => isAuthoritativeCurrentWorkVerification(work, record, currentRevision));
|
|
303
|
-
const reasons: string[] = [];
|
|
304
|
-
|
|
305
|
-
if (history.acceptanceFailures.length > 0) {
|
|
306
|
-
reasons.push(`Acceptance checks failed: ${history.acceptanceFailures.join(', ')}.`);
|
|
307
|
-
return { status: 'failed', history, missingChecks, durableResultEvidence, reasons };
|
|
308
|
-
}
|
|
309
|
-
if (history.infrastructureIssues.length > 0) {
|
|
310
|
-
reasons.push(`Infrastructure issues remain: ${history.infrastructureIssues.join(', ')}.`);
|
|
311
|
-
}
|
|
312
|
-
if (history.invalidCheckIds.length > 0) {
|
|
313
|
-
reasons.push(`Invalid check ids remain: ${history.invalidCheckIds.join(', ')}.`);
|
|
314
|
-
}
|
|
315
|
-
if (missingChecks.length > 0) {
|
|
316
|
-
reasons.push(`Declared checks are missing valid_pass evidence: ${missingChecks.join(', ')}.`);
|
|
317
|
-
}
|
|
318
|
-
if (work.checks.length === 0 && !durableResultEvidence) {
|
|
319
|
-
const staleWorkVerification = work.checkRefs.some((record) =>
|
|
320
|
-
Boolean(record.receipt)
|
|
321
|
-
&& !verificationRecordAppliesToCurrentWorkspace(record, currentRevision, currentWorkspaceFingerprint));
|
|
322
|
-
reasons.push(staleWorkVerification
|
|
323
|
-
? 'Work-bound verification evidence is stale for the current source/workspace identity.'
|
|
324
|
-
: 'No durable result evidence (evidenceId, artifactId, or current Work-bound verification receipt) was recorded for this no-check WorkContract.');
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
const complete = history.infrastructureIssues.length === 0
|
|
328
|
-
&& history.invalidCheckIds.length === 0
|
|
329
|
-
&& missingChecks.length === 0
|
|
330
|
-
&& (work.checks.length > 0 || durableResultEvidence);
|
|
331
|
-
return {
|
|
332
|
-
status: complete ? 'complete' : 'incomplete',
|
|
333
|
-
history,
|
|
334
|
-
missingChecks,
|
|
335
|
-
durableResultEvidence,
|
|
336
|
-
reasons,
|
|
337
|
-
};
|
|
338
|
-
}
|
|
339
|
-
|
|
340
235
|
/**
|
|
341
236
|
* Mode selection for facade routing. Direct control never creates a WorkContract.
|
|
342
237
|
* Goal workloop creates one. Handoff-only creates a handoff and stops.
|
|
@@ -345,14 +240,74 @@ export function routeWorkStart(
|
|
|
345
240
|
ctx: GoalWorkloopContext,
|
|
346
241
|
input: GoalWorkloopStartInput,
|
|
347
242
|
): FacadeResult {
|
|
243
|
+
const placementResolution = resolveWorkspaceAdmissionConstraint(input.constraints);
|
|
244
|
+
if (placementResolution.ok === false) {
|
|
245
|
+
return buildFacadeResult({
|
|
246
|
+
status: 'blocked',
|
|
247
|
+
summary: `${placementResolution.code}: ${placementResolution.message}`,
|
|
248
|
+
data: { executionStarted: false, workContractCreated: false, placementConstraintConflict: true },
|
|
249
|
+
rawAvailable: false,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
const placementConstraint = placementResolution.constraint;
|
|
253
|
+
const canonicalConstraints: WorkContract['constraints'] = {
|
|
254
|
+
...(input.constraints ?? {}),
|
|
255
|
+
workspaceMode: placementConstraint.workspaceMode,
|
|
256
|
+
requireWorktree: placementConstraint.requireWorktree,
|
|
257
|
+
directMainProhibited: placementConstraint.directMainProhibited,
|
|
258
|
+
};
|
|
348
259
|
const strategyConflictRequiresApproval = input.constraints?.architectureStrategyChange === true
|
|
349
260
|
|| input.constraints?.conflictsWithThinHarnessPolicy === true
|
|
350
261
|
|| input.constraints?.changesDefaultExecutionStrategy === true;
|
|
262
|
+
const relatedLifecycleSource = input.relatedWorkId ? getWorkContract(ctx.workStore, input.relatedWorkId) : undefined;
|
|
263
|
+
const inheritedReadOnlyReview = Boolean(
|
|
264
|
+
input.workKind === undefined
|
|
265
|
+
&& input.workRelation === 'new_goal'
|
|
266
|
+
&& relatedLifecycleSource?.workKind === 'read_only_review'
|
|
267
|
+
&& (relatedLifecycleSource.status === 'cancelled' || relatedLifecycleSource.status === 'completed' || relatedLifecycleSource.status === 'failed'),
|
|
268
|
+
);
|
|
269
|
+
if (
|
|
270
|
+
inheritedReadOnlyReview
|
|
271
|
+
&& relatedLifecycleSource?.baseRevision
|
|
272
|
+
&& ctx.sourceRevision
|
|
273
|
+
&& relatedLifecycleSource.baseRevision !== ctx.sourceRevision
|
|
274
|
+
) {
|
|
275
|
+
return buildFacadeResult({
|
|
276
|
+
status: 'blocked',
|
|
277
|
+
summary: `READ_ONLY_REVIEW_REPLACEMENT_SOURCE_DRIFT: replacement source ${ctx.sourceRevision} does not match frozen review base ${relatedLifecycleSource.baseRevision}.`,
|
|
278
|
+
data: { executionStarted: false, workContractCreated: false, relatedWorkId: relatedLifecycleSource.workId, sourceDrift: true },
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
const requestedWorkKind = input.workKind ?? (inheritedReadOnlyReview ? 'read_only_review' : undefined);
|
|
282
|
+
const typedReadOnlyReviewRequested = requestedWorkKind === undefined
|
|
283
|
+
&& (input.modeInput.mutation === false || (input.modeInput.mutation === undefined && input.modeInput.risk === 'readonly'))
|
|
284
|
+
&& input.modeInput.requiresInvestigation === true
|
|
285
|
+
&& input.modeInput.requiresRecovery === true;
|
|
286
|
+
const effectiveWorkKind = requestedWorkKind ?? (typedReadOnlyReviewRequested ? 'read_only_review' : undefined);
|
|
287
|
+
const readOnlyReviewRequested = effectiveWorkKind === 'read_only_review';
|
|
288
|
+
const readOnlyMutationConflict = readOnlyReviewRequested && (
|
|
289
|
+
input.modeInput.mutation === true
|
|
290
|
+
|| (input.modeInput.expectedFiles ?? 0) > 0
|
|
291
|
+
|| (input.modeInput.expectedChangedLines ?? 0) > 0
|
|
292
|
+
|| input.modeInput.requiresExternalEffect === true
|
|
293
|
+
|| input.modeInput.remoteWrite === true
|
|
294
|
+
|| input.modeInput.destructive === true
|
|
295
|
+
);
|
|
296
|
+
if (readOnlyMutationConflict) {
|
|
297
|
+
return buildFacadeResult({
|
|
298
|
+
status: 'blocked',
|
|
299
|
+
summary: 'READ_ONLY_REVIEW_MUTATION_CONFLICT: read_only_review cannot request repository mutation, external effects, remote writes, or destructive execution.',
|
|
300
|
+
data: { executionStarted: false, workContractCreated: false, workKind: effectiveWorkKind },
|
|
301
|
+
});
|
|
302
|
+
}
|
|
351
303
|
const effectiveModeInput: ExecutionModeSelectionInput = {
|
|
352
304
|
...input.modeInput,
|
|
353
305
|
objective: input.objective,
|
|
354
306
|
knownPaths: input.allowedPaths,
|
|
355
|
-
|
|
307
|
+
workspacePlacement: placementConstraint.workspaceMode,
|
|
308
|
+
directMainProhibited: placementConstraint.directMainProhibited,
|
|
309
|
+
mutation: readOnlyReviewRequested ? false : input.modeInput.mutation ?? input.modeInput.risk !== 'readonly',
|
|
310
|
+
risk: readOnlyReviewRequested ? 'readonly' : input.modeInput.risk,
|
|
356
311
|
approvalConfirmed: input.approvalConfirmed === true,
|
|
357
312
|
requiresUserApproval: input.approvalConfirmed === true
|
|
358
313
|
? false
|
|
@@ -376,7 +331,7 @@ export function routeWorkStart(
|
|
|
376
331
|
: selected;
|
|
377
332
|
const evaluateAccessPolicy = (selected: ReturnType<typeof selectExecutionMode>) => evaluatePolicyGate({
|
|
378
333
|
capabilityId: selected.mode === 'direct_control' ? 'repository.direct_edit' : selected.mode === 'goal_workloop' ? 'controller.goal_workloop' : 'controller.handoff_inbox',
|
|
379
|
-
risk:
|
|
334
|
+
risk: effectiveModeInput.risk
|
|
380
335
|
?? (input.modeInput.secretAccess === true ? 'raw_secret_config'
|
|
381
336
|
: input.modeInput.destructive === true ? 'destructive'
|
|
382
337
|
: input.modeInput.remoteWrite === true ? 'remote_write'
|
|
@@ -396,12 +351,24 @@ export function routeWorkStart(
|
|
|
396
351
|
});
|
|
397
352
|
|
|
398
353
|
let selectedMode = selectExecutionMode(effectiveModeInput);
|
|
354
|
+
if (input.forceMode === 'direct_control' && selectedMode.routeDecision.requiresIsolation) {
|
|
355
|
+
return buildFacadeResult({
|
|
356
|
+
status: 'blocked',
|
|
357
|
+
summary: 'WORKSPACE_PLACEMENT_DIRECT_CONTROL_FORBIDDEN: typed placement requires isolation, so force_mode=direct_control cannot override admission policy.',
|
|
358
|
+
data: { executionStarted: false, workContractCreated: false, routeDecision: selectedMode.routeDecision },
|
|
359
|
+
rawAvailable: false,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
399
362
|
let policy = evaluateAccessPolicy(selectedMode);
|
|
400
363
|
if (policy.decision === 'allowed' && effectiveModeInput.requiresUserApproval !== true) {
|
|
401
364
|
selectedMode = selectExecutionMode({ ...effectiveModeInput, approvalConfirmed: true });
|
|
402
365
|
policy = evaluateAccessPolicy(selectedMode);
|
|
403
366
|
}
|
|
404
367
|
let mode = applyForcedMode(selectedMode);
|
|
368
|
+
const routeApprovalRequired = mode.routeDecision.requiresApproval
|
|
369
|
+
&& mode.routeDecision.waitForUser
|
|
370
|
+
&& mode.routeDecision.approvalState !== 'blocked_by_policy';
|
|
371
|
+
const approvalRequired = policy.decision === 'approval_required' || routeApprovalRequired;
|
|
405
372
|
|
|
406
373
|
// Route Policy is the sole execution-depth authority. Existing Work is
|
|
407
374
|
// ownership context, not a reason to upgrade an otherwise Direct operation
|
|
@@ -488,7 +455,7 @@ export function routeWorkStart(
|
|
|
488
455
|
severity: policy.decision === 'denied' ? 'blocked' : 'needs_review',
|
|
489
456
|
creationReason: !input.modeInput.scopeClear
|
|
490
457
|
? 'invalid_objective'
|
|
491
|
-
:
|
|
458
|
+
: approvalRequired
|
|
492
459
|
? (input.modeInput.destructive ? 'destructive_action_requires_confirmation' : 'policy_approval_required')
|
|
493
460
|
: 'missing_authorization',
|
|
494
461
|
reason: mode.reason,
|
|
@@ -501,13 +468,13 @@ export function routeWorkStart(
|
|
|
501
468
|
},
|
|
502
469
|
attemptedActions: ['route_execution_mode'],
|
|
503
470
|
evidenceRefs: [],
|
|
504
|
-
blockingDecision:
|
|
471
|
+
blockingDecision: approvalRequired
|
|
505
472
|
? 'Approve side effects or restate a safer objective.'
|
|
506
473
|
: 'Clarify objective, scope, and acceptance criteria.',
|
|
507
474
|
recommendedDecision: 'Provide a clear objective and authorization, or cancel the request.',
|
|
508
475
|
recommendedPrompt: `Resolve handoff and restate work for repo ${ctx.repoId}.`,
|
|
509
476
|
recommendedContinuationPrompt: `After approval, start the approved work for ${ctx.repoId}.`,
|
|
510
|
-
approvalAction:
|
|
477
|
+
approvalAction: approvalRequired
|
|
511
478
|
? {
|
|
512
479
|
operation: 'start',
|
|
513
480
|
label: 'Approve and start work',
|
|
@@ -528,6 +495,9 @@ export function routeWorkStart(
|
|
|
528
495
|
requiresApproval: input.modeInput.requiresApproval === true || input.modeInput.requiresUserApproval === true,
|
|
529
496
|
destructive: input.modeInput.destructive === true,
|
|
530
497
|
accessMode: input.constraints?.accessMode,
|
|
498
|
+
workspaceMode: placementConstraint.workspaceMode,
|
|
499
|
+
requireWorktree: placementConstraint.requireWorktree,
|
|
500
|
+
directMainProhibited: placementConstraint.directMainProhibited,
|
|
531
501
|
approvalConfirmed: true,
|
|
532
502
|
forceMode: 'goal_workloop',
|
|
533
503
|
},
|
|
@@ -545,7 +515,7 @@ export function routeWorkStart(
|
|
|
545
515
|
});
|
|
546
516
|
|
|
547
517
|
return buildFacadeResult({
|
|
548
|
-
status: policy.decision === 'denied' ? 'blocked' :
|
|
518
|
+
status: policy.decision === 'denied' ? 'blocked' : approvalRequired ? 'approval_required' : 'blocked',
|
|
549
519
|
summary: `Handoff-only: no WorkContract created and no execution started. ${mode.reason}`,
|
|
550
520
|
data: {
|
|
551
521
|
mode,
|
|
@@ -575,7 +545,12 @@ export function routeWorkStart(
|
|
|
575
545
|
});
|
|
576
546
|
}
|
|
577
547
|
|
|
578
|
-
return startGoalWorkloop(ctx,
|
|
548
|
+
return startGoalWorkloop(ctx, {
|
|
549
|
+
...input,
|
|
550
|
+
constraints: canonicalConstraints,
|
|
551
|
+
modeInput: effectiveModeInput,
|
|
552
|
+
workKind: effectiveWorkKind,
|
|
553
|
+
}, policy, 'goal_workloop', mode.routeDecision);
|
|
579
554
|
}
|
|
580
555
|
|
|
581
556
|
export function startGoalWorkloop(
|
|
@@ -585,6 +560,22 @@ export function startGoalWorkloop(
|
|
|
585
560
|
executionMode: 'direct_control' | 'goal_workloop' = 'goal_workloop',
|
|
586
561
|
routeDecision = selectExecutionMode({ ...input.modeInput, objective: input.objective, knownPaths: input.allowedPaths }).routeDecision,
|
|
587
562
|
): FacadeResult {
|
|
563
|
+
const placementResolution = resolveWorkspaceAdmissionConstraint(input.constraints);
|
|
564
|
+
if (placementResolution.ok === false) {
|
|
565
|
+
return buildFacadeResult({
|
|
566
|
+
status: 'blocked',
|
|
567
|
+
summary: `${placementResolution.code}: ${placementResolution.message}`,
|
|
568
|
+
data: { executionStarted: false, workContractCreated: false, placementConstraintConflict: true },
|
|
569
|
+
rawAvailable: false,
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
const placementConstraint = placementResolution.constraint;
|
|
573
|
+
const canonicalConstraints: WorkContract['constraints'] = {
|
|
574
|
+
...(input.constraints ?? {}),
|
|
575
|
+
workspaceMode: placementConstraint.workspaceMode,
|
|
576
|
+
requireWorktree: placementConstraint.requireWorktree,
|
|
577
|
+
directMainProhibited: placementConstraint.directMainProhibited,
|
|
578
|
+
};
|
|
588
579
|
const hasStrongAdmissionBinding = Boolean(input.planId || input.planStepId || input.requirementId || input.relatedWorkId || input.workRelation);
|
|
589
580
|
if (hasStrongAdmissionBinding && !ctx.semanticAdmissionLocked) {
|
|
590
581
|
return withPrimaryWorkAdmissionLock(ctx.workStore, () => startGoalWorkloop(
|
|
@@ -597,7 +588,7 @@ export function startGoalWorkloop(
|
|
|
597
588
|
}
|
|
598
589
|
const at = nowIso(ctx);
|
|
599
590
|
const available = ctx.availableChecks ?? [];
|
|
600
|
-
const workspaceMode =
|
|
591
|
+
const workspaceMode = placementConstraint.workspaceMode;
|
|
601
592
|
const activeWorks = listWorkContracts({ ...ctx.workStore, status: 'active', limit: 100 })
|
|
602
593
|
.filter((candidate) => (candidate.lifecycleRole ?? 'primary') === 'primary');
|
|
603
594
|
const workspaceOwner = ctx.checkoutId
|
|
@@ -803,11 +794,10 @@ export function startGoalWorkloop(
|
|
|
803
794
|
}
|
|
804
795
|
|
|
805
796
|
const placementConflict = Boolean(workspaceOwner);
|
|
806
|
-
const needsWorktree = executionMode === 'goal_workloop' && (
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|| (workspaceMode === 'auto' && routeDecision.requiresIsolation === true)));
|
|
797
|
+
const needsWorktree = executionMode === 'goal_workloop' && (placementConstraint.requireWorktree
|
|
798
|
+
|| placementConflict
|
|
799
|
+
|| input.modeInput.requiresParallelism === true
|
|
800
|
+
|| routeDecision.requiresIsolation === true);
|
|
811
801
|
if (!needsWorktree && workspaceOwner) {
|
|
812
802
|
return buildFacadeResult({
|
|
813
803
|
status: 'blocked',
|
|
@@ -863,11 +853,17 @@ export function startGoalWorkloop(
|
|
|
863
853
|
const repositoryChangeIntent = input.workKind === 'repository_change'
|
|
864
854
|
|| (input.modeInput.expectedFiles ?? 0) > 0
|
|
865
855
|
|| (input.modeInput.expectedChangedLines ?? 0) > 0;
|
|
856
|
+
const typedRecoverableReadOnlyReview = input.workKind === undefined
|
|
857
|
+
&& input.modeInput.mutation === false
|
|
858
|
+
&& input.modeInput.requiresInvestigation === true
|
|
859
|
+
&& input.modeInput.requiresRecovery === true;
|
|
866
860
|
const resolvedWorkKind = input.workKind ?? (
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
861
|
+
typedRecoverableReadOnlyReview
|
|
862
|
+
? 'read_only_review'
|
|
863
|
+
: input.modeInput.requiresExternalEffect === true
|
|
864
|
+
&& !repositoryChangeIntent
|
|
865
|
+
? (input.modeInput.remoteWrite === true ? 'remote_effect' : 'local_effect')
|
|
866
|
+
: 'repository_change'
|
|
871
867
|
);
|
|
872
868
|
const remoteDeliveryRequired = resolvedWorkKind === 'repository_change'
|
|
873
869
|
&& input.modeInput.requiresExternalEffect === true
|
|
@@ -886,7 +882,7 @@ export function startGoalWorkloop(
|
|
|
886
882
|
objective: input.objective,
|
|
887
883
|
acceptanceCriteria: effectiveAcceptanceCriteria,
|
|
888
884
|
constraints: {
|
|
889
|
-
...
|
|
885
|
+
...canonicalConstraints,
|
|
890
886
|
...(remoteDeliveryRequired ? { remoteDeliveryRequired: true } : {}),
|
|
891
887
|
},
|
|
892
888
|
risk: workRiskFor(input),
|
|
@@ -1030,6 +1026,24 @@ export function continueGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorklo
|
|
|
1030
1026
|
});
|
|
1031
1027
|
}
|
|
1032
1028
|
|
|
1029
|
+
if ((input.reviewFindings?.length ?? 0) > 0 && work.workKind !== 'read_only_review') {
|
|
1030
|
+
return buildFacadeResult({
|
|
1031
|
+
status: 'blocked',
|
|
1032
|
+
summary: `READ_ONLY_REVIEW_FINDINGS_KIND_REQUIRED: ${work.workId} is ${work.workKind}, not read_only_review.`,
|
|
1033
|
+
data: { work: summarizeWorkContract(work), reviewFindingsRecorded: false },
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
if (work.workKind === 'read_only_review') {
|
|
1037
|
+
const sourceIdentity = evaluateReadOnlyReviewSourceIdentity(work, ctx.sourceRevision, ctx.workspaceChangedPaths);
|
|
1038
|
+
if (sourceIdentity.status !== 'complete') {
|
|
1039
|
+
return buildFacadeResult({
|
|
1040
|
+
status: 'blocked',
|
|
1041
|
+
summary: `READ_ONLY_REVIEW_SOURCE_IDENTITY_REQUIRED: ${sourceIdentity.reasons.join(' ')}`,
|
|
1042
|
+
data: { work: summarizeWorkContract(work), sourceIdentityProven: false },
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1033
1047
|
const explicitPolicyScope = input.allowedPaths !== undefined
|
|
1034
1048
|
|| input.forbiddenPaths !== undefined
|
|
1035
1049
|
|| input.checks !== undefined;
|
|
@@ -1072,6 +1086,104 @@ export function continueGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorklo
|
|
|
1072
1086
|
inspectedPaths: input.inspectedPaths,
|
|
1073
1087
|
});
|
|
1074
1088
|
}
|
|
1089
|
+
if ((input.acceptanceEvidence?.length ?? 0) > 0) {
|
|
1090
|
+
if (work.workKind !== 'local_effect') {
|
|
1091
|
+
return buildFacadeResult({
|
|
1092
|
+
status: 'blocked',
|
|
1093
|
+
summary: `WORK_SEMANTIC_ACCEPTANCE_LOCAL_EFFECT_REQUIRED: ${work.workId} is ${work.workKind}.`,
|
|
1094
|
+
data: { work: summarizeWorkContract(work), semanticAcceptanceRecorded: false },
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
const declaredCriteria = new Map(work.acceptanceCriteria.map((criterion) => [criterion.trim(), criterion]));
|
|
1098
|
+
const availableEvidenceIds = new Set([
|
|
1099
|
+
...work.evidenceRefs.flatMap((evidence) => [evidence.evidenceId, evidence.artifactId])
|
|
1100
|
+
.map((value) => value?.trim())
|
|
1101
|
+
.filter((value): value is string => Boolean(value)),
|
|
1102
|
+
...(ctx.workBoundProcessEvidenceIds ?? []).map((value) => value.trim()).filter(Boolean),
|
|
1103
|
+
]);
|
|
1104
|
+
const normalizedBindings: Array<{ criterion: string; evidenceIds: string[]; rationale: string }> = [];
|
|
1105
|
+
for (const binding of input.acceptanceEvidence ?? []) {
|
|
1106
|
+
const requestedCriterion = binding.criterion.trim();
|
|
1107
|
+
const criterion = declaredCriteria.get(requestedCriterion);
|
|
1108
|
+
if (!criterion) {
|
|
1109
|
+
return buildFacadeResult({
|
|
1110
|
+
status: 'blocked',
|
|
1111
|
+
summary: `WORK_SEMANTIC_ACCEPTANCE_CRITERION_UNKNOWN: ${requestedCriterion || '<empty>'}.`,
|
|
1112
|
+
data: { work: summarizeWorkContract(work), semanticAcceptanceRecorded: false },
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
const evidenceIds = [...new Set(binding.evidenceIds.map((value) => value.trim()).filter(Boolean))];
|
|
1116
|
+
if (evidenceIds.length === 0) {
|
|
1117
|
+
return buildFacadeResult({
|
|
1118
|
+
status: 'blocked',
|
|
1119
|
+
summary: `WORK_SEMANTIC_ACCEPTANCE_EVIDENCE_REQUIRED: ${criterion}.`,
|
|
1120
|
+
data: { work: summarizeWorkContract(work), semanticAcceptanceRecorded: false },
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
const unknownEvidenceIds = evidenceIds.filter((evidenceId) => !availableEvidenceIds.has(evidenceId));
|
|
1124
|
+
if (unknownEvidenceIds.length > 0) {
|
|
1125
|
+
return buildFacadeResult({
|
|
1126
|
+
status: 'blocked',
|
|
1127
|
+
summary: `WORK_SEMANTIC_ACCEPTANCE_EVIDENCE_UNKNOWN: ${unknownEvidenceIds.join(', ')}.`,
|
|
1128
|
+
data: { work: summarizeWorkContract(work), semanticAcceptanceRecorded: false },
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
const rationale = binding.rationale.trim();
|
|
1132
|
+
if (!rationale) {
|
|
1133
|
+
return buildFacadeResult({
|
|
1134
|
+
status: 'blocked',
|
|
1135
|
+
summary: `WORK_SEMANTIC_ACCEPTANCE_RATIONALE_REQUIRED: ${criterion}.`,
|
|
1136
|
+
data: { work: summarizeWorkContract(work), semanticAcceptanceRecorded: false },
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
normalizedBindings.push({ criterion, evidenceIds, rationale: rationale.slice(0, 1_000) });
|
|
1140
|
+
}
|
|
1141
|
+
const recordedAt = nowIso(ctx);
|
|
1142
|
+
const byCriterion = new Map((work.semanticAcceptanceEvidence ?? []).map((review) => [review.criterion, review]));
|
|
1143
|
+
for (const binding of normalizedBindings) {
|
|
1144
|
+
const previous = byCriterion.get(binding.criterion);
|
|
1145
|
+
byCriterion.set(binding.criterion, {
|
|
1146
|
+
criterion: binding.criterion,
|
|
1147
|
+
evidenceIds: [...new Set([...(previous?.evidenceIds ?? []), ...binding.evidenceIds])].slice(0, 50),
|
|
1148
|
+
rationale: binding.rationale,
|
|
1149
|
+
recordedAt,
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
work = updateWorkContract(ctx.workStore, work.workId, {
|
|
1153
|
+
semanticAcceptanceEvidence: [...byCriterion.values()].slice(0, 20),
|
|
1154
|
+
});
|
|
1155
|
+
work = appendWorkEvidence(ctx.workStore, work.workId, {
|
|
1156
|
+
title: 'Controller semantic acceptance review',
|
|
1157
|
+
summary: `Controller reviewed ${normalizedBindings.length} exact acceptance criterion binding(s) against durable Work evidence.`,
|
|
1158
|
+
detailLevel: 'summary',
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
if (work.workKind === 'read_only_review' && ((input.inspectedPaths?.length ?? 0) > 0 || input.reviewFindings !== undefined)) {
|
|
1163
|
+
const findings = [...new Set([
|
|
1164
|
+
...(work.readOnlyReviewEvidence?.findings ?? []),
|
|
1165
|
+
...(input.reviewFindings ?? []).map((finding) => finding.trim()).filter(Boolean),
|
|
1166
|
+
])].slice(0, 50);
|
|
1167
|
+
const inspectedPaths = [...new Set(work.scopeEvidence?.inspectedPaths ?? [])].slice(0, 200);
|
|
1168
|
+
work = updateWorkContract(ctx.workStore, work.workId, {
|
|
1169
|
+
readOnlyReviewEvidence: {
|
|
1170
|
+
sourceRevision: ctx.sourceRevision!,
|
|
1171
|
+
...(ctx.workspaceFingerprint ? { workspaceFingerprint: ctx.workspaceFingerprint } : {}),
|
|
1172
|
+
inspectedPaths,
|
|
1173
|
+
findings,
|
|
1174
|
+
recordedAt: nowIso(ctx),
|
|
1175
|
+
},
|
|
1176
|
+
});
|
|
1177
|
+
for (const finding of input.reviewFindings ?? []) {
|
|
1178
|
+
const summary = finding.trim();
|
|
1179
|
+
if (!summary) continue;
|
|
1180
|
+
work = appendWorkEvidence(ctx.workStore, work.workId, {
|
|
1181
|
+
title: 'read-only review finding',
|
|
1182
|
+
summary: summary.slice(0, 1_500),
|
|
1183
|
+
detailLevel: 'summary',
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1075
1187
|
|
|
1076
1188
|
const currentCheckRefs = work.checkRefs.filter((record) =>
|
|
1077
1189
|
verificationRecordAppliesToCurrentWorkspace(record, ctx.sourceRevision, ctx.workspaceFingerprint));
|
|
@@ -1104,7 +1216,7 @@ export function continueGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorklo
|
|
|
1104
1216
|
acceptanceFailures: history.acceptanceFailures,
|
|
1105
1217
|
infrastructureIssues: history.infrastructureIssues,
|
|
1106
1218
|
backgroundCompleted: false,
|
|
1107
|
-
nextStep: 'execute',
|
|
1219
|
+
nextStep: work.workKind === 'read_only_review' ? 'review' : 'execute',
|
|
1108
1220
|
acceptanceFailureDecision: decision,
|
|
1109
1221
|
},
|
|
1110
1222
|
evidenceRefs: work.evidenceRefs.slice(0, 5),
|
|
@@ -1368,7 +1480,13 @@ export function continueGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorklo
|
|
|
1368
1480
|
});
|
|
1369
1481
|
}
|
|
1370
1482
|
|
|
1371
|
-
const completionEvidence = evaluateWorkCompletionEvidence(
|
|
1483
|
+
const completionEvidence = evaluateWorkCompletionEvidence(
|
|
1484
|
+
work,
|
|
1485
|
+
ctx.sourceRevision,
|
|
1486
|
+
ctx.workspaceFingerprint,
|
|
1487
|
+
ctx.workBoundProcessEvidenceIds,
|
|
1488
|
+
ctx.workspaceChangedPaths,
|
|
1489
|
+
);
|
|
1372
1490
|
if (completionEvidence.status !== 'complete') {
|
|
1373
1491
|
const suggested = validateSuggestedNextActions([
|
|
1374
1492
|
{
|
|
@@ -1383,9 +1501,11 @@ export function continueGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorklo
|
|
|
1383
1501
|
]).actions;
|
|
1384
1502
|
transitionWorkContractPhase(ctx.workStore, work.workId, {
|
|
1385
1503
|
status: 'running',
|
|
1386
|
-
phase: 'implementation',
|
|
1504
|
+
phase: work.workKind === 'read_only_review' ? 'verification' : 'implementation',
|
|
1387
1505
|
state: 'active',
|
|
1388
|
-
summary:
|
|
1506
|
+
summary: work.workKind === 'read_only_review'
|
|
1507
|
+
? `Read-only review completion evidence is incomplete: ${completionEvidence.reasons.join(' ')}`
|
|
1508
|
+
: `Meaningful implementation evidence is still missing: ${completionEvidence.reasons.join(' ')}`,
|
|
1389
1509
|
evidenceRefs: work.evidenceRefs,
|
|
1390
1510
|
});
|
|
1391
1511
|
const updated = updateWorkContract(ctx.workStore, work.workId, {
|
|
@@ -1398,7 +1518,7 @@ export function continueGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorklo
|
|
|
1398
1518
|
work: summarizeWorkContract(updated),
|
|
1399
1519
|
backgroundCompleted: false,
|
|
1400
1520
|
nextStep: 'execute',
|
|
1401
|
-
executionEvidencePresent:
|
|
1521
|
+
executionEvidencePresent: Boolean(ctx.workBoundProcessEvidenceIds?.length),
|
|
1402
1522
|
missingChecks: completionEvidence.missingChecks,
|
|
1403
1523
|
durableResultEvidence: completionEvidence.durableResultEvidence,
|
|
1404
1524
|
ignoredWeakReferences: {
|
|
@@ -1424,7 +1544,9 @@ export function continueGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorklo
|
|
|
1424
1544
|
status: 'running',
|
|
1425
1545
|
phase: 'delivery',
|
|
1426
1546
|
state: 'active',
|
|
1427
|
-
summary:
|
|
1547
|
+
summary: work.workKind === 'read_only_review'
|
|
1548
|
+
? 'Read-only review evidence is source-bound and clean; semantic no-change finalization is next.'
|
|
1549
|
+
: 'Implementation and verification evidence are sufficient; exact delivery and cleanup receipt is next.',
|
|
1428
1550
|
evidenceRefs: work.evidenceRefs,
|
|
1429
1551
|
});
|
|
1430
1552
|
const updated = updateWorkContract(ctx.workStore, work.workId, {
|
|
@@ -1642,7 +1764,13 @@ export function finalizeGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorklo
|
|
|
1642
1764
|
});
|
|
1643
1765
|
}
|
|
1644
1766
|
|
|
1645
|
-
const completionEvidence = evaluateWorkCompletionEvidence(
|
|
1767
|
+
const completionEvidence = evaluateWorkCompletionEvidence(
|
|
1768
|
+
work,
|
|
1769
|
+
ctx.sourceRevision,
|
|
1770
|
+
ctx.workspaceFingerprint,
|
|
1771
|
+
ctx.workBoundProcessEvidenceIds,
|
|
1772
|
+
ctx.workspaceChangedPaths,
|
|
1773
|
+
);
|
|
1646
1774
|
const history = completionEvidence.history;
|
|
1647
1775
|
|
|
1648
1776
|
if (input.forceFailed || completionEvidence.status === 'failed') {
|
|
@@ -1717,6 +1845,50 @@ export function finalizeGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorklo
|
|
|
1717
1845
|
});
|
|
1718
1846
|
}
|
|
1719
1847
|
|
|
1848
|
+
// Read-only review completion is semantic no-change authority, not Git delivery.
|
|
1849
|
+
// The exact source/workspace fence is re-evaluated above at finalize time; only
|
|
1850
|
+
// a clean review with persisted inspected paths and zero findings can close.
|
|
1851
|
+
if (work.workKind === 'read_only_review' && !work.completionReceipt) {
|
|
1852
|
+
const reviewEvidence = work.readOnlyReviewEvidence!;
|
|
1853
|
+
const recordedAt = nowIso(ctx);
|
|
1854
|
+
const completed = completeWorkWithReceipt(
|
|
1855
|
+
ctx.workStore,
|
|
1856
|
+
work.workId,
|
|
1857
|
+
{
|
|
1858
|
+
schemaVersion: 1,
|
|
1859
|
+
receiptId: `ROR-WORK-${randomUUID()}`,
|
|
1860
|
+
source: 'read_only_review',
|
|
1861
|
+
workId: work.workId,
|
|
1862
|
+
baseRevision: work.baseRevision!,
|
|
1863
|
+
sourceRevision: ctx.sourceRevision!,
|
|
1864
|
+
...(ctx.workspaceFingerprint ? { workspaceFingerprint: ctx.workspaceFingerprint } : {}),
|
|
1865
|
+
workspaceChangedPaths: [],
|
|
1866
|
+
inspectedPaths: reviewEvidence.inspectedPaths,
|
|
1867
|
+
findingCount: 0,
|
|
1868
|
+
recordedAt,
|
|
1869
|
+
},
|
|
1870
|
+
'completed_no_change',
|
|
1871
|
+
'read_only_review',
|
|
1872
|
+
);
|
|
1873
|
+
if (completed.planId && completed.planStepId && ctx.planStore) {
|
|
1874
|
+
completePlanStepForWork(ctx.planStore, { planId: completed.planId, stepId: completed.planStepId, work: completed });
|
|
1875
|
+
}
|
|
1876
|
+
return buildFacadeResult({
|
|
1877
|
+
status: 'ok',
|
|
1878
|
+
summary: `Finalize result: clean read-only review completed with no source change for ${work.workId}.`,
|
|
1879
|
+
data: {
|
|
1880
|
+
work: summarizeWorkContract(completed),
|
|
1881
|
+
finalStatus: 'completed',
|
|
1882
|
+
completionOutcome: 'completed_no_change',
|
|
1883
|
+
completionReceipt: completed.completionReceipt,
|
|
1884
|
+
inspectedPathCount: reviewEvidence.inspectedPaths.length,
|
|
1885
|
+
hiddenFailure: false,
|
|
1886
|
+
},
|
|
1887
|
+
evidenceRefs: completed.evidenceRefs.slice(0, 5),
|
|
1888
|
+
suggestedNextActions: [{ label: 'Read controller status', tool: 'rh_status', operation: 'get', risk: 'readonly' }],
|
|
1889
|
+
});
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1720
1892
|
// For a controller-local effect Work, the semantic Controller's explicit
|
|
1721
1893
|
// finalize call is the terminalization decision, but it is admissible only
|
|
1722
1894
|
// after durable result evidence exists. Record one canonical Work receipt here
|
|
@@ -1724,7 +1896,7 @@ export function finalizeGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorklo
|
|
|
1724
1896
|
// remain bound to their durable plugin action receipt in the plugin layer.
|
|
1725
1897
|
if (work.workKind === 'local_effect' && !work.completionReceipt && completionEvidence.durableResultEvidence) {
|
|
1726
1898
|
const recordedAt = nowIso(ctx);
|
|
1727
|
-
const completed =
|
|
1899
|
+
const completed = completeWorkWithReceipt(
|
|
1728
1900
|
ctx.workStore,
|
|
1729
1901
|
work.workId,
|
|
1730
1902
|
{
|
|
@@ -1947,6 +2119,7 @@ export function runGoalWorkloop(
|
|
|
1947
2119
|
planStepId: typeof args.plan_step_id === 'string' ? args.plan_step_id : undefined,
|
|
1948
2120
|
workKind: args.work_kind === 'repository_change'
|
|
1949
2121
|
|| args.work_kind === 'completed_no_change'
|
|
2122
|
+
|| args.work_kind === 'read_only_review'
|
|
1950
2123
|
|| args.work_kind === 'investigation'
|
|
1951
2124
|
|| args.work_kind === 'local_effect'
|
|
1952
2125
|
|| args.work_kind === 'remote_effect'
|
|
@@ -1963,6 +2136,14 @@ export function runGoalWorkloop(
|
|
|
1963
2136
|
checks: Array.isArray(args.check_ids) ? args.check_ids.map(String) : undefined,
|
|
1964
2137
|
additionalLikelyPaths: Array.isArray(args.additional_likely_paths) ? args.additional_likely_paths.map(String) : undefined,
|
|
1965
2138
|
inspectedPaths: Array.isArray(args.inspected_paths) ? args.inspected_paths.map(String) : undefined,
|
|
2139
|
+
reviewFindings: Array.isArray(args.review_findings) ? args.review_findings.map(String) : undefined,
|
|
2140
|
+
acceptanceEvidence: Array.isArray(args.acceptance_evidence)
|
|
2141
|
+
? args.acceptance_evidence.filter((value): value is Record<string, unknown> => Boolean(value) && typeof value === 'object').map((value) => ({
|
|
2142
|
+
criterion: typeof value.criterion === 'string' ? value.criterion : '',
|
|
2143
|
+
evidenceIds: Array.isArray(value.evidence_ids) ? value.evidence_ids.map(String) : [],
|
|
2144
|
+
rationale: typeof value.rationale === 'string' ? value.rationale : '',
|
|
2145
|
+
}))
|
|
2146
|
+
: undefined,
|
|
1966
2147
|
acceptanceFailureDecision: args.acceptance_failure_decision === 'repair' || args.acceptance_failure_decision === 'rescope'
|
|
1967
2148
|
? args.acceptance_failure_decision
|
|
1968
2149
|
: undefined,
|