@@ -14,8 +14,9 @@ const {
1414 COMPACT_TOKEN_RATIO , DEFAULT_CONTEXT_TOKENS , HEALTH_CHECK_INTERVAL ,
1515 SESSIONS_DIR , SESSION_RESET_INPUT_TOKENS , SESSION_FORCE_COMPACT_TOKENS ,
1616 MIN_EFFECTIVE_RESPONSE_LENGTH , MAX_CONSECUTIVE_FAILURES ,
17+ IDLE_COMPLETE_MINS , STALE_TASK_MINS ,
1718} = require ( './constants.cjs' )
18- const { loadDeptConfig, loadDeptState, saveDeptState, getSessionTokenInfo, readAgentActivity } = require ( './readers.cjs' )
19+ const { loadDeptConfig, loadDeptState, saveDeptState, getSessionTokenInfo, readAgentActivity, readProjectTasks , readStandaloneTasks } = require ( './readers.cjs' )
1920const { sendToAgent, compactSession, killSession } = require ( './gateway.cjs' )
2021const { buildDepartmentDirective } = require ( './dept-directive.cjs' )
2122const { compressMemoryByRole } = require ( './memory.cjs' )
@@ -56,6 +57,136 @@ function parseTaskAssignments(text) {
5657 return assignments
5758}
5859
60+ /**
61+ * Parse task completions from the chief's structured response.
62+ * Extracts [任务完成] section (priority) and [进展汇报] section,
63+ * looking for task IDs with completion keywords.
64+ *
65+ * @param {string } text - Chief's response text
66+ * @returns {string[] } - Deduplicated list of completed task IDs
67+ */
68+ function parseTaskCompletions ( text ) {
69+ if ( ! text ) return [ ]
70+
71+ const completedIds = new Set ( )
72+ const completionKeywords = / 完 成 | 已 完 成 | 已 交 付 | d o n e | f i n i s h e d | c o m p l e t e d | 1 0 0 % / i
73+
74+ // Parse [任务完成] section (priority)
75+ const completionMatch = text . match ( / \[ 任 务 完 成 \] \s * \n ( [ \s \S ] * ?) (? = \n \[ | $ ) / )
76+ if ( completionMatch ) {
77+ const lines = completionMatch [ 1 ] . split ( '\n' )
78+ for ( const line of lines ) {
79+ const m = line . match ( / ( t a s k - [ a - z 0 - 9 - ] + ) / i)
80+ if ( m && ! / 无 / . test ( line . trim ( ) ) ) {
81+ completedIds . add ( m [ 1 ] )
82+ }
83+ }
84+ }
85+
86+ // Parse [进展汇报] section for completion keywords
87+ const progressMatch = text . match ( / \[ 进 展 汇 报 \] \s * \n ( [ \s \S ] * ?) (? = \n \[ | $ ) / )
88+ if ( progressMatch ) {
89+ const lines = progressMatch [ 1 ] . split ( '\n' )
90+ for ( const line of lines ) {
91+ const m = line . match ( / ( t a s k - [ a - z 0 - 9 - ] + ) / i)
92+ if ( m && completionKeywords . test ( line ) ) {
93+ completedIds . add ( m [ 1 ] )
94+ }
95+ }
96+ }
97+
98+ return [ ...completedIds ]
99+ }
100+
101+ /**
102+ * Auto-transition tasks based on agent activity and chief reports.
103+ *
104+ * Rules:
105+ * - Chief reports task completed → completed
106+ * - in_progress/rework + agent idle >= IDLE_COMPLETE_MINS → completed
107+ * - in_progress/rework + agent idle >= STALE_TASK_MINS + progress < 50 → failed
108+ * - assigned + agent active (idle < 5) → in_progress
109+ * - assigned + agent idle >= STALE_TASK_MINS → failed (never picked up)
110+ * - review + idle >= IDLE_COMPLETE_MINS → completed (auto-approve)
111+ *
112+ * @param {string } deptId
113+ * @param {object } config - Department config
114+ * @param {string } chiefResponseText - Chief's response text
115+ */
116+ async function autoTransitionTasks ( deptId , config , chiefResponseText ) {
117+ const agentActivity = readAgentActivity ( )
118+ const agents = config . agents || [ ]
119+
120+ // Collect all tasks assigned to department agents
121+ const projects = readProjectTasks ( )
122+ const allTasks = [ ]
123+ for ( const proj of projects ) {
124+ for ( const t of ( proj . tasks || [ ] ) ) {
125+ const assignees = [ t . assignedAgent , ...( t . assignees || [ ] ) ]
126+ if ( assignees . some ( a => agents . includes ( a ) ) ) {
127+ allTasks . push ( t )
128+ }
129+ }
130+ }
131+ const standalone = readStandaloneTasks ( )
132+ for ( const t of standalone ) {
133+ const assignees = [ t . assignedAgent , ...( t . assignees || [ ] ) ]
134+ if ( assignees . some ( a => agents . includes ( a ) ) ) {
135+ allTasks . push ( t )
136+ }
137+ }
138+
139+ if ( allTasks . length === 0 ) return
140+
141+ // 1. Chief-reported completions
142+ const chiefCompletions = parseTaskCompletions ( chiefResponseText )
143+ for ( const taskId of chiefCompletions ) {
144+ const task = allTasks . find ( t => t . id === taskId )
145+ if ( task && ( task . status === 'in_progress' || task . status === 'rework' ) ) {
146+ const assignee = task . assignedAgent || ( task . assignees && task . assignees [ 0 ] )
147+ if ( assignee ) updateTaskStatus ( assignee , taskId , 'completed' )
148+ logger . info ( 'dept-loop' , `Chief reported task ${ taskId } completed in ${ deptId } ` )
149+ }
150+ }
151+
152+ // 2. Idle-based auto-complete / stale cleanup
153+ for ( const task of allTasks ) {
154+ if ( ! [ 'in_progress' , 'rework' , 'assigned' , 'review' ] . includes ( task . status ) ) continue
155+ if ( chiefCompletions . includes ( task . id ) ) continue
156+
157+ const assignee = task . assignedAgent || ( task . assignees && task . assignees [ 0 ] )
158+ if ( ! assignee ) continue
159+ const activity = agentActivity [ assignee ]
160+ const idleMins = activity ? activity . idleMins : 9999
161+
162+ if ( task . status === 'assigned' ) {
163+ if ( idleMins < 5 ) {
164+ // Agent active → promote to in_progress
165+ updateTaskStatus ( assignee , task . id , 'in_progress' )
166+ logger . debug ( 'dept-loop' , `Auto-promoted assigned task ${ task . id } to in_progress` )
167+ } else if ( idleMins >= STALE_TASK_MINS ) {
168+ // Agent never picked it up → failed
169+ updateTaskStatus ( assignee , task . id , 'failed' )
170+ logger . warn ( 'dept-loop' , `Assigned task ${ task . id } never started, marked failed (agent ${ assignee } idle ${ idleMins } m)` )
171+ }
172+ } else if ( task . status === 'review' ) {
173+ if ( idleMins >= IDLE_COMPLETE_MINS ) {
174+ // No one reviewed → auto-approve
175+ updateTaskStatus ( assignee , task . id , 'completed' )
176+ logger . info ( 'dept-loop' , `Review task ${ task . id } auto-approved (idle ${ idleMins } m)` )
177+ }
178+ } else if ( task . status === 'in_progress' || task . status === 'rework' ) {
179+ if ( idleMins >= STALE_TASK_MINS && ( task . progress || 0 ) < 50 ) {
180+ updateTaskStatus ( assignee , task . id , 'failed' )
181+ logger . warn ( 'dept-loop' , `Stale task ${ task . id } marked failed (idle ${ idleMins } m, progress ${ task . progress || 0 } %)` )
182+ } else if ( idleMins >= IDLE_COMPLETE_MINS ) {
183+ updateTaskStatus ( assignee , task . id , 'completed' )
184+ logger . info ( 'dept-loop' , `Auto-completed task ${ task . id } (agent ${ assignee } idle ${ idleMins } m)` )
185+ }
186+ }
187+ }
188+ }
189+
59190/**
60191 * Run a single department cycle.
61192 *
@@ -139,6 +270,11 @@ async function runDepartmentCycle(deptId) {
139270 } )
140271 }
141272
273+ // ── Auto-transition stale tasks ──
274+ autoTransitionTasks ( deptId , config , result . text ) . catch ( e =>
275+ logger . debug ( 'dept-loop' , `Auto-transition error for ${ deptId } ` , e )
276+ )
277+
142278 // ── Response validation: token check ──
143279 const responseLength = ( result . text || '' ) . trim ( ) . length
144280 const isEffective = responseLength >= MIN_EFFECTIVE_RESPONSE_LENGTH
@@ -234,6 +370,10 @@ async function runDepartmentCycle(deptId) {
234370 return { ok : true , text : result . text }
235371 } else {
236372 logger . error ( 'dept-loop' , `Department ${ deptId } cycle failed: ${ result . error } ` )
373+ // Still run idle-based auto-transition even when chief fails
374+ autoTransitionTasks ( deptId , config , '' ) . catch ( e =>
375+ logger . debug ( 'dept-loop' , `Auto-transition error for ${ deptId } (on failure path)` , e )
376+ )
237377 state . consecutiveFailures = ( state . consecutiveFailures || 0 ) + 1
238378 state . status = 'error'
239379 state . lastCycleResult = `Error: ${ result . error } `
@@ -243,6 +383,8 @@ async function runDepartmentCycle(deptId) {
243383 }
244384 } catch ( err ) {
245385 logger . error ( 'dept-loop' , `Department ${ deptId } cycle error` , err )
386+ // Still run idle-based auto-transition even on exception
387+ autoTransitionTasks ( deptId , config , '' ) . catch ( ( ) => { } )
246388 state . consecutiveFailures = ( state . consecutiveFailures || 0 ) + 1
247389 state . status = 'error'
248390 state . lastCycleResult = `Error: ${ err . message } `
@@ -584,4 +726,4 @@ if (require.main === module) {
584726 } )
585727}
586728
587- module . exports = { runDepartmentCycle, generateDepartmentReport, ensureSessionHealth, fallbackDispatch, parseTaskAssignments }
729+ module . exports = { runDepartmentCycle, generateDepartmentReport, ensureSessionHealth, fallbackDispatch, parseTaskAssignments, parseTaskCompletions , autoTransitionTasks }
0 commit comments