helloagents 3.0.33 → 3.0.37

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.
Files changed (66) hide show
  1. package/.claude-plugin/marketplace.json +1 -4
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/.codex-plugin/plugin.json +3 -4
  4. package/README.md +78 -74
  5. package/README_CN.md +78 -74
  6. package/bootstrap-lite.md +9 -11
  7. package/bootstrap.md +21 -23
  8. package/gemini-extension.json +1 -1
  9. package/install.ps1 +27 -4
  10. package/install.sh +27 -3
  11. package/package.json +2 -2
  12. package/scripts/capability-registry.mjs +5 -3
  13. package/scripts/cli-doctor-codex.mjs +153 -1
  14. package/scripts/cli-doctor-render.mjs +2 -1
  15. package/scripts/cli-doctor.mjs +3 -3
  16. package/scripts/cli-hosts.mjs +1 -1
  17. package/scripts/cli-lifecycle-hosts.mjs +124 -54
  18. package/scripts/cli-lifecycle.mjs +50 -15
  19. package/scripts/cli-messages.mjs +7 -7
  20. package/scripts/cli-runtime-root.mjs +9 -1
  21. package/scripts/delivery-gate-messages.mjs +5 -4
  22. package/scripts/delivery-gate.mjs +11 -22
  23. package/scripts/guard.mjs +1 -1
  24. package/scripts/notify-closeout.mjs +61 -22
  25. package/scripts/notify-context.mjs +5 -5
  26. package/scripts/notify-route.mjs +1 -1
  27. package/scripts/notify-sound.mjs +2 -1
  28. package/scripts/notify.mjs +2 -2
  29. package/scripts/plan-contract.mjs +10 -14
  30. package/scripts/project-session-cleanup.mjs +91 -31
  31. package/scripts/qa-review-state.mjs +313 -0
  32. package/scripts/ralph-loop.mjs +32 -13
  33. package/scripts/runtime-artifacts.mjs +2 -2
  34. package/scripts/runtime-scope.mjs +14 -13
  35. package/scripts/runtime-ttl.mjs +7 -4
  36. package/scripts/session-capsule.mjs +75 -13
  37. package/scripts/session-token.mjs +44 -9
  38. package/scripts/state-document.mjs +77 -0
  39. package/scripts/workflow-core.mjs +13 -19
  40. package/scripts/workflow-plan-files.mjs +1 -1
  41. package/scripts/workflow-recommendation.mjs +55 -67
  42. package/scripts/workflow-state.mjs +8 -8
  43. package/skills/commands/auto/SKILL.md +12 -12
  44. package/skills/commands/build/SKILL.md +9 -10
  45. package/skills/commands/commit/SKILL.md +1 -1
  46. package/skills/commands/help/SKILL.md +11 -13
  47. package/skills/commands/init/SKILL.md +18 -9
  48. package/skills/commands/loop/SKILL.md +70 -96
  49. package/skills/commands/plan/SKILL.md +7 -8
  50. package/skills/commands/prd/SKILL.md +3 -3
  51. package/skills/commands/qa/SKILL.md +49 -0
  52. package/skills/hello-ui/SKILL.md +3 -3
  53. package/skills/helloagents/SKILL.md +11 -14
  54. package/skills/qa-review/SKILL.md +92 -0
  55. package/templates/plans/contract.json +4 -7
  56. package/templates/plans/plan.md +1 -1
  57. package/templates/plans/tasks.md +1 -1
  58. package/templates/verify.yaml +1 -1
  59. package/scripts/review-state.mjs +0 -193
  60. package/scripts/verify-state.mjs +0 -175
  61. package/skills/commands/global/SKILL.md +0 -71
  62. package/skills/commands/verify/SKILL.md +0 -46
  63. package/skills/commands/wiki/SKILL.md +0 -57
  64. package/skills/hello-review/SKILL.md +0 -42
  65. package/skills/hello-verify/SKILL.md +0 -144
  66. /package/hooks/{hooks.json → hooks-gemini.json} +0 -0
@@ -1,15 +1,56 @@
1
1
  import { createHash } from 'node:crypto'
2
2
  import { closeSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeFileSync } from 'node:fs'
3
- import { dirname } from 'node:path'
3
+ import { dirname, join } from 'node:path'
4
+ import { homedir } from 'node:os'
4
5
 
5
- import { getRuntimeEvidencePath, readRuntimeEvidence, writeRuntimeEvidence } from './runtime-artifacts.mjs'
6
-
7
- export const CODEX_CLOSEOUT_EVIDENCE_FILE = 'codex-native-stop.json'
8
- export const CODEX_QUICK_NOTIFY_EVIDENCE_FILE = 'codex-quick-notify.json'
9
- const CODEX_CLOSEOUT_LOCK_FILE = 'codex-native-stop.lock'
6
+ export const CODEX_NOTIFY_STATE_FILE = 'notify-state.json'
7
+ export const CODEX_NOTIFY_LOCK_FILE = 'notify.lock'
10
8
  const WEAK_KEY_TTL_MS = 10_000
11
9
  const LOCK_STALE_MS = 120_000
12
10
 
11
+ function getHomeDir(env = process.env) {
12
+ return env.HOME || env.USERPROFILE || homedir()
13
+ }
14
+
15
+ export function getCodexNotifyDir(env = process.env) {
16
+ return join(getHomeDir(env), '.codex', '.helloagents')
17
+ }
18
+
19
+ export function getCodexNotifyStatePath(env = process.env) {
20
+ return join(getCodexNotifyDir(env), CODEX_NOTIFY_STATE_FILE)
21
+ }
22
+
23
+ export function getCodexNotifyLockPath(env = process.env) {
24
+ return join(getCodexNotifyDir(env), CODEX_NOTIFY_LOCK_FILE)
25
+ }
26
+
27
+ function readNotifyState(env = process.env) {
28
+ try {
29
+ const value = JSON.parse(readFileSync(getCodexNotifyStatePath(env), 'utf-8'))
30
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
31
+ return { version: 1, nativeStop: null, quickNotify: null }
32
+ }
33
+ return {
34
+ version: 1,
35
+ nativeStop: value.nativeStop && typeof value.nativeStop === 'object' ? value.nativeStop : null,
36
+ quickNotify: value.quickNotify && typeof value.quickNotify === 'object' ? value.quickNotify : null,
37
+ }
38
+ } catch {
39
+ return { version: 1, nativeStop: null, quickNotify: null }
40
+ }
41
+ }
42
+
43
+ function writeNotifyState(state, env = process.env) {
44
+ const filePath = getCodexNotifyStatePath(env)
45
+ mkdirSync(dirname(filePath), { recursive: true })
46
+ writeFileSync(filePath, `${JSON.stringify({
47
+ version: 1,
48
+ nativeStop: state?.nativeStop || null,
49
+ quickNotify: state?.quickNotify || null,
50
+ }, null, 2)}\n`, 'utf-8')
51
+ return filePath
52
+ }
53
+
13
54
  function getTurnId(payload = {}) {
14
55
  return String(payload.turnId || payload.turn_id || payload['turn-id'] || '').trim()
15
56
  }
@@ -118,13 +159,10 @@ export function matchesCodexCloseoutEvidence(evidence, snapshot, now = Date.now(
118
159
  return intersects(snapshot.weakKeys, weakKeys)
119
160
  }
120
161
 
121
- /**
122
- * Try to claim the current Codex closeout so Stop and native notify handle one turn only once.
123
- */
124
162
  export function beginCodexCloseoutClaim(cwd, { payload = {}, turnState = null, source = '' } = {}) {
125
163
  const snapshot = buildCodexCloseoutSnapshot({ payload, turnState })
126
- const lockPath = getRuntimeEvidencePath(cwd, CODEX_CLOSEOUT_LOCK_FILE, { payload })
127
- const evidencePath = getRuntimeEvidencePath(cwd, CODEX_CLOSEOUT_EVIDENCE_FILE, { payload })
164
+ const lockPath = getCodexNotifyLockPath()
165
+ const evidencePath = getCodexNotifyStatePath()
128
166
  const now = Date.now()
129
167
  const lockPayload = {
130
168
  source,
@@ -164,8 +202,8 @@ export function beginCodexCloseoutClaim(cwd, { payload = {}, turnState = null, s
164
202
  }
165
203
  }
166
204
 
167
- const evidence = readRuntimeEvidence(cwd, CODEX_CLOSEOUT_EVIDENCE_FILE, { payload })
168
- if (matchesCodexCloseoutEvidence(evidence, snapshot, now)) {
205
+ const notifyState = readNotifyState()
206
+ if (matchesCodexCloseoutEvidence(notifyState.nativeStop, snapshot, now)) {
169
207
  releaseLockFile(lockPath)
170
208
  return {
171
209
  claimed: false,
@@ -186,15 +224,13 @@ export function beginCodexCloseoutClaim(cwd, { payload = {}, turnState = null, s
186
224
  }
187
225
  }
188
226
 
189
- /**
190
- * Persist the handled closeout fingerprint and release the in-flight lock.
191
- */
192
227
  export function finalizeCodexCloseoutClaim(claim, meta = {}) {
193
228
  if (!claim?.claimed) return
194
229
 
195
230
  try {
196
231
  if (meta.handled !== false) {
197
- writeRuntimeEvidence(claim.cwd, CODEX_CLOSEOUT_EVIDENCE_FILE, {
232
+ const notifyState = readNotifyState()
233
+ notifyState.nativeStop = {
198
234
  version: 2,
199
235
  updatedAt: new Date().toISOString(),
200
236
  source: meta.source || claim.source || '',
@@ -205,7 +241,8 @@ export function finalizeCodexCloseoutClaim(claim, meta = {}) {
205
241
  messageHash: claim.snapshot.messageHash,
206
242
  strongKeys: claim.snapshot.strongKeys,
207
243
  weakKeys: claim.snapshot.weakKeys,
208
- }, { payload: claim.payload })
244
+ }
245
+ writeNotifyState(notifyState)
209
246
  }
210
247
  } finally {
211
248
  releaseLockFile(claim.lockPath)
@@ -214,7 +251,8 @@ export function finalizeCodexCloseoutClaim(claim, meta = {}) {
214
251
 
215
252
  export function writeCodexQuickNotifyEvidence(cwd, { payload = {}, turnState = null, event = '' } = {}) {
216
253
  const snapshot = buildCodexCloseoutSnapshot({ payload, turnState })
217
- return writeRuntimeEvidence(cwd, CODEX_QUICK_NOTIFY_EVIDENCE_FILE, {
254
+ const notifyState = readNotifyState()
255
+ notifyState.quickNotify = {
218
256
  version: 1,
219
257
  updatedAt: new Date().toISOString(),
220
258
  event,
@@ -223,11 +261,12 @@ export function writeCodexQuickNotifyEvidence(cwd, { payload = {}, turnState = n
223
261
  messageHash: snapshot.messageHash,
224
262
  strongKeys: snapshot.strongKeys,
225
263
  weakKeys: snapshot.weakKeys,
226
- }, { payload })
264
+ }
265
+ return writeNotifyState(notifyState)
227
266
  }
228
267
 
229
268
  export function hasCodexQuickNotifyEvidence(cwd, { payload = {}, turnState = null } = {}) {
230
269
  const snapshot = buildCodexCloseoutSnapshot({ payload, turnState })
231
- const evidence = readRuntimeEvidence(cwd, CODEX_QUICK_NOTIFY_EVIDENCE_FILE, { payload })
232
- return matchesCodexCloseoutEvidence(evidence, snapshot)
270
+ const notifyState = readNotifyState()
271
+ return matchesCodexCloseoutEvidence(notifyState.quickNotify, snapshot)
233
272
  }
@@ -11,7 +11,7 @@ import {
11
11
  const COMMAND_ALIASES = {
12
12
  do: 'build',
13
13
  design: 'plan',
14
- review: 'verify',
14
+ review: 'qa',
15
15
  };
16
16
 
17
17
  function buildRuntimeRootBlock(pkgRoot) {
@@ -49,7 +49,7 @@ function buildAliasRouteNote(skillName) {
49
49
  return '兼容别名映射:本次按 ~plan 规则执行;方案文件使用 `plan.md`,项目级 UI 契约仍使用 `DESIGN.md`。';
50
50
  }
51
51
  if (skillName === 'review') {
52
- return '兼容别名映射:本次按 ~verify 的审查优先模式执行。';
52
+ return '兼容别名映射:本次按 ~qa 规则执行;统一走 qa-review 质量闭环。';
53
53
  }
54
54
  return '';
55
55
  }
@@ -137,7 +137,7 @@ export function buildInjectContext({ source, bootstrap, settings, pkgRoot, host,
137
137
  if (capabilityHint) context += `\n\n## 当前按需能力\n${capabilityHint}`;
138
138
  if (stateSyncHint) context += `\n\n## 状态文件提醒\n${stateSyncHint}`;
139
139
  context += settingsBlock;
140
- if (source === 'resume' || source === 'compact') {
140
+ if ((source === 'resume' || source === 'compact') && stateSnapshot.exists) {
141
141
  context += `\n\n> ⚠️ 会话已恢复/压缩,请先读取 \`state_path\` 指向的 \`${stateSnapshot.statePath.replace(/\\/g, '/')}\`;先看当前用户消息,如果仍是同一任务,再参考状态文件。`;
142
142
  }
143
143
  return context;
@@ -165,8 +165,8 @@ export function buildSemanticRouteInstruction(cwd, payload = {}) {
165
165
  '请根据用户请求的真实意图选路,不依赖关键词表。',
166
166
  buildDelegatedTaskHint(),
167
167
  'Delivery Tier: T0=探索/比较;T1=低风险小改动或显式验证;T2=多文件功能/新项目/需要结构化产物;T3=高风险或不可逆操作。',
168
- '路由映射:~idea=只读探索,不创建文件;~build=明确实现;~verify=审查/验证;~plan=结构化规划;~prd=重型规格;~auto=自动选择并继续执行后续阶段。',
169
- '若判定为 T3,默认先走 ~plan / ~prd;纯审查/验证请求才优先 ~verify。',
168
+ '路由映射:~idea=只读探索,不创建文件;~build=明确实现;~qa=统一质量审查/验证/修复/收尾;~plan=结构化规划;~prd=重型规格;~auto=自动选择并继续执行后续阶段。',
169
+ '若判定为 T3,默认先走 ~plan / ~prd;纯质量审查、验真或收尾请求才优先 ~qa。',
170
170
  `涉及 UI 任务时,设计决策优先级:当前活跃 plan / PRD → ${describeProjectStoreFile(cwd, 'DESIGN.md')} → 已读取的 hello-ui 规则;同时所有 UI 任务都必须满足 UI 质量基线。`,
171
171
  projectStorageHint,
172
172
  workflowHint ? `项目状态:${workflowHint}` : '',
@@ -22,7 +22,7 @@ function shouldBypassRoute(prompt) {
22
22
 
23
23
  function buildHelpExtraRules(skillName) {
24
24
  if (skillName !== 'help') return ''
25
- return ' 这是 HelloAGENTS 的帮助命令,不是宿主 CLI 的内置帮助。仅显示 HelloAGENTS 的帮助和当前设置;优先使用当前会话上下文中已注入的“当前用户设置”、配置文件原始 JSON 或此前读取结果摘要,上下文不存在或缺少要展示的配置项时才读取一次 ~/.helloagents/helloagents.json;自动激活技能说明仅在全局模式或已初始化项目时生效。不要调用宿主 CLI 的帮助工具(如 cli_help 或 /help),不要使用子代理,不要读取项目文件;若受工作区限制无法读取配置,必须明确说明并按已知默认值或已注入设置展示。'
25
+ return ' 这是 HelloAGENTS 的帮助命令,不是宿主 CLI 的内置帮助。仅显示 HelloAGENTS 的帮助和当前设置;优先使用当前会话上下文中已注入的“当前用户设置”、配置文件原始 JSON 或此前读取结果摘要,上下文不存在或缺少要展示的配置项时才读取一次 ~/.helloagents/helloagents.json;自动激活技能说明仅在宿主全局模式或已初始化项目时生效。不要调用宿主 CLI 的帮助工具(如 cli_help 或 /help),不要使用子代理,不要读取项目文件;若受工作区限制无法读取配置,必须明确说明并按已知默认值或已注入设置展示。'
26
26
  }
27
27
 
28
28
  function routeExplicitCommand({
@@ -24,7 +24,8 @@ export function buildWindowsSoundCommand(filePath = '') {
24
24
  }
25
25
 
26
26
  function playWindows(filePath) {
27
- const result = spawnSync('powershell', [
27
+ const command = process.platform === 'win32' ? 'powershell.exe' : 'powershell'
28
+ const result = spawnSync(command, [
28
29
  '-NoProfile',
29
30
  '-c',
30
31
  buildWindowsSoundCommand(filePath),
@@ -41,7 +41,7 @@ const EVENT_NAME = {
41
41
  UserPromptSubmit: IS_GEMINI ? 'BeforeAgent' : 'UserPromptSubmit',
42
42
  PreCompact: IS_GEMINI ? 'BeforeAgent' : 'PreCompact',
43
43
  };
44
- const RALPH_LOOP_ROUTE_COMMANDS = new Set(['verify', 'loop']);
44
+ const RALPH_LOOP_ROUTE_COMMANDS = new Set(['qa', 'loop']);
45
45
  const CODEX_HOOKS_FILE = join(homedir(), '.codex', 'hooks.json');
46
46
  const GATE_MODULE_LOADERS = {
47
47
  'turn-stop-gate': () => import('./turn-stop-gate.mjs'),
@@ -459,7 +459,7 @@ function cmdInject() {
459
459
  const cwd = payload.cwd || process.cwd();
460
460
  const settings = getSettings();
461
461
  const bootstrapFile = resolveBootstrapFile(cwd, settings, HOST);
462
- const shouldEnsureProjectLocal = bootstrapFile === 'bootstrap.md' || source === 'resume' || source === 'compact';
462
+ const shouldEnsureProjectLocal = isProjectRuntimeActive(cwd);
463
463
 
464
464
  startReplaySession(cwd, {
465
465
  host: HOST,
@@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url'
4
4
  import { resolveProjectPlanDir } from './project-storage.mjs'
5
5
 
6
6
  export const PLAN_CONTRACT_FILE_NAME = 'contract.json'
7
- const VALID_VERIFY_MODES = new Set(['test-first', 'review-first'])
7
+ const VALID_QA_MODES = new Set(['standard', 'deep'])
8
8
  const VALID_ADVISOR_SOURCES = new Set(['claude', 'codex', 'gemini'])
9
9
 
10
10
  function normalizeStringArray(values) {
@@ -14,9 +14,9 @@ function normalizeStringArray(values) {
14
14
  .filter(Boolean))]
15
15
  }
16
16
 
17
- function normalizeVerifyMode(value) {
17
+ function normalizeQaMode(value) {
18
18
  const normalized = typeof value === 'string' ? value.trim().toLowerCase() : ''
19
- return VALID_VERIFY_MODES.has(normalized) ? normalized : ''
19
+ return VALID_QA_MODES.has(normalized) ? normalized : ''
20
20
  }
21
21
 
22
22
  function normalizeUiStyleAdvisorContract(input = {}) {
@@ -88,12 +88,11 @@ export function readPlanContract(planDir) {
88
88
 
89
89
  export function normalizePlanContract(input = {}) {
90
90
  return {
91
- version: 1,
91
+ version: 2,
92
92
  source: typeof input.source === 'string' && input.source.trim() ? input.source.trim() : 'manual',
93
93
  originCommand: typeof input.originCommand === 'string' ? input.originCommand.trim() : '',
94
- verifyMode: normalizeVerifyMode(input.verifyMode),
95
- reviewerFocus: normalizeStringArray(input.reviewerFocus),
96
- testerFocus: normalizeStringArray(input.testerFocus),
94
+ qaMode: normalizeQaMode(input.qaMode),
95
+ qaFocus: normalizeStringArray(input.qaFocus),
97
96
  ui: normalizeUiContract(input.ui),
98
97
  advisor: normalizeAdvisorContract(input.advisor),
99
98
  }
@@ -128,14 +127,11 @@ export function getPlanContractIssues(contract = null) {
128
127
  const advisorRequirement = getAdvisorRequirement(normalized)
129
128
  const visualValidation = getVisualValidationRequirement(normalized)
130
129
  const issues = []
131
- if (!normalizeVerifyMode(normalized.verifyMode)) {
132
- issues.push('contract.json missing valid verifyMode')
130
+ if (!normalizeQaMode(normalized.qaMode)) {
131
+ issues.push('contract.json missing valid qaMode')
133
132
  }
134
- if (normalizeStringArray(normalized.testerFocus).length === 0) {
135
- issues.push('contract.json missing testerFocus')
136
- }
137
- if (normalizeVerifyMode(normalized.verifyMode) === 'review-first' && normalizeStringArray(normalized.reviewerFocus).length === 0) {
138
- issues.push('contract.json missing reviewerFocus for review-first flow')
133
+ if (normalizeStringArray(normalized.qaFocus).length === 0) {
134
+ issues.push('contract.json missing qaFocus')
139
135
  }
140
136
  if (normalized.ui?.required && normalizeStringArray(normalized.ui.sourcePriority).length === 0) {
141
137
  issues.push('contract.json missing ui.sourcePriority')
@@ -1,19 +1,20 @@
1
- import { existsSync, readdirSync, rmSync } from 'node:fs'
1
+ import { existsSync, readdirSync, rmSync, statSync } from 'node:fs'
2
2
  import { join } from 'node:path'
3
3
 
4
4
  import {
5
5
  ACTIVE_SESSION_FILE_NAME,
6
- CAPSULE_FILE_NAME,
7
- EVENTS_FILE_NAME,
8
- PROJECT_ARTIFACTS_DIR_NAME,
9
6
  PROJECT_SESSIONS_DIR_NAME,
10
7
  getProjectActivationDir,
11
8
  getProjectRoot,
12
9
  readJsonFile,
13
10
  writeJsonFileAtomic,
14
11
  } from './runtime-scope.mjs'
12
+ import { LONG_RUNNING_TTL_MS } from './runtime-ttl.mjs'
13
+ import { readStateDocument } from './state-document.mjs'
15
14
 
16
15
  export const PROJECT_SESSION_CLEANUP_COOLDOWN_MS = 10 * 60 * 1000
16
+ export const PROJECT_SESSION_MAX_AGE_MS = LONG_RUNNING_TTL_MS
17
+ const AUTO_CREATED_STATE_MARKER = '由运行时自动创建;后续按实际任务重写'
17
18
 
18
19
  function removePath(filePath, result, bucket) {
19
20
  try {
@@ -24,6 +25,10 @@ function removePath(filePath, result, bucket) {
24
25
  }
25
26
  }
26
27
 
28
+ function isDebugLog(entryName = '') {
29
+ return /\.log$/i.test(entryName)
30
+ }
31
+
27
32
  function isDirectoryEmptyRecursive(dirPath) {
28
33
  const entries = readdirSync(dirPath, { withFileTypes: true })
29
34
  if (entries.length === 0) return true
@@ -33,29 +38,6 @@ function isDirectoryEmptyRecursive(dirPath) {
33
38
  })
34
39
  }
35
40
 
36
- function listFilesRecursive(dirPath) {
37
- const entries = readdirSync(dirPath, { withFileTypes: true })
38
- return entries.flatMap((entry) => {
39
- const entryPath = join(dirPath, entry.name)
40
- if (entry.isDirectory()) {
41
- return listFilesRecursive(entryPath).map((child) => `${entry.name}/${child}`)
42
- }
43
- return entry.isFile() ? [entry.name] : []
44
- })
45
- }
46
-
47
- function isRouteOnlySessionDir(sessionDir) {
48
- if (existsSync(join(sessionDir, 'STATE.md'))) return false
49
- const files = listFilesRecursive(sessionDir).map((file) => file.replace(/\\/g, '/'))
50
- if (files.length === 0) return false
51
- if (!files.includes(`${PROJECT_ARTIFACTS_DIR_NAME}/codex-native-stop.json`)) return false
52
- return files.every((file) => [
53
- CAPSULE_FILE_NAME,
54
- EVENTS_FILE_NAME,
55
- `${PROJECT_ARTIFACTS_DIR_NAME}/codex-native-stop.json`,
56
- ].includes(file))
57
- }
58
-
59
41
  function shouldKeepSession(active, workspace, session) {
60
42
  const activeWorkspace = active.workspace || active.branch || ''
61
43
  return activeWorkspace === workspace && active.session === session
@@ -75,7 +57,70 @@ function writeCleanupCheckpoint(activePath, active, now) {
75
57
  })
76
58
  }
77
59
 
78
- export function cleanupProjectSessions(cwd, { now = Date.now(), minIntervalMs = 0 } = {}) {
60
+ function hasStateSnapshot(sessionDir) {
61
+ return existsSync(join(sessionDir, 'STATE.md'))
62
+ }
63
+
64
+ function isAutoCreatedSeedSession(sessionDir) {
65
+ const statePath = join(sessionDir, 'STATE.md')
66
+ if (!existsSync(statePath)) return false
67
+
68
+ const { metadata, body } = readStateDocument(statePath)
69
+ if (metadata && typeof metadata === 'object' && Object.keys(metadata).length > 0) return false
70
+ return String(body || '').includes(AUTO_CREATED_STATE_MARKER)
71
+ }
72
+
73
+ function readSessionStateMtimeMs(sessionDir) {
74
+ try {
75
+ return statSync(join(sessionDir, 'STATE.md')).mtimeMs
76
+ } catch {
77
+ return 0
78
+ }
79
+ }
80
+
81
+ function isStaleStateSession(sessionDir, now, maxAgeMs) {
82
+ const mtimeMs = readSessionStateMtimeMs(sessionDir)
83
+ return !Number.isFinite(mtimeMs) || mtimeMs <= 0 || (now - mtimeMs > maxAgeMs)
84
+ }
85
+
86
+ function isTransientSessionTemp(entryName = '') {
87
+ return /^\.[0-9]+-[0-9a-f-]+\.tmp$/i.test(entryName)
88
+ }
89
+
90
+ function cleanupTransientSessionTemps(sessionsDir, result) {
91
+ for (const entry of readdirSync(sessionsDir, { withFileTypes: true })) {
92
+ if (!entry.isFile() || !isTransientSessionTemp(entry.name)) continue
93
+ removePath(join(sessionsDir, entry.name), result, 'removedTempFiles')
94
+ }
95
+ }
96
+
97
+ function cleanupLegacyProjectArtifacts(activationDir, result) {
98
+ const artifactsDir = join(activationDir, 'artifacts')
99
+ if (!existsSync(artifactsDir)) return
100
+
101
+ let removableEntries = []
102
+ try {
103
+ removableEntries = readdirSync(artifactsDir, { withFileTypes: true })
104
+ } catch (error) {
105
+ result.errors.push(`${artifactsDir}: ${error.message}`)
106
+ return
107
+ }
108
+
109
+ for (const entry of removableEntries) {
110
+ if (!entry.isFile() || !isDebugLog(entry.name)) continue
111
+ removePath(join(artifactsDir, entry.name), result, 'removedLegacyArtifacts')
112
+ }
113
+
114
+ try {
115
+ if (isDirectoryEmptyRecursive(artifactsDir)) {
116
+ removePath(artifactsDir, result, 'removedLegacyArtifacts')
117
+ }
118
+ } catch (error) {
119
+ result.errors.push(`${artifactsDir}: ${error.message}`)
120
+ }
121
+ }
122
+
123
+ export function cleanupProjectSessions(cwd, { now = Date.now(), minIntervalMs = 0, maxAgeMs = PROJECT_SESSION_MAX_AGE_MS } = {}) {
79
124
  const projectRoot = getProjectRoot(cwd)
80
125
  const activationDir = getProjectActivationDir(projectRoot)
81
126
  const sessionsDir = join(activationDir, PROJECT_SESSIONS_DIR_NAME)
@@ -84,7 +129,11 @@ export function cleanupProjectSessions(cwd, { now = Date.now(), minIntervalMs =
84
129
  const result = {
85
130
  sessionsDir,
86
131
  removedEmptyDirs: [],
87
- removedRouteOnlyDirs: [],
132
+ removedInactiveDirs: [],
133
+ removedNoStateDirs: [],
134
+ removedSeedDirs: [],
135
+ removedTempFiles: [],
136
+ removedLegacyArtifacts: [],
88
137
  errors: [],
89
138
  skipped: false,
90
139
  }
@@ -98,6 +147,13 @@ export function cleanupProjectSessions(cwd, { now = Date.now(), minIntervalMs =
98
147
  }
99
148
  }
100
149
 
150
+ try {
151
+ cleanupTransientSessionTemps(sessionsDir, result)
152
+ } catch (error) {
153
+ result.errors.push(`${sessionsDir}: ${error.message}`)
154
+ }
155
+ cleanupLegacyProjectArtifacts(activationDir, result)
156
+
101
157
  for (const workspaceEntry of readdirSync(sessionsDir, { withFileTypes: true })) {
102
158
  if (!workspaceEntry.isDirectory()) continue
103
159
  const workspaceDir = join(sessionsDir, workspaceEntry.name)
@@ -110,8 +166,12 @@ export function cleanupProjectSessions(cwd, { now = Date.now(), minIntervalMs =
110
166
  try {
111
167
  if (isDirectoryEmptyRecursive(sessionDir)) {
112
168
  removePath(sessionDir, result, 'removedEmptyDirs')
113
- } else if (isRouteOnlySessionDir(sessionDir)) {
114
- removePath(sessionDir, result, 'removedRouteOnlyDirs')
169
+ } else if (!hasStateSnapshot(sessionDir)) {
170
+ removePath(sessionDir, result, 'removedNoStateDirs')
171
+ } else if (isAutoCreatedSeedSession(sessionDir)) {
172
+ removePath(sessionDir, result, 'removedSeedDirs')
173
+ } else if (isStaleStateSession(sessionDir, now, maxAgeMs)) {
174
+ removePath(sessionDir, result, 'removedInactiveDirs')
115
175
  }
116
176
  } catch (error) {
117
177
  result.errors.push(`${sessionDir}: ${error.message}`)