gsd-pi 2.58.0-dev.778d6ac → 2.58.0-dev.e002a57
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 +11 -0
- package/dist/resources/extensions/gsd/auto-worktree.js +11 -8
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +9 -16
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +22 -1
- package/dist/resources/extensions/gsd/codebase-generator.js +279 -0
- package/dist/resources/extensions/gsd/commands/catalog.js +10 -1
- package/dist/resources/extensions/gsd/commands/handlers/ops.js +5 -0
- package/dist/resources/extensions/gsd/commands-codebase.js +115 -0
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +41 -4
- package/dist/resources/extensions/gsd/complexity-classifier.js +8 -6
- package/dist/resources/extensions/gsd/doctor-git-checks.js +48 -1
- package/dist/resources/extensions/gsd/doctor-proactive.js +34 -1
- package/dist/resources/extensions/gsd/error-classifier.js +3 -4
- package/dist/resources/extensions/gsd/git-service.js +82 -1
- package/dist/resources/extensions/gsd/native-git-bridge.js +22 -0
- package/dist/resources/extensions/gsd/paths.js +2 -0
- package/dist/resources/extensions/gsd/preferences-types.js +1 -0
- package/dist/resources/extensions/gsd/watch/header-renderer.js +241 -0
- package/dist/resources/extensions/search-the-web/url-utils.js +17 -0
- package/dist/security-overrides.d.ts +11 -0
- package/dist/security-overrides.js +41 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +16 -16
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +16 -16
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/welcome-screen.d.ts +1 -0
- package/dist/welcome-screen.js +32 -6
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/resolve-config-value.d.ts +8 -0
- package/packages/pi-coding-agent/dist/core/resolve-config-value.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/resolve-config-value.js +23 -2
- package/packages/pi-coding-agent/dist/core/resolve-config-value.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js +89 -2
- package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager-security.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/settings-manager-security.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/settings-manager-security.test.js +83 -0
- package/packages/pi-coding-agent/dist/core/settings-manager-security.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +14 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +36 -3
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts +1 -0
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +1 -0
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/armin.d.ts +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/armin.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/armin.js +9 -8
- package/packages/pi-coding-agent/dist/modes/interactive/components/armin.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +0 -3
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.js +2 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/bordered-loader.js +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/bordered-loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/branch-summary-message.js +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/compaction-summary-message.js +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/config-selector.js +5 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/config-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/countdown-timer.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/countdown-timer.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/countdown-timer.js +4 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/countdown-timer.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/custom-message.js +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/custom-message.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/daxnuts.d.ts +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/daxnuts.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/daxnuts.js +4 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/daxnuts.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/diff.js +2 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/diff.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js +8 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.js +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-selector.js +4 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js +26 -12
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/oauth-selector.js +4 -4
- package/packages/pi-coding-agent/dist/modes/interactive/components/oauth-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts +3 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +46 -14
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/scoped-models-selector.js +2 -8
- package/packages/pi-coding-agent/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/session-selector.js +4 -4
- package/packages/pi-coding-agent/dist/modes/interactive/components/session-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js +2 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +8 -3
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/user-message-selector.js +3 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/user-message-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +15 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js +16 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +27 -4
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.js +6 -0
- package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js.map +1 -1
- package/packages/pi-coding-agent/src/core/resolve-config-value.test.ts +111 -1
- package/packages/pi-coding-agent/src/core/resolve-config-value.ts +26 -2
- package/packages/pi-coding-agent/src/core/settings-manager-security.test.ts +102 -0
- package/packages/pi-coding-agent/src/core/settings-manager.ts +44 -3
- package/packages/pi-coding-agent/src/index.ts +5 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/armin.ts +9 -9
- package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +0 -2
- package/packages/pi-coding-agent/src/modes/interactive/components/bash-execution.ts +3 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/bordered-loader.ts +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/branch-summary-message.ts +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/compaction-summary-message.ts +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/config-selector.ts +7 -2
- package/packages/pi-coding-agent/src/modes/interactive/components/countdown-timer.ts +3 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/custom-message.ts +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/daxnuts.ts +4 -3
- package/packages/pi-coding-agent/src/modes/interactive/components/diff.ts +2 -2
- package/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.ts +3 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/extension-input.ts +1 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/extension-selector.ts +4 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +27 -13
- package/packages/pi-coding-agent/src/modes/interactive/components/oauth-selector.ts +4 -4
- package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +45 -14
- package/packages/pi-coding-agent/src/modes/interactive/components/scoped-models-selector.ts +2 -7
- package/packages/pi-coding-agent/src/modes/interactive/components/session-selector.ts +4 -4
- package/packages/pi-coding-agent/src/modes/interactive/components/skill-invocation-message.ts +2 -2
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +8 -3
- package/packages/pi-coding-agent/src/modes/interactive/components/user-message-selector.ts +3 -2
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +17 -1
- package/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.ts +14 -1
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +35 -3
- package/packages/pi-coding-agent/src/modes/interactive/slash-command-handlers.ts +7 -0
- package/packages/pi-coding-agent/src/modes/interactive/theme/themes.ts +1 -1
- package/pkg/dist/modes/interactive/theme/themes.js +1 -1
- package/pkg/dist/modes/interactive/theme/themes.js.map +1 -1
- package/src/resources/extensions/gsd/auto-worktree.ts +10 -7
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +10 -16
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +22 -1
- package/src/resources/extensions/gsd/codebase-generator.ts +351 -0
- package/src/resources/extensions/gsd/commands/catalog.ts +10 -1
- package/src/resources/extensions/gsd/commands/handlers/ops.ts +5 -0
- package/src/resources/extensions/gsd/commands-codebase.ts +164 -0
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +46 -4
- package/src/resources/extensions/gsd/complexity-classifier.ts +8 -6
- package/src/resources/extensions/gsd/doctor-git-checks.ts +49 -1
- package/src/resources/extensions/gsd/doctor-proactive.ts +35 -1
- package/src/resources/extensions/gsd/doctor-types.ts +2 -0
- package/src/resources/extensions/gsd/error-classifier.ts +3 -4
- package/src/resources/extensions/gsd/git-service.ts +93 -0
- package/src/resources/extensions/gsd/native-git-bridge.ts +24 -0
- package/src/resources/extensions/gsd/paths.ts +2 -0
- package/src/resources/extensions/gsd/preferences-types.ts +8 -0
- package/src/resources/extensions/gsd/tests/codebase-generator.test.ts +488 -0
- package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/integration/auto-worktree-milestone-merge.test.ts +33 -0
- package/src/resources/extensions/gsd/tests/integration/doctor-git.test.ts +72 -0
- package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +68 -0
- package/src/resources/extensions/gsd/tests/terminated-transient.test.ts +44 -0
- package/src/resources/extensions/gsd/watch/header-renderer.ts +275 -0
- package/src/resources/extensions/search-the-web/url-utils.ts +19 -0
- /package/dist/web/standalone/.next/static/{R0D4xaIPl5kg93edN7Oo0 → nUA6d2OJrDSVq9RNb-c8b}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{R0D4xaIPl5kg93edN7Oo0 → nUA6d2OJrDSVq9RNb-c8b}/_ssgManifest.js +0 -0
|
@@ -11,14 +11,16 @@ const UNIT_TYPE_TIERS = {
|
|
|
11
11
|
// Tier 1 — Light: structured summaries, completion, UAT
|
|
12
12
|
"complete-slice": "light",
|
|
13
13
|
"run-uat": "light",
|
|
14
|
-
// Tier 2 — Standard: research, routine
|
|
14
|
+
// Tier 2 — Standard: research, routine discussion
|
|
15
15
|
"discuss-milestone": "standard",
|
|
16
16
|
"discuss-slice": "standard",
|
|
17
17
|
"research-milestone": "standard",
|
|
18
18
|
"research-slice": "standard",
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
//
|
|
19
|
+
// Tier 3 — Heavy: planning, execution, replanning (requires deep reasoning)
|
|
20
|
+
// Planning is heavy so it uses the best configured model (e.g. Opus) and is
|
|
21
|
+
// not downgraded by dynamic routing when a capable model is configured.
|
|
22
|
+
"plan-milestone": "heavy",
|
|
23
|
+
"plan-slice": "heavy",
|
|
22
24
|
"execute-task": "standard", // default standard, upgraded by metadata
|
|
23
25
|
"replan-slice": "heavy",
|
|
24
26
|
"reassess-roadmap": "heavy",
|
|
@@ -124,8 +126,8 @@ function analyzePlanComplexity(unitId, basePath) {
|
|
|
124
126
|
// Check if this is a milestone-level plan (more complex) vs single slice
|
|
125
127
|
const { milestone: mid, slice: sid } = parseUnitId(unitId);
|
|
126
128
|
if (!sid) {
|
|
127
|
-
// Milestone-level planning is always
|
|
128
|
-
return { tier: "
|
|
129
|
+
// Milestone-level planning is always heavy — requires full context and best model
|
|
130
|
+
return { tier: "heavy", reason: "milestone-level planning" };
|
|
129
131
|
}
|
|
130
132
|
// For slice planning, try to read the context/research to gauge complexity
|
|
131
133
|
// If research exists and is large, bump to heavy
|
|
@@ -8,7 +8,7 @@ import { deriveState, isMilestoneComplete } from "./state.js";
|
|
|
8
8
|
import { listWorktrees, resolveGitDir, worktreesDir } from "./worktree-manager.js";
|
|
9
9
|
import { abortAndReset } from "./git-self-heal.js";
|
|
10
10
|
import { RUNTIME_EXCLUSION_PATHS, resolveMilestoneIntegrationBranch, writeIntegrationBranch } from "./git-service.js";
|
|
11
|
-
import { nativeIsRepo, nativeWorktreeList, nativeWorktreeRemove, nativeBranchList, nativeBranchDelete, nativeLsFiles, nativeRmCached } from "./native-git-bridge.js";
|
|
11
|
+
import { nativeIsRepo, nativeWorktreeList, nativeWorktreeRemove, nativeBranchList, nativeBranchDelete, nativeLsFiles, nativeRmCached, nativeHasChanges, nativeLastCommitEpoch, nativeGetCurrentBranch, nativeAddTracked, nativeCommit } from "./native-git-bridge.js";
|
|
12
12
|
import { getAllWorktreeHealth } from "./worktree-health.js";
|
|
13
13
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
14
14
|
/**
|
|
@@ -359,6 +359,53 @@ export async function checkGitHealth(basePath, issues, fixesApplied, shouldFix,
|
|
|
359
359
|
catch {
|
|
360
360
|
// Non-fatal — orphaned worktree directory check failed
|
|
361
361
|
}
|
|
362
|
+
// ── Stale uncommitted changes ────────────────────────────────────────────
|
|
363
|
+
// If the working tree has uncommitted changes and the last commit was
|
|
364
|
+
// longer ago than the configured threshold, flag it and optionally
|
|
365
|
+
// auto-commit a safety snapshot so work isn't lost.
|
|
366
|
+
try {
|
|
367
|
+
const prefs = loadEffectiveGSDPreferences()?.preferences ?? {};
|
|
368
|
+
const thresholdMinutes = prefs.stale_commit_threshold_minutes ?? 30;
|
|
369
|
+
if (thresholdMinutes > 0) {
|
|
370
|
+
const dirty = nativeHasChanges(basePath);
|
|
371
|
+
if (dirty) {
|
|
372
|
+
const branch = nativeGetCurrentBranch(basePath);
|
|
373
|
+
const lastEpoch = nativeLastCommitEpoch(basePath, branch || "HEAD");
|
|
374
|
+
const nowEpoch = Math.floor(Date.now() / 1000);
|
|
375
|
+
const minutesSinceCommit = lastEpoch > 0 ? (nowEpoch - lastEpoch) / 60 : Infinity;
|
|
376
|
+
if (minutesSinceCommit >= thresholdMinutes) {
|
|
377
|
+
const mins = Math.floor(minutesSinceCommit);
|
|
378
|
+
issues.push({
|
|
379
|
+
severity: "warning",
|
|
380
|
+
code: "stale_uncommitted_changes",
|
|
381
|
+
scope: "project",
|
|
382
|
+
unitId: "project",
|
|
383
|
+
message: `Uncommitted changes detected with no commit in ${mins} minute${mins === 1 ? "" : "s"} (threshold: ${thresholdMinutes}m). Snapshotting tracked files.`,
|
|
384
|
+
fixable: true,
|
|
385
|
+
});
|
|
386
|
+
if (shouldFix("stale_uncommitted_changes")) {
|
|
387
|
+
try {
|
|
388
|
+
nativeAddTracked(basePath);
|
|
389
|
+
const commitMsg = `gsd snapshot: uncommitted changes after ${mins}m inactivity`;
|
|
390
|
+
const result = nativeCommit(basePath, commitMsg);
|
|
391
|
+
if (result) {
|
|
392
|
+
fixesApplied.push(`created gsd snapshot after ${mins}m of uncommitted changes`);
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
fixesApplied.push("gsd snapshot skipped — nothing to commit after staging tracked files");
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
fixesApplied.push("failed to create gsd snapshot commit");
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
// Non-fatal — stale commit check failed
|
|
408
|
+
}
|
|
362
409
|
// ── Worktree lifecycle checks ──────────────────────────────────────────
|
|
363
410
|
// Check GSD-managed worktrees for: merged branches, stale work, dirty
|
|
364
411
|
// state, and unpushed commits. Only worktrees under .gsd/worktrees/.
|
|
@@ -21,7 +21,7 @@ import { abortAndReset } from "./git-self-heal.js";
|
|
|
21
21
|
import { rebuildState } from "./doctor.js";
|
|
22
22
|
import { deriveState } from "./state.js";
|
|
23
23
|
import { resolveMilestoneIntegrationBranch } from "./git-service.js";
|
|
24
|
-
import { nativeIsRepo } from "./native-git-bridge.js";
|
|
24
|
+
import { nativeIsRepo, nativeHasChanges, nativeLastCommitEpoch, nativeGetCurrentBranch, nativeAddTracked, nativeCommit } from "./native-git-bridge.js";
|
|
25
25
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
26
26
|
import { runEnvironmentChecks } from "./doctor-environment.js";
|
|
27
27
|
/** In-memory health history for the current auto-mode session. */
|
|
@@ -232,6 +232,39 @@ export async function preDispatchHealthGate(basePath) {
|
|
|
232
232
|
catch {
|
|
233
233
|
// Non-fatal — dispatch continues if state/branch check fails
|
|
234
234
|
}
|
|
235
|
+
// ── Stale uncommitted changes — auto-snapshot before dispatch ──
|
|
236
|
+
// If the working tree is dirty and no commit has happened recently,
|
|
237
|
+
// create a safety snapshot so work isn't lost if the next unit crashes.
|
|
238
|
+
try {
|
|
239
|
+
if (nativeIsRepo(basePath)) {
|
|
240
|
+
const prefs = loadEffectiveGSDPreferences()?.preferences ?? {};
|
|
241
|
+
const thresholdMinutes = prefs.stale_commit_threshold_minutes ?? 30;
|
|
242
|
+
if (thresholdMinutes > 0 && nativeHasChanges(basePath)) {
|
|
243
|
+
const branch = nativeGetCurrentBranch(basePath);
|
|
244
|
+
const lastEpoch = nativeLastCommitEpoch(basePath, branch || "HEAD");
|
|
245
|
+
const nowEpoch = Math.floor(Date.now() / 1000);
|
|
246
|
+
const minutesSinceCommit = lastEpoch > 0 ? (nowEpoch - lastEpoch) / 60 : Infinity;
|
|
247
|
+
if (minutesSinceCommit >= thresholdMinutes) {
|
|
248
|
+
const mins = Math.floor(minutesSinceCommit);
|
|
249
|
+
try {
|
|
250
|
+
nativeAddTracked(basePath);
|
|
251
|
+
const commitMsg = `gsd snapshot: pre-dispatch, uncommitted changes after ${mins}m inactivity`;
|
|
252
|
+
const result = nativeCommit(basePath, commitMsg);
|
|
253
|
+
if (result) {
|
|
254
|
+
fixesApplied.push(`pre-dispatch: created gsd snapshot after ${mins}m of uncommitted changes`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
// Non-blocking — snapshot failed but dispatch can continue
|
|
259
|
+
fixesApplied.push("pre-dispatch: gsd snapshot failed");
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
// Non-fatal
|
|
267
|
+
}
|
|
235
268
|
// ── Disk space check ──
|
|
236
269
|
// Catches low-disk conditions before dispatch rather than letting the unit
|
|
237
270
|
// fail mid-execution with ENOSPC (which wastes a full LLM turn).
|
|
@@ -24,7 +24,9 @@ const NETWORK_RE = /network|ECONNRESET|ETIMEDOUT|ECONNREFUSED|socket hang up|fet
|
|
|
24
24
|
const SERVER_RE = /internal server error|500|502|503|overloaded|server_error|api_error|service.?unavailable/i;
|
|
25
25
|
// ECONNRESET/ECONNREFUSED are in NETWORK_RE (same-model retry first).
|
|
26
26
|
const CONNECTION_RE = /terminated|connection.?refused|other side closed|EPIPE|network.?(?:is\s+)?unavailable|stream_exhausted(?:_without_result)?/i;
|
|
27
|
-
|
|
27
|
+
// Catch-all for V8 JSON.parse errors: all modern variants end with "in JSON at position \d+".
|
|
28
|
+
// This eliminates the need to enumerate every error message variant individually.
|
|
29
|
+
const STREAM_RE = /in JSON at position \d+|Unexpected end of JSON|SyntaxError.*JSON/i;
|
|
28
30
|
const RESET_DELAY_RE = /reset in (\d+)s/i;
|
|
29
31
|
/**
|
|
30
32
|
* Classify an error message into one of the ErrorClass kinds.
|
|
@@ -62,9 +64,6 @@ export function classifyError(errorMsg, retryAfterMs) {
|
|
|
62
64
|
return { kind: "network", retryAfterMs: retryAfterMs ?? 3_000 };
|
|
63
65
|
}
|
|
64
66
|
// 4. Stream truncation — downstream symptom of connection drop
|
|
65
|
-
// Checked before server/connection because JSON parse errors can contain
|
|
66
|
-
// substrings like "position 500" (matches SERVER_RE) or "Unterminated"
|
|
67
|
-
// (matches CONNECTION_RE's "terminated" pattern).
|
|
68
67
|
if (STREAM_RE.test(errorMsg)) {
|
|
69
68
|
return { kind: "stream", retryAfterMs: retryAfterMs ?? 15_000 };
|
|
70
69
|
}
|
|
@@ -15,7 +15,7 @@ import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
|
|
|
15
15
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
16
16
|
import { detectWorktreeName, } from "./worktree.js";
|
|
17
17
|
import { SLICE_BRANCH_RE, QUICK_BRANCH_RE, WORKFLOW_BRANCH_RE } from "./branch-patterns.js";
|
|
18
|
-
import { nativeGetCurrentBranch, nativeDetectMainBranch, nativeBranchExists, nativeHasChanges, nativeAddAllWithExclusions, nativeHasStagedChanges, nativeCommit, nativeRmCached, nativeUpdateRef, } from "./native-git-bridge.js";
|
|
18
|
+
import { nativeGetCurrentBranch, nativeDetectMainBranch, nativeBranchExists, nativeHasChanges, nativeAddAllWithExclusions, nativeHasStagedChanges, nativeCommit, nativeRmCached, nativeUpdateRef, nativeResetSoft, nativeCommitSubject, } from "./native-git-bridge.js";
|
|
19
19
|
import { GSDError, GSD_MERGE_CONFLICT, GSD_GIT_ERROR } from "./errors.js";
|
|
20
20
|
import { getErrorMessage } from "./error-utils.js";
|
|
21
21
|
export const VALID_BRANCH_NAME = /^[a-zA-Z0-9_\-\/.]+$/;
|
|
@@ -412,8 +412,89 @@ export class GitServiceImpl {
|
|
|
412
412
|
? buildTaskCommitMessage(taskContext)
|
|
413
413
|
: `chore: auto-commit after ${unitType}\n\nGSD-Unit: ${unitId}`;
|
|
414
414
|
nativeCommit(this.basePath, message, { allowEmpty: false });
|
|
415
|
+
// Absorb any preceding gsd snapshot commits into this real commit.
|
|
416
|
+
// Walk backwards from HEAD~1 counting consecutive snapshot subjects,
|
|
417
|
+
// then soft-reset to before them and re-commit with the same message.
|
|
418
|
+
this.absorbSnapshotCommits(message);
|
|
415
419
|
return message;
|
|
416
420
|
}
|
|
421
|
+
/**
|
|
422
|
+
* Squash consecutive `gsd snapshot:` commits that sit immediately below
|
|
423
|
+
* HEAD into the current HEAD commit. This keeps the git history clean
|
|
424
|
+
* after automated snapshot commits are superseded by real work.
|
|
425
|
+
*
|
|
426
|
+
* Guards:
|
|
427
|
+
* - Opt-in via `absorb_snapshot_commits` preference (default: true).
|
|
428
|
+
* - Refuses to rewrite commits that have been pushed to the remote
|
|
429
|
+
* tracking branch (checks merge-base ancestry).
|
|
430
|
+
* - Saves HEAD SHA before reset; restores it if the re-commit fails.
|
|
431
|
+
*
|
|
432
|
+
* Does nothing if there are no snapshot commits to absorb.
|
|
433
|
+
*/
|
|
434
|
+
absorbSnapshotCommits(headMessage) {
|
|
435
|
+
try {
|
|
436
|
+
// Opt-in guard — users can disable to keep snapshot commits for forensics
|
|
437
|
+
if (this.prefs.absorb_snapshot_commits === false)
|
|
438
|
+
return;
|
|
439
|
+
const GSD_SNAPSHOT_PREFIX = "gsd snapshot:";
|
|
440
|
+
let count = 0;
|
|
441
|
+
// Walk back from HEAD~1 counting consecutive snapshot commits (cap at 10)
|
|
442
|
+
for (let i = 1; i <= 10; i++) {
|
|
443
|
+
const subject = nativeCommitSubject(this.basePath, `HEAD~${i}`);
|
|
444
|
+
if (!subject.startsWith(GSD_SNAPSHOT_PREFIX))
|
|
445
|
+
break;
|
|
446
|
+
count = i;
|
|
447
|
+
}
|
|
448
|
+
if (count === 0)
|
|
449
|
+
return;
|
|
450
|
+
// Guard: don't rewrite history that has been pushed to the remote.
|
|
451
|
+
// Check whether the newest snapshot commit (HEAD~1) is already
|
|
452
|
+
// reachable from the remote tracking branch. If it is, the snapshots
|
|
453
|
+
// have been pushed and must not be squashed via local history rewrite.
|
|
454
|
+
// (Checking resetTarget instead would false-positive when the remote
|
|
455
|
+
// is at the pre-snapshot base but the snapshots themselves are local.)
|
|
456
|
+
const resetTarget = `HEAD~${count + 1}`;
|
|
457
|
+
try {
|
|
458
|
+
const branch = nativeGetCurrentBranch(this.basePath);
|
|
459
|
+
if (branch) {
|
|
460
|
+
const remoteBranch = `origin/${branch}`;
|
|
461
|
+
// merge-base --is-ancestor exits 0 if HEAD~1 is ancestor of remote
|
|
462
|
+
execFileSync("git", ["merge-base", "--is-ancestor", "HEAD~1", remoteBranch], {
|
|
463
|
+
cwd: this.basePath,
|
|
464
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
465
|
+
});
|
|
466
|
+
// If we get here, newest snapshot IS reachable from remote — already pushed
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
// Not an ancestor or remote doesn't exist — safe to proceed
|
|
472
|
+
}
|
|
473
|
+
// Save HEAD SHA so we can restore if the re-commit fails
|
|
474
|
+
const savedHead = execFileSync("git", ["rev-parse", "HEAD"], {
|
|
475
|
+
cwd: this.basePath,
|
|
476
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
477
|
+
encoding: "utf-8",
|
|
478
|
+
}).trim();
|
|
479
|
+
nativeResetSoft(this.basePath, resetTarget);
|
|
480
|
+
// Re-run smartStage so the same RUNTIME_EXCLUSION_PATHS apply.
|
|
481
|
+
// Snapshot commits used nativeAddTracked (git add -u) which stages
|
|
482
|
+
// ALL tracked modifications including .gsd/ state files. Without
|
|
483
|
+
// re-staging, those .gsd/ changes leak into the absorbed commit.
|
|
484
|
+
this.smartStage();
|
|
485
|
+
try {
|
|
486
|
+
nativeCommit(this.basePath, headMessage, { allowEmpty: false });
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
// Re-commit failed — restore original HEAD to avoid leaving the
|
|
490
|
+
// repo in a partially-reset state with no commit
|
|
491
|
+
nativeResetSoft(this.basePath, savedHead);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
catch {
|
|
495
|
+
// Non-fatal — if squash fails, the commits remain unsquashed
|
|
496
|
+
}
|
|
497
|
+
}
|
|
417
498
|
// ─── Branch Queries ────────────────────────────────────────────────────
|
|
418
499
|
/**
|
|
419
500
|
* Get the integration branch for this repo — the branch that slice
|
|
@@ -525,6 +525,15 @@ export function nativeAddAll(basePath) {
|
|
|
525
525
|
}
|
|
526
526
|
gitFileExec(basePath, ["add", "-A"]);
|
|
527
527
|
}
|
|
528
|
+
/**
|
|
529
|
+
* Stage only already-tracked files (git add -u).
|
|
530
|
+
* Does NOT add new untracked files — only updates modifications and deletions
|
|
531
|
+
* for files git already knows about. Safe for automated snapshots where
|
|
532
|
+
* pulling in unknown untracked files (secrets, binaries) would be dangerous.
|
|
533
|
+
*/
|
|
534
|
+
export function nativeAddTracked(basePath) {
|
|
535
|
+
gitFileExec(basePath, ["add", "-u"]);
|
|
536
|
+
}
|
|
528
537
|
/**
|
|
529
538
|
* Stage all files with pathspec exclusions (git add -A -- ':!pattern' ...).
|
|
530
539
|
* Excluded paths are never hashed by git, preventing hangs on large
|
|
@@ -758,6 +767,19 @@ export function nativeResetHard(basePath) {
|
|
|
758
767
|
}
|
|
759
768
|
execSync("git reset --hard HEAD", { cwd: basePath, stdio: "pipe" });
|
|
760
769
|
}
|
|
770
|
+
/**
|
|
771
|
+
* Soft reset to a target ref (git reset --soft <ref>).
|
|
772
|
+
* Moves HEAD to `target` while keeping all changes staged in the index.
|
|
773
|
+
* Used to squash snapshot commits back into a single real commit.
|
|
774
|
+
*/
|
|
775
|
+
export function nativeResetSoft(basePath, target) {
|
|
776
|
+
execFileSync("git", ["reset", "--soft", target], {
|
|
777
|
+
cwd: basePath,
|
|
778
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
779
|
+
encoding: "utf-8",
|
|
780
|
+
env: GIT_NO_PROMPT_ENV,
|
|
781
|
+
});
|
|
782
|
+
}
|
|
761
783
|
/**
|
|
762
784
|
* Get the subject line of a commit (git log -1 --format=%s <ref>).
|
|
763
785
|
* Returns empty string if the ref doesn't exist.
|
|
@@ -254,6 +254,7 @@ export const GSD_ROOT_FILES = {
|
|
|
254
254
|
REQUIREMENTS: "REQUIREMENTS.md",
|
|
255
255
|
OVERRIDES: "OVERRIDES.md",
|
|
256
256
|
KNOWLEDGE: "KNOWLEDGE.md",
|
|
257
|
+
CODEBASE: "CODEBASE.md",
|
|
257
258
|
};
|
|
258
259
|
const LEGACY_GSD_ROOT_FILES = {
|
|
259
260
|
PROJECT: "project.md",
|
|
@@ -263,6 +264,7 @@ const LEGACY_GSD_ROOT_FILES = {
|
|
|
263
264
|
REQUIREMENTS: "requirements.md",
|
|
264
265
|
OVERRIDES: "overrides.md",
|
|
265
266
|
KNOWLEDGE: "knowledge.md",
|
|
267
|
+
CODEBASE: "codebase.md",
|
|
266
268
|
};
|
|
267
269
|
// ─── GSD Root Discovery ───────────────────────────────────────────────────────
|
|
268
270
|
const gsdRootCache = new Map();
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// GSD Watch — Header renderer: ASCII logo, session info, MCP status, remote questions
|
|
2
|
+
// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { visibleWidth, truncateToWidth } from "@gsd/pi-tui";
|
|
8
|
+
import { loadEffectiveGSDPreferences } from "../preferences.js";
|
|
9
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
10
|
+
/**
|
|
11
|
+
* GSD ASCII logo — inlined here because the canonical src/logo.ts is outside
|
|
12
|
+
* the resources rootDir and cannot be imported directly.
|
|
13
|
+
*/
|
|
14
|
+
const GSD_LOGO = [
|
|
15
|
+
' ██████╗ ███████╗██████╗ ',
|
|
16
|
+
' ██╔════╝ ██╔════╝██╔══██╗',
|
|
17
|
+
' ██║ ███╗███████╗██║ ██║',
|
|
18
|
+
' ██║ ██║╚════██║██║ ██║',
|
|
19
|
+
' ╚██████╔╝███████║██████╔╝',
|
|
20
|
+
' ╚═════╝ ╚══════╝╚═════╝ ',
|
|
21
|
+
];
|
|
22
|
+
/** Separator character for the horizontal divider line. */
|
|
23
|
+
const SEPARATOR_CHAR = "─";
|
|
24
|
+
/** Vertical bar between logo and info panel. */
|
|
25
|
+
const PANEL_DIVIDER = "│";
|
|
26
|
+
/** Label column width for Model/Provider/Directory/Branch rows. */
|
|
27
|
+
const LABEL_COL_WIDTH = 10;
|
|
28
|
+
// ─── Data Readers ─────────────────────────────────────────────────────────────
|
|
29
|
+
/**
|
|
30
|
+
* Read the configured execution model from GSD preferences.
|
|
31
|
+
* Falls back through execution -> planning -> research -> first found.
|
|
32
|
+
* Returns "default" if nothing is configured.
|
|
33
|
+
*/
|
|
34
|
+
export function readModelFromPreferences() {
|
|
35
|
+
try {
|
|
36
|
+
const prefs = loadEffectiveGSDPreferences();
|
|
37
|
+
if (!prefs?.preferences.models)
|
|
38
|
+
return "default";
|
|
39
|
+
const m = prefs.preferences.models;
|
|
40
|
+
// Try common phases in priority order
|
|
41
|
+
for (const phase of ["execution", "planning", "research", "discuss", "subagent"]) {
|
|
42
|
+
const val = m[phase];
|
|
43
|
+
if (typeof val === "string")
|
|
44
|
+
return val;
|
|
45
|
+
if (val && typeof val === "object" && "model" in val) {
|
|
46
|
+
const model = val.model;
|
|
47
|
+
if (typeof model === "string")
|
|
48
|
+
return model;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Non-fatal
|
|
54
|
+
}
|
|
55
|
+
return "default";
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Derive provider name from model ID prefix.
|
|
59
|
+
*/
|
|
60
|
+
export function deriveProvider(modelId) {
|
|
61
|
+
if (modelId.startsWith("claude"))
|
|
62
|
+
return "anthropic";
|
|
63
|
+
if (modelId.startsWith("gpt") || modelId.startsWith("o1") || modelId.startsWith("o3"))
|
|
64
|
+
return "openai";
|
|
65
|
+
if (modelId.startsWith("gemini"))
|
|
66
|
+
return "google";
|
|
67
|
+
if (modelId.startsWith("deepseek"))
|
|
68
|
+
return "deepseek";
|
|
69
|
+
if (modelId === "default")
|
|
70
|
+
return "anthropic";
|
|
71
|
+
return "unknown";
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Shorten a directory path by replacing the home directory with ~.
|
|
75
|
+
*/
|
|
76
|
+
export function shortenPath(fullPath) {
|
|
77
|
+
const home = homedir();
|
|
78
|
+
if (fullPath.startsWith(home)) {
|
|
79
|
+
return "~" + fullPath.slice(home.length);
|
|
80
|
+
}
|
|
81
|
+
return fullPath;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Read the current git branch name. Returns "unknown" on failure.
|
|
85
|
+
*/
|
|
86
|
+
export function readGitBranch(projectRoot) {
|
|
87
|
+
try {
|
|
88
|
+
return execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
89
|
+
cwd: projectRoot,
|
|
90
|
+
encoding: "utf-8",
|
|
91
|
+
timeout: 2000,
|
|
92
|
+
}).trim();
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return "unknown";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Read MCP server names from .mcp.json or .gsd/mcp.json.
|
|
100
|
+
* Returns array of server name strings.
|
|
101
|
+
*/
|
|
102
|
+
export function readMcpServerNames(projectRoot) {
|
|
103
|
+
const configPaths = [
|
|
104
|
+
join(projectRoot, ".mcp.json"),
|
|
105
|
+
join(projectRoot, ".gsd", "mcp.json"),
|
|
106
|
+
];
|
|
107
|
+
const names = [];
|
|
108
|
+
const seen = new Set();
|
|
109
|
+
for (const configPath of configPaths) {
|
|
110
|
+
try {
|
|
111
|
+
if (!existsSync(configPath))
|
|
112
|
+
continue;
|
|
113
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
114
|
+
const data = JSON.parse(raw);
|
|
115
|
+
const mcpServers = (data.mcpServers ?? data.servers);
|
|
116
|
+
if (!mcpServers || typeof mcpServers !== "object")
|
|
117
|
+
continue;
|
|
118
|
+
for (const name of Object.keys(mcpServers)) {
|
|
119
|
+
if (!seen.has(name)) {
|
|
120
|
+
seen.add(name);
|
|
121
|
+
names.push(name);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// Non-fatal
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return names;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Gather all header data from filesystem and preferences.
|
|
133
|
+
*/
|
|
134
|
+
export function gatherHeaderData(projectRoot) {
|
|
135
|
+
const model = readModelFromPreferences();
|
|
136
|
+
const provider = deriveProvider(model);
|
|
137
|
+
const directory = shortenPath(projectRoot);
|
|
138
|
+
const branch = readGitBranch(projectRoot);
|
|
139
|
+
const mcpServers = readMcpServerNames(projectRoot);
|
|
140
|
+
return { model, provider, directory, branch, mcpServers };
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Build an info panel line: "Label value" with proper padding.
|
|
144
|
+
* Returns empty string if value is empty.
|
|
145
|
+
*/
|
|
146
|
+
function formatInfoLine(label, value, availableWidth) {
|
|
147
|
+
const bold = `\x1b[1m${label}\x1b[0m`;
|
|
148
|
+
const labelVis = visibleWidth(bold);
|
|
149
|
+
const padding = " ".repeat(Math.max(1, LABEL_COL_WIDTH - labelVis));
|
|
150
|
+
const maxValueWidth = Math.max(1, availableWidth - LABEL_COL_WIDTH);
|
|
151
|
+
const truncValue = truncateToWidth(value, maxValueWidth, "…");
|
|
152
|
+
return bold + padding + truncValue;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Format MCP server names as a dot-separated row with checkmarks.
|
|
156
|
+
* e.g. "Brave ✓ · Answers ✓ · Context7 ✓"
|
|
157
|
+
*/
|
|
158
|
+
export function formatMcpRow(servers, width) {
|
|
159
|
+
if (servers.length === 0)
|
|
160
|
+
return "";
|
|
161
|
+
// Capitalize first letter of each server name
|
|
162
|
+
const items = servers.map(s => {
|
|
163
|
+
const cap = s.charAt(0).toUpperCase() + s.slice(1);
|
|
164
|
+
return `${cap} ✓`;
|
|
165
|
+
});
|
|
166
|
+
const full = items.join(" · ");
|
|
167
|
+
if (visibleWidth(full) <= width)
|
|
168
|
+
return full;
|
|
169
|
+
// Truncate if too wide
|
|
170
|
+
return truncateToWidth(full, width, "…");
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Render the full header as an array of terminal-safe strings.
|
|
174
|
+
*
|
|
175
|
+
* Layout: GSD ASCII logo on the left, info panel on the right separated by │.
|
|
176
|
+
* Below: MCP server row, remote questions row, separator line.
|
|
177
|
+
*/
|
|
178
|
+
export function renderHeaderLines(data, width) {
|
|
179
|
+
const lines = [];
|
|
180
|
+
// Logo is 6 lines tall. Info panel has: title + blank + model + provider + directory + branch = 6 lines
|
|
181
|
+
const logoLines = GSD_LOGO;
|
|
182
|
+
const logoWidth = Math.max(...logoLines.map(l => visibleWidth(l)));
|
|
183
|
+
// Calculate available width for the info panel
|
|
184
|
+
// Layout: logo + " " + "│" + " " = logoWidth + 3
|
|
185
|
+
const dividerOverhead = 3; // " │ "
|
|
186
|
+
const infoPanelWidth = width - logoWidth - dividerOverhead;
|
|
187
|
+
// If terminal is too narrow for side-by-side, fall back to stacked layout
|
|
188
|
+
if (infoPanelWidth < 20) {
|
|
189
|
+
return renderStackedHeader(data, width);
|
|
190
|
+
}
|
|
191
|
+
// Build info panel lines (6 lines to match logo height)
|
|
192
|
+
const infoLines = [
|
|
193
|
+
`\x1b[1mGet Shit Done\x1b[0m`,
|
|
194
|
+
"",
|
|
195
|
+
formatInfoLine("Model", data.model, infoPanelWidth),
|
|
196
|
+
formatInfoLine("Provider", data.provider, infoPanelWidth),
|
|
197
|
+
formatInfoLine("Directory", data.directory, infoPanelWidth),
|
|
198
|
+
formatInfoLine("Branch", data.branch, infoPanelWidth),
|
|
199
|
+
];
|
|
200
|
+
// Merge logo and info panel side by side
|
|
201
|
+
const maxLines = Math.max(logoLines.length, infoLines.length);
|
|
202
|
+
for (let i = 0; i < maxLines; i++) {
|
|
203
|
+
const logoLine = i < logoLines.length ? logoLines[i] : "";
|
|
204
|
+
const infoLine = i < infoLines.length ? infoLines[i] : "";
|
|
205
|
+
// Pad logo line to consistent width
|
|
206
|
+
const logoPad = " ".repeat(Math.max(0, logoWidth - visibleWidth(logoLine)));
|
|
207
|
+
lines.push(`${logoLine}${logoPad} ${PANEL_DIVIDER} ${infoLine}`);
|
|
208
|
+
}
|
|
209
|
+
// Blank line after logo+info block
|
|
210
|
+
lines.push("");
|
|
211
|
+
// MCP server row
|
|
212
|
+
const mcpRow = formatMcpRow(data.mcpServers, width);
|
|
213
|
+
if (mcpRow) {
|
|
214
|
+
lines.push(` ${mcpRow}`);
|
|
215
|
+
}
|
|
216
|
+
// Separator line
|
|
217
|
+
lines.push(SEPARATOR_CHAR.repeat(width));
|
|
218
|
+
return lines;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Fallback stacked layout for narrow terminals (< 20 cols for info panel).
|
|
222
|
+
*/
|
|
223
|
+
function renderStackedHeader(data, width) {
|
|
224
|
+
const lines = [];
|
|
225
|
+
// Title
|
|
226
|
+
lines.push(`\x1b[1mGet Shit Done\x1b[0m`);
|
|
227
|
+
lines.push("");
|
|
228
|
+
// Info
|
|
229
|
+
lines.push(formatInfoLine("Model", data.model, width));
|
|
230
|
+
lines.push(formatInfoLine("Provider", data.provider, width));
|
|
231
|
+
lines.push(formatInfoLine("Directory", data.directory, width));
|
|
232
|
+
lines.push(formatInfoLine("Branch", data.branch, width));
|
|
233
|
+
lines.push("");
|
|
234
|
+
// MCP
|
|
235
|
+
const mcpRow = formatMcpRow(data.mcpServers, width);
|
|
236
|
+
if (mcpRow)
|
|
237
|
+
lines.push(` ${mcpRow}`);
|
|
238
|
+
// Separator
|
|
239
|
+
lines.push(SEPARATOR_CHAR.repeat(width));
|
|
240
|
+
return lines;
|
|
241
|
+
}
|
|
@@ -18,12 +18,29 @@ const PRIVATE_IP_PATTERNS = [
|
|
|
18
18
|
/^fd/i,
|
|
19
19
|
/^fe80:/i,
|
|
20
20
|
];
|
|
21
|
+
/**
|
|
22
|
+
* Hostnames exempted from SSRF blocking. Set via setFetchAllowedUrls()
|
|
23
|
+
* from global settings.json or GSD_FETCH_ALLOWED_URLS env var.
|
|
24
|
+
*/
|
|
25
|
+
let fetchAllowedHostnames = new Set();
|
|
26
|
+
/**
|
|
27
|
+
* Replace the fetch URL allowlist (hostnames exempted from SSRF checks).
|
|
28
|
+
*/
|
|
29
|
+
export function setFetchAllowedUrls(hostnames) {
|
|
30
|
+
fetchAllowedHostnames = new Set(hostnames.map((h) => h.toLowerCase()));
|
|
31
|
+
}
|
|
32
|
+
/** Get the currently active fetch URL allowlist. */
|
|
33
|
+
export function getFetchAllowedUrls() {
|
|
34
|
+
return [...fetchAllowedHostnames];
|
|
35
|
+
}
|
|
21
36
|
export function isBlockedUrl(url) {
|
|
22
37
|
try {
|
|
23
38
|
const parsed = new URL(url);
|
|
24
39
|
if (parsed.protocol !== "https:" && parsed.protocol !== "http:")
|
|
25
40
|
return true;
|
|
26
41
|
const hostname = parsed.hostname.toLowerCase();
|
|
42
|
+
if (fetchAllowedHostnames.has(hostname))
|
|
43
|
+
return false;
|
|
27
44
|
if (BLOCKED_HOSTNAMES.has(hostname))
|
|
28
45
|
return true;
|
|
29
46
|
for (const pattern of PRIVATE_IP_PATTERNS) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apply user-configured security overrides from global settings.json and env vars.
|
|
3
|
+
*
|
|
4
|
+
* Both overrides are global-only (not project-level) because the threat model is
|
|
5
|
+
* malicious project-level config in cloned repos. Global settings and env vars
|
|
6
|
+
* represent the user's own authority on their machine.
|
|
7
|
+
*
|
|
8
|
+
* Precedence: env var > settings.json > built-in defaults
|
|
9
|
+
*/
|
|
10
|
+
import { type SettingsManager } from '@gsd/pi-coding-agent';
|
|
11
|
+
export declare function applySecurityOverrides(settingsManager: SettingsManager): void;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apply user-configured security overrides from global settings.json and env vars.
|
|
3
|
+
*
|
|
4
|
+
* Both overrides are global-only (not project-level) because the threat model is
|
|
5
|
+
* malicious project-level config in cloned repos. Global settings and env vars
|
|
6
|
+
* represent the user's own authority on their machine.
|
|
7
|
+
*
|
|
8
|
+
* Precedence: env var > settings.json > built-in defaults
|
|
9
|
+
*/
|
|
10
|
+
import { setAllowedCommandPrefixes } from '@gsd/pi-coding-agent';
|
|
11
|
+
import { setFetchAllowedUrls } from './resources/extensions/search-the-web/url-utils.js';
|
|
12
|
+
export function applySecurityOverrides(settingsManager) {
|
|
13
|
+
// --- Command prefix allowlist ---
|
|
14
|
+
const envPrefixes = process.env.GSD_ALLOWED_COMMAND_PREFIXES;
|
|
15
|
+
if (envPrefixes) {
|
|
16
|
+
const prefixes = envPrefixes.split(',').map(s => s.trim()).filter(Boolean);
|
|
17
|
+
if (prefixes.length > 0) {
|
|
18
|
+
setAllowedCommandPrefixes(prefixes);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
const settingsPrefixes = settingsManager.getAllowedCommandPrefixes();
|
|
23
|
+
if (settingsPrefixes && settingsPrefixes.length > 0) {
|
|
24
|
+
setAllowedCommandPrefixes(settingsPrefixes);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// --- Fetch URL allowlist (SSRF exemptions) ---
|
|
28
|
+
const envUrls = process.env.GSD_FETCH_ALLOWED_URLS;
|
|
29
|
+
if (envUrls) {
|
|
30
|
+
const urls = envUrls.split(',').map(s => s.trim()).filter(Boolean);
|
|
31
|
+
if (urls.length > 0) {
|
|
32
|
+
setFetchAllowedUrls(urls);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
const settingsUrls = settingsManager.getFetchAllowedUrls();
|
|
37
|
+
if (settingsUrls && settingsUrls.length > 0) {
|
|
38
|
+
setFetchAllowedUrls(settingsUrls);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
nUA6d2OJrDSVq9RNb-c8b
|