gsd-pi 2.23.0 → 2.25.0
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/README.md +2 -1
- package/dist/cli.js +12 -3
- package/dist/headless.d.ts +4 -0
- package/dist/headless.js +118 -10
- package/dist/help-text.js +22 -7
- package/dist/models-resolver.d.ts +0 -11
- package/dist/models-resolver.js +0 -15
- package/dist/resource-loader.d.ts +0 -1
- package/dist/resource-loader.js +64 -18
- package/dist/resources/GSD-WORKFLOW.md +12 -9
- package/dist/resources/extensions/bg-shell/overlay.ts +18 -17
- package/dist/resources/extensions/get-secrets-from-user.ts +5 -23
- package/dist/resources/extensions/gsd/activity-log.ts +5 -3
- package/dist/resources/extensions/gsd/auto-dispatch.ts +51 -2
- package/dist/resources/extensions/gsd/auto-prompts.ts +87 -0
- package/dist/resources/extensions/gsd/auto-recovery.ts +41 -2
- package/dist/resources/extensions/gsd/auto-worktree.ts +134 -4
- package/dist/resources/extensions/gsd/auto.ts +307 -77
- package/dist/resources/extensions/gsd/cache.ts +3 -1
- package/dist/resources/extensions/gsd/commands.ts +176 -10
- package/dist/resources/extensions/gsd/complexity.ts +1 -0
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +38 -0
- package/dist/resources/extensions/gsd/doctor.ts +58 -11
- package/dist/resources/extensions/gsd/exit-command.ts +2 -2
- package/dist/resources/extensions/gsd/git-service.ts +74 -14
- package/dist/resources/extensions/gsd/gitignore.ts +1 -0
- package/dist/resources/extensions/gsd/gsd-db.ts +78 -1
- package/dist/resources/extensions/gsd/guided-flow.ts +109 -12
- package/dist/resources/extensions/gsd/index.ts +48 -2
- package/dist/resources/extensions/gsd/memory-extractor.ts +352 -0
- package/dist/resources/extensions/gsd/memory-store.ts +441 -0
- package/dist/resources/extensions/gsd/migrate/command.ts +2 -2
- package/dist/resources/extensions/gsd/parallel-eligibility.ts +233 -0
- package/dist/resources/extensions/gsd/parallel-merge.ts +156 -0
- package/dist/resources/extensions/gsd/parallel-orchestrator.ts +496 -0
- package/dist/resources/extensions/gsd/preferences.ts +65 -1
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/discuss-headless.md +86 -0
- package/dist/resources/extensions/gsd/prompts/discuss.md +4 -4
- package/dist/resources/extensions/gsd/prompts/execute-task.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/queue.md +1 -1
- package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
- package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +40 -61
- package/dist/resources/extensions/gsd/provider-error-pause.ts +29 -2
- package/dist/resources/extensions/gsd/session-status-io.ts +197 -0
- package/dist/resources/extensions/gsd/state.ts +72 -30
- package/dist/resources/extensions/gsd/tests/agent-end-provider-error.test.ts +81 -0
- package/dist/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +20 -3
- package/dist/resources/extensions/gsd/tests/auto-preflight.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +256 -2
- package/dist/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +34 -0
- package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
- package/dist/resources/extensions/gsd/tests/complete-milestone.test.ts +8 -1
- package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +9 -15
- package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +9 -0
- package/dist/resources/extensions/gsd/tests/derive-state-draft.test.ts +8 -0
- package/dist/resources/extensions/gsd/tests/derive-state.test.ts +14 -0
- package/dist/resources/extensions/gsd/tests/git-service.test.ts +70 -4
- package/dist/resources/extensions/gsd/tests/gsd-db.test.ts +2 -2
- package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +8 -0
- package/dist/resources/extensions/gsd/tests/md-importer.test.ts +2 -3
- package/dist/resources/extensions/gsd/tests/memory-extractor.test.ts +180 -0
- package/dist/resources/extensions/gsd/tests/memory-store.test.ts +345 -0
- package/dist/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +5 -5
- package/dist/resources/extensions/gsd/tests/parallel-orchestration.test.ts +656 -0
- package/dist/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +354 -0
- package/dist/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/smart-entry-draft.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/validate-milestone.test.ts +316 -0
- package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +147 -2
- package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +88 -10
- package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +314 -87
- package/dist/resources/extensions/gsd/tests/worker-registry.test.ts +148 -0
- package/dist/resources/extensions/gsd/triage-ui.ts +1 -1
- package/dist/resources/extensions/gsd/types.ts +15 -1
- package/dist/resources/extensions/gsd/visualizer-data.ts +291 -10
- package/dist/resources/extensions/gsd/visualizer-overlay.ts +237 -28
- package/dist/resources/extensions/gsd/visualizer-views.ts +462 -48
- package/dist/resources/extensions/gsd/worktree.ts +9 -2
- package/dist/resources/extensions/search-the-web/native-search.ts +15 -5
- package/dist/resources/extensions/subagent/index.ts +5 -0
- package/dist/resources/extensions/subagent/worker-registry.ts +99 -0
- package/dist/update-check.d.ts +9 -0
- package/dist/update-check.js +97 -0
- package/package.json +6 -1
- package/packages/pi-agent-core/dist/agent-loop.js +2 -0
- package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
- package/packages/pi-agent-core/src/agent-loop.ts +2 -0
- package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic.js +55 -7
- package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
- package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/azure-openai-responses.js +12 -4
- package/packages/pi-ai/dist/providers/azure-openai-responses.js.map +1 -1
- package/packages/pi-ai/dist/providers/google-vertex.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/google-vertex.js +21 -9
- package/packages/pi-ai/dist/providers/google-vertex.js.map +1 -1
- package/packages/pi-ai/dist/providers/mistral.js +3 -0
- package/packages/pi-ai/dist/providers/mistral.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.js +12 -4
- package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-responses.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-responses.js +12 -4
- package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
- package/packages/pi-ai/dist/types.d.ts +23 -1
- package/packages/pi-ai/dist/types.d.ts.map +1 -1
- package/packages/pi-ai/dist/types.js.map +1 -1
- package/packages/pi-ai/src/providers/anthropic.ts +59 -9
- package/packages/pi-ai/src/providers/azure-openai-responses.ts +16 -4
- package/packages/pi-ai/src/providers/google-vertex.ts +32 -17
- package/packages/pi-ai/src/providers/mistral.ts +3 -0
- package/packages/pi-ai/src/providers/openai-completions.ts +16 -4
- package/packages/pi-ai/src/providers/openai-responses.ts +16 -4
- package/packages/pi-ai/src/types.ts +19 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +17 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +4 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +72 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/core/agent-session.ts +1 -1
- package/packages/pi-coding-agent/src/core/settings-manager.ts +2 -2
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +18 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +84 -0
- package/scripts/postinstall.js +7 -109
- package/src/resources/GSD-WORKFLOW.md +12 -9
- package/src/resources/extensions/bg-shell/overlay.ts +18 -17
- package/src/resources/extensions/get-secrets-from-user.ts +5 -23
- package/src/resources/extensions/gsd/activity-log.ts +5 -3
- package/src/resources/extensions/gsd/auto-dispatch.ts +51 -2
- package/src/resources/extensions/gsd/auto-prompts.ts +87 -0
- package/src/resources/extensions/gsd/auto-recovery.ts +41 -2
- package/src/resources/extensions/gsd/auto-worktree.ts +134 -4
- package/src/resources/extensions/gsd/auto.ts +307 -77
- package/src/resources/extensions/gsd/cache.ts +3 -1
- package/src/resources/extensions/gsd/commands.ts +176 -10
- package/src/resources/extensions/gsd/complexity.ts +1 -0
- package/src/resources/extensions/gsd/dashboard-overlay.ts +38 -0
- package/src/resources/extensions/gsd/doctor.ts +58 -11
- package/src/resources/extensions/gsd/exit-command.ts +2 -2
- package/src/resources/extensions/gsd/git-service.ts +74 -14
- package/src/resources/extensions/gsd/gitignore.ts +1 -0
- package/src/resources/extensions/gsd/gsd-db.ts +78 -1
- package/src/resources/extensions/gsd/guided-flow.ts +109 -12
- package/src/resources/extensions/gsd/index.ts +48 -2
- package/src/resources/extensions/gsd/memory-extractor.ts +352 -0
- package/src/resources/extensions/gsd/memory-store.ts +441 -0
- package/src/resources/extensions/gsd/migrate/command.ts +2 -2
- package/src/resources/extensions/gsd/parallel-eligibility.ts +233 -0
- package/src/resources/extensions/gsd/parallel-merge.ts +156 -0
- package/src/resources/extensions/gsd/parallel-orchestrator.ts +496 -0
- package/src/resources/extensions/gsd/preferences.ts +65 -1
- package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/discuss-headless.md +86 -0
- package/src/resources/extensions/gsd/prompts/discuss.md +4 -4
- package/src/resources/extensions/gsd/prompts/execute-task.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/queue.md +1 -1
- package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
- package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +40 -61
- package/src/resources/extensions/gsd/provider-error-pause.ts +29 -2
- package/src/resources/extensions/gsd/session-status-io.ts +197 -0
- package/src/resources/extensions/gsd/state.ts +72 -30
- package/src/resources/extensions/gsd/tests/agent-end-provider-error.test.ts +81 -0
- package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +20 -3
- package/src/resources/extensions/gsd/tests/auto-preflight.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +256 -2
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +34 -0
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +8 -1
- package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +9 -15
- package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +9 -0
- package/src/resources/extensions/gsd/tests/derive-state-draft.test.ts +8 -0
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/git-service.test.ts +70 -4
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +8 -0
- package/src/resources/extensions/gsd/tests/md-importer.test.ts +2 -3
- package/src/resources/extensions/gsd/tests/memory-extractor.test.ts +180 -0
- package/src/resources/extensions/gsd/tests/memory-store.test.ts +345 -0
- package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +5 -5
- package/src/resources/extensions/gsd/tests/parallel-orchestration.test.ts +656 -0
- package/src/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +354 -0
- package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/smart-entry-draft.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +316 -0
- package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +147 -2
- package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +88 -10
- package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +314 -87
- package/src/resources/extensions/gsd/tests/worker-registry.test.ts +148 -0
- package/src/resources/extensions/gsd/triage-ui.ts +1 -1
- package/src/resources/extensions/gsd/types.ts +15 -1
- package/src/resources/extensions/gsd/visualizer-data.ts +291 -10
- package/src/resources/extensions/gsd/visualizer-overlay.ts +237 -28
- package/src/resources/extensions/gsd/visualizer-views.ts +462 -48
- package/src/resources/extensions/gsd/worktree.ts +9 -2
- package/src/resources/extensions/search-the-web/native-search.ts +15 -5
- package/src/resources/extensions/subagent/index.ts +5 -0
- package/src/resources/extensions/subagent/worker-registry.ts +99 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
// GSD Memory Store — CRUD, ranked queries, maintenance, and prompt formatting
|
|
2
|
+
//
|
|
3
|
+
// Storage layer for auto-learned project memories. Follows context-store.ts patterns.
|
|
4
|
+
// All functions degrade gracefully: return empty results when DB unavailable, never throw.
|
|
5
|
+
|
|
6
|
+
import { isDbAvailable, _getAdapter, transaction } from './gsd-db.js';
|
|
7
|
+
|
|
8
|
+
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export interface Memory {
|
|
11
|
+
seq: number;
|
|
12
|
+
id: string;
|
|
13
|
+
category: string;
|
|
14
|
+
content: string;
|
|
15
|
+
confidence: number;
|
|
16
|
+
source_unit_type: string | null;
|
|
17
|
+
source_unit_id: string | null;
|
|
18
|
+
created_at: string;
|
|
19
|
+
updated_at: string;
|
|
20
|
+
superseded_by: string | null;
|
|
21
|
+
hit_count: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type MemoryActionCreate = {
|
|
25
|
+
action: 'CREATE';
|
|
26
|
+
category: string;
|
|
27
|
+
content: string;
|
|
28
|
+
confidence?: number;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type MemoryActionUpdate = {
|
|
32
|
+
action: 'UPDATE';
|
|
33
|
+
id: string;
|
|
34
|
+
content: string;
|
|
35
|
+
confidence?: number;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type MemoryActionReinforce = {
|
|
39
|
+
action: 'REINFORCE';
|
|
40
|
+
id: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type MemoryActionSupersede = {
|
|
44
|
+
action: 'SUPERSEDE';
|
|
45
|
+
id: string;
|
|
46
|
+
superseded_by: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type MemoryAction =
|
|
50
|
+
| MemoryActionCreate
|
|
51
|
+
| MemoryActionUpdate
|
|
52
|
+
| MemoryActionReinforce
|
|
53
|
+
| MemoryActionSupersede;
|
|
54
|
+
|
|
55
|
+
// ─── Category Display Order ─────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
const CATEGORY_PRIORITY: Record<string, number> = {
|
|
58
|
+
gotcha: 0,
|
|
59
|
+
convention: 1,
|
|
60
|
+
architecture: 2,
|
|
61
|
+
pattern: 3,
|
|
62
|
+
environment: 4,
|
|
63
|
+
preference: 5,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// ─── Row Mapping ────────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
function rowToMemory(row: Record<string, unknown>): Memory {
|
|
69
|
+
return {
|
|
70
|
+
seq: row['seq'] as number,
|
|
71
|
+
id: row['id'] as string,
|
|
72
|
+
category: row['category'] as string,
|
|
73
|
+
content: row['content'] as string,
|
|
74
|
+
confidence: row['confidence'] as number,
|
|
75
|
+
source_unit_type: (row['source_unit_type'] as string) ?? null,
|
|
76
|
+
source_unit_id: (row['source_unit_id'] as string) ?? null,
|
|
77
|
+
created_at: row['created_at'] as string,
|
|
78
|
+
updated_at: row['updated_at'] as string,
|
|
79
|
+
superseded_by: (row['superseded_by'] as string) ?? null,
|
|
80
|
+
hit_count: row['hit_count'] as number,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ─── Query Functions ────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get all memories where superseded_by IS NULL.
|
|
88
|
+
* Returns [] if DB is not available. Never throws.
|
|
89
|
+
*/
|
|
90
|
+
export function getActiveMemories(): Memory[] {
|
|
91
|
+
if (!isDbAvailable()) return [];
|
|
92
|
+
const adapter = _getAdapter();
|
|
93
|
+
if (!adapter) return [];
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const rows = adapter.prepare('SELECT * FROM memories WHERE superseded_by IS NULL').all();
|
|
97
|
+
return rows.map(rowToMemory);
|
|
98
|
+
} catch {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get active memories ordered by ranking score: confidence * (1 + hit_count * 0.1).
|
|
105
|
+
* Higher-scored memories are more relevant and frequently confirmed.
|
|
106
|
+
*/
|
|
107
|
+
export function getActiveMemoriesRanked(limit = 30): Memory[] {
|
|
108
|
+
if (!isDbAvailable()) return [];
|
|
109
|
+
const adapter = _getAdapter();
|
|
110
|
+
if (!adapter) return [];
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const rows = adapter.prepare(
|
|
114
|
+
`SELECT * FROM memories
|
|
115
|
+
WHERE superseded_by IS NULL
|
|
116
|
+
ORDER BY (confidence * (1.0 + hit_count * 0.1)) DESC
|
|
117
|
+
LIMIT :limit`,
|
|
118
|
+
).all({ ':limit': limit });
|
|
119
|
+
return rows.map(rowToMemory);
|
|
120
|
+
} catch {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Generate the next memory ID: MEM + zero-padded 3-digit from MAX(seq).
|
|
127
|
+
* Returns MEM001 if no memories exist.
|
|
128
|
+
*/
|
|
129
|
+
export function nextMemoryId(): string {
|
|
130
|
+
if (!isDbAvailable()) return 'MEM001';
|
|
131
|
+
const adapter = _getAdapter();
|
|
132
|
+
if (!adapter) return 'MEM001';
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const row = adapter
|
|
136
|
+
.prepare('SELECT MAX(seq) as max_seq FROM memories')
|
|
137
|
+
.get();
|
|
138
|
+
const maxSeq = row ? (row['max_seq'] as number | null) : null;
|
|
139
|
+
if (maxSeq == null || isNaN(maxSeq)) return 'MEM001';
|
|
140
|
+
const next = maxSeq + 1;
|
|
141
|
+
return `MEM${String(next).padStart(3, '0')}`;
|
|
142
|
+
} catch {
|
|
143
|
+
return 'MEM001';
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ─── Mutation Functions ─────────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Insert a new memory with auto-assigned ID.
|
|
151
|
+
* Returns the assigned ID, or null on failure.
|
|
152
|
+
*/
|
|
153
|
+
export function createMemory(fields: {
|
|
154
|
+
category: string;
|
|
155
|
+
content: string;
|
|
156
|
+
confidence?: number;
|
|
157
|
+
source_unit_type?: string;
|
|
158
|
+
source_unit_id?: string;
|
|
159
|
+
}): string | null {
|
|
160
|
+
if (!isDbAvailable()) return null;
|
|
161
|
+
const adapter = _getAdapter();
|
|
162
|
+
if (!adapter) return null;
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const id = nextMemoryId();
|
|
166
|
+
const now = new Date().toISOString();
|
|
167
|
+
adapter.prepare(
|
|
168
|
+
`INSERT INTO memories (id, category, content, confidence, source_unit_type, source_unit_id, created_at, updated_at)
|
|
169
|
+
VALUES (:id, :category, :content, :confidence, :source_unit_type, :source_unit_id, :created_at, :updated_at)`,
|
|
170
|
+
).run({
|
|
171
|
+
':id': id,
|
|
172
|
+
':category': fields.category,
|
|
173
|
+
':content': fields.content,
|
|
174
|
+
':confidence': fields.confidence ?? 0.8,
|
|
175
|
+
':source_unit_type': fields.source_unit_type ?? null,
|
|
176
|
+
':source_unit_id': fields.source_unit_id ?? null,
|
|
177
|
+
':created_at': now,
|
|
178
|
+
':updated_at': now,
|
|
179
|
+
});
|
|
180
|
+
return id;
|
|
181
|
+
} catch {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Update a memory's content and optionally its confidence.
|
|
188
|
+
*/
|
|
189
|
+
export function updateMemoryContent(id: string, content: string, confidence?: number): boolean {
|
|
190
|
+
if (!isDbAvailable()) return false;
|
|
191
|
+
const adapter = _getAdapter();
|
|
192
|
+
if (!adapter) return false;
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const now = new Date().toISOString();
|
|
196
|
+
if (confidence != null) {
|
|
197
|
+
adapter.prepare(
|
|
198
|
+
'UPDATE memories SET content = :content, confidence = :confidence, updated_at = :updated_at WHERE id = :id',
|
|
199
|
+
).run({ ':content': content, ':confidence': confidence, ':updated_at': now, ':id': id });
|
|
200
|
+
} else {
|
|
201
|
+
adapter.prepare(
|
|
202
|
+
'UPDATE memories SET content = :content, updated_at = :updated_at WHERE id = :id',
|
|
203
|
+
).run({ ':content': content, ':updated_at': now, ':id': id });
|
|
204
|
+
}
|
|
205
|
+
return true;
|
|
206
|
+
} catch {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Reinforce a memory: increment hit_count, update timestamp.
|
|
213
|
+
*/
|
|
214
|
+
export function reinforceMemory(id: string): boolean {
|
|
215
|
+
if (!isDbAvailable()) return false;
|
|
216
|
+
const adapter = _getAdapter();
|
|
217
|
+
if (!adapter) return false;
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
adapter.prepare(
|
|
221
|
+
'UPDATE memories SET hit_count = hit_count + 1, updated_at = :updated_at WHERE id = :id',
|
|
222
|
+
).run({ ':updated_at': new Date().toISOString(), ':id': id });
|
|
223
|
+
return true;
|
|
224
|
+
} catch {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Mark a memory as superseded by another.
|
|
231
|
+
*/
|
|
232
|
+
export function supersedeMemory(oldId: string, newId: string): boolean {
|
|
233
|
+
if (!isDbAvailable()) return false;
|
|
234
|
+
const adapter = _getAdapter();
|
|
235
|
+
if (!adapter) return false;
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
adapter.prepare(
|
|
239
|
+
'UPDATE memories SET superseded_by = :new_id, updated_at = :updated_at WHERE id = :old_id',
|
|
240
|
+
).run({ ':new_id': newId, ':updated_at': new Date().toISOString(), ':old_id': oldId });
|
|
241
|
+
return true;
|
|
242
|
+
} catch {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ─── Processed Unit Tracking ────────────────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Check if a unit has already been processed for memory extraction.
|
|
251
|
+
*/
|
|
252
|
+
export function isUnitProcessed(unitKey: string): boolean {
|
|
253
|
+
if (!isDbAvailable()) return false;
|
|
254
|
+
const adapter = _getAdapter();
|
|
255
|
+
if (!adapter) return false;
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
const row = adapter.prepare(
|
|
259
|
+
'SELECT 1 FROM memory_processed_units WHERE unit_key = :key',
|
|
260
|
+
).get({ ':key': unitKey });
|
|
261
|
+
return row != null;
|
|
262
|
+
} catch {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Record that a unit has been processed for memory extraction.
|
|
269
|
+
*/
|
|
270
|
+
export function markUnitProcessed(unitKey: string, activityFile: string): boolean {
|
|
271
|
+
if (!isDbAvailable()) return false;
|
|
272
|
+
const adapter = _getAdapter();
|
|
273
|
+
if (!adapter) return false;
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
adapter.prepare(
|
|
277
|
+
`INSERT OR IGNORE INTO memory_processed_units (unit_key, activity_file, processed_at)
|
|
278
|
+
VALUES (:key, :file, :at)`,
|
|
279
|
+
).run({ ':key': unitKey, ':file': activityFile, ':at': new Date().toISOString() });
|
|
280
|
+
return true;
|
|
281
|
+
} catch {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ─── Maintenance ────────────────────────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Reduce confidence for memories not updated within the last N processed units.
|
|
290
|
+
* "Stale" = updated_at is older than the Nth most recent processed_at.
|
|
291
|
+
*/
|
|
292
|
+
export function decayStaleMemories(thresholdUnits = 20): void {
|
|
293
|
+
if (!isDbAvailable()) return;
|
|
294
|
+
const adapter = _getAdapter();
|
|
295
|
+
if (!adapter) return;
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
// Find the timestamp of the Nth most recent processed unit
|
|
299
|
+
const row = adapter.prepare(
|
|
300
|
+
`SELECT processed_at FROM memory_processed_units
|
|
301
|
+
ORDER BY processed_at DESC
|
|
302
|
+
LIMIT 1 OFFSET :offset`,
|
|
303
|
+
).get({ ':offset': thresholdUnits - 1 });
|
|
304
|
+
|
|
305
|
+
if (!row) return; // not enough processed units yet
|
|
306
|
+
|
|
307
|
+
const cutoff = row['processed_at'] as string;
|
|
308
|
+
adapter.prepare(
|
|
309
|
+
`UPDATE memories
|
|
310
|
+
SET confidence = MAX(0.1, confidence - 0.1), updated_at = :now
|
|
311
|
+
WHERE superseded_by IS NULL AND updated_at < :cutoff AND confidence > 0.1`,
|
|
312
|
+
).run({ ':now': new Date().toISOString(), ':cutoff': cutoff });
|
|
313
|
+
} catch {
|
|
314
|
+
// non-fatal
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Supersede lowest-ranked memories when count exceeds cap.
|
|
320
|
+
*/
|
|
321
|
+
export function enforceMemoryCap(max = 50): void {
|
|
322
|
+
if (!isDbAvailable()) return;
|
|
323
|
+
const adapter = _getAdapter();
|
|
324
|
+
if (!adapter) return;
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
const countRow = adapter.prepare(
|
|
328
|
+
'SELECT count(*) as cnt FROM memories WHERE superseded_by IS NULL',
|
|
329
|
+
).get();
|
|
330
|
+
const count = (countRow?.['cnt'] as number) ?? 0;
|
|
331
|
+
if (count <= max) return;
|
|
332
|
+
|
|
333
|
+
const excess = count - max;
|
|
334
|
+
// Find the IDs of the lowest-ranked active memories
|
|
335
|
+
const rows = adapter.prepare(
|
|
336
|
+
`SELECT id FROM memories
|
|
337
|
+
WHERE superseded_by IS NULL
|
|
338
|
+
ORDER BY (confidence * (1.0 + hit_count * 0.1)) ASC
|
|
339
|
+
LIMIT :limit`,
|
|
340
|
+
).all({ ':limit': excess });
|
|
341
|
+
|
|
342
|
+
const now = new Date().toISOString();
|
|
343
|
+
for (const row of rows) {
|
|
344
|
+
adapter.prepare(
|
|
345
|
+
'UPDATE memories SET superseded_by = :reason, updated_at = :now WHERE id = :id',
|
|
346
|
+
).run({ ':reason': 'CAP_EXCEEDED', ':now': now, ':id': row['id'] as string });
|
|
347
|
+
}
|
|
348
|
+
} catch {
|
|
349
|
+
// non-fatal
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ─── Action Application ─────────────────────────────────────────────────────
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Process an array of memory actions in a transaction.
|
|
357
|
+
* Calls enforceMemoryCap at the end.
|
|
358
|
+
*/
|
|
359
|
+
export function applyMemoryActions(
|
|
360
|
+
actions: MemoryAction[],
|
|
361
|
+
unitType?: string,
|
|
362
|
+
unitId?: string,
|
|
363
|
+
): void {
|
|
364
|
+
if (!isDbAvailable() || actions.length === 0) return;
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
transaction(() => {
|
|
368
|
+
for (const action of actions) {
|
|
369
|
+
switch (action.action) {
|
|
370
|
+
case 'CREATE':
|
|
371
|
+
createMemory({
|
|
372
|
+
category: action.category,
|
|
373
|
+
content: action.content,
|
|
374
|
+
confidence: action.confidence,
|
|
375
|
+
source_unit_type: unitType,
|
|
376
|
+
source_unit_id: unitId,
|
|
377
|
+
});
|
|
378
|
+
break;
|
|
379
|
+
case 'UPDATE':
|
|
380
|
+
updateMemoryContent(action.id, action.content, action.confidence);
|
|
381
|
+
break;
|
|
382
|
+
case 'REINFORCE':
|
|
383
|
+
reinforceMemory(action.id);
|
|
384
|
+
break;
|
|
385
|
+
case 'SUPERSEDE':
|
|
386
|
+
supersedeMemory(action.id, action.superseded_by);
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
enforceMemoryCap();
|
|
391
|
+
});
|
|
392
|
+
} catch {
|
|
393
|
+
// non-fatal — transaction will have rolled back
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ─── Prompt Formatting ──────────────────────────────────────────────────────
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Format memories as categorized markdown for system prompt injection.
|
|
401
|
+
* Truncates to token budget (~4 chars per token).
|
|
402
|
+
*/
|
|
403
|
+
export function formatMemoriesForPrompt(memories: Memory[], tokenBudget = 2000): string {
|
|
404
|
+
if (memories.length === 0) return '';
|
|
405
|
+
|
|
406
|
+
const charBudget = tokenBudget * 4;
|
|
407
|
+
const header = '## Project Memory (auto-learned)\n';
|
|
408
|
+
let output = header;
|
|
409
|
+
let remaining = charBudget - header.length;
|
|
410
|
+
|
|
411
|
+
// Group by category
|
|
412
|
+
const grouped = new Map<string, Memory[]>();
|
|
413
|
+
for (const m of memories) {
|
|
414
|
+
const list = grouped.get(m.category) ?? [];
|
|
415
|
+
list.push(m);
|
|
416
|
+
grouped.set(m.category, list);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Sort categories by priority
|
|
420
|
+
const sortedCategories = [...grouped.keys()].sort(
|
|
421
|
+
(a, b) => (CATEGORY_PRIORITY[a] ?? 99) - (CATEGORY_PRIORITY[b] ?? 99),
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
for (const category of sortedCategories) {
|
|
425
|
+
const items = grouped.get(category)!;
|
|
426
|
+
const catHeader = `\n### ${category.charAt(0).toUpperCase() + category.slice(1)}\n`;
|
|
427
|
+
|
|
428
|
+
if (remaining < catHeader.length + 10) break;
|
|
429
|
+
output += catHeader;
|
|
430
|
+
remaining -= catHeader.length;
|
|
431
|
+
|
|
432
|
+
for (const item of items) {
|
|
433
|
+
const bullet = `- ${item.content}\n`;
|
|
434
|
+
if (remaining < bullet.length) break;
|
|
435
|
+
output += bullet;
|
|
436
|
+
remaining -= bullet.length;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return output.trimEnd();
|
|
441
|
+
}
|
|
@@ -151,7 +151,7 @@ export async function handleMigrate(
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
// ── Confirmation via showNextAction ────────────────────────────────────────
|
|
154
|
-
const choice = await showNextAction(ctx
|
|
154
|
+
const choice = await showNextAction(ctx, {
|
|
155
155
|
title: "Migration preview",
|
|
156
156
|
summary: lines,
|
|
157
157
|
actions: [
|
|
@@ -187,7 +187,7 @@ export async function handleMigrate(
|
|
|
187
187
|
);
|
|
188
188
|
|
|
189
189
|
// ── Post-write review offer ────────────────────────────────────────────────
|
|
190
|
-
const reviewChoice = await showNextAction(ctx
|
|
190
|
+
const reviewChoice = await showNextAction(ctx, {
|
|
191
191
|
title: "Migration written",
|
|
192
192
|
summary: [
|
|
193
193
|
`${result.paths.length} files written to .gsd/`,
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSD Parallel Eligibility — Milestone parallelism analysis.
|
|
3
|
+
*
|
|
4
|
+
* Analyzes which milestones can safely run in parallel by checking
|
|
5
|
+
* dependency satisfaction and file overlap across slice plans.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { deriveState } from "./state.js";
|
|
9
|
+
import { parseRoadmap, parsePlan, loadFile } from "./files.js";
|
|
10
|
+
import { resolveMilestoneFile, resolveSliceFile } from "./paths.js";
|
|
11
|
+
import { findMilestoneIds } from "./guided-flow.js";
|
|
12
|
+
import type { MilestoneRegistryEntry } from "./types.js";
|
|
13
|
+
|
|
14
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
export interface EligibilityResult {
|
|
17
|
+
milestoneId: string;
|
|
18
|
+
title: string;
|
|
19
|
+
eligible: boolean;
|
|
20
|
+
reason: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ParallelCandidates {
|
|
24
|
+
eligible: EligibilityResult[];
|
|
25
|
+
ineligible: EligibilityResult[];
|
|
26
|
+
fileOverlaps: Array<{ mid1: string; mid2: string; files: string[] }>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ─── File Collection ─────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Collect all `filesLikelyTouched` across every slice plan in a milestone.
|
|
33
|
+
* Returns a deduplicated list of file paths.
|
|
34
|
+
*/
|
|
35
|
+
async function collectTouchedFiles(
|
|
36
|
+
basePath: string,
|
|
37
|
+
milestoneId: string,
|
|
38
|
+
): Promise<string[]> {
|
|
39
|
+
const roadmapPath = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
|
|
40
|
+
if (!roadmapPath) return [];
|
|
41
|
+
|
|
42
|
+
const roadmapContent = await loadFile(roadmapPath);
|
|
43
|
+
if (!roadmapContent) return [];
|
|
44
|
+
|
|
45
|
+
const roadmap = parseRoadmap(roadmapContent);
|
|
46
|
+
const files = new Set<string>();
|
|
47
|
+
|
|
48
|
+
for (const slice of roadmap.slices) {
|
|
49
|
+
const planPath = resolveSliceFile(basePath, milestoneId, slice.id, "PLAN");
|
|
50
|
+
if (!planPath) continue;
|
|
51
|
+
|
|
52
|
+
const planContent = await loadFile(planPath);
|
|
53
|
+
if (!planContent) continue;
|
|
54
|
+
|
|
55
|
+
const plan = parsePlan(planContent);
|
|
56
|
+
for (const f of plan.filesLikelyTouched) {
|
|
57
|
+
files.add(f);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return [...files];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── Overlap Detection ──────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Compare file sets across milestones and return pairs with overlapping files.
|
|
68
|
+
*/
|
|
69
|
+
function detectFileOverlaps(
|
|
70
|
+
fileSets: Map<string, string[]>,
|
|
71
|
+
): Array<{ mid1: string; mid2: string; files: string[] }> {
|
|
72
|
+
const overlaps: Array<{ mid1: string; mid2: string; files: string[] }> = [];
|
|
73
|
+
const ids = [...fileSets.keys()];
|
|
74
|
+
|
|
75
|
+
for (let i = 0; i < ids.length; i++) {
|
|
76
|
+
const files1 = new Set(fileSets.get(ids[i])!);
|
|
77
|
+
for (let j = i + 1; j < ids.length; j++) {
|
|
78
|
+
const files2 = fileSets.get(ids[j])!;
|
|
79
|
+
const shared = files2.filter(f => files1.has(f));
|
|
80
|
+
if (shared.length > 0) {
|
|
81
|
+
overlaps.push({ mid1: ids[i], mid2: ids[j], files: shared.sort() });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return overlaps;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─── Analysis ────────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Analyze milestones for parallel execution eligibility.
|
|
93
|
+
*
|
|
94
|
+
* A milestone is eligible if:
|
|
95
|
+
* 1. It is not complete
|
|
96
|
+
* 2. Its dependencies (`dependsOn`) are all complete
|
|
97
|
+
* 3. It does not have file overlap with other eligible milestones
|
|
98
|
+
* (overlaps are flagged as warnings but do not disqualify)
|
|
99
|
+
*/
|
|
100
|
+
export async function analyzeParallelEligibility(
|
|
101
|
+
basePath: string,
|
|
102
|
+
): Promise<ParallelCandidates> {
|
|
103
|
+
const milestoneIds = findMilestoneIds(basePath);
|
|
104
|
+
const state = await deriveState(basePath);
|
|
105
|
+
const registry = state.registry;
|
|
106
|
+
|
|
107
|
+
// Build a lookup for quick status checks
|
|
108
|
+
const registryMap = new Map<string, MilestoneRegistryEntry>();
|
|
109
|
+
for (const entry of registry) {
|
|
110
|
+
registryMap.set(entry.id, entry);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const eligible: EligibilityResult[] = [];
|
|
114
|
+
const ineligible: EligibilityResult[] = [];
|
|
115
|
+
|
|
116
|
+
for (const mid of milestoneIds) {
|
|
117
|
+
const entry = registryMap.get(mid);
|
|
118
|
+
const title = entry?.title ?? mid;
|
|
119
|
+
const status = entry?.status ?? "pending";
|
|
120
|
+
|
|
121
|
+
// Rule 1: skip complete milestones
|
|
122
|
+
if (status === "complete") {
|
|
123
|
+
ineligible.push({
|
|
124
|
+
milestoneId: mid,
|
|
125
|
+
title,
|
|
126
|
+
eligible: false,
|
|
127
|
+
reason: "Already complete.",
|
|
128
|
+
});
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Rule 2: check dependency satisfaction
|
|
133
|
+
const deps = entry?.dependsOn ?? [];
|
|
134
|
+
const unsatisfied = deps.filter(dep => {
|
|
135
|
+
const depEntry = registryMap.get(dep);
|
|
136
|
+
return !depEntry || depEntry.status !== "complete";
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (unsatisfied.length > 0) {
|
|
140
|
+
ineligible.push({
|
|
141
|
+
milestoneId: mid,
|
|
142
|
+
title,
|
|
143
|
+
eligible: false,
|
|
144
|
+
reason: `Blocked by incomplete dependencies: ${unsatisfied.join(", ")}.`,
|
|
145
|
+
});
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
eligible.push({
|
|
150
|
+
milestoneId: mid,
|
|
151
|
+
title,
|
|
152
|
+
eligible: true,
|
|
153
|
+
reason: "All dependencies satisfied.",
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Rule 3: check file overlap among eligible milestones
|
|
158
|
+
const fileSets = new Map<string, string[]>();
|
|
159
|
+
for (const result of eligible) {
|
|
160
|
+
const files = await collectTouchedFiles(basePath, result.milestoneId);
|
|
161
|
+
fileSets.set(result.milestoneId, files);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const fileOverlaps = detectFileOverlaps(fileSets);
|
|
165
|
+
|
|
166
|
+
// Annotate eligible milestones that have file overlaps
|
|
167
|
+
const overlappingIds = new Set<string>();
|
|
168
|
+
for (const overlap of fileOverlaps) {
|
|
169
|
+
overlappingIds.add(overlap.mid1);
|
|
170
|
+
overlappingIds.add(overlap.mid2);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
for (const result of eligible) {
|
|
174
|
+
if (overlappingIds.has(result.milestoneId)) {
|
|
175
|
+
result.reason = "All dependencies satisfied. WARNING: has file overlap with another eligible milestone.";
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return { eligible, ineligible, fileOverlaps };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ─── Formatting ──────────────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Produce a human-readable report of parallel eligibility analysis.
|
|
186
|
+
*/
|
|
187
|
+
export function formatEligibilityReport(candidates: ParallelCandidates): string {
|
|
188
|
+
const lines: string[] = [];
|
|
189
|
+
|
|
190
|
+
lines.push("# Parallel Eligibility Report");
|
|
191
|
+
lines.push("");
|
|
192
|
+
|
|
193
|
+
// Eligible milestones
|
|
194
|
+
lines.push(`## Eligible for Parallel Execution (${candidates.eligible.length})`);
|
|
195
|
+
lines.push("");
|
|
196
|
+
if (candidates.eligible.length === 0) {
|
|
197
|
+
lines.push("No milestones are currently eligible for parallel execution.");
|
|
198
|
+
} else {
|
|
199
|
+
for (const e of candidates.eligible) {
|
|
200
|
+
lines.push(`- **${e.milestoneId}** — ${e.title}`);
|
|
201
|
+
lines.push(` ${e.reason}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
lines.push("");
|
|
205
|
+
|
|
206
|
+
// Ineligible milestones
|
|
207
|
+
lines.push(`## Ineligible (${candidates.ineligible.length})`);
|
|
208
|
+
lines.push("");
|
|
209
|
+
if (candidates.ineligible.length === 0) {
|
|
210
|
+
lines.push("All milestones are eligible.");
|
|
211
|
+
} else {
|
|
212
|
+
for (const e of candidates.ineligible) {
|
|
213
|
+
lines.push(`- **${e.milestoneId}** — ${e.title}`);
|
|
214
|
+
lines.push(` ${e.reason}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
lines.push("");
|
|
218
|
+
|
|
219
|
+
// File overlap warnings
|
|
220
|
+
if (candidates.fileOverlaps.length > 0) {
|
|
221
|
+
lines.push(`## File Overlap Warnings (${candidates.fileOverlaps.length})`);
|
|
222
|
+
lines.push("");
|
|
223
|
+
for (const overlap of candidates.fileOverlaps) {
|
|
224
|
+
lines.push(`- **${overlap.mid1}** <-> **${overlap.mid2}** — ${overlap.files.length} shared file(s):`);
|
|
225
|
+
for (const f of overlap.files) {
|
|
226
|
+
lines.push(` - \`${f}\``);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
lines.push("");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return lines.join("\n");
|
|
233
|
+
}
|