sequant 1.20.2 → 2.0.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/.claude-plugin/marketplace.json +2 -4
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +29 -9
- package/dist/bin/cli.js +25 -2
- package/dist/src/commands/doctor.js +42 -9
- package/dist/src/commands/init.d.ts +1 -0
- package/dist/src/commands/init.js +52 -0
- package/dist/src/commands/logs.d.ts +1 -0
- package/dist/src/commands/logs.js +18 -2
- package/dist/src/commands/run.d.ts +7 -0
- package/dist/src/commands/run.js +235 -68
- package/dist/src/commands/serve.d.ts +13 -0
- package/dist/src/commands/serve.js +131 -0
- package/dist/src/commands/stats.d.ts +1 -0
- package/dist/src/commands/stats.js +185 -26
- package/dist/src/commands/status.d.ts +2 -0
- package/dist/src/commands/status.js +99 -50
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.js +4 -1
- package/dist/src/lib/ac-parser.d.ts +2 -0
- package/dist/src/lib/ac-parser.js +12 -2
- package/dist/src/lib/assess-comment-parser.d.ts +137 -0
- package/dist/src/lib/assess-comment-parser.js +344 -0
- package/dist/src/lib/ci/config.d.ts +22 -0
- package/dist/src/lib/ci/config.js +134 -0
- package/dist/src/lib/ci/index.d.ts +12 -0
- package/dist/src/lib/ci/index.js +10 -0
- package/dist/src/lib/ci/inputs.d.ts +29 -0
- package/dist/src/lib/ci/inputs.js +103 -0
- package/dist/src/lib/ci/labels.d.ts +34 -0
- package/dist/src/lib/ci/labels.js +101 -0
- package/dist/src/lib/ci/outputs.d.ts +25 -0
- package/dist/src/lib/ci/outputs.js +84 -0
- package/dist/src/lib/ci/triggers.d.ts +9 -0
- package/dist/src/lib/ci/triggers.js +86 -0
- package/dist/src/lib/ci/types.d.ts +131 -0
- package/dist/src/lib/ci/types.js +47 -0
- package/dist/src/lib/mcp-config.d.ts +54 -0
- package/dist/src/lib/mcp-config.js +172 -0
- package/dist/src/lib/merge-check/index.js +6 -12
- package/dist/src/lib/merge-check/types.d.ts +20 -7
- package/dist/src/lib/merge-check/types.js +11 -0
- package/dist/src/lib/phase-signal.d.ts +3 -3
- package/dist/src/lib/phase-signal.js +5 -3
- package/dist/src/lib/settings.d.ts +52 -0
- package/dist/src/lib/settings.js +41 -0
- package/dist/src/lib/shutdown.d.ts +16 -5
- package/dist/src/lib/shutdown.js +32 -12
- package/dist/src/lib/solve-comment-parser.d.ts +9 -102
- package/dist/src/lib/solve-comment-parser.js +13 -248
- package/dist/src/lib/stacks.d.ts +8 -0
- package/dist/src/lib/stacks.js +34 -0
- package/dist/src/lib/system.js +3 -7
- package/dist/src/lib/test-tautology-detector.d.ts +10 -0
- package/dist/src/lib/test-tautology-detector.js +43 -4
- package/dist/src/lib/upstream/assessment.js +9 -59
- package/dist/src/lib/upstream/issues.js +12 -75
- package/dist/src/lib/version-check.d.ts +2 -2
- package/dist/src/lib/version-check.js +6 -3
- package/dist/src/lib/version.d.ts +4 -0
- package/dist/src/lib/version.js +25 -0
- package/dist/src/lib/workflow/batch-executor.d.ts +18 -86
- package/dist/src/lib/workflow/batch-executor.js +232 -55
- package/dist/src/lib/workflow/drivers/agent-driver.d.ts +56 -0
- package/dist/src/lib/workflow/drivers/agent-driver.js +8 -0
- package/dist/src/lib/workflow/drivers/aider.d.ts +18 -0
- package/dist/src/lib/workflow/drivers/aider.js +160 -0
- package/dist/src/lib/workflow/drivers/claude-code.d.ts +17 -0
- package/dist/src/lib/workflow/drivers/claude-code.js +165 -0
- package/dist/src/lib/workflow/drivers/index.d.ts +20 -0
- package/dist/src/lib/workflow/drivers/index.js +27 -0
- package/dist/src/lib/workflow/error-classifier.d.ts +16 -0
- package/dist/src/lib/workflow/error-classifier.js +90 -0
- package/dist/src/lib/workflow/log-writer.d.ts +6 -3
- package/dist/src/lib/workflow/log-writer.js +57 -27
- package/dist/src/lib/workflow/metrics-schema.d.ts +9 -9
- package/dist/src/lib/workflow/phase-detection.d.ts +23 -0
- package/dist/src/lib/workflow/phase-detection.js +45 -29
- package/dist/src/lib/workflow/phase-executor.d.ts +42 -3
- package/dist/src/lib/workflow/phase-executor.js +345 -220
- package/dist/src/lib/workflow/phase-mapper.d.ts +1 -1
- package/dist/src/lib/workflow/phase-mapper.js +7 -7
- package/dist/src/lib/workflow/platforms/github.d.ts +157 -0
- package/dist/src/lib/workflow/platforms/github.js +466 -0
- package/dist/src/lib/workflow/platforms/index.d.ts +17 -0
- package/dist/src/lib/workflow/platforms/index.js +25 -0
- package/dist/src/lib/workflow/platforms/platform-provider.d.ts +67 -0
- package/dist/src/lib/workflow/platforms/platform-provider.js +8 -0
- package/dist/src/lib/workflow/pr-status.d.ts +2 -4
- package/dist/src/lib/workflow/pr-status.js +3 -16
- package/dist/src/lib/workflow/qa-cache.d.ts +58 -0
- package/dist/src/lib/workflow/qa-cache.js +88 -0
- package/dist/src/lib/workflow/reconcile.d.ts +69 -0
- package/dist/src/lib/workflow/reconcile.js +290 -0
- package/dist/src/lib/workflow/ring-buffer.d.ts +17 -0
- package/dist/src/lib/workflow/ring-buffer.js +37 -0
- package/dist/src/lib/workflow/run-log-schema.d.ts +115 -24
- package/dist/src/lib/workflow/run-log-schema.js +47 -12
- package/dist/src/lib/workflow/run-reflect.js +1 -1
- package/dist/src/lib/workflow/state-cleanup.js +21 -0
- package/dist/src/lib/workflow/state-manager.d.ts +34 -3
- package/dist/src/lib/workflow/state-manager.js +278 -126
- package/dist/src/lib/workflow/state-schema.d.ts +34 -30
- package/dist/src/lib/workflow/state-schema.js +35 -25
- package/dist/src/lib/workflow/state-utils.d.ts +3 -1
- package/dist/src/lib/workflow/state-utils.js +1 -0
- package/dist/src/lib/workflow/types.d.ts +208 -6
- package/dist/src/lib/workflow/types.js +20 -1
- package/dist/src/lib/workflow/worktree-discovery.d.ts +1 -1
- package/dist/src/lib/workflow/worktree-discovery.js +6 -14
- package/dist/src/lib/workflow/worktree-manager.js +33 -51
- package/dist/src/mcp/index.d.ts +4 -0
- package/dist/src/mcp/index.js +4 -0
- package/dist/src/mcp/resources.d.ts +7 -0
- package/dist/src/mcp/resources.js +111 -0
- package/dist/src/mcp/run-registry.d.ts +34 -0
- package/dist/src/mcp/run-registry.js +42 -0
- package/dist/src/mcp/server.d.ts +12 -0
- package/dist/src/mcp/server.js +50 -0
- package/dist/src/mcp/tools/logs.d.ts +7 -0
- package/dist/src/mcp/tools/logs.js +149 -0
- package/dist/src/mcp/tools/run.d.ts +121 -0
- package/dist/src/mcp/tools/run.js +591 -0
- package/dist/src/mcp/tools/status.d.ts +7 -0
- package/dist/src/mcp/tools/status.js +127 -0
- package/package.json +10 -1
- package/templates/hooks/post-tool.sh +19 -8
- package/templates/hooks/pre-tool.sh +36 -49
- package/templates/mcp.json +6 -0
- package/templates/skills/assess/SKILL.md +354 -352
- package/templates/skills/exec/SKILL.md +64 -1
- package/templates/skills/fullsolve/SKILL.md +35 -4
- package/templates/skills/qa/SKILL.md +486 -9
- package/templates/skills/qa/scripts/quality-checks.sh +1 -1
- package/templates/skills/setup/SKILL.md +386 -0
- package/templates/skills/solve/SKILL.md +38 -664
- package/templates/skills/spec/SKILL.md +90 -31
|
@@ -103,7 +103,7 @@ function suggestImprovements(input, observations, suggestions) {
|
|
|
103
103
|
const allSame = phaseSets.every((s) => s === phaseSets[0]);
|
|
104
104
|
if (allSame) {
|
|
105
105
|
observations.push("All issues ran identical phases despite different requirements");
|
|
106
|
-
suggestions.push("Use `/
|
|
106
|
+
suggestions.push("Use `/assess` first to get per-issue phase recommendations");
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
// Check if quality loop was triggered
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { spawnSync } from "child_process";
|
|
19
19
|
import { StateManager } from "./state-manager.js";
|
|
20
|
+
import { isTerminalStatus } from "./state-schema.js";
|
|
20
21
|
import { checkPRMergeStatus, isIssueMergedIntoMain } from "./pr-status.js";
|
|
21
22
|
/**
|
|
22
23
|
* Get list of active worktree paths
|
|
@@ -109,6 +110,9 @@ export async function cleanupStaleEntries(options = {}) {
|
|
|
109
110
|
// Mark as abandoned (kept for review)
|
|
110
111
|
orphaned.push(issueNum);
|
|
111
112
|
issueState.status = "abandoned";
|
|
113
|
+
if (!issueState.resolvedAt) {
|
|
114
|
+
issueState.resolvedAt = new Date().toISOString();
|
|
115
|
+
}
|
|
112
116
|
if (options.verbose) {
|
|
113
117
|
console.log(` → Marked as abandoned (kept for review)`);
|
|
114
118
|
}
|
|
@@ -144,6 +148,20 @@ export async function cleanupStaleEntries(options = {}) {
|
|
|
144
148
|
delete state.issues[issueNumStr];
|
|
145
149
|
}
|
|
146
150
|
}
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
// Force-clean: remove ALL resolved entries regardless of worktree/TTL
|
|
154
|
+
if (isTerminalStatus(issueState.status)) {
|
|
155
|
+
if (options.verbose) {
|
|
156
|
+
console.log(`🧹 Resolved: #${issueNum} (${issueState.status})`);
|
|
157
|
+
}
|
|
158
|
+
if (issueState.status === "merged") {
|
|
159
|
+
merged.push(issueNum);
|
|
160
|
+
}
|
|
161
|
+
removed.push(issueNum);
|
|
162
|
+
if (!options.dryRun) {
|
|
163
|
+
delete state.issues[issueNumStr];
|
|
164
|
+
}
|
|
147
165
|
}
|
|
148
166
|
}
|
|
149
167
|
// Save updated state
|
|
@@ -223,6 +241,9 @@ export async function reconcileStateAtStartup(options = {}) {
|
|
|
223
241
|
// Advance state to merged
|
|
224
242
|
issueState.status = "merged";
|
|
225
243
|
issueState.lastActivity = new Date().toISOString();
|
|
244
|
+
if (!issueState.resolvedAt) {
|
|
245
|
+
issueState.resolvedAt = new Date().toISOString();
|
|
246
|
+
}
|
|
226
247
|
advanced.push(issueNum);
|
|
227
248
|
}
|
|
228
249
|
else {
|
|
@@ -27,6 +27,8 @@ export interface StateManagerOptions {
|
|
|
27
27
|
statePath?: string;
|
|
28
28
|
/** Enable verbose logging */
|
|
29
29
|
verbose?: boolean;
|
|
30
|
+
/** Lock acquisition timeout in ms (default: 5000) */
|
|
31
|
+
lockTimeout?: number;
|
|
30
32
|
}
|
|
31
33
|
/**
|
|
32
34
|
* Manages persistent workflow state
|
|
@@ -35,20 +37,42 @@ export declare class StateManager {
|
|
|
35
37
|
private statePath;
|
|
36
38
|
private verbose;
|
|
37
39
|
private cachedState;
|
|
40
|
+
private lockTimeout;
|
|
38
41
|
constructor(options?: StateManagerOptions);
|
|
42
|
+
/**
|
|
43
|
+
* Execute a callback while holding an exclusive file lock.
|
|
44
|
+
*
|
|
45
|
+
* Ensures that concurrent processes serialize their read-modify-write
|
|
46
|
+
* cycles on state.json. The cache is cleared before executing the
|
|
47
|
+
* callback so the latest on-disk state is read.
|
|
48
|
+
*
|
|
49
|
+
* External callers (e.g., reconcileState) should use this to wrap
|
|
50
|
+
* any read-modify-write cycle that includes getState() + saveState().
|
|
51
|
+
*/
|
|
52
|
+
withLock<T>(fn: () => Promise<T>): Promise<T>;
|
|
53
|
+
private acquireLock;
|
|
54
|
+
private releaseLock;
|
|
39
55
|
/**
|
|
40
56
|
* Get the full path to the state file
|
|
41
57
|
*/
|
|
42
58
|
getStatePath(): string;
|
|
43
59
|
/**
|
|
44
|
-
* Read the current workflow state
|
|
60
|
+
* Read the current workflow state.
|
|
61
|
+
*
|
|
62
|
+
* **Warning:** This method does NOT acquire a lock. For concurrent access
|
|
63
|
+
* (e.g., reconcileState), wrap your read-modify-write cycle in withLock()
|
|
64
|
+
* to prevent interleaving with other state mutations.
|
|
45
65
|
*
|
|
46
66
|
* Returns empty state if file doesn't exist.
|
|
47
67
|
* Throws on parse errors.
|
|
48
68
|
*/
|
|
49
69
|
getState(): Promise<WorkflowState>;
|
|
50
70
|
/**
|
|
51
|
-
* Write state to disk using atomic write
|
|
71
|
+
* Write state to disk using atomic write.
|
|
72
|
+
*
|
|
73
|
+
* **Warning:** This method does NOT acquire a lock. For concurrent access
|
|
74
|
+
* (e.g., reconcileState), wrap your read-modify-write cycle in withLock()
|
|
75
|
+
* to prevent interleaving with other state mutations.
|
|
52
76
|
*
|
|
53
77
|
* Writes to a temp file first, then renames to prevent corruption
|
|
54
78
|
* if the process is interrupted during write.
|
|
@@ -63,9 +87,16 @@ export declare class StateManager {
|
|
|
63
87
|
*/
|
|
64
88
|
getIssueState(issueNumber: number): Promise<IssueState | null>;
|
|
65
89
|
/**
|
|
66
|
-
* Get all issue states
|
|
90
|
+
* Get all issue states, filtering out expired resolved issues based on TTL.
|
|
91
|
+
*
|
|
92
|
+
* Uses `run.resolvedIssueTTL` from settings (default: 7 days).
|
|
93
|
+
* Expired entries are hidden in-memory; disk cleanup happens lazily on next write.
|
|
67
94
|
*/
|
|
68
95
|
getAllIssueStates(): Promise<Record<number, IssueState>>;
|
|
96
|
+
/**
|
|
97
|
+
* Get all issue states without TTL filtering (for --all escape hatch).
|
|
98
|
+
*/
|
|
99
|
+
getAllIssueStatesUnfiltered(): Promise<Record<number, IssueState>>;
|
|
69
100
|
/**
|
|
70
101
|
* Get the current phase for an issue
|
|
71
102
|
*/
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
import * as fs from "fs";
|
|
24
24
|
import * as path from "path";
|
|
25
25
|
import * as os from "os";
|
|
26
|
-
import { WorkflowStateSchema, STATE_FILE_PATH, createEmptyState, createIssueState, createPhaseState, updateAcceptanceCriteriaSummary, } from "./state-schema.js";
|
|
26
|
+
import { WorkflowStateSchema, STATE_FILE_PATH, createEmptyState, createIssueState, createPhaseState, updateAcceptanceCriteriaSummary, isTerminalStatus, isExpired, } from "./state-schema.js";
|
|
27
|
+
import { getSettings } from "../settings.js";
|
|
27
28
|
/**
|
|
28
29
|
* Manages persistent workflow state
|
|
29
30
|
*/
|
|
@@ -31,9 +32,88 @@ export class StateManager {
|
|
|
31
32
|
statePath;
|
|
32
33
|
verbose;
|
|
33
34
|
cachedState = null;
|
|
35
|
+
lockTimeout;
|
|
34
36
|
constructor(options = {}) {
|
|
35
37
|
this.statePath = options.statePath ?? STATE_FILE_PATH;
|
|
36
38
|
this.verbose = options.verbose ?? false;
|
|
39
|
+
this.lockTimeout = options.lockTimeout ?? 5000;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Execute a callback while holding an exclusive file lock.
|
|
43
|
+
*
|
|
44
|
+
* Ensures that concurrent processes serialize their read-modify-write
|
|
45
|
+
* cycles on state.json. The cache is cleared before executing the
|
|
46
|
+
* callback so the latest on-disk state is read.
|
|
47
|
+
*
|
|
48
|
+
* External callers (e.g., reconcileState) should use this to wrap
|
|
49
|
+
* any read-modify-write cycle that includes getState() + saveState().
|
|
50
|
+
*/
|
|
51
|
+
async withLock(fn) {
|
|
52
|
+
const lockPath = this.statePath + ".lock";
|
|
53
|
+
await this.acquireLock(lockPath);
|
|
54
|
+
try {
|
|
55
|
+
// Clear cache so we read the latest on-disk state
|
|
56
|
+
this.clearCache();
|
|
57
|
+
return await fn();
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
this.releaseLock(lockPath);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async acquireLock(lockPath) {
|
|
64
|
+
const start = Date.now();
|
|
65
|
+
const retryDelay = 50; // ms
|
|
66
|
+
// Ensure directory exists for lock file
|
|
67
|
+
const dir = path.dirname(lockPath);
|
|
68
|
+
if (!fs.existsSync(dir)) {
|
|
69
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
70
|
+
}
|
|
71
|
+
while (true) {
|
|
72
|
+
try {
|
|
73
|
+
// O_EXCL: fail atomically if file already exists
|
|
74
|
+
const fd = fs.openSync(lockPath, "wx");
|
|
75
|
+
fs.writeSync(fd, String(process.pid));
|
|
76
|
+
fs.closeSync(fd);
|
|
77
|
+
return; // Lock acquired
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
const code = err.code;
|
|
81
|
+
if (code !== "EEXIST")
|
|
82
|
+
throw err;
|
|
83
|
+
// Check for stale lock (older than timeout)
|
|
84
|
+
try {
|
|
85
|
+
const stat = fs.statSync(lockPath);
|
|
86
|
+
const age = Date.now() - stat.mtimeMs;
|
|
87
|
+
if (age > this.lockTimeout) {
|
|
88
|
+
// Stale lock — remove and retry
|
|
89
|
+
try {
|
|
90
|
+
fs.unlinkSync(lockPath);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Another process may have removed it
|
|
94
|
+
}
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// Lock file disappeared between open and stat — retry
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (Date.now() - start > this.lockTimeout) {
|
|
103
|
+
throw new Error(`Timeout acquiring state lock after ${this.lockTimeout}ms`);
|
|
104
|
+
}
|
|
105
|
+
// Wait and retry
|
|
106
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
releaseLock(lockPath) {
|
|
111
|
+
try {
|
|
112
|
+
fs.unlinkSync(lockPath);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// Ignore — lock may have been cleaned up by stale detection
|
|
116
|
+
}
|
|
37
117
|
}
|
|
38
118
|
/**
|
|
39
119
|
* Get the full path to the state file
|
|
@@ -42,7 +122,11 @@ export class StateManager {
|
|
|
42
122
|
return this.statePath;
|
|
43
123
|
}
|
|
44
124
|
/**
|
|
45
|
-
* Read the current workflow state
|
|
125
|
+
* Read the current workflow state.
|
|
126
|
+
*
|
|
127
|
+
* **Warning:** This method does NOT acquire a lock. For concurrent access
|
|
128
|
+
* (e.g., reconcileState), wrap your read-modify-write cycle in withLock()
|
|
129
|
+
* to prevent interleaving with other state mutations.
|
|
46
130
|
*
|
|
47
131
|
* Returns empty state if file doesn't exist.
|
|
48
132
|
* Throws on parse errors.
|
|
@@ -72,7 +156,11 @@ export class StateManager {
|
|
|
72
156
|
}
|
|
73
157
|
}
|
|
74
158
|
/**
|
|
75
|
-
* Write state to disk using atomic write
|
|
159
|
+
* Write state to disk using atomic write.
|
|
160
|
+
*
|
|
161
|
+
* **Warning:** This method does NOT acquire a lock. For concurrent access
|
|
162
|
+
* (e.g., reconcileState), wrap your read-modify-write cycle in withLock()
|
|
163
|
+
* to prevent interleaving with other state mutations.
|
|
76
164
|
*
|
|
77
165
|
* Writes to a temp file first, then renames to prevent corruption
|
|
78
166
|
* if the process is interrupted during write.
|
|
@@ -82,6 +170,24 @@ export class StateManager {
|
|
|
82
170
|
WorkflowStateSchema.parse(state);
|
|
83
171
|
// Update lastUpdated timestamp
|
|
84
172
|
state.lastUpdated = new Date().toISOString();
|
|
173
|
+
// Lazy disk cleanup: prune expired entries before writing
|
|
174
|
+
try {
|
|
175
|
+
const settings = await getSettings();
|
|
176
|
+
const ttlDays = settings.run.resolvedIssueTTL ?? 7;
|
|
177
|
+
const pruned = [];
|
|
178
|
+
for (const [key, entry] of Object.entries(state.issues)) {
|
|
179
|
+
if (isExpired(entry, ttlDays)) {
|
|
180
|
+
pruned.push(key);
|
|
181
|
+
delete state.issues[key];
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (pruned.length > 0 && this.verbose) {
|
|
185
|
+
console.log(`📊 Pruned ${pruned.length} expired issue(s): ${pruned.map((k) => `#${k}`).join(", ")}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
// Settings read failure should not block state writes
|
|
190
|
+
}
|
|
85
191
|
// Ensure directory exists
|
|
86
192
|
const dir = path.dirname(this.statePath);
|
|
87
193
|
if (!fs.existsSync(dir)) {
|
|
@@ -119,9 +225,27 @@ export class StateManager {
|
|
|
119
225
|
return state.issues[String(issueNumber)] ?? null;
|
|
120
226
|
}
|
|
121
227
|
/**
|
|
122
|
-
* Get all issue states
|
|
228
|
+
* Get all issue states, filtering out expired resolved issues based on TTL.
|
|
229
|
+
*
|
|
230
|
+
* Uses `run.resolvedIssueTTL` from settings (default: 7 days).
|
|
231
|
+
* Expired entries are hidden in-memory; disk cleanup happens lazily on next write.
|
|
123
232
|
*/
|
|
124
233
|
async getAllIssueStates() {
|
|
234
|
+
const state = await this.getState();
|
|
235
|
+
const settings = await getSettings();
|
|
236
|
+
const ttlDays = settings.run.resolvedIssueTTL ?? 7;
|
|
237
|
+
const result = {};
|
|
238
|
+
for (const [key, value] of Object.entries(state.issues)) {
|
|
239
|
+
if (!isExpired(value, ttlDays)) {
|
|
240
|
+
result[parseInt(key, 10)] = value;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Get all issue states without TTL filtering (for --all escape hatch).
|
|
247
|
+
*/
|
|
248
|
+
async getAllIssueStatesUnfiltered() {
|
|
125
249
|
const state = await this.getState();
|
|
126
250
|
const result = {};
|
|
127
251
|
for (const [key, value] of Object.entries(state.issues)) {
|
|
@@ -140,11 +264,13 @@ export class StateManager {
|
|
|
140
264
|
* Initialize tracking for a new issue
|
|
141
265
|
*/
|
|
142
266
|
async initializeIssue(issueNumber, title, options) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
267
|
+
await this.withLock(async () => {
|
|
268
|
+
const state = await this.getState();
|
|
269
|
+
// Create new issue state
|
|
270
|
+
const issueState = createIssueState(issueNumber, title, options);
|
|
271
|
+
state.issues[String(issueNumber)] = issueState;
|
|
272
|
+
await this.saveState(state);
|
|
273
|
+
});
|
|
148
274
|
if (this.verbose) {
|
|
149
275
|
console.log(`📊 Initialized issue #${issueNumber}: ${title}`);
|
|
150
276
|
}
|
|
@@ -153,35 +279,39 @@ export class StateManager {
|
|
|
153
279
|
* Update the status of a specific phase
|
|
154
280
|
*/
|
|
155
281
|
async updatePhaseStatus(issueNumber, phase, status, options) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
issueState.
|
|
183
|
-
|
|
184
|
-
|
|
282
|
+
await this.withLock(async () => {
|
|
283
|
+
const state = await this.getState();
|
|
284
|
+
const issueState = state.issues[String(issueNumber)];
|
|
285
|
+
if (!issueState) {
|
|
286
|
+
throw new Error(`Issue #${issueNumber} not found in state`);
|
|
287
|
+
}
|
|
288
|
+
// Update phase state
|
|
289
|
+
const phaseState = createPhaseState(status);
|
|
290
|
+
if (status === "completed" ||
|
|
291
|
+
status === "failed" ||
|
|
292
|
+
status === "skipped") {
|
|
293
|
+
phaseState.completedAt = new Date().toISOString();
|
|
294
|
+
}
|
|
295
|
+
if (options?.error) {
|
|
296
|
+
phaseState.error = options.error;
|
|
297
|
+
}
|
|
298
|
+
if (options?.iteration !== undefined) {
|
|
299
|
+
phaseState.iteration = options.iteration;
|
|
300
|
+
}
|
|
301
|
+
// Preserve startedAt if already set
|
|
302
|
+
const existingPhase = issueState.phases[phase];
|
|
303
|
+
if (existingPhase?.startedAt && status !== "pending") {
|
|
304
|
+
phaseState.startedAt = existingPhase.startedAt;
|
|
305
|
+
}
|
|
306
|
+
issueState.phases[phase] = phaseState;
|
|
307
|
+
issueState.currentPhase = phase;
|
|
308
|
+
issueState.lastActivity = new Date().toISOString();
|
|
309
|
+
// Update issue status based on phase status
|
|
310
|
+
if (status === "in_progress" && issueState.status === "not_started") {
|
|
311
|
+
issueState.status = "in_progress";
|
|
312
|
+
}
|
|
313
|
+
await this.saveState(state);
|
|
314
|
+
});
|
|
185
315
|
if (this.verbose) {
|
|
186
316
|
console.log(`📊 Phase ${phase} → ${status} for issue #${issueNumber}`);
|
|
187
317
|
}
|
|
@@ -190,14 +320,20 @@ export class StateManager {
|
|
|
190
320
|
* Update the overall issue status
|
|
191
321
|
*/
|
|
192
322
|
async updateIssueStatus(issueNumber, status) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
323
|
+
await this.withLock(async () => {
|
|
324
|
+
const state = await this.getState();
|
|
325
|
+
const issueState = state.issues[String(issueNumber)];
|
|
326
|
+
if (!issueState) {
|
|
327
|
+
throw new Error(`Issue #${issueNumber} not found in state`);
|
|
328
|
+
}
|
|
329
|
+
issueState.status = status;
|
|
330
|
+
issueState.lastActivity = new Date().toISOString();
|
|
331
|
+
// Record resolvedAt on first transition to terminal status
|
|
332
|
+
if (isTerminalStatus(status) && !issueState.resolvedAt) {
|
|
333
|
+
issueState.resolvedAt = new Date().toISOString();
|
|
334
|
+
}
|
|
335
|
+
await this.saveState(state);
|
|
336
|
+
});
|
|
201
337
|
if (this.verbose) {
|
|
202
338
|
console.log(`📊 Issue #${issueNumber} status → ${status}`);
|
|
203
339
|
}
|
|
@@ -206,14 +342,16 @@ export class StateManager {
|
|
|
206
342
|
* Update PR information for an issue
|
|
207
343
|
*/
|
|
208
344
|
async updatePRInfo(issueNumber, pr) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
345
|
+
await this.withLock(async () => {
|
|
346
|
+
const state = await this.getState();
|
|
347
|
+
const issueState = state.issues[String(issueNumber)];
|
|
348
|
+
if (!issueState) {
|
|
349
|
+
throw new Error(`Issue #${issueNumber} not found in state`);
|
|
350
|
+
}
|
|
351
|
+
issueState.pr = pr;
|
|
352
|
+
issueState.lastActivity = new Date().toISOString();
|
|
353
|
+
await this.saveState(state);
|
|
354
|
+
});
|
|
217
355
|
if (this.verbose) {
|
|
218
356
|
console.log(`📊 PR #${pr.number} linked to issue #${issueNumber}`);
|
|
219
357
|
}
|
|
@@ -222,15 +360,17 @@ export class StateManager {
|
|
|
222
360
|
* Update worktree information for an issue
|
|
223
361
|
*/
|
|
224
362
|
async updateWorktreeInfo(issueNumber, worktree, branch) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
363
|
+
await this.withLock(async () => {
|
|
364
|
+
const state = await this.getState();
|
|
365
|
+
const issueState = state.issues[String(issueNumber)];
|
|
366
|
+
if (!issueState) {
|
|
367
|
+
throw new Error(`Issue #${issueNumber} not found in state`);
|
|
368
|
+
}
|
|
369
|
+
issueState.worktree = worktree;
|
|
370
|
+
issueState.branch = branch;
|
|
371
|
+
issueState.lastActivity = new Date().toISOString();
|
|
372
|
+
await this.saveState(state);
|
|
373
|
+
});
|
|
234
374
|
if (this.verbose) {
|
|
235
375
|
console.log(`📊 Worktree updated for issue #${issueNumber}: ${worktree}`);
|
|
236
376
|
}
|
|
@@ -239,29 +379,33 @@ export class StateManager {
|
|
|
239
379
|
* Update session ID for an issue (for resume)
|
|
240
380
|
*/
|
|
241
381
|
async updateSessionId(issueNumber, sessionId) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
382
|
+
await this.withLock(async () => {
|
|
383
|
+
const state = await this.getState();
|
|
384
|
+
const issueState = state.issues[String(issueNumber)];
|
|
385
|
+
if (!issueState) {
|
|
386
|
+
throw new Error(`Issue #${issueNumber} not found in state`);
|
|
387
|
+
}
|
|
388
|
+
issueState.sessionId = sessionId;
|
|
389
|
+
issueState.lastActivity = new Date().toISOString();
|
|
390
|
+
await this.saveState(state);
|
|
391
|
+
});
|
|
250
392
|
}
|
|
251
393
|
/**
|
|
252
394
|
* Update loop iteration for an issue
|
|
253
395
|
*/
|
|
254
396
|
async updateLoopIteration(issueNumber, iteration) {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
issueState.loop
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
397
|
+
await this.withLock(async () => {
|
|
398
|
+
const state = await this.getState();
|
|
399
|
+
const issueState = state.issues[String(issueNumber)];
|
|
400
|
+
if (!issueState) {
|
|
401
|
+
throw new Error(`Issue #${issueNumber} not found in state`);
|
|
402
|
+
}
|
|
403
|
+
if (issueState.loop) {
|
|
404
|
+
issueState.loop.iteration = iteration;
|
|
405
|
+
}
|
|
406
|
+
issueState.lastActivity = new Date().toISOString();
|
|
407
|
+
await this.saveState(state);
|
|
408
|
+
});
|
|
265
409
|
if (this.verbose) {
|
|
266
410
|
console.log(`📊 Loop iteration ${iteration} for issue #${issueNumber}`);
|
|
267
411
|
}
|
|
@@ -270,12 +414,14 @@ export class StateManager {
|
|
|
270
414
|
* Remove an issue from state
|
|
271
415
|
*/
|
|
272
416
|
async removeIssue(issueNumber) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
417
|
+
await this.withLock(async () => {
|
|
418
|
+
const state = await this.getState();
|
|
419
|
+
if (!state.issues[String(issueNumber)]) {
|
|
420
|
+
return; // Already removed
|
|
421
|
+
}
|
|
422
|
+
delete state.issues[String(issueNumber)];
|
|
423
|
+
await this.saveState(state);
|
|
424
|
+
});
|
|
279
425
|
if (this.verbose) {
|
|
280
426
|
console.log(`📊 Removed issue #${issueNumber} from state`);
|
|
281
427
|
}
|
|
@@ -287,14 +433,16 @@ export class StateManager {
|
|
|
287
433
|
* Used by /spec to store extracted ACs from the issue body.
|
|
288
434
|
*/
|
|
289
435
|
async updateAcceptanceCriteria(issueNumber, acceptanceCriteria) {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
436
|
+
await this.withLock(async () => {
|
|
437
|
+
const state = await this.getState();
|
|
438
|
+
const issueState = state.issues[String(issueNumber)];
|
|
439
|
+
if (!issueState) {
|
|
440
|
+
throw new Error(`Issue #${issueNumber} not found in state`);
|
|
441
|
+
}
|
|
442
|
+
issueState.acceptanceCriteria = acceptanceCriteria;
|
|
443
|
+
issueState.lastActivity = new Date().toISOString();
|
|
444
|
+
await this.saveState(state);
|
|
445
|
+
});
|
|
298
446
|
if (this.verbose) {
|
|
299
447
|
console.log(`📊 AC updated for issue #${issueNumber}: ${acceptanceCriteria.items.length} items`);
|
|
300
448
|
}
|
|
@@ -313,29 +461,31 @@ export class StateManager {
|
|
|
313
461
|
* Automatically recalculates the summary counts.
|
|
314
462
|
*/
|
|
315
463
|
async updateACStatus(issueNumber, acId, status, notes) {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
`
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
464
|
+
await this.withLock(async () => {
|
|
465
|
+
const state = await this.getState();
|
|
466
|
+
const issueState = state.issues[String(issueNumber)];
|
|
467
|
+
if (!issueState) {
|
|
468
|
+
throw new Error(`Issue #${issueNumber} not found in state`);
|
|
469
|
+
}
|
|
470
|
+
if (!issueState.acceptanceCriteria) {
|
|
471
|
+
throw new Error(`Issue #${issueNumber} has no acceptance criteria`);
|
|
472
|
+
}
|
|
473
|
+
// Find and update the AC item
|
|
474
|
+
const acItem = issueState.acceptanceCriteria.items.find((item) => item.id === acId);
|
|
475
|
+
if (!acItem) {
|
|
476
|
+
throw new Error(`AC "${acId}" not found in issue #${issueNumber}. ` +
|
|
477
|
+
`Available IDs: ${issueState.acceptanceCriteria.items.map((i) => i.id).join(", ")}`);
|
|
478
|
+
}
|
|
479
|
+
acItem.status = status;
|
|
480
|
+
acItem.verifiedAt = new Date().toISOString();
|
|
481
|
+
if (notes !== undefined) {
|
|
482
|
+
acItem.notes = notes;
|
|
483
|
+
}
|
|
484
|
+
// Recalculate summary counts
|
|
485
|
+
issueState.acceptanceCriteria = updateAcceptanceCriteriaSummary(issueState.acceptanceCriteria);
|
|
486
|
+
issueState.lastActivity = new Date().toISOString();
|
|
487
|
+
await this.saveState(state);
|
|
488
|
+
});
|
|
339
489
|
if (this.verbose) {
|
|
340
490
|
console.log(`📊 AC ${acId} → ${status} for issue #${issueNumber}`);
|
|
341
491
|
}
|
|
@@ -347,14 +497,16 @@ export class StateManager {
|
|
|
347
497
|
* Used by /spec to store the scope assessment result.
|
|
348
498
|
*/
|
|
349
499
|
async updateScopeAssessment(issueNumber, scopeAssessment) {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
500
|
+
await this.withLock(async () => {
|
|
501
|
+
const state = await this.getState();
|
|
502
|
+
const issueState = state.issues[String(issueNumber)];
|
|
503
|
+
if (!issueState) {
|
|
504
|
+
throw new Error(`Issue #${issueNumber} not found in state`);
|
|
505
|
+
}
|
|
506
|
+
issueState.scopeAssessment = scopeAssessment;
|
|
507
|
+
issueState.lastActivity = new Date().toISOString();
|
|
508
|
+
await this.saveState(state);
|
|
509
|
+
});
|
|
358
510
|
if (this.verbose) {
|
|
359
511
|
console.log(`📊 Scope assessment updated for issue #${issueNumber}: ${scopeAssessment.verdict}`);
|
|
360
512
|
}
|