@stackmemoryai/stackmemory 0.3.16 ā 0.3.18
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 +48 -2
- package/dist/cli/commands/skills.js +15 -2
- package/dist/cli/commands/skills.js.map +2 -2
- package/dist/cli/index.js +113 -834
- package/dist/cli/index.js.map +3 -3
- package/dist/core/context/dual-stack-manager.js +1 -1
- package/dist/core/context/dual-stack-manager.js.map +1 -1
- package/dist/core/context/frame-manager.js +3 -0
- package/dist/core/context/frame-manager.js.map +2 -2
- package/dist/integrations/claude-code/subagent-client.js +106 -3
- package/dist/integrations/claude-code/subagent-client.js.map +2 -2
- package/dist/servers/railway/config.js +51 -0
- package/dist/servers/railway/config.js.map +7 -0
- package/dist/servers/railway/index-enhanced.js +156 -0
- package/dist/servers/railway/index-enhanced.js.map +7 -0
- package/dist/servers/railway/minimal.js +48 -3
- package/dist/servers/railway/minimal.js.map +2 -2
- package/dist/servers/railway/storage-test.js +455 -0
- package/dist/servers/railway/storage-test.js.map +7 -0
- package/dist/skills/claude-skills.js +13 -12
- package/dist/skills/claude-skills.js.map +2 -2
- package/dist/skills/recursive-agent-orchestrator.js +27 -18
- package/dist/skills/recursive-agent-orchestrator.js.map +2 -2
- package/dist/skills/unified-rlm-orchestrator.js.map +2 -2
- package/package.json +6 -18
- package/scripts/README-TESTING.md +186 -0
- package/scripts/analyze-cli-security.js +288 -0
- package/scripts/archive/add-phase-tasks-to-linear.js +163 -0
- package/scripts/archive/analyze-linear-duplicates.js +214 -0
- package/scripts/archive/analyze-remaining-duplicates.js +230 -0
- package/scripts/archive/analyze-sta-duplicates.js +292 -0
- package/scripts/archive/analyze-sta-graphql.js +399 -0
- package/scripts/archive/cancel-duplicate-tasks.ts +246 -0
- package/scripts/archive/check-all-duplicates.ts +419 -0
- package/scripts/archive/clean-duplicate-tasks.js +114 -0
- package/scripts/archive/cleanup-duplicate-tasks.ts +286 -0
- package/scripts/archive/create-phase-tasks.js +387 -0
- package/scripts/archive/delete-linear-duplicates.js +182 -0
- package/scripts/archive/delete-remaining-duplicates.js +158 -0
- package/scripts/archive/delete-sta-duplicates.js +201 -0
- package/scripts/archive/delete-sta-oauth.js +201 -0
- package/scripts/archive/export-sta-tasks.js +62 -0
- package/scripts/archive/install-auto-sync.js +266 -0
- package/scripts/archive/install-chromadb-hooks.sh +133 -0
- package/scripts/archive/install-enhanced-clear-hooks.sh +431 -0
- package/scripts/archive/install-post-task-hooks.sh +289 -0
- package/scripts/archive/install-stackmemory-hooks.sh +420 -0
- package/scripts/archive/merge-linear-duplicates-safe.ts +362 -0
- package/scripts/archive/merge-linear-duplicates.ts +180 -0
- package/scripts/archive/remove-sta-tasks.js +70 -0
- package/scripts/archive/setup-background-sync.sh +168 -0
- package/scripts/archive/setup-claude-auto-triggers.sh +181 -0
- package/scripts/archive/setup-claude-autostart.sh +305 -0
- package/scripts/archive/setup-git-hooks.sh +25 -0
- package/scripts/archive/setup-linear-oauth.sh +46 -0
- package/scripts/archive/setup-mcp.sh +113 -0
- package/scripts/archive/setup-railway-deployment.sh +81 -0
- package/scripts/auto-handoff.sh +262 -0
- package/scripts/background-sync-manager.js +416 -0
- package/scripts/benchmark-performance.ts +57 -0
- package/scripts/check-redis.ts +48 -0
- package/scripts/chromadb-auto-loader.sh +128 -0
- package/scripts/chromadb-context-loader.js +479 -0
- package/scripts/claude-chromadb-hook.js +460 -0
- package/scripts/claude-code-wrapper.sh +66 -0
- package/scripts/claude-linear-skill.js +455 -0
- package/scripts/claude-pre-commit.sh +302 -0
- package/scripts/claude-sm-autostart.js +532 -0
- package/scripts/claude-sm-setup.sh +367 -0
- package/scripts/claude-with-chromadb.sh +69 -0
- package/scripts/claude-worktree-manager.sh +323 -0
- package/scripts/claude-worktree-monitor.sh +371 -0
- package/scripts/claude-worktree-setup.sh +327 -0
- package/scripts/clean-linear-backlog.js +273 -0
- package/scripts/cleanup-old-sessions.sh +57 -0
- package/scripts/codex-wrapper.sh +88 -0
- package/scripts/create-sandbox.sh +269 -0
- package/scripts/debug-linear-update.js +174 -0
- package/scripts/delete-linear-tasks.js +167 -0
- package/scripts/deploy.sh +89 -0
- package/scripts/deployment/railway.sh +352 -0
- package/scripts/deployment/test-deployment.js +194 -0
- package/scripts/detect-and-rehydrate.js +162 -0
- package/scripts/detect-and-rehydrate.mjs +165 -0
- package/scripts/development/create-demo-tasks.js +143 -0
- package/scripts/development/debug-frame-test.js +16 -0
- package/scripts/development/demo-auto-sync.js +128 -0
- package/scripts/development/fix-all-imports.js +213 -0
- package/scripts/development/fix-imports.js +229 -0
- package/scripts/development/fix-lint-loop.cjs +103 -0
- package/scripts/development/fix-project-id.ts +161 -0
- package/scripts/development/fix-strict-mode-issues.ts +291 -0
- package/scripts/development/reorganize-structure.sh +228 -0
- package/scripts/development/test-persistence-direct.js +148 -0
- package/scripts/development/test-persistence.js +114 -0
- package/scripts/development/test-tasks.js +93 -0
- package/scripts/development/update-imports.js +212 -0
- package/scripts/fetch-linear-status.js +125 -0
- package/scripts/git-hooks/README.md +310 -0
- package/scripts/git-hooks/branch-context-manager.sh +342 -0
- package/scripts/git-hooks/post-checkout-stackmemory.sh +63 -0
- package/scripts/git-hooks/post-commit-stackmemory.sh +305 -0
- package/scripts/git-hooks/pre-commit-stackmemory.sh +275 -0
- package/scripts/hooks/cleanup-shell.sh +130 -0
- package/scripts/hooks/task-complete.sh +114 -0
- package/scripts/initialize.ts +129 -0
- package/scripts/install-claude-hooks-auto.js +104 -0
- package/scripts/install-claude-hooks.sh +133 -0
- package/scripts/install-global.sh +296 -0
- package/scripts/install.sh +235 -0
- package/scripts/linear-auto-sync.js +262 -0
- package/scripts/linear-auto-sync.sh +161 -0
- package/scripts/linear-sync-daemon.js +150 -0
- package/scripts/linear-task-review.js +237 -0
- package/scripts/list-linear-tasks.ts +178 -0
- package/scripts/mcp-proxy.js +66 -0
- package/scripts/opencode-wrapper.sh +85 -0
- package/scripts/publish-local.js +74 -0
- package/scripts/query-chromadb.ts +201 -0
- package/scripts/railway-env-setup.sh +39 -0
- package/scripts/reconcile-local-tasks.js +170 -0
- package/scripts/recreate-frames-db.js +89 -0
- package/scripts/setup/claude-integration.js +138 -0
- package/scripts/setup/configure-alias.js +125 -0
- package/scripts/setup/configure-codex-alias.js +161 -0
- package/scripts/setup/configure-opencode-alias.js +175 -0
- package/scripts/setup-claude-integration.js +204 -0
- package/scripts/setup-claude-integration.sh +183 -0
- package/scripts/setup.sh +31 -0
- package/scripts/show-linear-summary.ts +172 -0
- package/scripts/stackmemory-auto-handoff.sh +231 -0
- package/scripts/stackmemory-daemon.sh +40 -0
- package/scripts/start-linear-sync-daemon.sh +141 -0
- package/scripts/start-temporal-paradox.sh +214 -0
- package/scripts/status.ts +159 -0
- package/scripts/sync-and-clean-tasks.js +258 -0
- package/scripts/sync-frames-from-railway.js +228 -0
- package/scripts/sync-linear-graphql.js +303 -0
- package/scripts/sync-linear-tasks.js +186 -0
- package/scripts/test-auto-triggers.sh +57 -0
- package/scripts/test-browser-mcp.js +74 -0
- package/scripts/test-chromadb-full.js +115 -0
- package/scripts/test-chromadb-hooks.sh +28 -0
- package/scripts/test-chromadb-sync.ts +245 -0
- package/scripts/test-cli-security.js +293 -0
- package/scripts/test-hooks-persistence.sh +220 -0
- package/scripts/test-installation-scenarios.sh +359 -0
- package/scripts/test-installation.sh +224 -0
- package/scripts/test-mcp.js +163 -0
- package/scripts/test-pre-publish-quick.sh +75 -0
- package/scripts/test-quality-gates.sh +263 -0
- package/scripts/test-railway-db.js +222 -0
- package/scripts/test-redis-storage.ts +490 -0
- package/scripts/test-rlm-basic.sh +122 -0
- package/scripts/test-rlm-comprehensive.sh +260 -0
- package/scripts/test-rlm-e2e.sh +268 -0
- package/scripts/test-rlm-simple.js +90 -0
- package/scripts/test-rlm.js +110 -0
- package/scripts/test-session-handoff.sh +165 -0
- package/scripts/test-shell-integration.sh +275 -0
- package/scripts/testing/ab-test-runner.ts +508 -0
- package/scripts/testing/collect-metrics.ts +457 -0
- package/scripts/testing/quick-effectiveness-demo.js +187 -0
- package/scripts/testing/real-performance-test.js +422 -0
- package/scripts/testing/run-effectiveness-tests.sh +176 -0
- package/scripts/testing/scripts/testing/ab-test-runner.js +363 -0
- package/scripts/testing/scripts/testing/collect-metrics.js +292 -0
- package/scripts/testing/simple-effectiveness-test.js +310 -0
- package/scripts/testing/src/core/context/context-bridge.js +253 -0
- package/scripts/testing/src/core/context/frame-manager.js +746 -0
- package/scripts/testing/src/core/context/shared-context-layer.js +437 -0
- package/scripts/testing/src/core/database/database-adapter.js +54 -0
- package/scripts/testing/src/core/errors/index.js +291 -0
- package/scripts/testing/src/core/errors/recovery.js +268 -0
- package/scripts/testing/src/core/monitoring/logger.js +145 -0
- package/scripts/testing/src/core/retrieval/context-retriever.js +516 -0
- package/scripts/testing/src/core/session/index.js +1 -0
- package/scripts/testing/src/core/session/session-manager.js +323 -0
- package/scripts/testing/src/core/trace/cli-trace-wrapper.js +140 -0
- package/scripts/testing/src/core/trace/db-trace-wrapper.js +251 -0
- package/scripts/testing/src/core/trace/debug-trace.js +398 -0
- package/scripts/testing/src/core/trace/index.js +120 -0
- package/scripts/testing/src/core/trace/linear-api-wrapper.js +204 -0
- package/scripts/update-linear-status.js +268 -0
- package/scripts/update-linear-tasks-fixed.js +284 -0
- package/templates/claude-hooks/hooks.json +5 -0
- package/templates/claude-hooks/on-clear.js +56 -0
- package/templates/claude-hooks/on-startup.js +56 -0
- package/templates/claude-hooks/tool-use-trace.js +67 -0
- package/dist/features/tui/components/analytics-panel.js +0 -157
- package/dist/features/tui/components/analytics-panel.js.map +0 -7
- package/dist/features/tui/components/frame-visualizer.js +0 -377
- package/dist/features/tui/components/frame-visualizer.js.map +0 -7
- package/dist/features/tui/components/pr-tracker.js +0 -135
- package/dist/features/tui/components/pr-tracker.js.map +0 -7
- package/dist/features/tui/components/session-monitor.js +0 -299
- package/dist/features/tui/components/session-monitor.js.map +0 -7
- package/dist/features/tui/components/subagent-fleet.js +0 -395
- package/dist/features/tui/components/subagent-fleet.js.map +0 -7
- package/dist/features/tui/components/task-board.js +0 -1139
- package/dist/features/tui/components/task-board.js.map +0 -7
- package/dist/features/tui/index.js +0 -408
- package/dist/features/tui/index.js.map +0 -7
- package/dist/features/tui/services/data-service.js +0 -641
- package/dist/features/tui/services/data-service.js.map +0 -7
- package/dist/features/tui/services/linear-task-reader.js +0 -102
- package/dist/features/tui/services/linear-task-reader.js.map +0 -7
- package/dist/features/tui/services/websocket-client.js +0 -162
- package/dist/features/tui/services/websocket-client.js.map +0 -7
- package/dist/features/tui/terminal-compat.js +0 -220
- package/dist/features/tui/terminal-compat.js.map +0 -7
- package/dist/features/tui/types.js +0 -1
- package/dist/features/tui/types.js.map +0 -7
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Analyze Linear workspace for STA duplicates using GraphQL API directly
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import 'dotenv/config';
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
|
|
10
|
+
// Load API key from environment
|
|
11
|
+
const API_KEY = process.env.LINEAR_API_KEY;
|
|
12
|
+
if (!API_KEY) {
|
|
13
|
+
console.error('ā LINEAR_API_KEY environment variable not set');
|
|
14
|
+
console.log('Please set LINEAR_API_KEY in your .env file or export it in your shell');
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function fetchAllIssues() {
|
|
19
|
+
const query = `
|
|
20
|
+
query GetAllIssues($after: String) {
|
|
21
|
+
issues(first: 100, after: $after, includeArchived: false) {
|
|
22
|
+
nodes {
|
|
23
|
+
id
|
|
24
|
+
identifier
|
|
25
|
+
title
|
|
26
|
+
description
|
|
27
|
+
state {
|
|
28
|
+
id
|
|
29
|
+
name
|
|
30
|
+
type
|
|
31
|
+
}
|
|
32
|
+
createdAt
|
|
33
|
+
updatedAt
|
|
34
|
+
priority
|
|
35
|
+
estimate
|
|
36
|
+
project {
|
|
37
|
+
id
|
|
38
|
+
name
|
|
39
|
+
}
|
|
40
|
+
team {
|
|
41
|
+
id
|
|
42
|
+
key
|
|
43
|
+
name
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
pageInfo {
|
|
47
|
+
hasNextPage
|
|
48
|
+
endCursor
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
let allIssues = [];
|
|
55
|
+
let hasNextPage = true;
|
|
56
|
+
let cursor = null;
|
|
57
|
+
|
|
58
|
+
while (hasNextPage) {
|
|
59
|
+
const response = await fetch('https://api.linear.app/graphql', {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: {
|
|
62
|
+
'Authorization': API_KEY,
|
|
63
|
+
'Content-Type': 'application/json'
|
|
64
|
+
},
|
|
65
|
+
body: JSON.stringify({
|
|
66
|
+
query,
|
|
67
|
+
variables: { after: cursor }
|
|
68
|
+
})
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const result = await response.json();
|
|
72
|
+
|
|
73
|
+
if (result.errors) {
|
|
74
|
+
throw new Error(result.errors[0].message);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
allIssues = allIssues.concat(result.data.issues.nodes);
|
|
78
|
+
hasNextPage = result.data.issues.pageInfo.hasNextPage;
|
|
79
|
+
cursor = result.data.issues.pageInfo.endCursor;
|
|
80
|
+
|
|
81
|
+
console.log(` Fetched ${allIssues.length} issues...`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return allIssues;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function analyzeSTADuplicates() {
|
|
88
|
+
try {
|
|
89
|
+
console.log('š Analyzing Linear workspace for STA duplicates and unneeded tasks...\n');
|
|
90
|
+
console.log('š„ Fetching all tasks from Linear (using GraphQL)...');
|
|
91
|
+
|
|
92
|
+
const allTasks = await fetchAllIssues();
|
|
93
|
+
console.log(`š Total tasks in workspace: ${allTasks.length}\n`);
|
|
94
|
+
|
|
95
|
+
// Filter STA tasks
|
|
96
|
+
const staTasks = allTasks.filter(task =>
|
|
97
|
+
task.identifier.startsWith('STA-') ||
|
|
98
|
+
task.title.includes('STA-') ||
|
|
99
|
+
task.title.includes('[STA-')
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
console.log(`š Found ${staTasks.length} STA-related tasks\n`);
|
|
103
|
+
|
|
104
|
+
// Analyze patterns
|
|
105
|
+
const staByNumber = new Map();
|
|
106
|
+
const completedSTA = [];
|
|
107
|
+
const canceledSTA = [];
|
|
108
|
+
const duplicateTitles = new Map();
|
|
109
|
+
const lowValuePatterns = [];
|
|
110
|
+
const backlogSTA = [];
|
|
111
|
+
const todoSTA = [];
|
|
112
|
+
const inProgressSTA = [];
|
|
113
|
+
|
|
114
|
+
// Group tasks by patterns
|
|
115
|
+
staTasks.forEach(task => {
|
|
116
|
+
const state = task.state.type;
|
|
117
|
+
const status = task.state.name;
|
|
118
|
+
|
|
119
|
+
// Extract STA number
|
|
120
|
+
const staMatch = task.identifier.match(/STA-(\d+)/);
|
|
121
|
+
if (staMatch) {
|
|
122
|
+
const staNum = parseInt(staMatch[1]);
|
|
123
|
+
if (!staByNumber.has(staNum)) {
|
|
124
|
+
staByNumber.set(staNum, []);
|
|
125
|
+
}
|
|
126
|
+
staByNumber.get(staNum).push(task);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Check state
|
|
130
|
+
if (state === 'completed') {
|
|
131
|
+
completedSTA.push(task);
|
|
132
|
+
} else if (state === 'canceled') {
|
|
133
|
+
canceledSTA.push(task);
|
|
134
|
+
} else if (state === 'backlog' || status === 'Backlog') {
|
|
135
|
+
backlogSTA.push(task);
|
|
136
|
+
} else if (state === 'started' || status === 'In Progress') {
|
|
137
|
+
inProgressSTA.push(task);
|
|
138
|
+
} else if (state === 'unstarted' || state === 'triage' || status === 'Todo') {
|
|
139
|
+
todoSTA.push(task);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check for low-value patterns
|
|
143
|
+
const lowValueKeywords = [
|
|
144
|
+
'Documentation TODO',
|
|
145
|
+
'Meeting',
|
|
146
|
+
'Task Analytics Dashboard',
|
|
147
|
+
'Weekly Sync',
|
|
148
|
+
'Standup',
|
|
149
|
+
'[Duplicate]',
|
|
150
|
+
'Test Task',
|
|
151
|
+
'Demo Task',
|
|
152
|
+
'Example Task'
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
if (lowValueKeywords.some(keyword => task.title.includes(keyword)) ||
|
|
156
|
+
task.description?.includes('auto-generated') ||
|
|
157
|
+
task.description?.includes('automatically created')) {
|
|
158
|
+
lowValuePatterns.push(task);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Find duplicate titles
|
|
162
|
+
const baseTitle = task.title
|
|
163
|
+
.replace(/\[.*?\]/g, '')
|
|
164
|
+
.replace(/STA-\d+/g, '')
|
|
165
|
+
.trim()
|
|
166
|
+
.toLowerCase();
|
|
167
|
+
|
|
168
|
+
if (baseTitle.length > 10) { // Only consider meaningful titles
|
|
169
|
+
if (!duplicateTitles.has(baseTitle)) {
|
|
170
|
+
duplicateTitles.set(baseTitle, []);
|
|
171
|
+
}
|
|
172
|
+
duplicateTitles.get(baseTitle).push(task);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Find true duplicates
|
|
177
|
+
const duplicateSTANumbers = Array.from(staByNumber.entries())
|
|
178
|
+
.filter(([_, tasks]) => tasks.length > 1)
|
|
179
|
+
.sort((a, b) => b[1].length - a[1].length);
|
|
180
|
+
|
|
181
|
+
const trueDuplicateTitles = Array.from(duplicateTitles.entries())
|
|
182
|
+
.filter(([_, tasks]) => tasks.length > 1)
|
|
183
|
+
.sort((a, b) => b[1].length - a[1].length);
|
|
184
|
+
|
|
185
|
+
// Generate report
|
|
186
|
+
console.log('š STA TASK ANALYSIS REPORT');
|
|
187
|
+
console.log('=' .repeat(60));
|
|
188
|
+
|
|
189
|
+
console.log('\nš OVERVIEW:');
|
|
190
|
+
console.log(`Total workspace tasks: ${allTasks.length}`);
|
|
191
|
+
console.log(`STA-prefixed tasks: ${staTasks.length} (${Math.round(staTasks.length / allTasks.length * 100)}% of workspace)`);
|
|
192
|
+
console.log(`\nStatus breakdown:`);
|
|
193
|
+
console.log(` ⢠In Progress: ${inProgressSTA.length}`);
|
|
194
|
+
console.log(` ⢠Todo/Ready: ${todoSTA.length}`);
|
|
195
|
+
console.log(` ⢠Backlog: ${backlogSTA.length}`);
|
|
196
|
+
console.log(` ⢠Completed: ${completedSTA.length}`);
|
|
197
|
+
console.log(` ⢠Canceled: ${canceledSTA.length}`);
|
|
198
|
+
|
|
199
|
+
if (duplicateSTANumbers.length > 0) {
|
|
200
|
+
console.log('\nš DUPLICATE STA NUMBERS:');
|
|
201
|
+
console.log(`Found ${duplicateSTANumbers.length} STA numbers with multiple tasks:\n`);
|
|
202
|
+
duplicateSTANumbers.forEach(([staNum, tasks]) => {
|
|
203
|
+
console.log(` STA-${staNum} has ${tasks.length} instances:`);
|
|
204
|
+
tasks.forEach(task => {
|
|
205
|
+
console.log(` ⢠${task.identifier}: "${task.title.substring(0, 60)}..." (${task.state.name})`);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (trueDuplicateTitles.length > 0) {
|
|
211
|
+
console.log('\nš SIMILAR TITLES (potential duplicates):');
|
|
212
|
+
console.log(`Found ${trueDuplicateTitles.length} groups of similar titles:\n`);
|
|
213
|
+
trueDuplicateTitles.slice(0, 15).forEach(([title, tasks]) => {
|
|
214
|
+
console.log(` "${title.substring(0, 50)}..." appears ${tasks.length} times:`);
|
|
215
|
+
tasks.slice(0, 5).forEach(task => {
|
|
216
|
+
console.log(` ⢠${task.identifier}: ${task.state.name}`);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (lowValuePatterns.length > 0) {
|
|
222
|
+
console.log('\nšļø LOW-VALUE/AUTO-GENERATED TASKS:');
|
|
223
|
+
console.log(`Found ${lowValuePatterns.length} potentially low-value tasks:\n`);
|
|
224
|
+
const categories = {
|
|
225
|
+
'Meeting/Sync tasks': lowValuePatterns.filter(t =>
|
|
226
|
+
t.title.includes('Meeting') || t.title.includes('Sync') || t.title.includes('Standup')),
|
|
227
|
+
'Documentation TODOs': lowValuePatterns.filter(t =>
|
|
228
|
+
t.title.includes('Documentation TODO')),
|
|
229
|
+
'Test/Demo tasks': lowValuePatterns.filter(t =>
|
|
230
|
+
t.title.includes('Test Task') || t.title.includes('Demo') || t.title.includes('Example')),
|
|
231
|
+
'Auto-generated': lowValuePatterns.filter(t =>
|
|
232
|
+
t.description?.includes('auto-generated') || t.description?.includes('automatically'))
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
Object.entries(categories).forEach(([category, tasks]) => {
|
|
236
|
+
if (tasks.length > 0) {
|
|
237
|
+
console.log(` ${category}: ${tasks.length} tasks`);
|
|
238
|
+
tasks.slice(0, 5).forEach(task => {
|
|
239
|
+
console.log(` ⢠${task.identifier}: ${task.title.substring(0, 50)}...`);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Build deletion recommendations
|
|
246
|
+
const toDelete = new Set();
|
|
247
|
+
|
|
248
|
+
// Add duplicates (keep most recent or in-progress)
|
|
249
|
+
duplicateSTANumbers.forEach(([_, tasks]) => {
|
|
250
|
+
const sorted = tasks.sort((a, b) => {
|
|
251
|
+
if (a.state.type === 'started') return -1;
|
|
252
|
+
if (b.state.type === 'started') return 1;
|
|
253
|
+
return new Date(b.updatedAt) - new Date(a.updatedAt);
|
|
254
|
+
});
|
|
255
|
+
sorted.slice(1).forEach(t => toDelete.add(t));
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// Add similar titles (be more conservative)
|
|
259
|
+
trueDuplicateTitles.forEach(([_, tasks]) => {
|
|
260
|
+
if (tasks.every(t => t.state.type !== 'started')) {
|
|
261
|
+
// If none are in progress, keep newest
|
|
262
|
+
const sorted = tasks.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
|
263
|
+
sorted.slice(1).forEach(t => toDelete.add(t));
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Add completed and canceled
|
|
268
|
+
completedSTA.forEach(t => toDelete.add(t));
|
|
269
|
+
canceledSTA.forEach(t => toDelete.add(t));
|
|
270
|
+
|
|
271
|
+
// Add clear low-value patterns
|
|
272
|
+
lowValuePatterns.forEach(task => {
|
|
273
|
+
if (task.state.type !== 'started' &&
|
|
274
|
+
(task.title.includes('[Duplicate]') ||
|
|
275
|
+
task.title.includes('Test Task') ||
|
|
276
|
+
task.description?.includes('auto-generated'))) {
|
|
277
|
+
toDelete.add(task);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const uniqueToDelete = Array.from(toDelete)
|
|
282
|
+
.sort((a, b) => {
|
|
283
|
+
const aNum = parseInt(a.identifier.replace('STA-', ''));
|
|
284
|
+
const bNum = parseInt(b.identifier.replace('STA-', ''));
|
|
285
|
+
return aNum - bNum;
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
console.log('\nšÆ DELETION RECOMMENDATIONS');
|
|
289
|
+
console.log('=' .repeat(60));
|
|
290
|
+
console.log(`\nCapacity Analysis:`);
|
|
291
|
+
console.log(` Current workspace total: ${allTasks.length} tasks`);
|
|
292
|
+
console.log(` Current STA tasks: ${staTasks.length}`);
|
|
293
|
+
console.log(` Recommended to delete: ${uniqueToDelete.length} tasks`);
|
|
294
|
+
console.log(` Workspace after deletion: ${allTasks.length - uniqueToDelete.length} tasks`);
|
|
295
|
+
console.log(` STA tasks after deletion: ${staTasks.length - uniqueToDelete.length}`);
|
|
296
|
+
console.log(` **Capacity freed: ${uniqueToDelete.length} task slots**`);
|
|
297
|
+
|
|
298
|
+
// Categorize deletions
|
|
299
|
+
const deleteReasons = {
|
|
300
|
+
'Duplicate STA numbers': [],
|
|
301
|
+
'Similar/duplicate titles': [],
|
|
302
|
+
'Completed tasks': [],
|
|
303
|
+
'Canceled tasks': [],
|
|
304
|
+
'Low-value/auto-generated': []
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
uniqueToDelete.forEach(task => {
|
|
308
|
+
let categorized = false;
|
|
309
|
+
|
|
310
|
+
// Check if it's a duplicate STA number
|
|
311
|
+
const staMatch = task.identifier.match(/STA-(\d+)/);
|
|
312
|
+
if (staMatch) {
|
|
313
|
+
const staNum = parseInt(staMatch[1]);
|
|
314
|
+
const dupes = staByNumber.get(staNum);
|
|
315
|
+
if (dupes && dupes.length > 1 && dupes[0].id !== task.id) {
|
|
316
|
+
deleteReasons['Duplicate STA numbers'].push(task);
|
|
317
|
+
categorized = true;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (!categorized && completedSTA.find(t => t.id === task.id)) {
|
|
322
|
+
deleteReasons['Completed tasks'].push(task);
|
|
323
|
+
categorized = true;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (!categorized && canceledSTA.find(t => t.id === task.id)) {
|
|
327
|
+
deleteReasons['Canceled tasks'].push(task);
|
|
328
|
+
categorized = true;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (!categorized && lowValuePatterns.find(t => t.id === task.id)) {
|
|
332
|
+
deleteReasons['Low-value/auto-generated'].push(task);
|
|
333
|
+
categorized = true;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (!categorized) {
|
|
337
|
+
deleteReasons['Similar/duplicate titles'].push(task);
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
console.log('\nš Deletion breakdown by reason:');
|
|
342
|
+
Object.entries(deleteReasons).forEach(([reason, tasks]) => {
|
|
343
|
+
if (tasks.length > 0) {
|
|
344
|
+
console.log(`\n${reason}: ${tasks.length} tasks`);
|
|
345
|
+
const preview = tasks.slice(0, 10)
|
|
346
|
+
.map(t => `${t.identifier}`)
|
|
347
|
+
.join(', ');
|
|
348
|
+
console.log(` ${preview}${tasks.length > 10 ? ` ... +${tasks.length - 10} more` : ''}`);
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// Save deletion list
|
|
353
|
+
const deleteList = {
|
|
354
|
+
timestamp: new Date().toISOString(),
|
|
355
|
+
summary: {
|
|
356
|
+
totalWorkspace: allTasks.length,
|
|
357
|
+
totalSTA: staTasks.length,
|
|
358
|
+
toDelete: uniqueToDelete.length,
|
|
359
|
+
capacityFreed: uniqueToDelete.length,
|
|
360
|
+
remainingSTA: staTasks.length - uniqueToDelete.length,
|
|
361
|
+
remainingTotal: allTasks.length - uniqueToDelete.length
|
|
362
|
+
},
|
|
363
|
+
categories: Object.entries(deleteReasons).reduce((acc, [reason, tasks]) => {
|
|
364
|
+
acc[reason] = tasks.map(t => t.identifier);
|
|
365
|
+
return acc;
|
|
366
|
+
}, {}),
|
|
367
|
+
tasks: uniqueToDelete.map(t => ({
|
|
368
|
+
id: t.id,
|
|
369
|
+
identifier: t.identifier,
|
|
370
|
+
title: t.title,
|
|
371
|
+
state: t.state.name,
|
|
372
|
+
team: t.team?.name || 'No team'
|
|
373
|
+
}))
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const filename = `sta-deletion-list-${new Date().toISOString().split('T')[0]}.json`;
|
|
377
|
+
fs.writeFileSync(filename, JSON.stringify(deleteList, null, 2));
|
|
378
|
+
|
|
379
|
+
console.log(`\nš¾ Deletion list saved to: ${filename}`);
|
|
380
|
+
console.log('\nā
Ready to free up capacity!');
|
|
381
|
+
console.log('\nNext: Create a deletion script or manually review the JSON file');
|
|
382
|
+
|
|
383
|
+
return deleteList;
|
|
384
|
+
|
|
385
|
+
} catch (error) {
|
|
386
|
+
console.error('ā Analysis failed:', error.message);
|
|
387
|
+
if (error.message.includes('401') || error.message.includes('Unauthorized')) {
|
|
388
|
+
console.error('\nā ļø Authentication failed. Please check your LINEAR_API_KEY');
|
|
389
|
+
}
|
|
390
|
+
process.exit(1);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Run if called directly
|
|
395
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
396
|
+
analyzeSTADuplicates().catch(console.error);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export { analyzeSTADuplicates };
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Script to cancel duplicate Linear tasks
|
|
5
|
+
* Uses the actual Linear API to find and cancel duplicates
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
|
|
11
|
+
interface TaskGroup {
|
|
12
|
+
pattern: string;
|
|
13
|
+
keepFirst: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface LinearIssue {
|
|
17
|
+
id: string;
|
|
18
|
+
identifier: string;
|
|
19
|
+
title: string;
|
|
20
|
+
createdAt: string;
|
|
21
|
+
team?: {
|
|
22
|
+
states?: {
|
|
23
|
+
nodes?: Array<{ id: string; type: string }>;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface WorkflowState {
|
|
29
|
+
id: string;
|
|
30
|
+
type: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Define patterns to identify duplicate tasks
|
|
34
|
+
const duplicatePatterns: TaskGroup[] = [
|
|
35
|
+
{ pattern: 'Linear API Integration', keepFirst: true },
|
|
36
|
+
{ pattern: 'Performance Optimization', keepFirst: true },
|
|
37
|
+
{ pattern: 'Security Audit', keepFirst: true },
|
|
38
|
+
{ pattern: '[HIGH] Implement Proper Error Handling', keepFirst: true },
|
|
39
|
+
{ pattern: '[HIGH] Implement Comprehensive Testing Suite', keepFirst: true },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
async function cancelDuplicateTasks(dryRun = true) {
|
|
43
|
+
const mode = dryRun ? 'š DRY RUN MODE' : 'ā” LIVE MODE';
|
|
44
|
+
console.log(`\n${mode} - Cancel duplicate Linear tasks\n`);
|
|
45
|
+
console.log('='.repeat(60));
|
|
46
|
+
|
|
47
|
+
// Load Linear tokens
|
|
48
|
+
const tokensPath = join(process.cwd(), '.stackmemory', 'linear-tokens.json');
|
|
49
|
+
let accessToken: string;
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const tokensData = readFileSync(tokensPath, 'utf8');
|
|
53
|
+
const tokens = JSON.parse(tokensData);
|
|
54
|
+
accessToken = tokens.accessToken;
|
|
55
|
+
console.log('ā
Loaded Linear authentication tokens\n');
|
|
56
|
+
} catch {
|
|
57
|
+
console.error(
|
|
58
|
+
'ā Failed to load Linear tokens. Please run: stackmemory linear setup'
|
|
59
|
+
);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// GraphQL helper
|
|
64
|
+
const linearApiUrl = 'https://api.linear.app/graphql';
|
|
65
|
+
|
|
66
|
+
async function graphqlRequest(
|
|
67
|
+
query: string,
|
|
68
|
+
variables: Record<string, unknown> = {}
|
|
69
|
+
) {
|
|
70
|
+
const response = await fetch(linearApiUrl, {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
headers: {
|
|
73
|
+
Authorization: `Bearer ${accessToken}`,
|
|
74
|
+
'Content-Type': 'application/json',
|
|
75
|
+
},
|
|
76
|
+
body: JSON.stringify({ query, variables }),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`Linear API error: ${response.status} ${response.statusText}`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const result = (await response.json()) as {
|
|
86
|
+
errors?: unknown[];
|
|
87
|
+
data: unknown;
|
|
88
|
+
};
|
|
89
|
+
if (result.errors) {
|
|
90
|
+
throw new Error(`GraphQL errors: ${JSON.stringify(result.errors)}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return result.data;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// First, get all issues
|
|
97
|
+
console.log('Fetching all issues...\n');
|
|
98
|
+
const issuesQuery = `
|
|
99
|
+
query {
|
|
100
|
+
issues(first: 250, filter: { state: { type: { nin: ["completed", "canceled"] } } }) {
|
|
101
|
+
nodes {
|
|
102
|
+
id
|
|
103
|
+
identifier
|
|
104
|
+
title
|
|
105
|
+
description
|
|
106
|
+
createdAt
|
|
107
|
+
state {
|
|
108
|
+
id
|
|
109
|
+
name
|
|
110
|
+
type
|
|
111
|
+
}
|
|
112
|
+
team {
|
|
113
|
+
id
|
|
114
|
+
key
|
|
115
|
+
states {
|
|
116
|
+
nodes {
|
|
117
|
+
id
|
|
118
|
+
name
|
|
119
|
+
type
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
`;
|
|
127
|
+
|
|
128
|
+
const issuesData = (await graphqlRequest(issuesQuery)) as {
|
|
129
|
+
issues: { nodes: LinearIssue[] };
|
|
130
|
+
};
|
|
131
|
+
const allIssues = issuesData.issues.nodes;
|
|
132
|
+
|
|
133
|
+
console.log(`Found ${allIssues.length} active issues\n`);
|
|
134
|
+
|
|
135
|
+
// Get canceled state from the first issue's team
|
|
136
|
+
const canceledState = allIssues[0]?.team?.states?.nodes?.find(
|
|
137
|
+
(s: WorkflowState) => s.type === 'canceled'
|
|
138
|
+
);
|
|
139
|
+
if (!canceledState) {
|
|
140
|
+
console.error('ā No canceled state found in workflow');
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Group issues by pattern
|
|
145
|
+
const groupedIssues = new Map<string, LinearIssue[]>();
|
|
146
|
+
|
|
147
|
+
for (const pattern of duplicatePatterns) {
|
|
148
|
+
const matches = allIssues.filter((issue: LinearIssue) =>
|
|
149
|
+
issue.title.includes(pattern.pattern)
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
if (matches.length > 1) {
|
|
153
|
+
// Sort by creation date to keep the oldest
|
|
154
|
+
matches.sort(
|
|
155
|
+
(a: LinearIssue, b: LinearIssue) =>
|
|
156
|
+
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
|
157
|
+
);
|
|
158
|
+
groupedIssues.set(pattern.pattern, matches);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Process each group
|
|
163
|
+
let totalCanceled = 0;
|
|
164
|
+
let totalKept = 0;
|
|
165
|
+
|
|
166
|
+
for (const [pattern, issues] of groupedIssues.entries()) {
|
|
167
|
+
console.log(`\nš Pattern: "${pattern}"`);
|
|
168
|
+
console.log(` Found ${issues.length} matching issues:`);
|
|
169
|
+
|
|
170
|
+
const [primary, ...duplicates] = issues;
|
|
171
|
+
|
|
172
|
+
console.log(` ā
Keep: ${primary.identifier} - ${primary.title}`);
|
|
173
|
+
totalKept++;
|
|
174
|
+
|
|
175
|
+
for (const duplicate of duplicates) {
|
|
176
|
+
console.log(
|
|
177
|
+
` ${dryRun ? 'š' : 'ā'} Cancel: ${duplicate.identifier} - ${duplicate.title}`
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
if (!dryRun) {
|
|
181
|
+
try {
|
|
182
|
+
const cancelMutation = `
|
|
183
|
+
mutation CancelIssue($id: String!, $stateId: String!) {
|
|
184
|
+
issueUpdate(
|
|
185
|
+
id: $id,
|
|
186
|
+
input: {
|
|
187
|
+
stateId: $stateId,
|
|
188
|
+
description: "Duplicate task - kept ${primary.identifier}"
|
|
189
|
+
}
|
|
190
|
+
) {
|
|
191
|
+
success
|
|
192
|
+
issue {
|
|
193
|
+
identifier
|
|
194
|
+
state {
|
|
195
|
+
name
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
`;
|
|
201
|
+
|
|
202
|
+
await graphqlRequest(cancelMutation, {
|
|
203
|
+
id: duplicate.id,
|
|
204
|
+
stateId: canceledState.id,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
console.log(` ā
Successfully canceled ${duplicate.identifier}`);
|
|
208
|
+
totalCanceled++;
|
|
209
|
+
} catch (error: unknown) {
|
|
210
|
+
const errorMessage =
|
|
211
|
+
error instanceof Error ? error.message : String(error);
|
|
212
|
+
console.log(
|
|
213
|
+
` ā Failed to cancel ${duplicate.identifier}: ${errorMessage}`
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
totalCanceled++;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Summary
|
|
223
|
+
console.log('\n' + '='.repeat(60));
|
|
224
|
+
console.log(`\n⨠${dryRun ? 'DRY RUN' : 'CLEANUP'} COMPLETE!\n`);
|
|
225
|
+
console.log('š Summary:');
|
|
226
|
+
console.log(` Duplicate groups found: ${groupedIssues.size}`);
|
|
227
|
+
console.log(` Tasks to keep: ${totalKept}`);
|
|
228
|
+
console.log(
|
|
229
|
+
` Tasks ${dryRun ? 'to cancel' : 'canceled'}: ${totalCanceled}`
|
|
230
|
+
);
|
|
231
|
+
console.log(` Total active tasks: ${allIssues.length}`);
|
|
232
|
+
console.log(` Tasks after cleanup: ${allIssues.length - totalCanceled}`);
|
|
233
|
+
|
|
234
|
+
if (dryRun) {
|
|
235
|
+
console.log('\nš” To execute these changes, run with --execute flag');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Parse command line arguments
|
|
240
|
+
const isDryRun = !process.argv.includes('--execute');
|
|
241
|
+
|
|
242
|
+
// Run the cleanup
|
|
243
|
+
cancelDuplicateTasks(isDryRun).catch((error) => {
|
|
244
|
+
console.error('ā Fatal error:', error);
|
|
245
|
+
process.exit(1);
|
|
246
|
+
});
|