gsd-pi 2.35.0-dev.cd3b7ea → 2.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/cli.js +7 -2
- package/dist/resource-loader.d.ts +1 -1
- package/dist/resource-loader.js +13 -1
- package/dist/resources/extensions/async-jobs/await-tool.js +0 -2
- package/dist/resources/extensions/async-jobs/job-manager.js +0 -6
- package/dist/resources/extensions/bg-shell/output-formatter.js +1 -19
- package/dist/resources/extensions/bg-shell/process-manager.js +0 -4
- package/dist/resources/extensions/bg-shell/types.js +0 -2
- package/dist/resources/extensions/context7/index.js +5 -0
- package/dist/resources/extensions/get-secrets-from-user.js +2 -30
- package/dist/resources/extensions/google-search/index.js +5 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +43 -1
- package/dist/resources/extensions/gsd/auto-loop.js +17 -3
- package/dist/resources/extensions/gsd/auto-model-selection.js +15 -3
- package/dist/resources/extensions/gsd/auto-recovery.js +35 -0
- package/dist/resources/extensions/gsd/auto-start.js +35 -2
- package/dist/resources/extensions/gsd/auto.js +59 -4
- package/dist/resources/extensions/gsd/commands-handlers.js +2 -2
- package/dist/resources/extensions/gsd/commands-inspect.js +10 -3
- package/dist/resources/extensions/gsd/commands-rate.js +31 -0
- package/dist/resources/extensions/gsd/commands.js +43 -1
- package/dist/resources/extensions/gsd/doctor-environment.js +26 -17
- package/dist/resources/extensions/gsd/files.js +11 -2
- package/dist/resources/extensions/gsd/gitignore.js +54 -7
- package/dist/resources/extensions/gsd/guided-flow.js +8 -2
- package/dist/resources/extensions/gsd/health-widget-core.js +96 -0
- package/dist/resources/extensions/gsd/health-widget.js +97 -46
- package/dist/resources/extensions/gsd/index.js +26 -33
- package/dist/resources/extensions/gsd/migrate-external.js +55 -2
- package/dist/resources/extensions/gsd/milestone-ids.js +3 -2
- package/dist/resources/extensions/gsd/paths.js +74 -7
- package/dist/resources/extensions/gsd/post-unit-hooks.js +4 -1
- package/dist/resources/extensions/gsd/preferences-validation.js +16 -1
- package/dist/resources/extensions/gsd/preferences.js +12 -0
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +2 -0
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -0
- package/dist/resources/extensions/gsd/roadmap-mutations.js +55 -0
- package/dist/resources/extensions/gsd/session-lock.js +53 -2
- package/dist/resources/extensions/gsd/state.js +2 -1
- package/dist/resources/extensions/gsd/templates/plan.md +8 -0
- package/dist/resources/extensions/gsd/worktree-resolver.js +12 -0
- package/dist/resources/extensions/remote-questions/remote-command.js +2 -22
- package/dist/resources/extensions/shared/mod.js +1 -1
- package/dist/resources/extensions/shared/sanitize.js +30 -0
- package/dist/resources/extensions/subagent/index.js +6 -14
- package/dist/resources/skills/core-web-vitals/SKILL.md +1 -1
- package/dist/resources/skills/create-gsd-extension/workflows/debug-extension.md +1 -1
- package/dist/resources/skills/github-workflows/SKILL.md +0 -2
- package/dist/resources/skills/web-quality-audit/SKILL.md +0 -2
- package/package.json +2 -1
- package/packages/pi-agent-core/dist/agent.d.ts +10 -2
- package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/agent.js +19 -8
- package/packages/pi-agent-core/dist/agent.js.map +1 -1
- package/packages/pi-agent-core/src/agent.ts +31 -10
- package/packages/pi-ai/dist/providers/openai-responses.js +1 -1
- package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
- package/packages/pi-ai/src/providers/openai-responses.ts +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +20 -4
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/resource-loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/resource-loader.js +13 -2
- package/packages/pi-coding-agent/dist/core/resource-loader.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/agent-session.ts +36 -12
- package/packages/pi-coding-agent/src/core/resource-loader.ts +13 -2
- package/pkg/package.json +1 -1
- package/src/resources/extensions/async-jobs/await-tool.ts +0 -2
- package/src/resources/extensions/async-jobs/job-manager.ts +0 -7
- package/src/resources/extensions/bg-shell/output-formatter.ts +0 -17
- package/src/resources/extensions/bg-shell/process-manager.ts +0 -4
- package/src/resources/extensions/bg-shell/types.ts +0 -12
- package/src/resources/extensions/context7/index.ts +7 -0
- package/src/resources/extensions/get-secrets-from-user.ts +2 -35
- package/src/resources/extensions/google-search/index.ts +7 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +49 -1
- package/src/resources/extensions/gsd/auto-loop.ts +22 -2
- package/src/resources/extensions/gsd/auto-model-selection.ts +23 -2
- package/src/resources/extensions/gsd/auto-recovery.ts +39 -0
- package/src/resources/extensions/gsd/auto-start.ts +42 -2
- package/src/resources/extensions/gsd/auto.ts +61 -3
- package/src/resources/extensions/gsd/commands-handlers.ts +2 -2
- package/src/resources/extensions/gsd/commands-inspect.ts +10 -3
- package/src/resources/extensions/gsd/commands-rate.ts +55 -0
- package/src/resources/extensions/gsd/commands.ts +43 -1
- package/src/resources/extensions/gsd/doctor-environment.ts +26 -16
- package/src/resources/extensions/gsd/files.ts +12 -2
- package/src/resources/extensions/gsd/gitignore.ts +54 -7
- package/src/resources/extensions/gsd/guided-flow.ts +8 -2
- package/src/resources/extensions/gsd/health-widget-core.ts +129 -0
- package/src/resources/extensions/gsd/health-widget.ts +103 -59
- package/src/resources/extensions/gsd/index.ts +30 -33
- package/src/resources/extensions/gsd/migrate-external.ts +47 -2
- package/src/resources/extensions/gsd/milestone-ids.ts +3 -2
- package/src/resources/extensions/gsd/paths.ts +73 -7
- package/src/resources/extensions/gsd/post-unit-hooks.ts +5 -1
- package/src/resources/extensions/gsd/preferences-validation.ts +16 -1
- package/src/resources/extensions/gsd/preferences.ts +14 -1
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +2 -0
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -0
- package/src/resources/extensions/gsd/roadmap-mutations.ts +66 -0
- package/src/resources/extensions/gsd/session-lock.ts +59 -2
- package/src/resources/extensions/gsd/state.ts +2 -1
- package/src/resources/extensions/gsd/templates/plan.md +8 -0
- package/src/resources/extensions/gsd/tests/commands-inspect-open-db.test.ts +46 -0
- package/src/resources/extensions/gsd/tests/files-loadfile-eisdir.test.ts +20 -0
- package/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts +214 -0
- package/src/resources/extensions/gsd/tests/health-widget.test.ts +158 -0
- package/src/resources/extensions/gsd/tests/paths.test.ts +113 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +12 -2
- package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +26 -0
- package/src/resources/extensions/gsd/tests/test-utils.ts +165 -0
- package/src/resources/extensions/gsd/tests/validate-directory.test.ts +15 -0
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +7 -0
- package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +32 -0
- package/src/resources/extensions/gsd/worktree-resolver.ts +11 -0
- package/src/resources/extensions/remote-questions/remote-command.ts +2 -23
- package/src/resources/extensions/shared/mod.ts +1 -1
- package/src/resources/extensions/shared/sanitize.ts +36 -0
- package/src/resources/extensions/subagent/index.ts +6 -12
- package/src/resources/skills/core-web-vitals/SKILL.md +1 -1
- package/src/resources/skills/create-gsd-extension/workflows/debug-extension.md +1 -1
- package/src/resources/skills/github-workflows/SKILL.md +0 -2
- package/src/resources/skills/web-quality-audit/SKILL.md +0 -2
- package/dist/resources/extensions/shared/wizard-ui.js +0 -478
- package/dist/resources/skills/swiftui/SKILL.md +0 -208
- package/dist/resources/skills/swiftui/references/animations.md +0 -921
- package/dist/resources/skills/swiftui/references/architecture.md +0 -1561
- package/dist/resources/skills/swiftui/references/layout-system.md +0 -1186
- package/dist/resources/skills/swiftui/references/navigation.md +0 -1492
- package/dist/resources/skills/swiftui/references/networking-async.md +0 -214
- package/dist/resources/skills/swiftui/references/performance.md +0 -1706
- package/dist/resources/skills/swiftui/references/platform-integration.md +0 -204
- package/dist/resources/skills/swiftui/references/state-management.md +0 -1443
- package/dist/resources/skills/swiftui/references/swiftdata.md +0 -297
- package/dist/resources/skills/swiftui/references/testing-debugging.md +0 -247
- package/dist/resources/skills/swiftui/references/uikit-appkit-interop.md +0 -218
- package/dist/resources/skills/swiftui/workflows/add-feature.md +0 -191
- package/dist/resources/skills/swiftui/workflows/build-new-app.md +0 -311
- package/dist/resources/skills/swiftui/workflows/debug-swiftui.md +0 -192
- package/dist/resources/skills/swiftui/workflows/optimize-performance.md +0 -197
- package/dist/resources/skills/swiftui/workflows/ship-app.md +0 -203
- package/dist/resources/skills/swiftui/workflows/write-tests.md +0 -235
- package/src/resources/extensions/shared/wizard-ui.ts +0 -551
- package/src/resources/skills/swiftui/SKILL.md +0 -208
- package/src/resources/skills/swiftui/references/animations.md +0 -921
- package/src/resources/skills/swiftui/references/architecture.md +0 -1561
- package/src/resources/skills/swiftui/references/layout-system.md +0 -1186
- package/src/resources/skills/swiftui/references/navigation.md +0 -1492
- package/src/resources/skills/swiftui/references/networking-async.md +0 -214
- package/src/resources/skills/swiftui/references/performance.md +0 -1706
- package/src/resources/skills/swiftui/references/platform-integration.md +0 -204
- package/src/resources/skills/swiftui/references/state-management.md +0 -1443
- package/src/resources/skills/swiftui/references/swiftdata.md +0 -297
- package/src/resources/skills/swiftui/references/testing-debugging.md +0 -247
- package/src/resources/skills/swiftui/references/uikit-appkit-interop.md +0 -218
- package/src/resources/skills/swiftui/workflows/add-feature.md +0 -191
- package/src/resources/skills/swiftui/workflows/build-new-app.md +0 -311
- package/src/resources/skills/swiftui/workflows/debug-swiftui.md +0 -192
- package/src/resources/skills/swiftui/workflows/optimize-performance.md +0 -197
- package/src/resources/skills/swiftui/workflows/ship-app.md +0 -203
- package/src/resources/skills/swiftui/workflows/write-tests.md +0 -235
|
@@ -15,7 +15,7 @@ import { isAutoActive } from "./auto.js";
|
|
|
15
15
|
import { projectRoot } from "./commands.js";
|
|
16
16
|
import { loadPrompt } from "./prompt-loader.js";
|
|
17
17
|
export function dispatchDoctorHeal(pi, scope, reportText, structuredIssues) {
|
|
18
|
-
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".
|
|
18
|
+
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".gsd", "agent", "GSD-WORKFLOW.md");
|
|
19
19
|
const workflow = readFileSync(workflowPath, "utf-8");
|
|
20
20
|
const prompt = loadPrompt("doctor-heal", {
|
|
21
21
|
doctorSummary: reportText,
|
|
@@ -144,7 +144,7 @@ export async function handleTriage(ctx, pi, basePath) {
|
|
|
144
144
|
currentPlan: currentPlan || "(no active slice plan)",
|
|
145
145
|
roadmapContext: roadmapContext || "(no active roadmap)",
|
|
146
146
|
});
|
|
147
|
-
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".
|
|
147
|
+
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".gsd", "agent", "GSD-WORKFLOW.md");
|
|
148
148
|
const workflow = readFileSync(workflowPath, "utf-8");
|
|
149
149
|
pi.sendMessage({
|
|
150
150
|
customType: "gsd-triage",
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Contains: InspectData type, formatInspectOutput, handleInspect
|
|
5
5
|
*/
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { gsdRoot } from "./paths.js";
|
|
6
9
|
import { getErrorMessage } from "./error-utils.js";
|
|
7
10
|
export function formatInspectOutput(data) {
|
|
8
11
|
const lines = [];
|
|
@@ -30,10 +33,14 @@ export function formatInspectOutput(data) {
|
|
|
30
33
|
}
|
|
31
34
|
export async function handleInspect(ctx) {
|
|
32
35
|
try {
|
|
33
|
-
const { isDbAvailable, _getAdapter } = await import("./gsd-db.js");
|
|
36
|
+
const { isDbAvailable, _getAdapter, openDatabase } = await import("./gsd-db.js");
|
|
34
37
|
if (!isDbAvailable()) {
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
const gsdDir = gsdRoot(process.cwd());
|
|
39
|
+
const dbPath = join(gsdDir, "gsd.db");
|
|
40
|
+
if (!existsSync(gsdDir) || !existsSync(dbPath) || !openDatabase(dbPath)) {
|
|
41
|
+
ctx.ui.notify("No GSD database available. Run /gsd auto to create one.", "info");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
37
44
|
}
|
|
38
45
|
const adapter = _getAdapter();
|
|
39
46
|
if (!adapter) {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /gsd rate — Submit feedback on the last unit's model tier assignment.
|
|
3
|
+
* Feeds into the adaptive routing history so future dispatches improve.
|
|
4
|
+
*/
|
|
5
|
+
import { loadLedgerFromDisk } from "./metrics.js";
|
|
6
|
+
import { recordFeedback, initRoutingHistory } from "./routing-history.js";
|
|
7
|
+
const VALID_RATINGS = new Set(["over", "under", "ok"]);
|
|
8
|
+
export async function handleRate(args, ctx, basePath) {
|
|
9
|
+
const rating = args.trim().toLowerCase();
|
|
10
|
+
if (!rating || !VALID_RATINGS.has(rating)) {
|
|
11
|
+
ctx.ui.notify("Usage: /gsd rate <over|ok|under>\n" +
|
|
12
|
+
" over — model was overpowered for that task (encourage cheaper)\n" +
|
|
13
|
+
" ok — model was appropriate\n" +
|
|
14
|
+
" under — model was too weak (encourage stronger)", "info");
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const ledger = loadLedgerFromDisk(basePath);
|
|
18
|
+
if (!ledger || ledger.units.length === 0) {
|
|
19
|
+
ctx.ui.notify("No completed units found — nothing to rate.", "warning");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const lastUnit = ledger.units[ledger.units.length - 1];
|
|
23
|
+
const tier = lastUnit.tier;
|
|
24
|
+
if (!tier) {
|
|
25
|
+
ctx.ui.notify("Last unit has no tier data (dynamic routing was not active). Rating skipped.", "warning");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
initRoutingHistory(basePath);
|
|
29
|
+
recordFeedback(lastUnit.type, lastUnit.id, tier, rating);
|
|
30
|
+
ctx.ui.notify(`Recorded "${rating}" for ${lastUnit.type}/${lastUnit.id} at tier ${tier}.`, "info");
|
|
31
|
+
}
|
|
@@ -36,6 +36,7 @@ import { computeProgressScore, formatProgressLine } from "./progress-score.js";
|
|
|
36
36
|
import { runEnvironmentChecks } from "./doctor-environment.js";
|
|
37
37
|
import { handleLogs } from "./commands-logs.js";
|
|
38
38
|
import { handleStart, handleTemplates, getTemplateCompletions } from "./commands-workflow-templates.js";
|
|
39
|
+
import { readSessionLockData, isSessionLockProcessAlive } from "./session-lock.js";
|
|
39
40
|
/** Resolve the effective project root, accounting for worktree paths. */
|
|
40
41
|
export function projectRoot() {
|
|
41
42
|
const cwd = process.cwd();
|
|
@@ -54,6 +55,38 @@ export function projectRoot() {
|
|
|
54
55
|
}
|
|
55
56
|
return root;
|
|
56
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Check if another process holds the auto-mode session lock.
|
|
60
|
+
* Returns the lock data if a remote session is alive, null otherwise.
|
|
61
|
+
*/
|
|
62
|
+
function getRemoteAutoSession(basePath) {
|
|
63
|
+
const lockData = readSessionLockData(basePath);
|
|
64
|
+
if (!lockData)
|
|
65
|
+
return null;
|
|
66
|
+
if (lockData.pid === process.pid)
|
|
67
|
+
return null;
|
|
68
|
+
if (!isSessionLockProcessAlive(lockData))
|
|
69
|
+
return null;
|
|
70
|
+
return { pid: lockData.pid };
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Show a steering menu when auto-mode is running in another process.
|
|
74
|
+
* Returns true if a remote session was detected (caller should return early).
|
|
75
|
+
*/
|
|
76
|
+
function notifyRemoteAutoActive(ctx, basePath) {
|
|
77
|
+
const remote = getRemoteAutoSession(basePath);
|
|
78
|
+
if (!remote)
|
|
79
|
+
return false;
|
|
80
|
+
ctx.ui.notify(`Auto-mode is running in another process (PID ${remote.pid}).\n` +
|
|
81
|
+
`Use these commands to interact with it:\n` +
|
|
82
|
+
` /gsd status — check progress\n` +
|
|
83
|
+
` /gsd discuss — discuss architecture decisions\n` +
|
|
84
|
+
` /gsd queue — queue the next milestone\n` +
|
|
85
|
+
` /gsd steer — apply an override to active work\n` +
|
|
86
|
+
` /gsd capture — fire-and-forget thought\n` +
|
|
87
|
+
` /gsd stop — stop auto-mode`, "warning");
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
57
90
|
export function registerGSDCommand(pi) {
|
|
58
91
|
pi.registerCommand("gsd", {
|
|
59
92
|
description: "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|visualize|queue|quick|capture|triage|dispatch|history|undo|skip|export|cleanup|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|parallel|update",
|
|
@@ -74,6 +107,7 @@ export function registerGSDCommand(pi) {
|
|
|
74
107
|
{ cmd: "triage", desc: "Manually trigger triage of pending captures" },
|
|
75
108
|
{ cmd: "dispatch", desc: "Dispatch a specific phase directly" },
|
|
76
109
|
{ cmd: "history", desc: "View execution history" },
|
|
110
|
+
{ cmd: "rate", desc: "Rate last unit's model tier (over/ok/under) — improves adaptive routing" },
|
|
77
111
|
{ cmd: "undo", desc: "Revert last completed unit" },
|
|
78
112
|
{ cmd: "skip", desc: "Prevent a unit from auto-mode dispatch" },
|
|
79
113
|
{ cmd: "export", desc: "Export milestone/slice results" },
|
|
@@ -459,6 +493,8 @@ export async function handleGSDCommand(args, ctx, pi) {
|
|
|
459
493
|
await handleDryRun(ctx, projectRoot());
|
|
460
494
|
return;
|
|
461
495
|
}
|
|
496
|
+
if (notifyRemoteAutoActive(ctx, projectRoot()))
|
|
497
|
+
return;
|
|
462
498
|
const verboseMode = trimmed.includes("--verbose");
|
|
463
499
|
const debugMode = trimmed.includes("--debug");
|
|
464
500
|
if (debugMode)
|
|
@@ -513,6 +549,11 @@ export async function handleGSDCommand(args, ctx, pi) {
|
|
|
513
549
|
await handleUndo(trimmed.replace(/^undo\s*/, "").trim(), ctx, pi, projectRoot());
|
|
514
550
|
return;
|
|
515
551
|
}
|
|
552
|
+
if (trimmed === "rate" || trimmed.startsWith("rate ")) {
|
|
553
|
+
const { handleRate } = await import("./commands-rate.js");
|
|
554
|
+
await handleRate(trimmed.replace(/^rate\s*/, "").trim(), ctx, projectRoot());
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
516
557
|
if (trimmed.startsWith("skip ")) {
|
|
517
558
|
await handleSkip(trimmed.replace(/^skip\s*/, "").trim(), ctx, projectRoot());
|
|
518
559
|
return;
|
|
@@ -809,7 +850,8 @@ Examples:
|
|
|
809
850
|
return;
|
|
810
851
|
}
|
|
811
852
|
if (trimmed === "") {
|
|
812
|
-
|
|
853
|
+
if (notifyRemoteAutoActive(ctx, projectRoot()))
|
|
854
|
+
return;
|
|
813
855
|
await startAuto(ctx, pi, projectRoot(), false, { step: true });
|
|
814
856
|
return;
|
|
815
857
|
}
|
|
@@ -148,27 +148,36 @@ function checkPortConflicts(basePath) {
|
|
|
148
148
|
// Try to detect ports from package.json scripts
|
|
149
149
|
const portsToCheck = new Set();
|
|
150
150
|
const pkgPath = join(basePath, "package.json");
|
|
151
|
-
if (existsSync(pkgPath)) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
151
|
+
if (!existsSync(pkgPath)) {
|
|
152
|
+
// No package.json — this isn't a Node.js project. Skip port checks
|
|
153
|
+
// entirely to avoid false positives from system services (e.g., macOS
|
|
154
|
+
// AirPlay Receiver on port 5000). (#1381)
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
159
|
+
const scripts = pkg.scripts ?? {};
|
|
160
|
+
const scriptText = Object.values(scripts).join(" ");
|
|
161
|
+
// Look for --port NNNN, -p NNNN, PORT=NNNN, :NNNN patterns
|
|
162
|
+
const portMatches = scriptText.matchAll(/(?:--port\s+|(?:^|[^a-z])PORT[=:]\s*|-p\s+|:)(\d{4,5})\b/gi);
|
|
163
|
+
for (const m of portMatches) {
|
|
164
|
+
const port = parseInt(m[1], 10);
|
|
165
|
+
if (port >= 1024 && port <= 65535)
|
|
166
|
+
portsToCheck.add(port);
|
|
166
167
|
}
|
|
167
168
|
}
|
|
168
|
-
|
|
169
|
+
catch {
|
|
170
|
+
// parse failed — skip port checks rather than using defaults
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
// If no ports found in scripts, check common defaults.
|
|
174
|
+
// Filter out port 5000 on macOS — AirPlay Receiver uses it by default (#1381).
|
|
169
175
|
if (portsToCheck.size === 0) {
|
|
170
|
-
for (const p of DEFAULT_DEV_PORTS)
|
|
176
|
+
for (const p of DEFAULT_DEV_PORTS) {
|
|
177
|
+
if (p === 5000 && process.platform === "darwin")
|
|
178
|
+
continue;
|
|
171
179
|
portsToCheck.add(p);
|
|
180
|
+
}
|
|
172
181
|
}
|
|
173
182
|
for (const port of portsToCheck) {
|
|
174
183
|
const result = tryExec(`lsof -i :${port} -sTCP:LISTEN -t`, basePath);
|
|
@@ -509,7 +509,8 @@ export async function loadFile(path) {
|
|
|
509
509
|
return await fs.readFile(path, 'utf-8');
|
|
510
510
|
}
|
|
511
511
|
catch (err) {
|
|
512
|
-
|
|
512
|
+
const code = err.code;
|
|
513
|
+
if (code === 'ENOENT' || code === 'EISDIR')
|
|
513
514
|
return null;
|
|
514
515
|
throw err;
|
|
515
516
|
}
|
|
@@ -704,7 +705,7 @@ export async function inlinePriorMilestoneSummary(mid, base) {
|
|
|
704
705
|
* Returns `null` when no manifest file exists (path resolution failure or
|
|
705
706
|
* file not on disk) - callers can distinguish "no manifest" from "empty manifest".
|
|
706
707
|
*/
|
|
707
|
-
export async function getManifestStatus(base, milestoneId) {
|
|
708
|
+
export async function getManifestStatus(base, milestoneId, projectRoot) {
|
|
708
709
|
const resolvedPath = resolveMilestoneFile(base, milestoneId, 'SECRETS');
|
|
709
710
|
if (!resolvedPath)
|
|
710
711
|
return null;
|
|
@@ -713,8 +714,16 @@ export async function getManifestStatus(base, milestoneId) {
|
|
|
713
714
|
return null;
|
|
714
715
|
const manifest = parseSecretsManifest(content);
|
|
715
716
|
const keys = manifest.entries.map(e => e.key);
|
|
717
|
+
// Check both the base path .env AND the project root .env (#1387).
|
|
718
|
+
// In worktree mode, base is the worktree path which may not have .env.
|
|
719
|
+
// The project root's .env is where the user actually defined their keys.
|
|
716
720
|
const existingKeys = await checkExistingEnvKeys(keys, resolve(base, '.env'));
|
|
717
721
|
const existingSet = new Set(existingKeys);
|
|
722
|
+
if (projectRoot && projectRoot !== base) {
|
|
723
|
+
const rootKeys = await checkExistingEnvKeys(keys, resolve(projectRoot, '.env'));
|
|
724
|
+
for (const k of rootKeys)
|
|
725
|
+
existingSet.add(k);
|
|
726
|
+
}
|
|
718
727
|
const result = {
|
|
719
728
|
pending: [],
|
|
720
729
|
collected: [],
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* Both idempotent — non-destructive if already present.
|
|
7
7
|
*/
|
|
8
8
|
import { join } from "node:path";
|
|
9
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
10
|
-
import { nativeRmCached } from "./native-git-bridge.js";
|
|
9
|
+
import { existsSync, lstatSync, readFileSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { nativeRmCached, nativeLsFiles } from "./native-git-bridge.js";
|
|
11
11
|
import { gsdRoot } from "./paths.js";
|
|
12
12
|
/**
|
|
13
13
|
* GSD runtime patterns for git index cleanup.
|
|
@@ -67,12 +67,48 @@ const BASELINE_PATTERNS = [
|
|
|
67
67
|
"tmp/",
|
|
68
68
|
];
|
|
69
69
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
70
|
+
* Check whether `.gsd/` contains files tracked by git.
|
|
71
|
+
* If so, the project intentionally keeps `.gsd/` in version control
|
|
72
|
+
* and we must NOT add `.gsd` to `.gitignore` or attempt migration.
|
|
73
|
+
*
|
|
74
|
+
* Returns true if git tracks at least one file under `.gsd/`.
|
|
75
|
+
* Returns false (safe to ignore) if:
|
|
76
|
+
* - Not a git repo
|
|
77
|
+
* - `.gsd/` is a symlink (external state, should be ignored)
|
|
78
|
+
* - `.gsd/` doesn't exist
|
|
79
|
+
* - No tracked files found under `.gsd/`
|
|
80
|
+
*/
|
|
81
|
+
export function hasGitTrackedGsdFiles(basePath) {
|
|
82
|
+
const localGsd = join(basePath, ".gsd");
|
|
83
|
+
// If .gsd doesn't exist or is already a symlink, no tracked files concern
|
|
84
|
+
if (!existsSync(localGsd))
|
|
85
|
+
return false;
|
|
86
|
+
try {
|
|
87
|
+
if (lstatSync(localGsd).isSymbolicLink())
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
// Check if git tracks any files under .gsd/
|
|
94
|
+
try {
|
|
95
|
+
const tracked = nativeLsFiles(basePath, ".gsd");
|
|
96
|
+
return tracked.length > 0;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// Not a git repo or git not available — safe to proceed
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Ensure basePath/.gitignore contains baseline ignore patterns.
|
|
105
|
+
* Creates the file if missing; appends missing patterns.
|
|
72
106
|
* Returns true if the file was created or modified, false if already complete.
|
|
73
107
|
*
|
|
74
|
-
* `.gsd/`
|
|
75
|
-
*
|
|
108
|
+
* **Safety check:** If `.gsd/` contains git-tracked files (i.e., the project
|
|
109
|
+
* intentionally keeps `.gsd/` in version control), the `.gsd` ignore pattern
|
|
110
|
+
* is excluded to prevent data loss. Only the `.gsd` pattern is affected —
|
|
111
|
+
* all other baseline patterns are still applied normally.
|
|
76
112
|
*/
|
|
77
113
|
export function ensureGitignore(basePath, options) {
|
|
78
114
|
// If manage_gitignore is explicitly false, do not touch .gitignore at all
|
|
@@ -88,8 +124,14 @@ export function ensureGitignore(basePath, options) {
|
|
|
88
124
|
.split("\n")
|
|
89
125
|
.map((l) => l.trim())
|
|
90
126
|
.filter((l) => l && !l.startsWith("#")));
|
|
127
|
+
// Determine which patterns to apply. If .gsd/ has tracked files,
|
|
128
|
+
// exclude the ".gsd" pattern to prevent deleting tracked state.
|
|
129
|
+
const gsdIsTracked = hasGitTrackedGsdFiles(basePath);
|
|
130
|
+
const patternsToApply = gsdIsTracked
|
|
131
|
+
? BASELINE_PATTERNS.filter((p) => p !== ".gsd")
|
|
132
|
+
: BASELINE_PATTERNS;
|
|
91
133
|
// Find patterns not yet present
|
|
92
|
-
const missing =
|
|
134
|
+
const missing = patternsToApply.filter((p) => !existingLines.has(p));
|
|
93
135
|
if (missing.length === 0)
|
|
94
136
|
return false;
|
|
95
137
|
// Build the block to append
|
|
@@ -111,6 +153,11 @@ export function ensureGitignore(basePath, options) {
|
|
|
111
153
|
* already in the index even after .gitignore is updated.
|
|
112
154
|
*
|
|
113
155
|
* Only removes from the index (`--cached`), never from disk. Idempotent.
|
|
156
|
+
*
|
|
157
|
+
* Note: These are strictly runtime/ephemeral paths (activity logs, lock files,
|
|
158
|
+
* metrics, STATE.md). They are always safe to untrack, even when the project
|
|
159
|
+
* intentionally keeps other `.gsd/` files (like PROJECT.md, milestones/) in
|
|
160
|
+
* version control.
|
|
114
161
|
*/
|
|
115
162
|
export function untrackRuntimeFiles(basePath) {
|
|
116
163
|
const runtimePaths = GSD_RUNTIME_PATTERNS;
|
|
@@ -17,6 +17,7 @@ import { resolveExpectedArtifactPath } from "./auto.js";
|
|
|
17
17
|
import { gsdRoot, milestonesDir, resolveMilestoneFile, resolveSliceFile, resolveSlicePath, resolveGsdRootFile, relGsdRootFile, relMilestoneFile, relSliceFile, } from "./paths.js";
|
|
18
18
|
import { join } from "node:path";
|
|
19
19
|
import { readFileSync, existsSync, mkdirSync, readdirSync, unlinkSync } from "node:fs";
|
|
20
|
+
import { readSessionLockData, isSessionLockProcessAlive } from "./session-lock.js";
|
|
20
21
|
import { nativeIsRepo, nativeInit } from "./native-git-bridge.js";
|
|
21
22
|
import { ensureGitignore, ensurePreferences, untrackRuntimeFiles } from "./gitignore.js";
|
|
22
23
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
@@ -154,7 +155,7 @@ function parseMilestoneSequenceFromProject(content) {
|
|
|
154
155
|
* This is the only way the wizard triggers work — everything else is the LLM's job.
|
|
155
156
|
*/
|
|
156
157
|
function dispatchWorkflow(pi, note, customType = "gsd-run") {
|
|
157
|
-
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".
|
|
158
|
+
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".gsd", "agent", "GSD-WORKFLOW.md");
|
|
158
159
|
const workflow = readFileSync(workflowPath, "utf-8");
|
|
159
160
|
pi.sendMessage({
|
|
160
161
|
customType,
|
|
@@ -426,7 +427,12 @@ export async function showDiscuss(ctx, pi, basePath) {
|
|
|
426
427
|
// If all pending slices are discussed, notify and exit instead of looping
|
|
427
428
|
const allDiscussed = pendingSlices.every(s => discussedMap.get(s.id));
|
|
428
429
|
if (allDiscussed) {
|
|
429
|
-
|
|
430
|
+
const lockData = readSessionLockData(basePath);
|
|
431
|
+
const remoteAutoRunning = lockData && lockData.pid !== process.pid && isSessionLockProcessAlive(lockData);
|
|
432
|
+
const nextStep = remoteAutoRunning
|
|
433
|
+
? "Auto-mode is already running — use /gsd status to check progress."
|
|
434
|
+
: "Run /gsd to start planning.";
|
|
435
|
+
ctx.ui.notify(`All ${pendingSlices.length} slices discussed. ${nextStep}`, "info");
|
|
430
436
|
return;
|
|
431
437
|
}
|
|
432
438
|
// Find the first undiscussed slice to recommend
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure GSD health widget logic.
|
|
3
|
+
*
|
|
4
|
+
* Separates project-state detection and line rendering from the widget's
|
|
5
|
+
* runtime integrations so the regressions can be tested directly.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
8
|
+
import { gsdRoot } from "./paths.js";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
export function detectHealthWidgetProjectState(basePath) {
|
|
11
|
+
const root = gsdRoot(basePath);
|
|
12
|
+
if (!existsSync(root))
|
|
13
|
+
return "none";
|
|
14
|
+
// Lightweight milestone count — avoids the full detectProjectState() scan
|
|
15
|
+
// (CI markers, Makefile targets, etc.) that is unnecessary on the 60s refresh.
|
|
16
|
+
try {
|
|
17
|
+
const milestonesDir = join(root, "milestones");
|
|
18
|
+
if (existsSync(milestonesDir)) {
|
|
19
|
+
const entries = readdirSync(milestonesDir, { withFileTypes: true });
|
|
20
|
+
if (entries.some(e => e.isDirectory()))
|
|
21
|
+
return "active";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch { /* non-fatal */ }
|
|
25
|
+
return "initialized";
|
|
26
|
+
}
|
|
27
|
+
function formatCost(n) {
|
|
28
|
+
return n >= 1 ? `$${n.toFixed(2)}` : `${(n * 100).toFixed(1)}¢`;
|
|
29
|
+
}
|
|
30
|
+
function formatProgress(progress) {
|
|
31
|
+
if (!progress)
|
|
32
|
+
return null;
|
|
33
|
+
const parts = [];
|
|
34
|
+
parts.push(`M ${progress.milestones.done}/${progress.milestones.total}`);
|
|
35
|
+
if (progress.slices)
|
|
36
|
+
parts.push(`S ${progress.slices.done}/${progress.slices.total}`);
|
|
37
|
+
if (progress.tasks)
|
|
38
|
+
parts.push(`T ${progress.tasks.done}/${progress.tasks.total}`);
|
|
39
|
+
return parts.length > 0 ? `Progress: ${parts.join(" · ")}` : null;
|
|
40
|
+
}
|
|
41
|
+
function formatEnvironmentSummary(errorCount, warningCount) {
|
|
42
|
+
if (errorCount <= 0 && warningCount <= 0)
|
|
43
|
+
return null;
|
|
44
|
+
const parts = [];
|
|
45
|
+
if (errorCount > 0)
|
|
46
|
+
parts.push(`${errorCount} error${errorCount > 1 ? "s" : ""}`);
|
|
47
|
+
if (warningCount > 0)
|
|
48
|
+
parts.push(`${warningCount} warning${warningCount > 1 ? "s" : ""}`);
|
|
49
|
+
return `Env: ${parts.join(", ")}`;
|
|
50
|
+
}
|
|
51
|
+
function formatBudgetSummary(data) {
|
|
52
|
+
if (data.budgetCeiling !== undefined && data.budgetCeiling > 0) {
|
|
53
|
+
const pct = Math.min(100, (data.budgetSpent / data.budgetCeiling) * 100);
|
|
54
|
+
return `Budget: ${formatCost(data.budgetSpent)}/${formatCost(data.budgetCeiling)} (${pct.toFixed(0)}%)`;
|
|
55
|
+
}
|
|
56
|
+
if (data.budgetSpent > 0) {
|
|
57
|
+
return `Spent: ${formatCost(data.budgetSpent)}`;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
function buildExecutionHeadline(data) {
|
|
62
|
+
const status = data.executionStatus ?? "Active project";
|
|
63
|
+
const target = data.executionTarget ?? data.blocker ?? "loading status…";
|
|
64
|
+
return ` GSD ${status}${target ? ` - ${target}` : ""}`;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Build compact health lines for the widget.
|
|
68
|
+
* Returns a string array suitable for setWidget().
|
|
69
|
+
*/
|
|
70
|
+
export function buildHealthLines(data) {
|
|
71
|
+
if (data.projectState === "none") {
|
|
72
|
+
return [" GSD No project loaded — run /gsd to start"];
|
|
73
|
+
}
|
|
74
|
+
if (data.projectState === "initialized") {
|
|
75
|
+
return [" GSD Project initialized — run /gsd to continue setup"];
|
|
76
|
+
}
|
|
77
|
+
const lines = [buildExecutionHeadline(data)];
|
|
78
|
+
const details = [];
|
|
79
|
+
const progress = formatProgress(data.progress);
|
|
80
|
+
if (progress)
|
|
81
|
+
details.push(progress);
|
|
82
|
+
if (data.providerIssue)
|
|
83
|
+
details.push(data.providerIssue);
|
|
84
|
+
const environment = formatEnvironmentSummary(data.environmentErrorCount, data.environmentWarningCount);
|
|
85
|
+
if (environment)
|
|
86
|
+
details.push(environment);
|
|
87
|
+
const budget = formatBudgetSummary(data);
|
|
88
|
+
if (budget)
|
|
89
|
+
details.push(budget);
|
|
90
|
+
if (data.eta)
|
|
91
|
+
details.push(data.eta);
|
|
92
|
+
if (details.length > 0) {
|
|
93
|
+
lines.push(` ${details.join(" │ ")}`);
|
|
94
|
+
}
|
|
95
|
+
return lines;
|
|
96
|
+
}
|