@@ -66,6 +66,7 @@ import { IPC } from '../ipc/channels.js';
6666
6767const DEFAULT_WAIT_TIMEOUT_MS = 300_000 ; // 5 minutes
6868const PROMPT_WRITE_DELAY_MS = 50 ;
69+ const GIT_LOCK_RETRY_DELAY_MS = 2_000 ;
6970const REST_COORDINATOR_SENTINEL = 'api' ;
7071const PREAMBLE_ARTIFACT_PATHS = new Set ( [
7172 'AGENTS.md' ,
@@ -1000,6 +1001,11 @@ export class Coordinator {
10001001 }
10011002
10021003 private syncLandingState ( task : CoordinatedTask ) : void {
1004+ // Detach renderer MCP state only after a successful merge (landed states);
1005+ // escalation/failure states leave it attached so the task stays reachable.
1006+ const detachMcpState =
1007+ task . landingState === 'landed_pending_review' ||
1008+ task . landingState === 'landed_cleanup_failed' ;
10031009 this . notifyRenderer ( IPC . MCP_TaskStateSync , {
10041010 taskId : task . id ,
10051011 verification : task . verification ,
@@ -1012,21 +1018,9 @@ export class Coordinator {
10121018 task . landingState === 'landed_cleanup_failed' ||
10131019 task . landingState === 'landing_escalated' ||
10141020 task . landingState === 'landing_failed' ,
1015- controlledBy :
1016- task . landingState === 'landed_pending_review' ||
1017- task . landingState === 'landed_cleanup_failed'
1018- ? null
1019- : undefined ,
1020- mcpConfigPath :
1021- task . landingState === 'landed_pending_review' ||
1022- task . landingState === 'landed_cleanup_failed'
1023- ? null
1024- : undefined ,
1025- mcpStartupStatus :
1026- task . landingState === 'landed_pending_review' ||
1027- task . landingState === 'landed_cleanup_failed'
1028- ? null
1029- : undefined ,
1021+ controlledBy : detachMcpState ? null : undefined ,
1022+ mcpConfigPath : detachMcpState ? null : undefined ,
1023+ mcpStartupStatus : detachMcpState ? null : undefined ,
10301024 } ) ;
10311025 }
10321026
@@ -1082,6 +1076,15 @@ export class Coordinator {
10821076 }
10831077
10841078 if ( dirtyPaths . length > 0 ) {
1079+ // nonPreamblePaths check above guarantees every path here is a preamble artifact.
1080+ const unexpectedPaths = dirtyPaths . filter (
1081+ ( p ) => ! preambleFiles . has ( p ) || ! PREAMBLE_ARTIFACT_PATHS . has ( p ) ,
1082+ ) ;
1083+ if ( unexpectedPaths . length > 0 ) {
1084+ throw new Error (
1085+ `Unexpected non-preamble paths staged before cleanup commit: ${ unexpectedPaths . join ( ', ' ) } ` ,
1086+ ) ;
1087+ }
10851088 await execAsync ( 'git' , [ 'add' , '-A' , '--' , ...dirtyPaths ] , { cwd : task . worktreePath } ) ;
10861089 try {
10871090 await execAsync ( 'git' , [ 'commit' , '-m' , 'Remove Parallel Code sub-task preamble' ] , {
@@ -1122,7 +1125,7 @@ export class Coordinator {
11221125 } catch ( err ) {
11231126 const msg = err instanceof Error ? err . message : String ( err ) ;
11241127 if ( msg . includes ( 'Another git process' ) || msg . includes ( 'index.lock' ) ) {
1125- await new Promise ( ( r ) => setTimeout ( r , 2000 ) ) ;
1128+ await new Promise ( ( r ) => setTimeout ( r , GIT_LOCK_RETRY_DELAY_MS ) ) ;
11261129 result = await runMerge ( ) ;
11271130 } else {
11281131 throw err ;
@@ -1210,9 +1213,6 @@ export class Coordinator {
12101213 const task = this . tasks . get ( taskId ) ;
12111214 if ( ! task ) throw new Error ( `Task not found: ${ taskId } ` ) ;
12121215
1213- task . verification = input . verification ;
1214- task . landingSummary = input . summary ;
1215-
12161216 if ( ! this . coordinators . has ( task . coordinatorTaskId ) ) {
12171217 const reason = `Cannot self-land orphaned task: coordinator ${ task . coordinatorTaskId } is not registered` ;
12181218 this . escalateLanding ( task , 'landing_escalated' , reason ) ;
@@ -1225,6 +1225,11 @@ export class Coordinator {
12251225 throw new Error ( reason ) ;
12261226 }
12271227
1228+ // Only persist verification/summary after all validation passes so rejected
1229+ // calls don't leave bogus data visible in the renderer.
1230+ task . verification = input . verification ;
1231+ task . landingSummary = input . summary ;
1232+
12281233 try {
12291234 await this . prepareCleanSelfLandingWorktree ( task ) ;
12301235 } catch ( err ) {
@@ -1366,6 +1371,10 @@ export class Coordinator {
13661371 }
13671372
13681373 const hasManualReviewSignal = task . signalDoneAt !== undefined ;
1374+ // landing_escalated unlocks merge_task as an escape hatch for the coordinator.
1375+ // This is not an authority leak: sub-task tokens can only reach land_self (via
1376+ // the /api/tasks/:id/land endpoint); merge_task is a coordinator-only MCP tool,
1377+ // so only a coordinator agent can invoke it here.
13691378 const hasLandingEscalation =
13701379 task . landingState === 'landing_escalated' || task . landingState === 'landing_failed' ;
13711380 if ( task . status === 'running' && ! hasManualReviewSignal && ! hasLandingEscalation ) {
0 commit comments