gsd-pi 2.35.0-dev.30eec3f → 2.35.0-dev.640d5c7
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/cli.js +2 -7
- package/dist/resource-loader.d.ts +1 -1
- package/dist/resource-loader.js +1 -13
- package/dist/resources/extensions/async-jobs/await-tool.js +2 -0
- package/dist/resources/extensions/async-jobs/job-manager.js +6 -0
- package/dist/resources/extensions/bg-shell/output-formatter.js +19 -1
- package/dist/resources/extensions/bg-shell/process-manager.js +4 -0
- package/dist/resources/extensions/bg-shell/types.js +2 -0
- package/dist/resources/extensions/context7/index.js +0 -5
- package/dist/resources/extensions/get-secrets-from-user.js +30 -2
- package/dist/resources/extensions/google-search/index.js +0 -5
- package/dist/resources/extensions/gsd/auto-dispatch.js +1 -43
- package/dist/resources/extensions/gsd/auto-loop.js +1 -10
- package/dist/resources/extensions/gsd/auto-recovery.js +0 -35
- package/dist/resources/extensions/gsd/auto-start.js +2 -35
- package/dist/resources/extensions/gsd/auto.js +4 -59
- package/dist/resources/extensions/gsd/commands-handlers.js +2 -2
- package/dist/resources/extensions/gsd/doctor-environment.js +17 -26
- package/dist/resources/extensions/gsd/files.js +1 -9
- package/dist/resources/extensions/gsd/gitignore.js +7 -54
- package/dist/resources/extensions/gsd/guided-flow.js +1 -1
- package/dist/resources/extensions/gsd/health-widget.js +46 -97
- package/dist/resources/extensions/gsd/index.js +1 -10
- package/dist/resources/extensions/gsd/migrate-external.js +2 -55
- package/dist/resources/extensions/gsd/paths.js +7 -74
- package/dist/resources/extensions/gsd/post-unit-hooks.js +1 -4
- package/dist/resources/extensions/gsd/preferences-validation.js +1 -16
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +0 -2
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +0 -2
- package/dist/resources/extensions/gsd/session-lock.js +2 -26
- package/dist/resources/extensions/gsd/templates/plan.md +0 -8
- package/dist/resources/extensions/gsd/worktree-resolver.js +0 -12
- package/dist/resources/extensions/remote-questions/remote-command.js +22 -2
- package/dist/resources/extensions/shared/mod.js +1 -1
- package/dist/resources/extensions/shared/sanitize.js +0 -30
- package/dist/resources/extensions/shared/wizard-ui.js +478 -0
- package/dist/resources/extensions/subagent/index.js +14 -6
- package/package.json +1 -2
- package/packages/pi-coding-agent/dist/core/resource-loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/resource-loader.js +2 -13
- package/packages/pi-coding-agent/dist/core/resource-loader.js.map +1 -1
- package/packages/pi-coding-agent/src/core/resource-loader.ts +2 -13
- package/src/resources/extensions/async-jobs/await-tool.ts +2 -0
- package/src/resources/extensions/async-jobs/job-manager.ts +7 -0
- package/src/resources/extensions/bg-shell/output-formatter.ts +17 -0
- package/src/resources/extensions/bg-shell/process-manager.ts +4 -0
- package/src/resources/extensions/bg-shell/types.ts +12 -0
- package/src/resources/extensions/context7/index.ts +0 -7
- package/src/resources/extensions/get-secrets-from-user.ts +35 -2
- package/src/resources/extensions/google-search/index.ts +0 -7
- package/src/resources/extensions/gsd/auto-dispatch.ts +1 -49
- package/src/resources/extensions/gsd/auto-loop.ts +1 -11
- package/src/resources/extensions/gsd/auto-recovery.ts +0 -39
- package/src/resources/extensions/gsd/auto-start.ts +2 -42
- package/src/resources/extensions/gsd/auto.ts +3 -61
- package/src/resources/extensions/gsd/commands-handlers.ts +2 -2
- package/src/resources/extensions/gsd/doctor-environment.ts +16 -26
- package/src/resources/extensions/gsd/files.ts +1 -10
- package/src/resources/extensions/gsd/gitignore.ts +7 -54
- package/src/resources/extensions/gsd/guided-flow.ts +1 -1
- package/src/resources/extensions/gsd/health-widget.ts +59 -103
- package/src/resources/extensions/gsd/index.ts +1 -10
- package/src/resources/extensions/gsd/migrate-external.ts +2 -47
- package/src/resources/extensions/gsd/paths.ts +7 -73
- package/src/resources/extensions/gsd/post-unit-hooks.ts +1 -5
- package/src/resources/extensions/gsd/preferences-validation.ts +1 -16
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +0 -2
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +0 -2
- package/src/resources/extensions/gsd/session-lock.ts +2 -29
- package/src/resources/extensions/gsd/templates/plan.md +0 -8
- package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -12
- package/src/resources/extensions/gsd/tests/validate-directory.test.ts +0 -15
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +0 -2
- package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +0 -32
- package/src/resources/extensions/gsd/worktree-resolver.ts +0 -11
- package/src/resources/extensions/remote-questions/remote-command.ts +23 -2
- package/src/resources/extensions/shared/mod.ts +1 -1
- package/src/resources/extensions/shared/sanitize.ts +0 -36
- package/src/resources/extensions/shared/wizard-ui.ts +551 -0
- package/src/resources/extensions/subagent/index.ts +12 -6
- package/dist/resources/extensions/gsd/health-widget-core.js +0 -96
- package/dist/resources/extensions/gsd/roadmap-mutations.js +0 -55
- package/src/resources/extensions/gsd/health-widget-core.ts +0 -129
- package/src/resources/extensions/gsd/roadmap-mutations.ts +0 -66
- package/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts +0 -214
- package/src/resources/extensions/gsd/tests/health-widget.test.ts +0 -158
- package/src/resources/extensions/gsd/tests/paths.test.ts +0 -113
- package/src/resources/extensions/gsd/tests/test-utils.ts +0 -165
|
@@ -12,16 +12,14 @@
|
|
|
12
12
|
import type { GSDState } from "./types.js";
|
|
13
13
|
import type { GSDPreferences } from "./preferences.js";
|
|
14
14
|
import type { UatType } from "./files.js";
|
|
15
|
-
import { loadFile, extractUatType, loadActiveOverrides
|
|
15
|
+
import { loadFile, extractUatType, loadActiveOverrides } from "./files.js";
|
|
16
16
|
import {
|
|
17
17
|
resolveMilestoneFile,
|
|
18
18
|
resolveMilestonePath,
|
|
19
19
|
resolveSliceFile,
|
|
20
|
-
resolveSlicePath,
|
|
21
20
|
resolveTaskFile,
|
|
22
21
|
relSliceFile,
|
|
23
22
|
buildMilestoneFileName,
|
|
24
|
-
buildSliceFileName,
|
|
25
23
|
} from "./paths.js";
|
|
26
24
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
27
25
|
import { join } from "node:path";
|
|
@@ -371,30 +369,6 @@ const DISPATCH_RULES: DispatchRule[] = [
|
|
|
371
369
|
name: "validating-milestone → validate-milestone",
|
|
372
370
|
match: async ({ state, mid, midTitle, basePath, prefs }) => {
|
|
373
371
|
if (state.phase !== "validating-milestone") return null;
|
|
374
|
-
|
|
375
|
-
// Safety guard (#1368): verify all roadmap slices have SUMMARY files before
|
|
376
|
-
// allowing milestone validation. If any slice lacks a summary, the milestone
|
|
377
|
-
// is not genuinely complete — something skipped earlier slices.
|
|
378
|
-
const roadmapFile = resolveMilestoneFile(basePath, mid, "ROADMAP");
|
|
379
|
-
const roadmapContent = roadmapFile ? await loadFile(roadmapFile) : null;
|
|
380
|
-
if (roadmapContent) {
|
|
381
|
-
const roadmap = parseRoadmap(roadmapContent);
|
|
382
|
-
const missingSlices: string[] = [];
|
|
383
|
-
for (const slice of roadmap.slices) {
|
|
384
|
-
const summaryPath = resolveSliceFile(basePath, mid, slice.id, "SUMMARY");
|
|
385
|
-
if (!summaryPath || !existsSync(summaryPath)) {
|
|
386
|
-
missingSlices.push(slice.id);
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
if (missingSlices.length > 0) {
|
|
390
|
-
return {
|
|
391
|
-
action: "stop",
|
|
392
|
-
reason: `Cannot validate milestone ${mid}: slices ${missingSlices.join(", ")} are missing SUMMARY files. These slices may have been skipped.`,
|
|
393
|
-
level: "error",
|
|
394
|
-
};
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
372
|
// Skip preference: write a minimal pass-through VALIDATION file
|
|
399
373
|
if (prefs?.phases?.skip_milestone_validation) {
|
|
400
374
|
const mDir = resolveMilestonePath(basePath, mid);
|
|
@@ -430,28 +404,6 @@ const DISPATCH_RULES: DispatchRule[] = [
|
|
|
430
404
|
name: "completing-milestone → complete-milestone",
|
|
431
405
|
match: async ({ state, mid, midTitle, basePath }) => {
|
|
432
406
|
if (state.phase !== "completing-milestone") return null;
|
|
433
|
-
|
|
434
|
-
// Safety guard (#1368): verify all roadmap slices have SUMMARY files.
|
|
435
|
-
const roadmapFile = resolveMilestoneFile(basePath, mid, "ROADMAP");
|
|
436
|
-
const roadmapContent = roadmapFile ? await loadFile(roadmapFile) : null;
|
|
437
|
-
if (roadmapContent) {
|
|
438
|
-
const roadmap = parseRoadmap(roadmapContent);
|
|
439
|
-
const missingSlices: string[] = [];
|
|
440
|
-
for (const slice of roadmap.slices) {
|
|
441
|
-
const summaryPath = resolveSliceFile(basePath, mid, slice.id, "SUMMARY");
|
|
442
|
-
if (!summaryPath || !existsSync(summaryPath)) {
|
|
443
|
-
missingSlices.push(slice.id);
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
if (missingSlices.length > 0) {
|
|
447
|
-
return {
|
|
448
|
-
action: "stop",
|
|
449
|
-
reason: `Cannot complete milestone ${mid}: slices ${missingSlices.join(", ")} are missing SUMMARY files. Run /gsd doctor to diagnose.`,
|
|
450
|
-
level: "error",
|
|
451
|
-
};
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
407
|
return {
|
|
456
408
|
action: "dispatch",
|
|
457
409
|
unitType: "complete-milestone",
|
|
@@ -221,15 +221,6 @@ export async function runUnit(
|
|
|
221
221
|
s.pendingResolve = resolve;
|
|
222
222
|
});
|
|
223
223
|
|
|
224
|
-
// Ensure cwd matches basePath before dispatch (#1389).
|
|
225
|
-
// async_bash and background jobs can drift cwd away from the worktree.
|
|
226
|
-
// Realigning here prevents commits from landing on the wrong branch.
|
|
227
|
-
try {
|
|
228
|
-
if (process.cwd() !== s.basePath) {
|
|
229
|
-
process.chdir(s.basePath);
|
|
230
|
-
}
|
|
231
|
-
} catch { /* non-fatal — chdir may fail if dir was removed */ }
|
|
232
|
-
|
|
233
224
|
// ── Send the prompt ──
|
|
234
225
|
debugLog("runUnit", { phase: "send-message", unitType, unitId });
|
|
235
226
|
|
|
@@ -353,7 +344,6 @@ export interface LoopDeps {
|
|
|
353
344
|
getManifestStatus: (
|
|
354
345
|
basePath: string,
|
|
355
346
|
mid: string | undefined,
|
|
356
|
-
projectRoot?: string,
|
|
357
347
|
) => Promise<{ pending: unknown[] } | null>;
|
|
358
348
|
collectSecretsFromManifest: (
|
|
359
349
|
basePath: string,
|
|
@@ -993,7 +983,7 @@ export async function autoLoop(
|
|
|
993
983
|
|
|
994
984
|
// Secrets re-check gate
|
|
995
985
|
try {
|
|
996
|
-
const manifestStatus = await deps.getManifestStatus(s.basePath, mid
|
|
986
|
+
const manifestStatus = await deps.getManifestStatus(s.basePath, mid);
|
|
997
987
|
if (manifestStatus && manifestStatus.pending.length > 0) {
|
|
998
988
|
const result = await deps.collectSecretsFromManifest(
|
|
999
989
|
s.basePath,
|
|
@@ -8,8 +8,6 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { ExtensionContext } from "@gsd/pi-coding-agent";
|
|
11
|
-
import { parseUnitId } from "./unit-id.js";
|
|
12
|
-
import { atomicWriteSync } from "./atomic-write.js";
|
|
13
11
|
import { clearUnitRuntimeRecord } from "./unit-runtime.js";
|
|
14
12
|
import { clearParseCache, parseRoadmap, parsePlan } from "./files.js";
|
|
15
13
|
import { isValidationTerminal } from "./state.js";
|
|
@@ -37,7 +35,6 @@ import {
|
|
|
37
35
|
clearPathCache,
|
|
38
36
|
resolveGsdRootFile,
|
|
39
37
|
} from "./paths.js";
|
|
40
|
-
import { markSliceDoneInRoadmap } from "./roadmap-mutations.js";
|
|
41
38
|
import {
|
|
42
39
|
existsSync,
|
|
43
40
|
mkdirSync,
|
|
@@ -502,42 +499,6 @@ export async function selfHealRuntimeRecords(
|
|
|
502
499
|
for (const record of records) {
|
|
503
500
|
const { unitType, unitId } = record;
|
|
504
501
|
|
|
505
|
-
// Case 0: complete-slice with SUMMARY + UAT but unchecked roadmap (#1350).
|
|
506
|
-
// If a complete-slice was interrupted after writing artifacts but before
|
|
507
|
-
// flipping the roadmap checkbox, the verification fails and the dispatch
|
|
508
|
-
// loop relaunches the same unit forever. Auto-fix the checkbox.
|
|
509
|
-
if (unitType === "complete-slice") {
|
|
510
|
-
const { milestone: mid, slice: sid } = parseUnitId(unitId);
|
|
511
|
-
if (mid && sid) {
|
|
512
|
-
const dir = resolveSlicePath(base, mid, sid);
|
|
513
|
-
if (dir) {
|
|
514
|
-
const summaryPath = join(dir, buildSliceFileName(sid, "SUMMARY"));
|
|
515
|
-
const uatPath = join(dir, buildSliceFileName(sid, "UAT"));
|
|
516
|
-
if (existsSync(summaryPath) && existsSync(uatPath)) {
|
|
517
|
-
const roadmapFile = resolveMilestoneFile(base, mid, "ROADMAP");
|
|
518
|
-
if (roadmapFile && existsSync(roadmapFile)) {
|
|
519
|
-
try {
|
|
520
|
-
const roadmapContent = readFileSync(roadmapFile, "utf-8");
|
|
521
|
-
const roadmap = parseRoadmap(roadmapContent);
|
|
522
|
-
const slice = (roadmap.slices ?? []).find(s => s.id === sid);
|
|
523
|
-
if (slice && !slice.done) {
|
|
524
|
-
// Auto-fix: flip the checkbox using shared utility
|
|
525
|
-
if (markSliceDoneInRoadmap(base, mid, sid)) {
|
|
526
|
-
ctx.ui.notify(
|
|
527
|
-
`Self-heal: marked ${sid} done in roadmap (SUMMARY + UAT exist but checkbox was stale).`,
|
|
528
|
-
"info",
|
|
529
|
-
);
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
} catch {
|
|
533
|
-
// Roadmap parse failure — don't block self-heal
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
|
|
541
502
|
// Clear stale dispatched records (dispatched > 1h ago, process crashed)
|
|
542
503
|
const age = now - (record.startedAt ?? 0);
|
|
543
504
|
if (record.phase === "dispatched" && age > STALE_THRESHOLD_MS) {
|
|
@@ -20,8 +20,6 @@ import {
|
|
|
20
20
|
resolveSkillDiscoveryMode,
|
|
21
21
|
getIsolationMode,
|
|
22
22
|
} from "./preferences.js";
|
|
23
|
-
import { ensureGsdSymlink } from "./repo-identity.js";
|
|
24
|
-
import { migrateToExternalState, recoverFailedMigration } from "./migrate-external.js";
|
|
25
23
|
import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
|
|
26
24
|
import { gsdRoot, resolveMilestoneFile, milestonesDir } from "./paths.js";
|
|
27
25
|
import { invalidateAllCaches } from "./cache.js";
|
|
@@ -94,13 +92,6 @@ export interface BootstrapDeps {
|
|
|
94
92
|
* Returns false if the bootstrap aborted (e.g., guided flow returned,
|
|
95
93
|
* concurrent session detected). Returns true when ready to dispatch.
|
|
96
94
|
*/
|
|
97
|
-
|
|
98
|
-
/** Guard: tracks consecutive bootstrap attempts that found phase === "complete".
|
|
99
|
-
* Prevents the recursive dialog loop described in #1348 where
|
|
100
|
-
* bootstrapAutoSession → showSmartEntry → checkAutoStartAfterDiscuss → startAuto
|
|
101
|
-
* cycles indefinitely when the discuss workflow doesn't produce a milestone. */
|
|
102
|
-
let _consecutiveCompleteBootstraps = 0;
|
|
103
|
-
const MAX_CONSECUTIVE_COMPLETE_BOOTSTRAPS = 2;
|
|
104
95
|
export async function bootstrapAutoSession(
|
|
105
96
|
s: AutoSession,
|
|
106
97
|
ctx: ExtensionCommandContext,
|
|
@@ -137,20 +128,7 @@ export async function bootstrapAutoSession(
|
|
|
137
128
|
nativeInit(base, mainBranch);
|
|
138
129
|
}
|
|
139
130
|
|
|
140
|
-
//
|
|
141
|
-
// Migration MUST run before ensureGitignore to avoid adding ".gsd" to
|
|
142
|
-
// .gitignore when .gsd/ is git-tracked (data-loss bug #1364).
|
|
143
|
-
recoverFailedMigration(base);
|
|
144
|
-
const migration = migrateToExternalState(base);
|
|
145
|
-
if (migration.error) {
|
|
146
|
-
ctx.ui.notify(`External state migration warning: ${migration.error}`, "warning");
|
|
147
|
-
}
|
|
148
|
-
// Ensure symlink exists (handles fresh projects and post-migration)
|
|
149
|
-
ensureGsdSymlink(base);
|
|
150
|
-
|
|
151
|
-
// Ensure .gitignore has baseline patterns.
|
|
152
|
-
// ensureGitignore checks for git-tracked .gsd/ files and skips the
|
|
153
|
-
// ".gsd" pattern if the project intentionally tracks .gsd/ in git.
|
|
131
|
+
// Ensure .gitignore has baseline patterns
|
|
154
132
|
const gitPrefs = loadEffectiveGSDPreferences()?.preferences?.git;
|
|
155
133
|
const commitDocs = gitPrefs?.commit_docs;
|
|
156
134
|
const manageGitignore = gitPrefs?.manage_gitignore;
|
|
@@ -308,20 +286,6 @@ export async function bootstrapAutoSession(
|
|
|
308
286
|
if (!hasSurvivorBranch) {
|
|
309
287
|
// No active work — start a new milestone via discuss flow
|
|
310
288
|
if (!state.activeMilestone || state.phase === "complete") {
|
|
311
|
-
// Guard against recursive dialog loop (#1348):
|
|
312
|
-
// If we've entered this branch multiple times in quick succession,
|
|
313
|
-
// the discuss workflow isn't producing a milestone. Break the cycle.
|
|
314
|
-
_consecutiveCompleteBootstraps++;
|
|
315
|
-
if (_consecutiveCompleteBootstraps > MAX_CONSECUTIVE_COMPLETE_BOOTSTRAPS) {
|
|
316
|
-
_consecutiveCompleteBootstraps = 0;
|
|
317
|
-
ctx.ui.notify(
|
|
318
|
-
"All milestones are complete and the discussion didn't produce a new one. " +
|
|
319
|
-
"Run /gsd to start a new milestone manually.",
|
|
320
|
-
"warning",
|
|
321
|
-
);
|
|
322
|
-
return releaseLockAndReturn();
|
|
323
|
-
}
|
|
324
|
-
|
|
325
289
|
const { showSmartEntry } = await import("./guided-flow.js");
|
|
326
290
|
await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
|
|
327
291
|
|
|
@@ -332,7 +296,6 @@ export async function bootstrapAutoSession(
|
|
|
332
296
|
postState.phase !== "complete" &&
|
|
333
297
|
postState.phase !== "pre-planning"
|
|
334
298
|
) {
|
|
335
|
-
_consecutiveCompleteBootstraps = 0; // Successfully advanced past "complete"
|
|
336
299
|
state = postState;
|
|
337
300
|
} else if (
|
|
338
301
|
postState.activeMilestone &&
|
|
@@ -389,9 +352,6 @@ export async function bootstrapAutoSession(
|
|
|
389
352
|
return releaseLockAndReturn();
|
|
390
353
|
}
|
|
391
354
|
|
|
392
|
-
// Successfully resolved an active milestone — reset the re-entry guard
|
|
393
|
-
_consecutiveCompleteBootstraps = 0;
|
|
394
|
-
|
|
395
355
|
// ── Initialize session state ──
|
|
396
356
|
s.active = true;
|
|
397
357
|
s.stepMode = requestedStepMode;
|
|
@@ -524,7 +484,7 @@ export async function bootstrapAutoSession(
|
|
|
524
484
|
// Secrets collection gate
|
|
525
485
|
const mid = state.activeMilestone!.id;
|
|
526
486
|
try {
|
|
527
|
-
const manifestStatus = await getManifestStatus(base, mid
|
|
487
|
+
const manifestStatus = await getManifestStatus(base, mid);
|
|
528
488
|
if (manifestStatus && manifestStatus.pending.length > 0) {
|
|
529
489
|
const result = await collectSecretsFromManifest(base, mid, ctx);
|
|
530
490
|
if (
|
|
@@ -127,7 +127,7 @@ import {
|
|
|
127
127
|
formatTokenCount,
|
|
128
128
|
} from "./metrics.js";
|
|
129
129
|
import { join } from "node:path";
|
|
130
|
-
import { readFileSync, existsSync, mkdirSync
|
|
130
|
+
import { readFileSync, existsSync, mkdirSync } from "node:fs";
|
|
131
131
|
import { atomicWriteSync } from "./atomic-write.js";
|
|
132
132
|
import {
|
|
133
133
|
autoCommitCurrentBranch,
|
|
@@ -554,13 +554,6 @@ export async function stopAuto(
|
|
|
554
554
|
resetRoutingHistory();
|
|
555
555
|
resetHookState();
|
|
556
556
|
if (s.basePath) clearPersistedHookState(s.basePath);
|
|
557
|
-
|
|
558
|
-
// Remove paused-session metadata if present (#1383)
|
|
559
|
-
try {
|
|
560
|
-
const pausedPath = join(gsdRoot(s.originalBasePath || s.basePath), "runtime", "paused-session.json");
|
|
561
|
-
if (existsSync(pausedPath)) unlinkSync(pausedPath);
|
|
562
|
-
} catch { /* non-fatal */ }
|
|
563
|
-
|
|
564
557
|
s.active = false;
|
|
565
558
|
s.paused = false;
|
|
566
559
|
s.stepMode = false;
|
|
@@ -614,32 +607,8 @@ export async function pauseAuto(
|
|
|
614
607
|
|
|
615
608
|
s.pausedSessionFile = ctx?.sessionManager?.getSessionFile() ?? null;
|
|
616
609
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
try {
|
|
620
|
-
const pausedMeta = {
|
|
621
|
-
milestoneId: s.currentMilestoneId,
|
|
622
|
-
worktreePath: isInAutoWorktree(s.basePath) ? s.basePath : null,
|
|
623
|
-
originalBasePath: s.originalBasePath,
|
|
624
|
-
stepMode: s.stepMode,
|
|
625
|
-
pausedAt: new Date().toISOString(),
|
|
626
|
-
sessionFile: s.pausedSessionFile,
|
|
627
|
-
};
|
|
628
|
-
const runtimeDir = join(gsdRoot(s.originalBasePath || s.basePath), "runtime");
|
|
629
|
-
mkdirSync(runtimeDir, { recursive: true });
|
|
630
|
-
writeFileSync(
|
|
631
|
-
join(runtimeDir, "paused-session.json"),
|
|
632
|
-
JSON.stringify(pausedMeta, null, 2),
|
|
633
|
-
"utf-8",
|
|
634
|
-
);
|
|
635
|
-
} catch {
|
|
636
|
-
// Non-fatal — resume will still work via full bootstrap, just without worktree context
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
if (lockBase()) {
|
|
640
|
-
releaseSessionLock(lockBase());
|
|
641
|
-
clearLock(lockBase());
|
|
642
|
-
}
|
|
610
|
+
if (lockBase()) clearLock(lockBase());
|
|
611
|
+
if (lockBase()) releaseSessionLock(lockBase());
|
|
643
612
|
|
|
644
613
|
deregisterSigtermHandler();
|
|
645
614
|
|
|
@@ -823,30 +792,6 @@ export async function startAuto(
|
|
|
823
792
|
base = escapeStaleWorktree(base);
|
|
824
793
|
|
|
825
794
|
// If resuming from paused state, just re-activate and dispatch next unit.
|
|
826
|
-
// Check persisted paused-session first (#1383) — survives /exit.
|
|
827
|
-
if (!s.paused) {
|
|
828
|
-
try {
|
|
829
|
-
const pausedPath = join(gsdRoot(base), "runtime", "paused-session.json");
|
|
830
|
-
if (existsSync(pausedPath)) {
|
|
831
|
-
const meta = JSON.parse(readFileSync(pausedPath, "utf-8"));
|
|
832
|
-
if (meta.milestoneId) {
|
|
833
|
-
s.currentMilestoneId = meta.milestoneId;
|
|
834
|
-
s.originalBasePath = meta.originalBasePath || base;
|
|
835
|
-
s.stepMode = meta.stepMode ?? requestedStepMode;
|
|
836
|
-
s.paused = true;
|
|
837
|
-
// Clean up the persisted file — we're consuming it
|
|
838
|
-
try { unlinkSync(pausedPath); } catch { /* non-fatal */ }
|
|
839
|
-
ctx.ui.notify(
|
|
840
|
-
`Resuming paused session for ${meta.milestoneId}${meta.worktreePath ? ` (worktree)` : ""}.`,
|
|
841
|
-
"info",
|
|
842
|
-
);
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
} catch {
|
|
846
|
-
// Malformed or missing — proceed with fresh bootstrap
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
|
|
850
795
|
if (s.paused) {
|
|
851
796
|
const resumeLock = acquireSessionLock(base);
|
|
852
797
|
if (!resumeLock.acquired) {
|
|
@@ -1200,9 +1145,6 @@ export async function dispatchHookUnit(
|
|
|
1200
1145
|
ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
|
|
1201
1146
|
ctx.ui.notify(`Running post-unit hook: ${hookName}`, "info");
|
|
1202
1147
|
|
|
1203
|
-
// Ensure cwd matches basePath before hook dispatch (#1389)
|
|
1204
|
-
try { if (process.cwd() !== s.basePath) process.chdir(s.basePath); } catch {}
|
|
1205
|
-
|
|
1206
1148
|
debugLog("dispatchHookUnit", {
|
|
1207
1149
|
phase: "send-message",
|
|
1208
1150
|
promptLength: hookPrompt.length,
|
|
@@ -24,7 +24,7 @@ import { projectRoot } from "./commands.js";
|
|
|
24
24
|
import { loadPrompt } from "./prompt-loader.js";
|
|
25
25
|
|
|
26
26
|
export function dispatchDoctorHeal(pi: ExtensionAPI, scope: string | undefined, reportText: string, structuredIssues: string): void {
|
|
27
|
-
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".
|
|
27
|
+
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".pi", "GSD-WORKFLOW.md");
|
|
28
28
|
const workflow = readFileSync(workflowPath, "utf-8");
|
|
29
29
|
const prompt = loadPrompt("doctor-heal", {
|
|
30
30
|
doctorSummary: reportText,
|
|
@@ -187,7 +187,7 @@ export async function handleTriage(ctx: ExtensionCommandContext, pi: ExtensionAP
|
|
|
187
187
|
roadmapContext: roadmapContext || "(no active roadmap)",
|
|
188
188
|
});
|
|
189
189
|
|
|
190
|
-
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".
|
|
190
|
+
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".pi", "GSD-WORKFLOW.md");
|
|
191
191
|
const workflow = readFileSync(workflowPath, "utf-8");
|
|
192
192
|
|
|
193
193
|
pi.sendMessage(
|
|
@@ -180,36 +180,26 @@ function checkPortConflicts(basePath: string): EnvironmentCheckResult[] {
|
|
|
180
180
|
const portsToCheck = new Set<number>();
|
|
181
181
|
const pkgPath = join(basePath, "package.json");
|
|
182
182
|
|
|
183
|
-
if (
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
for (const m of portMatches) {
|
|
198
|
-
const port = parseInt(m[1], 10);
|
|
199
|
-
if (port >= 1024 && port <= 65535) portsToCheck.add(port);
|
|
183
|
+
if (existsSync(pkgPath)) {
|
|
184
|
+
try {
|
|
185
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
186
|
+
const scripts = pkg.scripts ?? {};
|
|
187
|
+
const scriptText = Object.values(scripts).join(" ");
|
|
188
|
+
|
|
189
|
+
// Look for --port NNNN, -p NNNN, PORT=NNNN, :NNNN patterns
|
|
190
|
+
const portMatches = scriptText.matchAll(/(?:--port\s+|(?:^|[^a-z])PORT[=:]\s*|-p\s+|:)(\d{4,5})\b/gi);
|
|
191
|
+
for (const m of portMatches) {
|
|
192
|
+
const port = parseInt(m[1], 10);
|
|
193
|
+
if (port >= 1024 && port <= 65535) portsToCheck.add(port);
|
|
194
|
+
}
|
|
195
|
+
} catch {
|
|
196
|
+
// parse failed — use defaults
|
|
200
197
|
}
|
|
201
|
-
} catch {
|
|
202
|
-
// parse failed — skip port checks rather than using defaults
|
|
203
|
-
return [];
|
|
204
198
|
}
|
|
205
199
|
|
|
206
|
-
// If no ports found in scripts, check common defaults
|
|
207
|
-
// Filter out port 5000 on macOS — AirPlay Receiver uses it by default (#1381).
|
|
200
|
+
// If no ports found in scripts, check common defaults
|
|
208
201
|
if (portsToCheck.size === 0) {
|
|
209
|
-
for (const p of DEFAULT_DEV_PORTS)
|
|
210
|
-
if (p === 5000 && process.platform === "darwin") continue;
|
|
211
|
-
portsToCheck.add(p);
|
|
212
|
-
}
|
|
202
|
+
for (const p of DEFAULT_DEV_PORTS) portsToCheck.add(p);
|
|
213
203
|
}
|
|
214
204
|
|
|
215
205
|
for (const port of portsToCheck) {
|
|
@@ -805,7 +805,7 @@ export async function inlinePriorMilestoneSummary(mid: string, base: string): Pr
|
|
|
805
805
|
* file not on disk) - callers can distinguish "no manifest" from "empty manifest".
|
|
806
806
|
*/
|
|
807
807
|
export async function getManifestStatus(
|
|
808
|
-
base: string, milestoneId: string,
|
|
808
|
+
base: string, milestoneId: string,
|
|
809
809
|
): Promise<ManifestStatus | null> {
|
|
810
810
|
const resolvedPath = resolveMilestoneFile(base, milestoneId, 'SECRETS');
|
|
811
811
|
if (!resolvedPath) return null;
|
|
@@ -815,18 +815,9 @@ export async function getManifestStatus(
|
|
|
815
815
|
|
|
816
816
|
const manifest = parseSecretsManifest(content);
|
|
817
817
|
const keys = manifest.entries.map(e => e.key);
|
|
818
|
-
|
|
819
|
-
// Check both the base path .env AND the project root .env (#1387).
|
|
820
|
-
// In worktree mode, base is the worktree path which may not have .env.
|
|
821
|
-
// The project root's .env is where the user actually defined their keys.
|
|
822
818
|
const existingKeys = await checkExistingEnvKeys(keys, resolve(base, '.env'));
|
|
823
819
|
const existingSet = new Set(existingKeys);
|
|
824
820
|
|
|
825
|
-
if (projectRoot && projectRoot !== base) {
|
|
826
|
-
const rootKeys = await checkExistingEnvKeys(keys, resolve(projectRoot, '.env'));
|
|
827
|
-
for (const k of rootKeys) existingSet.add(k);
|
|
828
|
-
}
|
|
829
|
-
|
|
830
821
|
const result: ManifestStatus = {
|
|
831
822
|
pending: [],
|
|
832
823
|
collected: [],
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { join } from "node:path";
|
|
10
|
-
import { existsSync,
|
|
11
|
-
import { nativeRmCached
|
|
10
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { nativeRmCached } from "./native-git-bridge.js";
|
|
12
12
|
import { gsdRoot } from "./paths.js";
|
|
13
13
|
|
|
14
14
|
/**
|
|
@@ -79,47 +79,12 @@ const BASELINE_PATTERNS = [
|
|
|
79
79
|
];
|
|
80
80
|
|
|
81
81
|
/**
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* and we must NOT add `.gsd` to `.gitignore` or attempt migration.
|
|
85
|
-
*
|
|
86
|
-
* Returns true if git tracks at least one file under `.gsd/`.
|
|
87
|
-
* Returns false (safe to ignore) if:
|
|
88
|
-
* - Not a git repo
|
|
89
|
-
* - `.gsd/` is a symlink (external state, should be ignored)
|
|
90
|
-
* - `.gsd/` doesn't exist
|
|
91
|
-
* - No tracked files found under `.gsd/`
|
|
92
|
-
*/
|
|
93
|
-
export function hasGitTrackedGsdFiles(basePath: string): boolean {
|
|
94
|
-
const localGsd = join(basePath, ".gsd");
|
|
95
|
-
|
|
96
|
-
// If .gsd doesn't exist or is already a symlink, no tracked files concern
|
|
97
|
-
if (!existsSync(localGsd)) return false;
|
|
98
|
-
try {
|
|
99
|
-
if (lstatSync(localGsd).isSymbolicLink()) return false;
|
|
100
|
-
} catch {
|
|
101
|
-
return false;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Check if git tracks any files under .gsd/
|
|
105
|
-
try {
|
|
106
|
-
const tracked = nativeLsFiles(basePath, ".gsd");
|
|
107
|
-
return tracked.length > 0;
|
|
108
|
-
} catch {
|
|
109
|
-
// Not a git repo or git not available — safe to proceed
|
|
110
|
-
return false;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Ensure basePath/.gitignore contains baseline ignore patterns.
|
|
116
|
-
* Creates the file if missing; appends missing patterns.
|
|
82
|
+
* Ensure basePath/.gitignore contains a blanket `.gsd/` ignore.
|
|
83
|
+
* Creates the file if missing; appends `.gsd/` if not present.
|
|
117
84
|
* Returns true if the file was created or modified, false if already complete.
|
|
118
85
|
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* is excluded to prevent data loss. Only the `.gsd` pattern is affected —
|
|
122
|
-
* all other baseline patterns are still applied normally.
|
|
86
|
+
* `.gsd/` state is managed externally (symlinked to `~/.gsd/projects/<hash>/`),
|
|
87
|
+
* so the entire directory is always gitignored.
|
|
123
88
|
*/
|
|
124
89
|
export function ensureGitignore(
|
|
125
90
|
basePath: string,
|
|
@@ -143,15 +108,8 @@ export function ensureGitignore(
|
|
|
143
108
|
.filter((l) => l && !l.startsWith("#")),
|
|
144
109
|
);
|
|
145
110
|
|
|
146
|
-
// Determine which patterns to apply. If .gsd/ has tracked files,
|
|
147
|
-
// exclude the ".gsd" pattern to prevent deleting tracked state.
|
|
148
|
-
const gsdIsTracked = hasGitTrackedGsdFiles(basePath);
|
|
149
|
-
const patternsToApply = gsdIsTracked
|
|
150
|
-
? BASELINE_PATTERNS.filter((p) => p !== ".gsd")
|
|
151
|
-
: BASELINE_PATTERNS;
|
|
152
|
-
|
|
153
111
|
// Find patterns not yet present
|
|
154
|
-
const missing =
|
|
112
|
+
const missing = BASELINE_PATTERNS.filter((p) => !existingLines.has(p));
|
|
155
113
|
|
|
156
114
|
if (missing.length === 0) return false;
|
|
157
115
|
|
|
@@ -177,11 +135,6 @@ export function ensureGitignore(
|
|
|
177
135
|
* already in the index even after .gitignore is updated.
|
|
178
136
|
*
|
|
179
137
|
* Only removes from the index (`--cached`), never from disk. Idempotent.
|
|
180
|
-
*
|
|
181
|
-
* Note: These are strictly runtime/ephemeral paths (activity logs, lock files,
|
|
182
|
-
* metrics, STATE.md). They are always safe to untrack, even when the project
|
|
183
|
-
* intentionally keeps other `.gsd/` files (like PROJECT.md, milestones/) in
|
|
184
|
-
* version control.
|
|
185
138
|
*/
|
|
186
139
|
export function untrackRuntimeFiles(basePath: string): void {
|
|
187
140
|
const runtimePaths = GSD_RUNTIME_PATTERNS;
|
|
@@ -191,7 +191,7 @@ type UIContext = ExtensionContext;
|
|
|
191
191
|
* This is the only way the wizard triggers work — everything else is the LLM's job.
|
|
192
192
|
*/
|
|
193
193
|
function dispatchWorkflow(pi: ExtensionAPI, note: string, customType = "gsd-run"): void {
|
|
194
|
-
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".
|
|
194
|
+
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".pi", "GSD-WORKFLOW.md");
|
|
195
195
|
const workflow = readFileSync(workflowPath, "utf-8");
|
|
196
196
|
|
|
197
197
|
pi.sendMessage(
|