gsd-pi 2.28.0-dev.4009980 → 2.28.0-dev.e19bf89
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/dist/resources/extensions/gsd/auto-post-unit.ts +3 -8
- package/dist/resources/extensions/gsd/auto-start.ts +9 -24
- package/dist/resources/extensions/gsd/auto.ts +2 -45
- package/dist/resources/extensions/gsd/commands.ts +0 -19
- package/dist/resources/extensions/gsd/doctor-types.ts +0 -13
- package/dist/resources/extensions/gsd/doctor.ts +6 -2
- package/dist/resources/extensions/gsd/gsd-db.ts +0 -19
- package/package.json +1 -1
- package/src/resources/extensions/gsd/auto-post-unit.ts +3 -8
- package/src/resources/extensions/gsd/auto-start.ts +9 -24
- package/src/resources/extensions/gsd/auto.ts +2 -45
- package/src/resources/extensions/gsd/commands.ts +0 -19
- package/src/resources/extensions/gsd/doctor-types.ts +0 -13
- package/src/resources/extensions/gsd/doctor.ts +6 -2
- package/src/resources/extensions/gsd/gsd-db.ts +0 -19
- package/dist/resources/extensions/gsd/commands-logs.ts +0 -537
- package/dist/resources/extensions/gsd/session-lock.ts +0 -284
- package/dist/resources/extensions/gsd/tests/commands-logs.test.ts +0 -241
- package/dist/resources/extensions/gsd/tests/session-lock.test.ts +0 -315
- package/src/resources/extensions/gsd/commands-logs.ts +0 -537
- package/src/resources/extensions/gsd/session-lock.ts +0 -284
- package/src/resources/extensions/gsd/tests/commands-logs.test.ts +0 -241
- package/src/resources/extensions/gsd/tests/session-lock.test.ts +0 -315
|
@@ -35,7 +35,6 @@ import {
|
|
|
35
35
|
import { writeUnitRuntimeRecord, clearUnitRuntimeRecord } from "./unit-runtime.js";
|
|
36
36
|
import { resolveAutoSupervisorConfig, loadEffectiveGSDPreferences } from "./preferences.js";
|
|
37
37
|
import { runGSDDoctor, rebuildState, summarizeDoctorIssues } from "./doctor.js";
|
|
38
|
-
import { COMPLETION_TRANSITION_CODES } from "./doctor-types.js";
|
|
39
38
|
import { recordHealthSnapshot, checkHealEscalation } from "./doctor-proactive.js";
|
|
40
39
|
import { syncStateToProjectRoot } from "./auto-worktree-sync.js";
|
|
41
40
|
import { resetRewriteCircuitBreaker } from "./auto-dispatch.js";
|
|
@@ -155,17 +154,13 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
|
|
|
155
154
|
ctx.ui.notify(`Post-hook: applied ${report.fixesApplied.length} fix(es).`, "info");
|
|
156
155
|
}
|
|
157
156
|
|
|
158
|
-
// Proactive health tracking
|
|
159
|
-
|
|
160
|
-
const issuesForHealth = effectiveFixLevel === "task"
|
|
161
|
-
? report.issues.filter(i => !COMPLETION_TRANSITION_CODES.has(i.code))
|
|
162
|
-
: report.issues;
|
|
163
|
-
const summary = summarizeDoctorIssues(issuesForHealth);
|
|
157
|
+
// Proactive health tracking
|
|
158
|
+
const summary = summarizeDoctorIssues(report.issues);
|
|
164
159
|
recordHealthSnapshot(summary.errors, summary.warnings, report.fixesApplied.length);
|
|
165
160
|
|
|
166
161
|
// Check if we should escalate to LLM-assisted heal
|
|
167
162
|
if (summary.errors > 0) {
|
|
168
|
-
const unresolvedErrors =
|
|
163
|
+
const unresolvedErrors = report.issues
|
|
169
164
|
.filter(i => i.severity === "error" && !i.fixable)
|
|
170
165
|
.map(i => ({ code: i.code, message: i.message, unitId: i.unitId }));
|
|
171
166
|
const escalation = checkHealEscalation(summary.errors, unresolvedErrors);
|
|
@@ -26,13 +26,6 @@ import {
|
|
|
26
26
|
import { invalidateAllCaches } from "./cache.js";
|
|
27
27
|
import { synthesizeCrashRecovery } from "./session-forensics.js";
|
|
28
28
|
import { writeLock, clearLock, readCrashLock, formatCrashInfo, isLockProcessAlive } from "./crash-recovery.js";
|
|
29
|
-
import {
|
|
30
|
-
acquireSessionLock,
|
|
31
|
-
updateSessionLock,
|
|
32
|
-
releaseSessionLock,
|
|
33
|
-
readSessionLockData,
|
|
34
|
-
isSessionLockProcessAlive,
|
|
35
|
-
} from "./session-lock.js";
|
|
36
29
|
import { selfHealRuntimeRecords } from "./auto-recovery.js";
|
|
37
30
|
import { ensureGitignore, untrackRuntimeFiles } from "./gitignore.js";
|
|
38
31
|
import { nativeIsRepo, nativeInit, nativeAddAll, nativeCommit } from "./native-git-bridge.js";
|
|
@@ -88,18 +81,6 @@ export async function bootstrapAutoSession(
|
|
|
88
81
|
): Promise<boolean> {
|
|
89
82
|
const { shouldUseWorktreeIsolation, registerSigtermHandler, lockBase } = deps;
|
|
90
83
|
|
|
91
|
-
// ── Session lock: acquire FIRST, before any state mutation ──────────────
|
|
92
|
-
// This is the primary guard against concurrent sessions on the same project.
|
|
93
|
-
// Uses OS-level file locking (proper-lockfile) to prevent TOCTOU races.
|
|
94
|
-
const lockResult = acquireSessionLock(base);
|
|
95
|
-
if (!lockResult.acquired) {
|
|
96
|
-
ctx.ui.notify(
|
|
97
|
-
`${lockResult.reason}\nStop it with \`kill ${lockResult.existingPid ?? "the other process"}\` before starting a new session.`,
|
|
98
|
-
"error",
|
|
99
|
-
);
|
|
100
|
-
return false;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
84
|
// Ensure git repo exists
|
|
104
85
|
if (!nativeIsRepo(base)) {
|
|
105
86
|
const mainBranch = loadEffectiveGSDPreferences()?.preferences?.git?.main_branch || "main";
|
|
@@ -128,11 +109,16 @@ export async function bootstrapAutoSession(
|
|
|
128
109
|
// Initialize GitServiceImpl
|
|
129
110
|
s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
|
|
130
111
|
|
|
131
|
-
// Check for crash from previous session
|
|
112
|
+
// Check for crash from previous session
|
|
132
113
|
const crashLock = readCrashLock(base);
|
|
133
114
|
if (crashLock) {
|
|
134
|
-
|
|
135
|
-
|
|
115
|
+
if (isLockProcessAlive(crashLock)) {
|
|
116
|
+
ctx.ui.notify(
|
|
117
|
+
`Another auto-mode session (PID ${crashLock.pid}) appears to be running.\nStop it with \`kill ${crashLock.pid}\` before starting a new session.`,
|
|
118
|
+
"error",
|
|
119
|
+
);
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
136
122
|
const recoveredMid = crashLock.unitId.split("/")[0];
|
|
137
123
|
const milestoneAlreadyComplete = recoveredMid
|
|
138
124
|
? !!resolveMilestoneFile(base, recoveredMid, "SUMMARY")
|
|
@@ -421,8 +407,7 @@ export async function bootstrapAutoSession(
|
|
|
421
407
|
: "Will loop until milestone complete.";
|
|
422
408
|
ctx.ui.notify(`${modeLabel} started. ${scopeMsg}`, "info");
|
|
423
409
|
|
|
424
|
-
//
|
|
425
|
-
updateSessionLock(lockBase(), "starting", s.currentMilestoneId ?? "unknown", 0);
|
|
410
|
+
// Write initial lock file
|
|
426
411
|
writeLock(lockBase(), "starting", s.currentMilestoneId ?? "unknown", 0);
|
|
427
412
|
|
|
428
413
|
// Secrets collection gate — pause instead of blocking (#1146)
|
|
@@ -33,12 +33,6 @@ import { invalidateAllCaches } from "./cache.js";
|
|
|
33
33
|
import { saveActivityLog, clearActivityLogState } from "./activity-log.js";
|
|
34
34
|
import { synthesizeCrashRecovery, getDeepDiagnostic } from "./session-forensics.js";
|
|
35
35
|
import { writeLock, clearLock, readCrashLock, formatCrashInfo, isLockProcessAlive } from "./crash-recovery.js";
|
|
36
|
-
import {
|
|
37
|
-
acquireSessionLock,
|
|
38
|
-
validateSessionLock,
|
|
39
|
-
releaseSessionLock,
|
|
40
|
-
updateSessionLock,
|
|
41
|
-
} from "./session-lock.js";
|
|
42
36
|
import {
|
|
43
37
|
clearUnitRuntimeRecord,
|
|
44
38
|
inspectExecuteTaskDurability,
|
|
@@ -457,10 +451,7 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI, reason
|
|
|
457
451
|
if (!s.active && !s.paused) return;
|
|
458
452
|
const reasonSuffix = reason ? ` — ${reason}` : "";
|
|
459
453
|
clearUnitTimeout();
|
|
460
|
-
if (lockBase())
|
|
461
|
-
releaseSessionLock(lockBase());
|
|
462
|
-
clearLock(lockBase());
|
|
463
|
-
}
|
|
454
|
+
if (lockBase()) clearLock(lockBase());
|
|
464
455
|
clearSkillSnapshot();
|
|
465
456
|
resetSkillTelemetry();
|
|
466
457
|
s.dispatching = false;
|
|
@@ -574,10 +565,7 @@ export async function pauseAuto(ctx?: ExtensionContext, _pi?: ExtensionAPI): Pro
|
|
|
574
565
|
|
|
575
566
|
s.pausedSessionFile = ctx?.sessionManager?.getSessionFile() ?? null;
|
|
576
567
|
|
|
577
|
-
if (lockBase())
|
|
578
|
-
releaseSessionLock(lockBase());
|
|
579
|
-
clearLock(lockBase());
|
|
580
|
-
}
|
|
568
|
+
if (lockBase()) clearLock(lockBase());
|
|
581
569
|
|
|
582
570
|
deregisterSigtermHandler();
|
|
583
571
|
|
|
@@ -610,16 +598,6 @@ export async function startAuto(
|
|
|
610
598
|
|
|
611
599
|
// If resuming from paused state, just re-activate and dispatch next unit.
|
|
612
600
|
if (s.paused) {
|
|
613
|
-
// Re-acquire session lock before resuming
|
|
614
|
-
const resumeLock = acquireSessionLock(base);
|
|
615
|
-
if (!resumeLock.acquired) {
|
|
616
|
-
ctx.ui.notify(
|
|
617
|
-
`Cannot resume: ${resumeLock.reason}`,
|
|
618
|
-
"error",
|
|
619
|
-
);
|
|
620
|
-
return;
|
|
621
|
-
}
|
|
622
|
-
|
|
623
601
|
s.paused = false;
|
|
624
602
|
s.active = true;
|
|
625
603
|
s.verbose = verboseMode;
|
|
@@ -721,7 +699,6 @@ export async function startAuto(
|
|
|
721
699
|
s.pausedForSecrets = false;
|
|
722
700
|
}
|
|
723
701
|
|
|
724
|
-
updateSessionLock(lockBase(), "resuming", s.currentMilestoneId ?? "unknown", s.completedUnits.length);
|
|
725
702
|
writeLock(lockBase(), "resuming", s.currentMilestoneId ?? "unknown", s.completedUnits.length);
|
|
726
703
|
|
|
727
704
|
await dispatchNextUnit(ctx, pi);
|
|
@@ -973,24 +950,6 @@ async function dispatchNextUnit(
|
|
|
973
950
|
return;
|
|
974
951
|
}
|
|
975
952
|
|
|
976
|
-
// ── Session lock validation: detect if another process has taken over ──
|
|
977
|
-
if (lockBase() && !validateSessionLock(lockBase())) {
|
|
978
|
-
debugLog("dispatchNextUnit session-lock-lost — another process may have taken over");
|
|
979
|
-
ctx.ui.notify(
|
|
980
|
-
"Session lock lost — another GSD process appears to have taken over. Stopping gracefully.",
|
|
981
|
-
"error",
|
|
982
|
-
);
|
|
983
|
-
// Don't call stopAuto here to avoid releasing the lock we don't own
|
|
984
|
-
s.active = false;
|
|
985
|
-
s.paused = false;
|
|
986
|
-
clearUnitTimeout();
|
|
987
|
-
deregisterSigtermHandler();
|
|
988
|
-
ctx.ui.setStatus("gsd-auto", undefined);
|
|
989
|
-
ctx.ui.setWidget("gsd-progress", undefined);
|
|
990
|
-
ctx.ui.setFooter(undefined);
|
|
991
|
-
return;
|
|
992
|
-
}
|
|
993
|
-
|
|
994
953
|
// Reentrancy guard
|
|
995
954
|
if (s.dispatching && s.skipDepth === 0) {
|
|
996
955
|
debugLog("dispatchNextUnit reentrancy guard — another dispatch in progress, bailing");
|
|
@@ -1624,7 +1583,6 @@ async function dispatchNextUnit(
|
|
|
1624
1583
|
}
|
|
1625
1584
|
|
|
1626
1585
|
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
1627
|
-
updateSessionLock(lockBase(), unitType, unitId, s.completedUnits.length, sessionFile);
|
|
1628
1586
|
writeLock(lockBase(), unitType, unitId, s.completedUnits.length, sessionFile);
|
|
1629
1587
|
|
|
1630
1588
|
// Prompt injection
|
|
@@ -1851,7 +1809,6 @@ export async function dispatchHookUnit(
|
|
|
1851
1809
|
}
|
|
1852
1810
|
|
|
1853
1811
|
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
1854
|
-
updateSessionLock(lockBase(), hookUnitType, triggerUnitId, s.completedUnits.length, sessionFile);
|
|
1855
1812
|
writeLock(lockBase(), hookUnitType, triggerUnitId, s.completedUnits.length, sessionFile);
|
|
1856
1813
|
|
|
1857
1814
|
clearUnitTimeout();
|
|
@@ -43,7 +43,6 @@ import { handleConfig } from "./commands-config.js";
|
|
|
43
43
|
import { handleInspect } from "./commands-inspect.js";
|
|
44
44
|
import { handleCleanupBranches, handleCleanupSnapshots, handleSkip, handleDryRun } from "./commands-maintenance.js";
|
|
45
45
|
import { handleDoctor, handleSteer, handleCapture, handleTriage, handleKnowledge, handleRunHook, handleUpdate, handleSkillHealth } from "./commands-handlers.js";
|
|
46
|
-
import { handleLogs } from "./commands-logs.js";
|
|
47
46
|
|
|
48
47
|
// ─── Re-exports (preserve public API surface) ───────────────────────────────
|
|
49
48
|
export { handlePrefs, handlePrefsMode, handlePrefsWizard, ensurePreferencesFile, handleImportClaude, buildCategorySummaries, serializePreferencesToFrontmatter, yamlSafeString, configureMode } from "./commands-prefs-wizard.js";
|
|
@@ -108,7 +107,6 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
|
108
107
|
{ cmd: "run-hook", desc: "Manually trigger a specific hook" },
|
|
109
108
|
{ cmd: "skill-health", desc: "Skill lifecycle dashboard" },
|
|
110
109
|
{ cmd: "doctor", desc: "Runtime health checks with auto-fix" },
|
|
111
|
-
{ cmd: "logs", desc: "Browse activity logs, debug logs, and metrics" },
|
|
112
110
|
{ cmd: "forensics", desc: "Examine execution logs" },
|
|
113
111
|
{ cmd: "init", desc: "Project init wizard — detect, configure, bootstrap .gsd/" },
|
|
114
112
|
{ cmd: "setup", desc: "Global setup status and configuration" },
|
|
@@ -186,18 +184,6 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
|
186
184
|
.map((s) => ({ value: `setup ${s.cmd}`, label: s.cmd, description: s.desc }));
|
|
187
185
|
}
|
|
188
186
|
|
|
189
|
-
if (parts[0] === "logs" && parts.length <= 2) {
|
|
190
|
-
const subPrefix = parts[1] ?? "";
|
|
191
|
-
const subs = [
|
|
192
|
-
{ cmd: "debug", desc: "List or view debug log files" },
|
|
193
|
-
{ cmd: "tail", desc: "Show last N activity log summaries" },
|
|
194
|
-
{ cmd: "clear", desc: "Remove old activity and debug logs" },
|
|
195
|
-
];
|
|
196
|
-
return subs
|
|
197
|
-
.filter((s) => s.cmd.startsWith(subPrefix))
|
|
198
|
-
.map((s) => ({ value: `logs ${s.cmd}`, label: s.cmd, description: s.desc }));
|
|
199
|
-
}
|
|
200
|
-
|
|
201
187
|
if (parts[0] === "keys" && parts.length <= 2) {
|
|
202
188
|
const subPrefix = parts[1] ?? "";
|
|
203
189
|
const subs = [
|
|
@@ -406,11 +392,6 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
|
406
392
|
return;
|
|
407
393
|
}
|
|
408
394
|
|
|
409
|
-
if (trimmed === "logs" || trimmed.startsWith("logs ")) {
|
|
410
|
-
await handleLogs(trimmed.replace(/^logs\s*/, "").trim(), ctx);
|
|
411
|
-
return;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
395
|
if (trimmed === "forensics" || trimmed.startsWith("forensics ")) {
|
|
415
396
|
const { handleForensics } = await import("./forensics.js");
|
|
416
397
|
await handleForensics(trimmed.replace(/^forensics\s*/, "").trim(), ctx, pi);
|
|
@@ -32,19 +32,6 @@ export type DoctorIssueCode =
|
|
|
32
32
|
| "gitignore_missing_patterns"
|
|
33
33
|
| "unresolvable_dependency";
|
|
34
34
|
|
|
35
|
-
/**
|
|
36
|
-
* Issue codes that represent expected completion-transition states.
|
|
37
|
-
* These are detected by the doctor but should NOT be auto-fixed at task level —
|
|
38
|
-
* they are resolved by the complete-slice/complete-milestone dispatch units.
|
|
39
|
-
* Consumers (e.g. auto-post-unit health tracking) should exclude these from
|
|
40
|
-
* error counts when running at task fixLevel to avoid false escalation.
|
|
41
|
-
*/
|
|
42
|
-
export const COMPLETION_TRANSITION_CODES = new Set<DoctorIssueCode>([
|
|
43
|
-
"all_tasks_done_missing_slice_summary",
|
|
44
|
-
"all_tasks_done_missing_slice_uat",
|
|
45
|
-
"all_tasks_done_roadmap_not_checked",
|
|
46
|
-
]);
|
|
47
|
-
|
|
48
35
|
export interface DoctorIssue {
|
|
49
36
|
severity: DoctorSeverity;
|
|
50
37
|
code: DoctorIssueCode;
|
|
@@ -8,7 +8,6 @@ import { invalidateAllCaches } from "./cache.js";
|
|
|
8
8
|
import { loadEffectiveGSDPreferences, type GSDPreferences } from "./preferences.js";
|
|
9
9
|
|
|
10
10
|
import type { DoctorIssue, DoctorIssueCode } from "./doctor-types.js";
|
|
11
|
-
import { COMPLETION_TRANSITION_CODES } from "./doctor-types.js";
|
|
12
11
|
import { checkGitHealth, checkRuntimeHealth } from "./doctor-checks.js";
|
|
13
12
|
|
|
14
13
|
// ── Re-exports ─────────────────────────────────────────────────────────────
|
|
@@ -357,11 +356,16 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
357
356
|
// dispatch lifecycle (complete-slice, complete-milestone units), not to
|
|
358
357
|
// mechanical post-hook bookkeeping. When fixLevel is "task", these are
|
|
359
358
|
// detected and reported but never auto-fixed.
|
|
359
|
+
const completionTransitionCodes = new Set<DoctorIssueCode>([
|
|
360
|
+
"all_tasks_done_missing_slice_summary",
|
|
361
|
+
"all_tasks_done_missing_slice_uat",
|
|
362
|
+
"all_tasks_done_roadmap_not_checked",
|
|
363
|
+
]);
|
|
360
364
|
|
|
361
365
|
/** Whether a given issue code should be auto-fixed at the current fixLevel. */
|
|
362
366
|
const shouldFix = (code: DoctorIssueCode): boolean => {
|
|
363
367
|
if (!fix) return false;
|
|
364
|
-
if (fixLevel === "task" &&
|
|
368
|
+
if (fixLevel === "task" && completionTransitionCodes.has(code)) return false;
|
|
365
369
|
return true;
|
|
366
370
|
};
|
|
367
371
|
|
|
@@ -348,8 +348,6 @@ function migrateSchema(db: DbAdapter): void {
|
|
|
348
348
|
|
|
349
349
|
let currentDb: DbAdapter | null = null;
|
|
350
350
|
let currentPath: string | null = null;
|
|
351
|
-
/** PID that opened the current connection — used for diagnostic logging. */
|
|
352
|
-
let currentPid: number = 0;
|
|
353
351
|
|
|
354
352
|
// ─── Public API ────────────────────────────────────────────────────────────
|
|
355
353
|
|
|
@@ -397,7 +395,6 @@ export function openDatabase(path: string): boolean {
|
|
|
397
395
|
|
|
398
396
|
currentDb = adapter;
|
|
399
397
|
currentPath = path;
|
|
400
|
-
currentPid = process.pid;
|
|
401
398
|
return true;
|
|
402
399
|
}
|
|
403
400
|
|
|
@@ -413,7 +410,6 @@ export function closeDatabase(): void {
|
|
|
413
410
|
}
|
|
414
411
|
currentDb = null;
|
|
415
412
|
currentPath = null;
|
|
416
|
-
currentPid = 0;
|
|
417
413
|
}
|
|
418
414
|
}
|
|
419
415
|
|
|
@@ -728,21 +724,6 @@ export function reconcileWorktreeDb(
|
|
|
728
724
|
}
|
|
729
725
|
}
|
|
730
726
|
|
|
731
|
-
/**
|
|
732
|
-
* Returns the PID of the process that opened the current DB connection.
|
|
733
|
-
* Returns 0 if no connection is open.
|
|
734
|
-
*/
|
|
735
|
-
export function getDbOwnerPid(): number {
|
|
736
|
-
return currentPid;
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
/**
|
|
740
|
-
* Returns the path of the currently open database, or null if none.
|
|
741
|
-
*/
|
|
742
|
-
export function getDbPath(): string | null {
|
|
743
|
-
return currentPath;
|
|
744
|
-
}
|
|
745
|
-
|
|
746
727
|
// ─── Internal Access (for testing) ─────────────────────────────────────────
|
|
747
728
|
|
|
748
729
|
/**
|
package/package.json
CHANGED
|
@@ -35,7 +35,6 @@ import {
|
|
|
35
35
|
import { writeUnitRuntimeRecord, clearUnitRuntimeRecord } from "./unit-runtime.js";
|
|
36
36
|
import { resolveAutoSupervisorConfig, loadEffectiveGSDPreferences } from "./preferences.js";
|
|
37
37
|
import { runGSDDoctor, rebuildState, summarizeDoctorIssues } from "./doctor.js";
|
|
38
|
-
import { COMPLETION_TRANSITION_CODES } from "./doctor-types.js";
|
|
39
38
|
import { recordHealthSnapshot, checkHealEscalation } from "./doctor-proactive.js";
|
|
40
39
|
import { syncStateToProjectRoot } from "./auto-worktree-sync.js";
|
|
41
40
|
import { resetRewriteCircuitBreaker } from "./auto-dispatch.js";
|
|
@@ -155,17 +154,13 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
|
|
|
155
154
|
ctx.ui.notify(`Post-hook: applied ${report.fixesApplied.length} fix(es).`, "info");
|
|
156
155
|
}
|
|
157
156
|
|
|
158
|
-
// Proactive health tracking
|
|
159
|
-
|
|
160
|
-
const issuesForHealth = effectiveFixLevel === "task"
|
|
161
|
-
? report.issues.filter(i => !COMPLETION_TRANSITION_CODES.has(i.code))
|
|
162
|
-
: report.issues;
|
|
163
|
-
const summary = summarizeDoctorIssues(issuesForHealth);
|
|
157
|
+
// Proactive health tracking
|
|
158
|
+
const summary = summarizeDoctorIssues(report.issues);
|
|
164
159
|
recordHealthSnapshot(summary.errors, summary.warnings, report.fixesApplied.length);
|
|
165
160
|
|
|
166
161
|
// Check if we should escalate to LLM-assisted heal
|
|
167
162
|
if (summary.errors > 0) {
|
|
168
|
-
const unresolvedErrors =
|
|
163
|
+
const unresolvedErrors = report.issues
|
|
169
164
|
.filter(i => i.severity === "error" && !i.fixable)
|
|
170
165
|
.map(i => ({ code: i.code, message: i.message, unitId: i.unitId }));
|
|
171
166
|
const escalation = checkHealEscalation(summary.errors, unresolvedErrors);
|
|
@@ -26,13 +26,6 @@ import {
|
|
|
26
26
|
import { invalidateAllCaches } from "./cache.js";
|
|
27
27
|
import { synthesizeCrashRecovery } from "./session-forensics.js";
|
|
28
28
|
import { writeLock, clearLock, readCrashLock, formatCrashInfo, isLockProcessAlive } from "./crash-recovery.js";
|
|
29
|
-
import {
|
|
30
|
-
acquireSessionLock,
|
|
31
|
-
updateSessionLock,
|
|
32
|
-
releaseSessionLock,
|
|
33
|
-
readSessionLockData,
|
|
34
|
-
isSessionLockProcessAlive,
|
|
35
|
-
} from "./session-lock.js";
|
|
36
29
|
import { selfHealRuntimeRecords } from "./auto-recovery.js";
|
|
37
30
|
import { ensureGitignore, untrackRuntimeFiles } from "./gitignore.js";
|
|
38
31
|
import { nativeIsRepo, nativeInit, nativeAddAll, nativeCommit } from "./native-git-bridge.js";
|
|
@@ -88,18 +81,6 @@ export async function bootstrapAutoSession(
|
|
|
88
81
|
): Promise<boolean> {
|
|
89
82
|
const { shouldUseWorktreeIsolation, registerSigtermHandler, lockBase } = deps;
|
|
90
83
|
|
|
91
|
-
// ── Session lock: acquire FIRST, before any state mutation ──────────────
|
|
92
|
-
// This is the primary guard against concurrent sessions on the same project.
|
|
93
|
-
// Uses OS-level file locking (proper-lockfile) to prevent TOCTOU races.
|
|
94
|
-
const lockResult = acquireSessionLock(base);
|
|
95
|
-
if (!lockResult.acquired) {
|
|
96
|
-
ctx.ui.notify(
|
|
97
|
-
`${lockResult.reason}\nStop it with \`kill ${lockResult.existingPid ?? "the other process"}\` before starting a new session.`,
|
|
98
|
-
"error",
|
|
99
|
-
);
|
|
100
|
-
return false;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
84
|
// Ensure git repo exists
|
|
104
85
|
if (!nativeIsRepo(base)) {
|
|
105
86
|
const mainBranch = loadEffectiveGSDPreferences()?.preferences?.git?.main_branch || "main";
|
|
@@ -128,11 +109,16 @@ export async function bootstrapAutoSession(
|
|
|
128
109
|
// Initialize GitServiceImpl
|
|
129
110
|
s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
|
|
130
111
|
|
|
131
|
-
// Check for crash from previous session
|
|
112
|
+
// Check for crash from previous session
|
|
132
113
|
const crashLock = readCrashLock(base);
|
|
133
114
|
if (crashLock) {
|
|
134
|
-
|
|
135
|
-
|
|
115
|
+
if (isLockProcessAlive(crashLock)) {
|
|
116
|
+
ctx.ui.notify(
|
|
117
|
+
`Another auto-mode session (PID ${crashLock.pid}) appears to be running.\nStop it with \`kill ${crashLock.pid}\` before starting a new session.`,
|
|
118
|
+
"error",
|
|
119
|
+
);
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
136
122
|
const recoveredMid = crashLock.unitId.split("/")[0];
|
|
137
123
|
const milestoneAlreadyComplete = recoveredMid
|
|
138
124
|
? !!resolveMilestoneFile(base, recoveredMid, "SUMMARY")
|
|
@@ -421,8 +407,7 @@ export async function bootstrapAutoSession(
|
|
|
421
407
|
: "Will loop until milestone complete.";
|
|
422
408
|
ctx.ui.notify(`${modeLabel} started. ${scopeMsg}`, "info");
|
|
423
409
|
|
|
424
|
-
//
|
|
425
|
-
updateSessionLock(lockBase(), "starting", s.currentMilestoneId ?? "unknown", 0);
|
|
410
|
+
// Write initial lock file
|
|
426
411
|
writeLock(lockBase(), "starting", s.currentMilestoneId ?? "unknown", 0);
|
|
427
412
|
|
|
428
413
|
// Secrets collection gate — pause instead of blocking (#1146)
|
|
@@ -33,12 +33,6 @@ import { invalidateAllCaches } from "./cache.js";
|
|
|
33
33
|
import { saveActivityLog, clearActivityLogState } from "./activity-log.js";
|
|
34
34
|
import { synthesizeCrashRecovery, getDeepDiagnostic } from "./session-forensics.js";
|
|
35
35
|
import { writeLock, clearLock, readCrashLock, formatCrashInfo, isLockProcessAlive } from "./crash-recovery.js";
|
|
36
|
-
import {
|
|
37
|
-
acquireSessionLock,
|
|
38
|
-
validateSessionLock,
|
|
39
|
-
releaseSessionLock,
|
|
40
|
-
updateSessionLock,
|
|
41
|
-
} from "./session-lock.js";
|
|
42
36
|
import {
|
|
43
37
|
clearUnitRuntimeRecord,
|
|
44
38
|
inspectExecuteTaskDurability,
|
|
@@ -457,10 +451,7 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI, reason
|
|
|
457
451
|
if (!s.active && !s.paused) return;
|
|
458
452
|
const reasonSuffix = reason ? ` — ${reason}` : "";
|
|
459
453
|
clearUnitTimeout();
|
|
460
|
-
if (lockBase())
|
|
461
|
-
releaseSessionLock(lockBase());
|
|
462
|
-
clearLock(lockBase());
|
|
463
|
-
}
|
|
454
|
+
if (lockBase()) clearLock(lockBase());
|
|
464
455
|
clearSkillSnapshot();
|
|
465
456
|
resetSkillTelemetry();
|
|
466
457
|
s.dispatching = false;
|
|
@@ -574,10 +565,7 @@ export async function pauseAuto(ctx?: ExtensionContext, _pi?: ExtensionAPI): Pro
|
|
|
574
565
|
|
|
575
566
|
s.pausedSessionFile = ctx?.sessionManager?.getSessionFile() ?? null;
|
|
576
567
|
|
|
577
|
-
if (lockBase())
|
|
578
|
-
releaseSessionLock(lockBase());
|
|
579
|
-
clearLock(lockBase());
|
|
580
|
-
}
|
|
568
|
+
if (lockBase()) clearLock(lockBase());
|
|
581
569
|
|
|
582
570
|
deregisterSigtermHandler();
|
|
583
571
|
|
|
@@ -610,16 +598,6 @@ export async function startAuto(
|
|
|
610
598
|
|
|
611
599
|
// If resuming from paused state, just re-activate and dispatch next unit.
|
|
612
600
|
if (s.paused) {
|
|
613
|
-
// Re-acquire session lock before resuming
|
|
614
|
-
const resumeLock = acquireSessionLock(base);
|
|
615
|
-
if (!resumeLock.acquired) {
|
|
616
|
-
ctx.ui.notify(
|
|
617
|
-
`Cannot resume: ${resumeLock.reason}`,
|
|
618
|
-
"error",
|
|
619
|
-
);
|
|
620
|
-
return;
|
|
621
|
-
}
|
|
622
|
-
|
|
623
601
|
s.paused = false;
|
|
624
602
|
s.active = true;
|
|
625
603
|
s.verbose = verboseMode;
|
|
@@ -721,7 +699,6 @@ export async function startAuto(
|
|
|
721
699
|
s.pausedForSecrets = false;
|
|
722
700
|
}
|
|
723
701
|
|
|
724
|
-
updateSessionLock(lockBase(), "resuming", s.currentMilestoneId ?? "unknown", s.completedUnits.length);
|
|
725
702
|
writeLock(lockBase(), "resuming", s.currentMilestoneId ?? "unknown", s.completedUnits.length);
|
|
726
703
|
|
|
727
704
|
await dispatchNextUnit(ctx, pi);
|
|
@@ -973,24 +950,6 @@ async function dispatchNextUnit(
|
|
|
973
950
|
return;
|
|
974
951
|
}
|
|
975
952
|
|
|
976
|
-
// ── Session lock validation: detect if another process has taken over ──
|
|
977
|
-
if (lockBase() && !validateSessionLock(lockBase())) {
|
|
978
|
-
debugLog("dispatchNextUnit session-lock-lost — another process may have taken over");
|
|
979
|
-
ctx.ui.notify(
|
|
980
|
-
"Session lock lost — another GSD process appears to have taken over. Stopping gracefully.",
|
|
981
|
-
"error",
|
|
982
|
-
);
|
|
983
|
-
// Don't call stopAuto here to avoid releasing the lock we don't own
|
|
984
|
-
s.active = false;
|
|
985
|
-
s.paused = false;
|
|
986
|
-
clearUnitTimeout();
|
|
987
|
-
deregisterSigtermHandler();
|
|
988
|
-
ctx.ui.setStatus("gsd-auto", undefined);
|
|
989
|
-
ctx.ui.setWidget("gsd-progress", undefined);
|
|
990
|
-
ctx.ui.setFooter(undefined);
|
|
991
|
-
return;
|
|
992
|
-
}
|
|
993
|
-
|
|
994
953
|
// Reentrancy guard
|
|
995
954
|
if (s.dispatching && s.skipDepth === 0) {
|
|
996
955
|
debugLog("dispatchNextUnit reentrancy guard — another dispatch in progress, bailing");
|
|
@@ -1624,7 +1583,6 @@ async function dispatchNextUnit(
|
|
|
1624
1583
|
}
|
|
1625
1584
|
|
|
1626
1585
|
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
1627
|
-
updateSessionLock(lockBase(), unitType, unitId, s.completedUnits.length, sessionFile);
|
|
1628
1586
|
writeLock(lockBase(), unitType, unitId, s.completedUnits.length, sessionFile);
|
|
1629
1587
|
|
|
1630
1588
|
// Prompt injection
|
|
@@ -1851,7 +1809,6 @@ export async function dispatchHookUnit(
|
|
|
1851
1809
|
}
|
|
1852
1810
|
|
|
1853
1811
|
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
1854
|
-
updateSessionLock(lockBase(), hookUnitType, triggerUnitId, s.completedUnits.length, sessionFile);
|
|
1855
1812
|
writeLock(lockBase(), hookUnitType, triggerUnitId, s.completedUnits.length, sessionFile);
|
|
1856
1813
|
|
|
1857
1814
|
clearUnitTimeout();
|
|
@@ -43,7 +43,6 @@ import { handleConfig } from "./commands-config.js";
|
|
|
43
43
|
import { handleInspect } from "./commands-inspect.js";
|
|
44
44
|
import { handleCleanupBranches, handleCleanupSnapshots, handleSkip, handleDryRun } from "./commands-maintenance.js";
|
|
45
45
|
import { handleDoctor, handleSteer, handleCapture, handleTriage, handleKnowledge, handleRunHook, handleUpdate, handleSkillHealth } from "./commands-handlers.js";
|
|
46
|
-
import { handleLogs } from "./commands-logs.js";
|
|
47
46
|
|
|
48
47
|
// ─── Re-exports (preserve public API surface) ───────────────────────────────
|
|
49
48
|
export { handlePrefs, handlePrefsMode, handlePrefsWizard, ensurePreferencesFile, handleImportClaude, buildCategorySummaries, serializePreferencesToFrontmatter, yamlSafeString, configureMode } from "./commands-prefs-wizard.js";
|
|
@@ -108,7 +107,6 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
|
108
107
|
{ cmd: "run-hook", desc: "Manually trigger a specific hook" },
|
|
109
108
|
{ cmd: "skill-health", desc: "Skill lifecycle dashboard" },
|
|
110
109
|
{ cmd: "doctor", desc: "Runtime health checks with auto-fix" },
|
|
111
|
-
{ cmd: "logs", desc: "Browse activity logs, debug logs, and metrics" },
|
|
112
110
|
{ cmd: "forensics", desc: "Examine execution logs" },
|
|
113
111
|
{ cmd: "init", desc: "Project init wizard — detect, configure, bootstrap .gsd/" },
|
|
114
112
|
{ cmd: "setup", desc: "Global setup status and configuration" },
|
|
@@ -186,18 +184,6 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
|
186
184
|
.map((s) => ({ value: `setup ${s.cmd}`, label: s.cmd, description: s.desc }));
|
|
187
185
|
}
|
|
188
186
|
|
|
189
|
-
if (parts[0] === "logs" && parts.length <= 2) {
|
|
190
|
-
const subPrefix = parts[1] ?? "";
|
|
191
|
-
const subs = [
|
|
192
|
-
{ cmd: "debug", desc: "List or view debug log files" },
|
|
193
|
-
{ cmd: "tail", desc: "Show last N activity log summaries" },
|
|
194
|
-
{ cmd: "clear", desc: "Remove old activity and debug logs" },
|
|
195
|
-
];
|
|
196
|
-
return subs
|
|
197
|
-
.filter((s) => s.cmd.startsWith(subPrefix))
|
|
198
|
-
.map((s) => ({ value: `logs ${s.cmd}`, label: s.cmd, description: s.desc }));
|
|
199
|
-
}
|
|
200
|
-
|
|
201
187
|
if (parts[0] === "keys" && parts.length <= 2) {
|
|
202
188
|
const subPrefix = parts[1] ?? "";
|
|
203
189
|
const subs = [
|
|
@@ -406,11 +392,6 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
|
406
392
|
return;
|
|
407
393
|
}
|
|
408
394
|
|
|
409
|
-
if (trimmed === "logs" || trimmed.startsWith("logs ")) {
|
|
410
|
-
await handleLogs(trimmed.replace(/^logs\s*/, "").trim(), ctx);
|
|
411
|
-
return;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
395
|
if (trimmed === "forensics" || trimmed.startsWith("forensics ")) {
|
|
415
396
|
const { handleForensics } = await import("./forensics.js");
|
|
416
397
|
await handleForensics(trimmed.replace(/^forensics\s*/, "").trim(), ctx, pi);
|
|
@@ -32,19 +32,6 @@ export type DoctorIssueCode =
|
|
|
32
32
|
| "gitignore_missing_patterns"
|
|
33
33
|
| "unresolvable_dependency";
|
|
34
34
|
|
|
35
|
-
/**
|
|
36
|
-
* Issue codes that represent expected completion-transition states.
|
|
37
|
-
* These are detected by the doctor but should NOT be auto-fixed at task level —
|
|
38
|
-
* they are resolved by the complete-slice/complete-milestone dispatch units.
|
|
39
|
-
* Consumers (e.g. auto-post-unit health tracking) should exclude these from
|
|
40
|
-
* error counts when running at task fixLevel to avoid false escalation.
|
|
41
|
-
*/
|
|
42
|
-
export const COMPLETION_TRANSITION_CODES = new Set<DoctorIssueCode>([
|
|
43
|
-
"all_tasks_done_missing_slice_summary",
|
|
44
|
-
"all_tasks_done_missing_slice_uat",
|
|
45
|
-
"all_tasks_done_roadmap_not_checked",
|
|
46
|
-
]);
|
|
47
|
-
|
|
48
35
|
export interface DoctorIssue {
|
|
49
36
|
severity: DoctorSeverity;
|
|
50
37
|
code: DoctorIssueCode;
|
|
@@ -8,7 +8,6 @@ import { invalidateAllCaches } from "./cache.js";
|
|
|
8
8
|
import { loadEffectiveGSDPreferences, type GSDPreferences } from "./preferences.js";
|
|
9
9
|
|
|
10
10
|
import type { DoctorIssue, DoctorIssueCode } from "./doctor-types.js";
|
|
11
|
-
import { COMPLETION_TRANSITION_CODES } from "./doctor-types.js";
|
|
12
11
|
import { checkGitHealth, checkRuntimeHealth } from "./doctor-checks.js";
|
|
13
12
|
|
|
14
13
|
// ── Re-exports ─────────────────────────────────────────────────────────────
|
|
@@ -357,11 +356,16 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
357
356
|
// dispatch lifecycle (complete-slice, complete-milestone units), not to
|
|
358
357
|
// mechanical post-hook bookkeeping. When fixLevel is "task", these are
|
|
359
358
|
// detected and reported but never auto-fixed.
|
|
359
|
+
const completionTransitionCodes = new Set<DoctorIssueCode>([
|
|
360
|
+
"all_tasks_done_missing_slice_summary",
|
|
361
|
+
"all_tasks_done_missing_slice_uat",
|
|
362
|
+
"all_tasks_done_roadmap_not_checked",
|
|
363
|
+
]);
|
|
360
364
|
|
|
361
365
|
/** Whether a given issue code should be auto-fixed at the current fixLevel. */
|
|
362
366
|
const shouldFix = (code: DoctorIssueCode): boolean => {
|
|
363
367
|
if (!fix) return false;
|
|
364
|
-
if (fixLevel === "task" &&
|
|
368
|
+
if (fixLevel === "task" && completionTransitionCodes.has(code)) return false;
|
|
365
369
|
return true;
|
|
366
370
|
};
|
|
367
371
|
|