scene-capability-engine 3.6.32 → 3.6.36

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 (83) hide show
  1. package/CHANGELOG.md +86 -1
  2. package/README.md +119 -122
  3. package/README.zh.md +123 -121
  4. package/bin/scene-capability-engine.js +11 -0
  5. package/docs/README.md +21 -32
  6. package/docs/auto-refactor-index.md +384 -0
  7. package/docs/command-reference.md +94 -2
  8. package/docs/magicball-adaptation-task-checklist-v1.md +385 -0
  9. package/docs/magicball-app-bundle-sqlite-and-command-draft.md +539 -0
  10. package/docs/magicball-capability-iteration-api.md +2 -0
  11. package/docs/magicball-capability-iteration-ui.md +2 -0
  12. package/docs/magicball-capability-library.md +2 -0
  13. package/docs/magicball-cli-invocation-examples.md +336 -0
  14. package/docs/magicball-frontend-state-and-command-mapping.md +244 -0
  15. package/docs/magicball-integration-doc-index.md +137 -0
  16. package/docs/magicball-integration-issue-tracker.md +218 -0
  17. package/docs/magicball-mode-home-and-ontology-empty-state-playbook.md +249 -0
  18. package/docs/magicball-sce-adaptation-guide.md +203 -0
  19. package/docs/magicball-three-mode-alignment-plan.md +551 -0
  20. package/docs/magicball-ui-surface-checklist.md +126 -0
  21. package/docs/magicball-write-auth-adaptation-guide.md +328 -0
  22. package/docs/refactor-completion-roadmap.md +116 -0
  23. package/docs/zh/README.md +27 -30
  24. package/docs/zh/refactor-completion-roadmap.md +116 -0
  25. package/lib/app/registry-config.js +73 -0
  26. package/lib/app/registry-sync-service.js +228 -0
  27. package/lib/auto/archive-schema-service.js +276 -0
  28. package/lib/auto/archive-summary.js +60 -0
  29. package/lib/auto/batch-goal-input-service.js +543 -0
  30. package/lib/auto/batch-output.js +201 -0
  31. package/lib/auto/batch-summary-storage-service.js +110 -0
  32. package/lib/auto/close-loop-batch-service.js +116 -0
  33. package/lib/auto/close-loop-controller-service.js +287 -0
  34. package/lib/auto/close-loop-program-service.js +283 -0
  35. package/lib/auto/close-loop-recovery-service.js +191 -0
  36. package/lib/auto/close-loop-session-storage-service.js +50 -0
  37. package/lib/auto/controller-lock-service.js +55 -0
  38. package/lib/auto/controller-output.js +32 -0
  39. package/lib/auto/controller-queue-service.js +127 -0
  40. package/lib/auto/controller-session-storage-service.js +105 -0
  41. package/lib/auto/governance-advisory-service.js +208 -0
  42. package/lib/auto/governance-close-loop-service.js +411 -0
  43. package/lib/auto/governance-maintenance-presenter.js +162 -0
  44. package/lib/auto/governance-maintenance-service.js +112 -0
  45. package/lib/auto/governance-session-presenter.js +70 -0
  46. package/lib/auto/governance-session-storage-service.js +198 -0
  47. package/lib/auto/governance-signals.js +139 -0
  48. package/lib/auto/governance-stats-presenter.js +337 -0
  49. package/lib/auto/governance-stats-service.js +115 -0
  50. package/lib/auto/governance-summary.js +703 -0
  51. package/lib/auto/handoff-capability-matrix-service.js +281 -0
  52. package/lib/auto/handoff-evidence-review-service.js +251 -0
  53. package/lib/auto/handoff-release-evidence-service.js +190 -0
  54. package/lib/auto/handoff-release-gate-history-loaders-service.js +502 -0
  55. package/lib/auto/handoff-release-gate-history-service.js +257 -0
  56. package/lib/auto/handoff-reporting-service.js +1407 -0
  57. package/lib/auto/handoff-run-service.js +486 -0
  58. package/lib/auto/handoff-snapshots-service.js +645 -0
  59. package/lib/auto/observability-service.js +132 -0
  60. package/lib/auto/output-writer.js +34 -0
  61. package/lib/auto/program-auto-remediation-service.js +130 -0
  62. package/lib/auto/program-diagnostics.js +138 -0
  63. package/lib/auto/program-governance-helpers.js +306 -0
  64. package/lib/auto/program-governance-loop-service.js +413 -0
  65. package/lib/auto/program-output.js +106 -0
  66. package/lib/auto/program-summary.js +183 -0
  67. package/lib/auto/recovery-memory-service.js +684 -0
  68. package/lib/auto/recovery-selection-service.js +52 -0
  69. package/lib/auto/retention-policy.js +98 -0
  70. package/lib/auto/session-persistence-service.js +106 -0
  71. package/lib/auto/session-presenter.js +105 -0
  72. package/lib/auto/session-prune-service.js +190 -0
  73. package/lib/auto/session-query-service.js +249 -0
  74. package/lib/auto/spec-protection.js +141 -0
  75. package/lib/commands/app.js +911 -0
  76. package/lib/commands/assurance.js +212 -0
  77. package/lib/commands/auto.js +1091 -11063
  78. package/lib/commands/mode.js +321 -0
  79. package/lib/commands/ontology.js +415 -0
  80. package/lib/commands/pm.js +422 -0
  81. package/lib/ontology/seed-profiles.js +160 -0
  82. package/lib/state/sce-state-store.js +3369 -1200
  83. package/package.json +1 -1
@@ -0,0 +1,52 @@
1
+ async function resolveLatestRecoverableBatchSummary(projectPath, resumeStrategy = 'pending', dependencies = {}) {
2
+ const {
3
+ readCloseLoopBatchSummaryEntries,
4
+ loadCloseLoopBatchSummaryPayload,
5
+ buildCloseLoopBatchGoalsFromSummaryPayload
6
+ } = dependencies;
7
+ const entries = await readCloseLoopBatchSummaryEntries(projectPath);
8
+ for (const entry of entries) {
9
+ if (!entry || !entry.file) {
10
+ continue;
11
+ }
12
+ let loaded = null;
13
+ try {
14
+ loaded = await loadCloseLoopBatchSummaryPayload(projectPath, entry.file);
15
+ const goalsResult = await buildCloseLoopBatchGoalsFromSummaryPayload(
16
+ loaded.payload,
17
+ loaded.file,
18
+ projectPath,
19
+ 'auto',
20
+ resumeStrategy
21
+ );
22
+ if (Array.isArray(goalsResult.goals) && goalsResult.goals.length > 0) {
23
+ return loaded;
24
+ }
25
+ } catch (_error) {
26
+ continue;
27
+ }
28
+ }
29
+ return null;
30
+ }
31
+
32
+ async function resolveLatestPendingControllerSession(projectPath, dependencies = {}) {
33
+ const {
34
+ readCloseLoopControllerSessionEntries,
35
+ loadCloseLoopControllerSessionPayload
36
+ } = dependencies;
37
+ const sessions = await readCloseLoopControllerSessionEntries(projectPath);
38
+ const pendingSession = sessions.find((session) => Number(session && session.pending_goals) > 0);
39
+ if (!pendingSession || !pendingSession.file) {
40
+ return null;
41
+ }
42
+ try {
43
+ return await loadCloseLoopControllerSessionPayload(projectPath, pendingSession.file);
44
+ } catch (_error) {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ module.exports = {
50
+ resolveLatestRecoverableBatchSummary,
51
+ resolveLatestPendingControllerSession
52
+ };
@@ -0,0 +1,98 @@
1
+ function normalizeKeep(keepCandidate) {
2
+ if (keepCandidate === undefined || keepCandidate === null) {
3
+ return 20;
4
+ }
5
+ const parsed = Number(keepCandidate);
6
+ if (!Number.isInteger(parsed) || parsed < 0 || parsed > 1000) {
7
+ throw new Error('--keep must be an integer between 0 and 1000.');
8
+ }
9
+ return parsed;
10
+ }
11
+
12
+ function normalizeSpecKeep(keepCandidate, fallback = 200) {
13
+ if (keepCandidate === undefined || keepCandidate === null) {
14
+ return fallback;
15
+ }
16
+ const parsed = Number(keepCandidate);
17
+ if (!Number.isInteger(parsed) || parsed < 0 || parsed > 5000) {
18
+ throw new Error('--keep must be an integer between 0 and 5000.');
19
+ }
20
+ return parsed;
21
+ }
22
+
23
+ function normalizeOlderThanDays(daysCandidate) {
24
+ if (daysCandidate === undefined || daysCandidate === null) {
25
+ return null;
26
+ }
27
+ const parsed = Number(daysCandidate);
28
+ if (!Number.isInteger(parsed) || parsed < 0 || parsed > 36500) {
29
+ throw new Error('--older-than-days must be an integer between 0 and 36500.');
30
+ }
31
+ return parsed;
32
+ }
33
+
34
+ function normalizeSpecSessionProtectWindowDays(daysCandidate) {
35
+ if (daysCandidate === undefined || daysCandidate === null) {
36
+ return 7;
37
+ }
38
+ const parsed = Number(daysCandidate);
39
+ if (!Number.isInteger(parsed) || parsed < 0 || parsed > 36500) {
40
+ throw new Error('--spec-session-protect-window-days must be an integer between 0 and 36500.');
41
+ }
42
+ return parsed;
43
+ }
44
+
45
+ function normalizeSpecSessionMaxTotal(maxTotalCandidate) {
46
+ if (maxTotalCandidate === undefined || maxTotalCandidate === null) {
47
+ return null;
48
+ }
49
+ const parsed = Number(maxTotalCandidate);
50
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 500000) {
51
+ throw new Error('--spec-session-max-total must be an integer between 1 and 500000.');
52
+ }
53
+ return parsed;
54
+ }
55
+
56
+ function normalizeSpecSessionMaxCreated(maxCreatedCandidate) {
57
+ if (maxCreatedCandidate === undefined || maxCreatedCandidate === null) {
58
+ return null;
59
+ }
60
+ const parsed = Number(maxCreatedCandidate);
61
+ if (!Number.isInteger(parsed) || parsed < 0 || parsed > 500000) {
62
+ throw new Error('--spec-session-max-created must be an integer between 0 and 500000.');
63
+ }
64
+ return parsed;
65
+ }
66
+
67
+ function normalizeSpecSessionMaxCreatedPerGoal(maxCreatedPerGoalCandidate) {
68
+ if (maxCreatedPerGoalCandidate === undefined || maxCreatedPerGoalCandidate === null) {
69
+ return null;
70
+ }
71
+ const parsed = Number(maxCreatedPerGoalCandidate);
72
+ if (Number.isNaN(parsed) || parsed < 0 || parsed > 1000) {
73
+ throw new Error('--spec-session-max-created-per-goal must be a number between 0 and 1000.');
74
+ }
75
+ return Number(parsed.toFixed(2));
76
+ }
77
+
78
+ function normalizeSpecSessionMaxDuplicateGoals(maxDuplicateGoalsCandidate) {
79
+ if (maxDuplicateGoalsCandidate === undefined || maxDuplicateGoalsCandidate === null) {
80
+ return null;
81
+ }
82
+ const parsed = Number(maxDuplicateGoalsCandidate);
83
+ if (!Number.isInteger(parsed) || parsed < 0 || parsed > 500000) {
84
+ throw new Error('--spec-session-max-duplicate-goals must be an integer between 0 and 500000.');
85
+ }
86
+ return parsed;
87
+ }
88
+
89
+ module.exports = {
90
+ normalizeKeep,
91
+ normalizeSpecKeep,
92
+ normalizeOlderThanDays,
93
+ normalizeSpecSessionProtectWindowDays,
94
+ normalizeSpecSessionMaxTotal,
95
+ normalizeSpecSessionMaxCreated,
96
+ normalizeSpecSessionMaxCreatedPerGoal,
97
+ normalizeSpecSessionMaxDuplicateGoals
98
+ };
@@ -0,0 +1,106 @@
1
+ async function maybePersistCloseLoopControllerSummary(summary, options, projectPath, dependencies = {}) {
2
+ const {
3
+ normalizeControllerSessionKeep,
4
+ normalizeControllerSessionOlderThanDays,
5
+ sanitizeBatchSessionId,
6
+ createControllerSessionId,
7
+ getCloseLoopControllerSessionDir,
8
+ pruneCloseLoopControllerSessions,
9
+ schemaVersion,
10
+ fs,
11
+ now = () => new Date()
12
+ } = dependencies;
13
+ if (options.controllerSession === false) {
14
+ return;
15
+ }
16
+
17
+ const keep = normalizeControllerSessionKeep(options.controllerSessionKeep);
18
+ const olderThanDays = normalizeControllerSessionOlderThanDays(options.controllerSessionOlderThanDays);
19
+ const requestedId = typeof options.controllerSessionId === 'string' && options.controllerSessionId.trim()
20
+ ? sanitizeBatchSessionId(options.controllerSessionId.trim())
21
+ : null;
22
+ const sessionId = requestedId || createControllerSessionId();
23
+ if (!sessionId) {
24
+ throw new Error('--controller-session-id is invalid after sanitization.');
25
+ }
26
+
27
+ const sessionDir = getCloseLoopControllerSessionDir(projectPath);
28
+ const sessionFile = require('path').join(sessionDir, `${sessionId}.json`);
29
+ summary.controller_session = { id: sessionId, file: sessionFile };
30
+ summary.schema_version = schemaVersion;
31
+
32
+ await fs.ensureDir(sessionDir);
33
+ const nowValue = now();
34
+ const updatedAt = nowValue instanceof Date ? nowValue.toISOString() : new Date(nowValue).toISOString();
35
+ await fs.writeJson(sessionFile, {
36
+ ...summary,
37
+ schema_version: schemaVersion,
38
+ controller_session: { id: sessionId, file: sessionFile },
39
+ updated_at: updatedAt
40
+ }, { spaces: 2 });
41
+
42
+ if (keep !== null || olderThanDays !== null) {
43
+ summary.controller_session_prune = await pruneCloseLoopControllerSessions(projectPath, {
44
+ keep: keep === null ? null : keep,
45
+ olderThanDays,
46
+ currentFile: sessionFile,
47
+ dryRun: false
48
+ });
49
+ }
50
+ }
51
+
52
+ async function maybePersistCloseLoopBatchSummary(summary, options, projectPath, dependencies = {}) {
53
+ const {
54
+ normalizeBatchSessionKeep,
55
+ normalizeBatchSessionOlderThanDays,
56
+ sanitizeBatchSessionId,
57
+ createBatchSessionId,
58
+ getCloseLoopBatchSummaryDir,
59
+ pruneCloseLoopBatchSummarySessions,
60
+ schemaVersion,
61
+ fs,
62
+ now = () => new Date()
63
+ } = dependencies;
64
+ if (options.batchSession === false) {
65
+ return;
66
+ }
67
+
68
+ const keep = normalizeBatchSessionKeep(options.batchSessionKeep);
69
+ const olderThanDays = normalizeBatchSessionOlderThanDays(options.batchSessionOlderThanDays);
70
+ const requestedId = typeof options.batchSessionId === 'string' && options.batchSessionId.trim()
71
+ ? sanitizeBatchSessionId(options.batchSessionId.trim())
72
+ : null;
73
+ const sessionId = requestedId || createBatchSessionId();
74
+ if (!sessionId) {
75
+ throw new Error('--batch-session-id is invalid after sanitization.');
76
+ }
77
+
78
+ const summaryDir = getCloseLoopBatchSummaryDir(projectPath);
79
+ const summaryFile = require('path').join(summaryDir, `${sessionId}.json`);
80
+ summary.batch_session = { id: sessionId, file: summaryFile };
81
+ summary.schema_version = schemaVersion;
82
+
83
+ await fs.ensureDir(summaryDir);
84
+ const nowValue = now();
85
+ const updatedAt = nowValue instanceof Date ? nowValue.toISOString() : new Date(nowValue).toISOString();
86
+ await fs.writeJson(summaryFile, {
87
+ ...summary,
88
+ schema_version: schemaVersion,
89
+ batch_session: { id: sessionId, file: summaryFile },
90
+ updated_at: updatedAt
91
+ }, { spaces: 2 });
92
+
93
+ if (keep !== null || olderThanDays !== null) {
94
+ summary.batch_session_prune = await pruneCloseLoopBatchSummarySessions(projectPath, {
95
+ keep: keep === null ? null : keep,
96
+ olderThanDays,
97
+ currentFile: summaryFile,
98
+ dryRun: false
99
+ });
100
+ }
101
+ }
102
+
103
+ module.exports = {
104
+ maybePersistCloseLoopControllerSummary,
105
+ maybePersistCloseLoopBatchSummary
106
+ };
@@ -0,0 +1,105 @@
1
+ function presentCloseLoopSessionList(projectPath, sessions, statusFilter, limit, buildStatusCounts, getCloseLoopSessionDir) {
2
+ const filteredSessions = Array.isArray(sessions) ? sessions : [];
3
+ return {
4
+ mode: 'auto-session-list',
5
+ session_dir: getCloseLoopSessionDir(projectPath),
6
+ total: filteredSessions.length,
7
+ status_filter: statusFilter,
8
+ status_counts: buildStatusCounts(filteredSessions),
9
+ sessions: filteredSessions.slice(0, limit)
10
+ };
11
+ }
12
+
13
+ function presentCloseLoopSessionStats(sessionDir, filteredSessions, statusFilter, days, cutoffMs, buildStatusCounts, buildMasterSpecCounts, isFailedStatus) {
14
+ // Backward compatibility: older callers omitted cutoffMs and passed builders directly.
15
+ if (typeof cutoffMs === 'function') {
16
+ isFailedStatus = buildMasterSpecCounts;
17
+ buildMasterSpecCounts = buildStatusCounts;
18
+ buildStatusCounts = cutoffMs;
19
+ cutoffMs = null;
20
+ }
21
+
22
+ let completedSessions = 0;
23
+ let failedSessions = 0;
24
+ let subSpecCountSum = 0;
25
+ let sessionsWithSubSpecs = 0;
26
+ for (const session of filteredSessions) {
27
+ const status = String(session && session.status || 'unknown').trim().toLowerCase();
28
+ if (status === 'completed') {
29
+ completedSessions += 1;
30
+ }
31
+ if (isFailedStatus(status)) {
32
+ failedSessions += 1;
33
+ }
34
+ const subSpecCount = Number(session && session.sub_spec_count);
35
+ if (Number.isFinite(subSpecCount)) {
36
+ subSpecCountSum += subSpecCount;
37
+ sessionsWithSubSpecs += 1;
38
+ }
39
+ }
40
+ const totalSessions = filteredSessions.length;
41
+ const completionRate = totalSessions > 0 ? Number(((completedSessions / totalSessions) * 100).toFixed(2)) : 0;
42
+ const failureRate = totalSessions > 0 ? Number(((failedSessions / totalSessions) * 100).toFixed(2)) : 0;
43
+ const masterSpecCounts = buildMasterSpecCounts(filteredSessions);
44
+ const latestSession = totalSessions > 0 ? filteredSessions[0] : null;
45
+ const oldestSession = totalSessions > 0 ? filteredSessions[totalSessions - 1] : null;
46
+ return {
47
+ mode: 'auto-session-stats',
48
+ session_dir: sessionDir,
49
+ criteria: {
50
+ days,
51
+ status_filter: statusFilter,
52
+ since: cutoffMs === null ? null : new Date(cutoffMs).toISOString()
53
+ },
54
+ total_sessions: totalSessions,
55
+ completed_sessions: completedSessions,
56
+ failed_sessions: failedSessions,
57
+ completion_rate_percent: completionRate,
58
+ failure_rate_percent: failureRate,
59
+ sub_spec_count_sum: subSpecCountSum,
60
+ average_sub_specs_per_session: sessionsWithSubSpecs > 0 ? Number((subSpecCountSum / sessionsWithSubSpecs).toFixed(2)) : 0,
61
+ unique_master_spec_count: Object.keys(masterSpecCounts).length,
62
+ master_spec_counts: masterSpecCounts,
63
+ status_counts: buildStatusCounts(filteredSessions),
64
+ latest_updated_at: latestSession ? latestSession.updated_at : null,
65
+ oldest_updated_at: oldestSession ? oldestSession.updated_at : null,
66
+ latest_sessions: filteredSessions.slice(0, 10).map((item) => ({
67
+ id: item.id,
68
+ status: item.status,
69
+ goal: item.goal,
70
+ master_spec: item.master_spec,
71
+ sub_spec_count: item.sub_spec_count,
72
+ updated_at: item.updated_at,
73
+ parse_error: item.parse_error
74
+ }))
75
+ };
76
+ }
77
+
78
+ function presentControllerSessionList(projectPath, filteredSessions, statusFilter, limit, buildStatusCounts, getCloseLoopControllerSessionDir) {
79
+ return {
80
+ mode: 'auto-controller-session-list',
81
+ session_dir: getCloseLoopControllerSessionDir(projectPath),
82
+ total: filteredSessions.length,
83
+ status_filter: statusFilter,
84
+ status_counts: buildStatusCounts(filteredSessions),
85
+ sessions: filteredSessions.slice(0, limit).map((item) => ({
86
+ id: item.id,
87
+ file: item.file,
88
+ status: item.status,
89
+ queue_file: item.queue_file,
90
+ queue_format: item.queue_format,
91
+ processed_goals: item.processed_goals,
92
+ pending_goals: item.pending_goals,
93
+ updated_at: item.updated_at,
94
+ parse_error: item.parse_error
95
+ }))
96
+ };
97
+ }
98
+
99
+ module.exports = {
100
+ presentCloseLoopSessionList,
101
+ presentCloseLoopSessionStats,
102
+ presentControllerSessionList
103
+ };
104
+
105
+
@@ -0,0 +1,190 @@
1
+ async function pruneCloseLoopBatchSummarySessions(projectPath, policy = {}, dependencies = {}) {
2
+ const { readCloseLoopBatchSummaryEntries, getCloseLoopBatchSummaryDir, fs, now = () => Date.now() } = dependencies;
3
+ const keep = policy.keep;
4
+ const olderThanDays = policy.olderThanDays;
5
+ const currentFile = policy.currentFile || null;
6
+ const dryRun = Boolean(policy.dryRun);
7
+ const sessions = await readCloseLoopBatchSummaryEntries(projectPath);
8
+ const nowMs = typeof now === 'function' ? Number(now()) : Date.now();
9
+ const cutoffMs = olderThanDays === null ? null : nowMs - (olderThanDays * 24 * 60 * 60 * 1000);
10
+
11
+ const keepLimit = Number.isInteger(keep) ? keep : Number.POSITIVE_INFINITY;
12
+ const deletable = [];
13
+ for (let index = 0; index < sessions.length; index += 1) {
14
+ const session = sessions[index];
15
+ if (session.file === currentFile) {
16
+ continue;
17
+ }
18
+ const beyondKeep = Number.isFinite(keepLimit) ? index >= keepLimit : true;
19
+ const beyondAge = cutoffMs === null || session.mtime_ms < cutoffMs;
20
+ if (beyondKeep && beyondAge) {
21
+ deletable.push(session);
22
+ }
23
+ }
24
+
25
+ const deleted = [];
26
+ const errors = [];
27
+ if (!dryRun) {
28
+ for (const session of deletable) {
29
+ try {
30
+ await fs.remove(session.file);
31
+ deleted.push(session);
32
+ } catch (error) {
33
+ errors.push({ id: session.id, file: session.file, error: error.message });
34
+ }
35
+ }
36
+ }
37
+
38
+ return {
39
+ enabled: true,
40
+ session_dir: getCloseLoopBatchSummaryDir(projectPath),
41
+ dry_run: dryRun,
42
+ criteria: {
43
+ keep: Number.isFinite(keepLimit) ? keepLimit : null,
44
+ older_than_days: olderThanDays
45
+ },
46
+ total_sessions: sessions.length,
47
+ kept_sessions: sessions.length - deletable.length,
48
+ deleted_count: dryRun ? deletable.length : deleted.length,
49
+ candidates: deletable.map((item) => ({ id: item.id, file: item.file, status: item.status, updated_at: item.updated_at })),
50
+ errors
51
+ };
52
+ }
53
+
54
+ async function pruneCloseLoopControllerSessions(projectPath, policy = {}, dependencies = {}) {
55
+ const { readCloseLoopControllerSessionEntries, getCloseLoopControllerSessionDir, fs, now = () => Date.now() } = dependencies;
56
+ const keep = policy.keep;
57
+ const olderThanDays = policy.olderThanDays;
58
+ const currentFile = policy.currentFile || null;
59
+ const dryRun = Boolean(policy.dryRun);
60
+ const sessions = await readCloseLoopControllerSessionEntries(projectPath);
61
+ const nowMs = typeof now === 'function' ? Number(now()) : Date.now();
62
+ const cutoffMs = olderThanDays === null ? null : nowMs - (olderThanDays * 24 * 60 * 60 * 1000);
63
+
64
+ const keepLimit = Number.isInteger(keep) ? keep : Number.POSITIVE_INFINITY;
65
+ const deletable = [];
66
+ for (let index = 0; index < sessions.length; index += 1) {
67
+ const session = sessions[index];
68
+ if (session.file === currentFile) {
69
+ continue;
70
+ }
71
+ const beyondKeep = Number.isFinite(keepLimit) ? index >= keepLimit : true;
72
+ const beyondAge = cutoffMs === null || session.mtime_ms < cutoffMs;
73
+ if (beyondKeep && beyondAge) {
74
+ deletable.push(session);
75
+ }
76
+ }
77
+
78
+ const deleted = [];
79
+ const errors = [];
80
+ if (!dryRun) {
81
+ for (const session of deletable) {
82
+ try {
83
+ await fs.remove(session.file);
84
+ deleted.push(session);
85
+ } catch (error) {
86
+ errors.push({ id: session.id, file: session.file, error: error.message });
87
+ }
88
+ }
89
+ }
90
+
91
+ return {
92
+ enabled: true,
93
+ session_dir: getCloseLoopControllerSessionDir(projectPath),
94
+ dry_run: dryRun,
95
+ criteria: {
96
+ keep: Number.isFinite(keepLimit) ? keepLimit : null,
97
+ older_than_days: olderThanDays
98
+ },
99
+ total_sessions: sessions.length,
100
+ kept_sessions: sessions.length - deletable.length,
101
+ deleted_count: dryRun ? deletable.length : deleted.length,
102
+ candidates: deletable.map((item) => ({ id: item.id, file: item.file, status: item.status, updated_at: item.updated_at })),
103
+ errors
104
+ };
105
+ }
106
+
107
+ async function pruneCloseLoopSessions(projectPath, options = {}, dependencies = {}) {
108
+ const { normalizeKeep, normalizeOlderThanDays, readCloseLoopSessionEntries, getCloseLoopSessionDir, fs, now = () => Date.now() } = dependencies;
109
+ const keep = normalizeKeep(options.keep);
110
+ const olderThanDays = normalizeOlderThanDays(options.olderThanDays);
111
+ const dryRun = Boolean(options.dryRun);
112
+ const sessions = await readCloseLoopSessionEntries(projectPath);
113
+ const nowMs = typeof now === 'function' ? Number(now()) : Date.now();
114
+ const cutoffMs = olderThanDays === null ? null : nowMs - (olderThanDays * 24 * 60 * 60 * 1000);
115
+
116
+ const keepSet = new Set(sessions.slice(0, keep).map((session) => session.file));
117
+ const deletable = sessions.filter((session) => {
118
+ if (keepSet.has(session.file)) {
119
+ return false;
120
+ }
121
+ if (cutoffMs === null) {
122
+ return true;
123
+ }
124
+ return session.mtime_ms < cutoffMs;
125
+ });
126
+
127
+ const deleted = [];
128
+ const errors = [];
129
+ if (!dryRun) {
130
+ for (const session of deletable) {
131
+ try {
132
+ await fs.remove(session.file);
133
+ deleted.push(session);
134
+ } catch (error) {
135
+ errors.push({ id: session.id, file: session.file, error: error.message });
136
+ }
137
+ }
138
+ }
139
+
140
+ return {
141
+ mode: 'auto-session-prune',
142
+ session_dir: getCloseLoopSessionDir(projectPath),
143
+ dry_run: dryRun,
144
+ criteria: {
145
+ keep,
146
+ older_than_days: olderThanDays
147
+ },
148
+ total_sessions: sessions.length,
149
+ kept_sessions: sessions.length - deletable.length,
150
+ deleted_count: dryRun ? deletable.length : deleted.length,
151
+ candidates: deletable.map((item) => ({ id: item.id, file: item.file, status: item.status, updated_at: item.updated_at })),
152
+ errors
153
+ };
154
+ }
155
+
156
+ async function pruneCloseLoopBatchSummarySessionsCli(projectPath, options = {}, dependencies = {}) {
157
+ const { normalizeKeep, normalizeOlderThanDays, pruneCloseLoopBatchSummarySessions } = dependencies;
158
+ const keep = normalizeKeep(options.keep);
159
+ const olderThanDays = normalizeOlderThanDays(options.olderThanDays);
160
+ const dryRun = Boolean(options.dryRun);
161
+ const result = await pruneCloseLoopBatchSummarySessions(projectPath, {
162
+ keep,
163
+ olderThanDays,
164
+ currentFile: null,
165
+ dryRun
166
+ });
167
+ return { mode: 'auto-batch-session-prune', ...result };
168
+ }
169
+
170
+ async function pruneCloseLoopControllerSessionsCli(projectPath, options = {}, dependencies = {}) {
171
+ const { normalizeKeep, normalizeOlderThanDays, pruneCloseLoopControllerSessions } = dependencies;
172
+ const keep = normalizeKeep(options.keep);
173
+ const olderThanDays = normalizeOlderThanDays(options.olderThanDays);
174
+ const dryRun = Boolean(options.dryRun);
175
+ const result = await pruneCloseLoopControllerSessions(projectPath, {
176
+ keep,
177
+ olderThanDays,
178
+ currentFile: null,
179
+ dryRun
180
+ });
181
+ return { mode: 'auto-controller-session-prune', ...result };
182
+ }
183
+
184
+ module.exports = {
185
+ pruneCloseLoopBatchSummarySessions,
186
+ pruneCloseLoopControllerSessions,
187
+ pruneCloseLoopSessions,
188
+ pruneCloseLoopBatchSummarySessionsCli,
189
+ pruneCloseLoopControllerSessionsCli
190
+ };