gsd-pi 2.44.0-dev.0b97ffd → 2.44.0-dev.73f2fd5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/resources/extensions/gsd/auto/infra-errors.js +3 -0
- package/dist/resources/extensions/gsd/auto/phases.js +36 -36
- package/dist/resources/extensions/gsd/auto-prompts.js +24 -1
- package/dist/resources/extensions/gsd/auto-timers.js +57 -3
- package/dist/resources/extensions/gsd/auto-worktree-sync.js +4 -0
- package/dist/resources/extensions/gsd/auto-worktree.js +9 -6
- package/dist/resources/extensions/gsd/auto.js +30 -3
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +156 -0
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +46 -12
- package/dist/resources/extensions/gsd/commands/catalog.js +6 -1
- package/dist/resources/extensions/gsd/commands/handlers/core.js +1 -0
- package/dist/resources/extensions/gsd/commands/handlers/ops.js +5 -0
- package/dist/resources/extensions/gsd/commands-mcp-status.js +187 -0
- package/dist/resources/extensions/gsd/db-writer.js +34 -16
- package/dist/resources/extensions/gsd/doctor.js +8 -0
- package/dist/resources/extensions/gsd/git-service.js +8 -3
- package/dist/resources/extensions/gsd/gsd-db.js +12 -1
- package/dist/resources/extensions/gsd/markdown-renderer.js +1 -1
- package/dist/resources/extensions/gsd/preferences.js +9 -1
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +2 -4
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -6
- package/dist/resources/extensions/gsd/prompts/replan-slice.md +3 -14
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +7 -37
- package/dist/resources/extensions/gsd/provider-error-pause.js +7 -0
- package/dist/resources/extensions/gsd/state.js +19 -2
- package/dist/resources/extensions/gsd/tools/plan-slice.js +1 -0
- package/dist/resources/extensions/gsd/tools/plan-task.js +1 -0
- package/dist/resources/extensions/gsd/tools/replan-slice.js +2 -0
- package/dist/resources/extensions/gsd/tools/validate-milestone.js +88 -0
- package/dist/resources/extensions/gsd/worktree-resolver.js +6 -0
- package/dist/resources/extensions/mcp-client/index.js +14 -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/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.d.ts +3 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.js +15 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/local-model-check.d.ts +15 -0
- package/packages/pi-coding-agent/dist/core/local-model-check.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/local-model-check.js +41 -0
- package/packages/pi-coding-agent/dist/core/local-model-check.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts +11 -0
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.js +20 -1
- package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -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 +6 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/main.js +17 -0
- package/packages/pi-coding-agent/dist/main.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/timestamp.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/timestamp.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/timestamp.test.js +32 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/timestamp.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts +3 -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 +8 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +12 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/timestamp.d.ts +15 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/timestamp.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/timestamp.js +40 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/timestamp.js.map +1 -0
- 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 +4 -1
- 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.d.ts +5 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.js +13 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.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 +17 -8
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- 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 +7 -3
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/core/auth-storage.ts +15 -1
- package/packages/pi-coding-agent/src/core/local-model-check.ts +45 -0
- package/packages/pi-coding-agent/src/core/model-registry.ts +21 -1
- package/packages/pi-coding-agent/src/core/settings-manager.ts +9 -0
- package/packages/pi-coding-agent/src/main.ts +19 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/timestamp.test.ts +38 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +10 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +15 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/timestamp.ts +48 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +3 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/user-message.ts +18 -3
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +16 -7
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +8 -1
- package/src/resources/extensions/gsd/auto/infra-errors.ts +3 -0
- package/src/resources/extensions/gsd/auto/phases.ts +45 -48
- package/src/resources/extensions/gsd/auto-prompts.ts +24 -1
- package/src/resources/extensions/gsd/auto-timers.ts +64 -3
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +5 -0
- package/src/resources/extensions/gsd/auto-worktree.ts +9 -6
- package/src/resources/extensions/gsd/auto.ts +37 -3
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +148 -0
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +48 -11
- package/src/resources/extensions/gsd/commands/catalog.ts +6 -1
- package/src/resources/extensions/gsd/commands/handlers/core.ts +1 -0
- package/src/resources/extensions/gsd/commands/handlers/ops.ts +5 -0
- package/src/resources/extensions/gsd/commands-mcp-status.ts +247 -0
- package/src/resources/extensions/gsd/db-writer.ts +39 -17
- package/src/resources/extensions/gsd/doctor.ts +7 -1
- package/src/resources/extensions/gsd/git-service.ts +6 -2
- package/src/resources/extensions/gsd/gsd-db.ts +16 -1
- package/src/resources/extensions/gsd/markdown-renderer.ts +1 -1
- package/src/resources/extensions/gsd/preferences.ts +11 -1
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +2 -4
- package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -6
- package/src/resources/extensions/gsd/prompts/replan-slice.md +3 -14
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +7 -37
- package/src/resources/extensions/gsd/provider-error-pause.ts +9 -0
- package/src/resources/extensions/gsd/state.ts +19 -1
- package/src/resources/extensions/gsd/tests/auto-pr-bugs.test.ts +88 -0
- package/src/resources/extensions/gsd/tests/completed-units-metrics-sync.test.ts +114 -0
- package/src/resources/extensions/gsd/tests/db-writer.test.ts +79 -0
- package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +60 -0
- package/src/resources/extensions/gsd/tests/est-annotation-timeout.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/infra-error.test.ts +20 -2
- package/src/resources/extensions/gsd/tests/knowledge.test.ts +89 -0
- package/src/resources/extensions/gsd/tests/mcp-status.test.ts +103 -0
- package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +66 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +27 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +11 -7
- package/src/resources/extensions/gsd/tests/stop-auto-merge-back.test.ts +67 -0
- package/src/resources/extensions/gsd/tests/terminated-transient.test.ts +49 -0
- package/src/resources/extensions/gsd/tests/tool-naming.test.ts +2 -1
- package/src/resources/extensions/gsd/tools/plan-slice.ts +2 -0
- package/src/resources/extensions/gsd/tools/plan-task.ts +2 -0
- package/src/resources/extensions/gsd/tools/replan-slice.ts +3 -0
- package/src/resources/extensions/gsd/tools/validate-milestone.ts +127 -0
- package/src/resources/extensions/gsd/worktree-resolver.ts +7 -0
- package/src/resources/extensions/mcp-client/index.ts +20 -0
- /package/dist/web/standalone/.next/static/{alS4hoANx0TK4UVZY27da → kxxAA66bah_yhPYqLBHE2}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{alS4hoANx0TK4UVZY27da → kxxAA66bah_yhPYqLBHE2}/_ssgManifest.js +0 -0
|
@@ -744,7 +744,21 @@ export class AuthStorage {
|
|
|
744
744
|
* @param providerId - The provider to get an API key for
|
|
745
745
|
* @param sessionId - Optional session ID for sticky credential selection
|
|
746
746
|
*/
|
|
747
|
-
async getApiKey(providerId: string, sessionId?: string): Promise<string | undefined> {
|
|
747
|
+
async getApiKey(providerId: string, sessionId?: string, options?: { baseUrl?: string }): Promise<string | undefined> {
|
|
748
|
+
// If the model has a local baseUrl, return a dummy key to avoid auth blocking
|
|
749
|
+
if (options?.baseUrl) {
|
|
750
|
+
try {
|
|
751
|
+
const hostname = new URL(options.baseUrl).hostname;
|
|
752
|
+
if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "0.0.0.0" || hostname === "::1") {
|
|
753
|
+
return "local-no-key-needed";
|
|
754
|
+
}
|
|
755
|
+
} catch {
|
|
756
|
+
if (options.baseUrl.startsWith("unix:")) {
|
|
757
|
+
return "local-no-key-needed";
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
748
762
|
// Runtime override takes highest priority
|
|
749
763
|
const runtimeKey = this.runtimeOverrides.get(providerId);
|
|
750
764
|
if (runtimeKey) {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* local-model-check.ts — Utility to detect if a model baseUrl is local.
|
|
3
|
+
*
|
|
4
|
+
* Leaf module with zero transitive dependencies on TypeScript parameter properties.
|
|
5
|
+
* Used by ModelRegistry and tests.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Check if a model's baseUrl points to a local endpoint.
|
|
10
|
+
* Returns true for localhost, 127.0.0.1, 0.0.0.0, ::1, or unix socket paths.
|
|
11
|
+
* Returns false if baseUrl is empty (cloud provider) or points to a remote host.
|
|
12
|
+
*/
|
|
13
|
+
export function isLocalModel(model: { baseUrl: string }): boolean {
|
|
14
|
+
const url = model.baseUrl;
|
|
15
|
+
if (!url) return false;
|
|
16
|
+
|
|
17
|
+
// Unix socket paths
|
|
18
|
+
if (url.startsWith("unix://") || url.startsWith("unix:")) return true;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const parsed = new URL(url);
|
|
22
|
+
const hostname = parsed.hostname;
|
|
23
|
+
if (
|
|
24
|
+
hostname === "localhost" ||
|
|
25
|
+
hostname === "127.0.0.1" ||
|
|
26
|
+
hostname === "0.0.0.0" ||
|
|
27
|
+
hostname === "::1" ||
|
|
28
|
+
hostname === "[::1]"
|
|
29
|
+
) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
} catch {
|
|
33
|
+
// If URL parsing fails, check raw string for local patterns
|
|
34
|
+
if (
|
|
35
|
+
url.includes("localhost") ||
|
|
36
|
+
url.includes("127.0.0.1") ||
|
|
37
|
+
url.includes("0.0.0.0") ||
|
|
38
|
+
url.includes("[::1]")
|
|
39
|
+
) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
@@ -28,6 +28,7 @@ import { ModelDiscoveryCache } from "./discovery-cache.js";
|
|
|
28
28
|
import type { DiscoveredModel, DiscoveryResult } from "./model-discovery.js";
|
|
29
29
|
import { getDefaultTTL, getDiscoverableProviders, getDiscoveryAdapter } from "./model-discovery.js";
|
|
30
30
|
import { clearConfigValueCache, resolveConfigValue, resolveHeaders } from "./resolve-config-value.js";
|
|
31
|
+
import { isLocalModel } from "./local-model-check.js";
|
|
31
32
|
|
|
32
33
|
const Ajv = (AjvModule as any).default || AjvModule;
|
|
33
34
|
const ajv = new Ajv();
|
|
@@ -557,7 +558,7 @@ export class ModelRegistry {
|
|
|
557
558
|
async getApiKey(model: Model<Api>, sessionId?: string): Promise<string | undefined> {
|
|
558
559
|
const authMode = this.getProviderAuthMode(model.provider);
|
|
559
560
|
if (authMode === "externalCli" || authMode === "none") return undefined;
|
|
560
|
-
return this.authStorage.getApiKey(model.provider, sessionId);
|
|
561
|
+
return this.authStorage.getApiKey(model.provider, sessionId, { baseUrl: model.baseUrl });
|
|
561
562
|
}
|
|
562
563
|
|
|
563
564
|
/**
|
|
@@ -807,6 +808,25 @@ export class ModelRegistry {
|
|
|
807
808
|
}
|
|
808
809
|
return converted;
|
|
809
810
|
}
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Check if a model's baseUrl points to a local endpoint.
|
|
814
|
+
* Delegates to standalone isLocalModel() function.
|
|
815
|
+
*/
|
|
816
|
+
static isLocalModel(model: Model<Api>): boolean {
|
|
817
|
+
return isLocalModel(model);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Check if all models in the registry are local.
|
|
822
|
+
* Returns true only if every model passes isLocalModel().
|
|
823
|
+
* Returns false if there are no models.
|
|
824
|
+
*/
|
|
825
|
+
isAllLocalChain(): boolean {
|
|
826
|
+
const models = this.getAll();
|
|
827
|
+
if (models.length === 0) return false;
|
|
828
|
+
return models.every((m) => isLocalModel(m));
|
|
829
|
+
}
|
|
810
830
|
}
|
|
811
831
|
|
|
812
832
|
/**
|
|
@@ -151,6 +151,7 @@ export interface Settings {
|
|
|
151
151
|
fallback?: FallbackSettings;
|
|
152
152
|
modelDiscovery?: ModelDiscoverySettings;
|
|
153
153
|
editMode?: "standard" | "hashline"; // Edit tool mode: "standard" (text match) or "hashline" (LINE#ID anchors). Default: "standard"
|
|
154
|
+
timestampFormat?: "date-time-iso" | "date-time-us"; // Timestamp display format for messages. Default: "date-time-iso"
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
/** Deep merge settings: project/overrides take precedence, nested objects merge recursively */
|
|
@@ -1087,4 +1088,12 @@ export class SettingsManager {
|
|
|
1087
1088
|
setEditMode(mode: "standard" | "hashline"): void {
|
|
1088
1089
|
this.setGlobalSetting("editMode", mode);
|
|
1089
1090
|
}
|
|
1091
|
+
|
|
1092
|
+
getTimestampFormat(): "date-time-iso" | "date-time-us" {
|
|
1093
|
+
return this.settings.timestampFormat ?? "date-time-iso";
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
setTimestampFormat(format: "date-time-iso" | "date-time-us"): void {
|
|
1097
|
+
this.setGlobalSetting("timestampFormat", format);
|
|
1098
|
+
}
|
|
1090
1099
|
}
|
|
@@ -391,6 +391,25 @@ export async function main(args: string[]) {
|
|
|
391
391
|
const authStorage = AuthStorage.create();
|
|
392
392
|
const modelRegistry = new ModelRegistry(authStorage, getModelsPath());
|
|
393
393
|
|
|
394
|
+
// Offline mode validation / auto-detection
|
|
395
|
+
if (offlineMode) {
|
|
396
|
+
// --offline flag: validate all models are local
|
|
397
|
+
if (!modelRegistry.isAllLocalChain()) {
|
|
398
|
+
const remoteModel = modelRegistry.getAll().find((m) => !ModelRegistry.isLocalModel(m));
|
|
399
|
+
if (remoteModel) {
|
|
400
|
+
console.error(
|
|
401
|
+
`Error: --offline requires all configured models to be local. Found remote model: ${remoteModel.name} (${remoteModel.baseUrl || "cloud API"})`,
|
|
402
|
+
);
|
|
403
|
+
process.exit(1);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
} else if (modelRegistry.isAllLocalChain() && modelRegistry.getAll().length > 0) {
|
|
407
|
+
// Auto-detect: all models are local, enable offline mode
|
|
408
|
+
process.env.PI_OFFLINE = "1";
|
|
409
|
+
process.env.PI_SKIP_VERSION_CHECK = "1";
|
|
410
|
+
console.log("[gsd] All configured models are local \u2014 enabling offline mode automatically.");
|
|
411
|
+
}
|
|
412
|
+
|
|
394
413
|
const resourceLoader = new DefaultResourceLoader({
|
|
395
414
|
cwd,
|
|
396
415
|
agentDir,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { test, describe } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { formatTimestamp } from "../timestamp.js";
|
|
4
|
+
|
|
5
|
+
describe("formatTimestamp", () => {
|
|
6
|
+
// Use a fixed local timestamp to avoid timezone issues
|
|
7
|
+
const d = new Date(2026, 2, 24, 10, 34, 0); // Mar 24, 2026 10:34:00 local time
|
|
8
|
+
const ts = d.getTime();
|
|
9
|
+
|
|
10
|
+
test("date-time-iso format (default)", () => {
|
|
11
|
+
assert.equal(formatTimestamp(ts, "date-time-iso"), "2026-03-24 10:34");
|
|
12
|
+
assert.equal(formatTimestamp(ts), "2026-03-24 10:34"); // default
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("date-time-us format", () => {
|
|
16
|
+
assert.equal(formatTimestamp(ts, "date-time-us"), "03-24-2026 10:34 AM");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("US format handles PM correctly", () => {
|
|
20
|
+
const pm = new Date(2026, 2, 24, 14, 5, 0).getTime();
|
|
21
|
+
assert.equal(formatTimestamp(pm, "date-time-us"), "03-24-2026 2:05 PM");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("US format handles noon as 12 PM", () => {
|
|
25
|
+
const noon = new Date(2026, 2, 24, 12, 0, 0).getTime();
|
|
26
|
+
assert.equal(formatTimestamp(noon, "date-time-us"), "03-24-2026 12:00 PM");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("US format handles midnight as 12 AM", () => {
|
|
30
|
+
const midnight = new Date(2026, 2, 24, 0, 0, 0).getTime();
|
|
31
|
+
assert.equal(formatTimestamp(midnight, "date-time-us"), "03-24-2026 12:00 AM");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("ISO format pads single digit months and days", () => {
|
|
35
|
+
const jan1 = new Date(2026, 0, 1, 9, 5, 0).getTime();
|
|
36
|
+
assert.equal(formatTimestamp(jan1, "date-time-iso"), "2026-01-01 09:05");
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AssistantMessage } from "@gsd/pi-ai";
|
|
2
2
|
import { Container, Markdown, type MarkdownTheme, Spacer, Text } from "@gsd/pi-tui";
|
|
3
3
|
import { getMarkdownTheme, theme } from "../theme/theme.js";
|
|
4
|
+
import { formatTimestamp, type TimestampFormat } from "./timestamp.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Component that renders a complete assistant message
|
|
@@ -10,16 +11,19 @@ export class AssistantMessageComponent extends Container {
|
|
|
10
11
|
private hideThinkingBlock: boolean;
|
|
11
12
|
private markdownTheme: MarkdownTheme;
|
|
12
13
|
private lastMessage?: AssistantMessage;
|
|
14
|
+
private timestampFormat: TimestampFormat;
|
|
13
15
|
|
|
14
16
|
constructor(
|
|
15
17
|
message?: AssistantMessage,
|
|
16
18
|
hideThinkingBlock = false,
|
|
17
19
|
markdownTheme: MarkdownTheme = getMarkdownTheme(),
|
|
20
|
+
timestampFormat: TimestampFormat = "date-time-iso",
|
|
18
21
|
) {
|
|
19
22
|
super();
|
|
20
23
|
|
|
21
24
|
this.hideThinkingBlock = hideThinkingBlock;
|
|
22
25
|
this.markdownTheme = markdownTheme;
|
|
26
|
+
this.timestampFormat = timestampFormat;
|
|
23
27
|
|
|
24
28
|
// Container for text/thinking content
|
|
25
29
|
this.contentContainer = new Container();
|
|
@@ -111,5 +115,11 @@ export class AssistantMessageComponent extends Container {
|
|
|
111
115
|
this.contentContainer.addChild(new Text(theme.fg("error", `Error: ${errorMsg}`), 1, 0));
|
|
112
116
|
}
|
|
113
117
|
}
|
|
118
|
+
|
|
119
|
+
// Show timestamp when the message is complete (has a stop reason)
|
|
120
|
+
if (message.stopReason && message.timestamp) {
|
|
121
|
+
const timeStr = formatTimestamp(message.timestamp, this.timestampFormat);
|
|
122
|
+
this.contentContainer.addChild(new Text(theme.fg("dim", timeStr), 1, 0));
|
|
123
|
+
}
|
|
114
124
|
}
|
|
115
125
|
}
|
|
@@ -45,6 +45,7 @@ export interface SettingsConfig {
|
|
|
45
45
|
respectGitignoreInPicker: boolean;
|
|
46
46
|
quietStartup: boolean;
|
|
47
47
|
clearOnShrink: boolean;
|
|
48
|
+
timestampFormat: "date-time-iso" | "date-time-us";
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
export interface SettingsCallbacks {
|
|
@@ -69,6 +70,7 @@ export interface SettingsCallbacks {
|
|
|
69
70
|
onRespectGitignoreInPickerChange: (enabled: boolean) => void;
|
|
70
71
|
onQuietStartupChange: (enabled: boolean) => void;
|
|
71
72
|
onClearOnShrinkChange: (enabled: boolean) => void;
|
|
73
|
+
onTimestampFormatChange: (format: "date-time-iso" | "date-time-us") => void;
|
|
72
74
|
onCancel: () => void;
|
|
73
75
|
}
|
|
74
76
|
|
|
@@ -355,6 +357,16 @@ export class SettingsSelectorComponent extends Container {
|
|
|
355
357
|
values: ["true", "false"],
|
|
356
358
|
});
|
|
357
359
|
|
|
360
|
+
// Timestamp format (insert after respect-gitignore-in-picker)
|
|
361
|
+
const gitignoreIndex = items.findIndex((item) => item.id === "respect-gitignore-in-picker");
|
|
362
|
+
items.splice(gitignoreIndex + 1, 0, {
|
|
363
|
+
id: "timestamp-format",
|
|
364
|
+
label: "Timestamp format",
|
|
365
|
+
description: "Date/time format for message timestamps",
|
|
366
|
+
currentValue: config.timestampFormat,
|
|
367
|
+
values: ["date-time-iso", "date-time-us"],
|
|
368
|
+
});
|
|
369
|
+
|
|
358
370
|
// Add borders
|
|
359
371
|
this.addChild(new DynamicBorder());
|
|
360
372
|
|
|
@@ -420,6 +432,9 @@ export class SettingsSelectorComponent extends Container {
|
|
|
420
432
|
case "respect-gitignore-in-picker":
|
|
421
433
|
callbacks.onRespectGitignoreInPickerChange(newValue === "true");
|
|
422
434
|
break;
|
|
435
|
+
case "timestamp-format":
|
|
436
|
+
callbacks.onTimestampFormatChange(newValue as "date-time-iso" | "date-time-us");
|
|
437
|
+
break;
|
|
423
438
|
}
|
|
424
439
|
},
|
|
425
440
|
callbacks.onCancel,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timestamp formatting for message display.
|
|
3
|
+
*
|
|
4
|
+
* Formats:
|
|
5
|
+
* - "time-date-iso": 10:34 2025-03-24 (default)
|
|
6
|
+
* - "date-time-iso": 2025-03-24 10:34
|
|
7
|
+
* - "time-date-us": 10:34 AM 03/24/2025
|
|
8
|
+
* - "date-time-us": 03/24/2025 10:34 AM
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type TimestampFormat = "date-time-iso" | "date-time-us";
|
|
12
|
+
|
|
13
|
+
function pad2(n: number): string {
|
|
14
|
+
return n.toString().padStart(2, "0");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isoDate(d: Date): string {
|
|
18
|
+
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isoTime(d: Date): string {
|
|
22
|
+
return `${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function usDate(d: Date): string {
|
|
26
|
+
return `${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}-${d.getFullYear()}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function usTime(d: Date): string {
|
|
30
|
+
const hours = d.getHours();
|
|
31
|
+
const period = hours >= 12 ? "PM" : "AM";
|
|
32
|
+
const h = hours % 12 || 12;
|
|
33
|
+
return `${h}:${pad2(d.getMinutes())} ${period}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Format a timestamp for message display using the specified format.
|
|
38
|
+
*/
|
|
39
|
+
export function formatTimestamp(timestamp: number, format: TimestampFormat = "date-time-iso"): string {
|
|
40
|
+
const d = new Date(timestamp);
|
|
41
|
+
|
|
42
|
+
switch (format) {
|
|
43
|
+
case "date-time-iso":
|
|
44
|
+
return `${isoDate(d)} ${isoTime(d)}`;
|
|
45
|
+
case "date-time-us":
|
|
46
|
+
return `${usDate(d)} ${usTime(d)}`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -895,7 +895,9 @@ export class ToolExecutionComponent extends Container {
|
|
|
895
895
|
// Server-side Anthropic web search
|
|
896
896
|
text = theme.fg("toolTitle", theme.bold("web search"));
|
|
897
897
|
|
|
898
|
-
if (
|
|
898
|
+
if (process.env.PI_OFFLINE === "1") {
|
|
899
|
+
text += "\n\n" + theme.fg("muted", "\u{1F50C} Offline \u{2014} web search unavailable");
|
|
900
|
+
} else if (this.result) {
|
|
899
901
|
const output = this.getTextOutput().trim();
|
|
900
902
|
if (output) {
|
|
901
903
|
const lines = output.split("\n");
|
|
@@ -1,15 +1,21 @@
|
|
|
1
|
-
import { Container, Markdown, type MarkdownTheme, Spacer } from "@gsd/pi-tui";
|
|
1
|
+
import { Container, Markdown, type MarkdownTheme, Spacer, Text } from "@gsd/pi-tui";
|
|
2
2
|
import { getMarkdownTheme, theme } from "../theme/theme.js";
|
|
3
|
+
import { formatTimestamp, type TimestampFormat } from "./timestamp.js";
|
|
3
4
|
|
|
4
5
|
const OSC133_ZONE_START = "\x1b]133;A\x07";
|
|
5
6
|
const OSC133_ZONE_END = "\x1b]133;B\x07";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
|
-
* Component that renders a user message
|
|
9
|
+
* Component that renders a user message with a right-aligned timestamp.
|
|
9
10
|
*/
|
|
10
11
|
export class UserMessageComponent extends Container {
|
|
11
|
-
|
|
12
|
+
private timestamp: number | undefined;
|
|
13
|
+
private timestampFormat: TimestampFormat;
|
|
14
|
+
|
|
15
|
+
constructor(text: string, markdownTheme: MarkdownTheme = getMarkdownTheme(), timestamp?: number, timestampFormat: TimestampFormat = "date-time-iso") {
|
|
12
16
|
super();
|
|
17
|
+
this.timestamp = timestamp;
|
|
18
|
+
this.timestampFormat = timestampFormat;
|
|
13
19
|
this.addChild(new Spacer(1));
|
|
14
20
|
this.addChild(
|
|
15
21
|
new Markdown(text, 1, 1, markdownTheme, {
|
|
@@ -25,6 +31,15 @@ export class UserMessageComponent extends Container {
|
|
|
25
31
|
return lines;
|
|
26
32
|
}
|
|
27
33
|
|
|
34
|
+
// Insert right-aligned timestamp above the message content
|
|
35
|
+
if (this.timestamp) {
|
|
36
|
+
const timeStr = formatTimestamp(this.timestamp, this.timestampFormat);
|
|
37
|
+
const label = theme.fg("dim", timeStr);
|
|
38
|
+
const padding = Math.max(0, width - timeStr.length - 1);
|
|
39
|
+
const timestampLine = " ".repeat(padding) + label;
|
|
40
|
+
lines.splice(0, 0, timestampLine);
|
|
41
|
+
}
|
|
42
|
+
|
|
28
43
|
lines[0] = OSC133_ZONE_START + lines[0];
|
|
29
44
|
lines[lines.length - 1] = lines[lines.length - 1] + OSC133_ZONE_END;
|
|
30
45
|
return lines;
|
|
@@ -100,6 +100,7 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
100
100
|
undefined,
|
|
101
101
|
host.hideThinkingBlock,
|
|
102
102
|
host.getMarkdownThemeWithSettings(),
|
|
103
|
+
host.settingsManager.getTimestampFormat(),
|
|
103
104
|
);
|
|
104
105
|
host.streamingMessage = event.message;
|
|
105
106
|
host.chatContainer.addChild(host.streamingComponent);
|
|
@@ -144,13 +145,21 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
144
145
|
} else if (content.type === "webSearchResult") {
|
|
145
146
|
const component = host.pendingTools.get(content.toolUseId);
|
|
146
147
|
if (component) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
148
|
+
if (process.env.PI_OFFLINE === "1") {
|
|
149
|
+
component.updateResult({
|
|
150
|
+
content: [{ type: "text", text: "Web search disabled (offline mode)" }],
|
|
151
|
+
isError: false,
|
|
152
|
+
});
|
|
153
|
+
host.pendingTools.delete(content.toolUseId);
|
|
154
|
+
} else {
|
|
155
|
+
const searchContent = content.content;
|
|
156
|
+
const isError = searchContent && typeof searchContent === "object" && "type" in (searchContent as any) && (searchContent as any).type === "web_search_tool_result_error";
|
|
157
|
+
component.updateResult({
|
|
158
|
+
content: [{ type: "text", text: host.formatWebSearchResult(searchContent) }],
|
|
159
|
+
isError: !!isError,
|
|
160
|
+
});
|
|
161
|
+
host.pendingTools.delete(content.toolUseId);
|
|
162
|
+
}
|
|
154
163
|
}
|
|
155
164
|
}
|
|
156
165
|
}
|
|
@@ -2099,11 +2099,13 @@ export class InteractiveMode {
|
|
|
2099
2099
|
const userComponent = new UserMessageComponent(
|
|
2100
2100
|
skillBlock.userMessage,
|
|
2101
2101
|
this.getMarkdownThemeWithSettings(),
|
|
2102
|
+
message.timestamp,
|
|
2103
|
+
this.settingsManager.getTimestampFormat(),
|
|
2102
2104
|
);
|
|
2103
2105
|
this.chatContainer.addChild(userComponent);
|
|
2104
2106
|
}
|
|
2105
2107
|
} else {
|
|
2106
|
-
const userComponent = new UserMessageComponent(textContent, this.getMarkdownThemeWithSettings());
|
|
2108
|
+
const userComponent = new UserMessageComponent(textContent, this.getMarkdownThemeWithSettings(), message.timestamp, this.settingsManager.getTimestampFormat());
|
|
2107
2109
|
this.chatContainer.addChild(userComponent);
|
|
2108
2110
|
}
|
|
2109
2111
|
if (options?.populateHistory) {
|
|
@@ -2117,6 +2119,7 @@ export class InteractiveMode {
|
|
|
2117
2119
|
message,
|
|
2118
2120
|
this.hideThinkingBlock,
|
|
2119
2121
|
this.getMarkdownThemeWithSettings(),
|
|
2122
|
+
this.settingsManager.getTimestampFormat(),
|
|
2120
2123
|
);
|
|
2121
2124
|
this.chatContainer.addChild(assistantComponent);
|
|
2122
2125
|
break;
|
|
@@ -2795,6 +2798,7 @@ export class InteractiveMode {
|
|
|
2795
2798
|
respectGitignoreInPicker: this.settingsManager.getRespectGitignoreInPicker(),
|
|
2796
2799
|
quietStartup: this.settingsManager.getQuietStartup(),
|
|
2797
2800
|
clearOnShrink: this.settingsManager.getClearOnShrink(),
|
|
2801
|
+
timestampFormat: this.settingsManager.getTimestampFormat(),
|
|
2798
2802
|
},
|
|
2799
2803
|
{
|
|
2800
2804
|
onAutoCompactChange: (enabled) => {
|
|
@@ -2898,6 +2902,9 @@ export class InteractiveMode {
|
|
|
2898
2902
|
this.settingsManager.setRespectGitignoreInPicker(enabled);
|
|
2899
2903
|
this.autocompleteProvider?.setRespectGitignore(enabled);
|
|
2900
2904
|
},
|
|
2905
|
+
onTimestampFormatChange: (format) => {
|
|
2906
|
+
this.settingsManager.setTimestampFormat(format);
|
|
2907
|
+
},
|
|
2901
2908
|
onCancel: () => {
|
|
2902
2909
|
done();
|
|
2903
2910
|
this.ui.requestRender();
|
|
@@ -18,6 +18,9 @@ export const INFRA_ERROR_CODES: ReadonlySet<string> = new Set([
|
|
|
18
18
|
"EDQUOT", // disk quota exceeded
|
|
19
19
|
"EMFILE", // too many open files (process)
|
|
20
20
|
"ENFILE", // too many open files (system)
|
|
21
|
+
"ECONNREFUSED", // connection refused (offline / local server down)
|
|
22
|
+
"ENOTFOUND", // DNS lookup failed (offline / no network)
|
|
23
|
+
"ENETUNREACH", // network unreachable (offline / no route)
|
|
21
24
|
]);
|
|
22
25
|
|
|
23
26
|
/**
|
|
@@ -27,7 +27,9 @@ import { debugLog } from "../debug-logger.js";
|
|
|
27
27
|
import { gsdRoot } from "../paths.js";
|
|
28
28
|
import { atomicWriteSync } from "../atomic-write.js";
|
|
29
29
|
import { PROJECT_FILES } from "../detection.js";
|
|
30
|
+
import { MergeConflictError } from "../git-service.js";
|
|
30
31
|
import { join } from "node:path";
|
|
32
|
+
import { existsSync, cpSync } from "node:fs";
|
|
31
33
|
|
|
32
34
|
// ─── generateMilestoneReport ──────────────────────────────────────────────────
|
|
33
35
|
|
|
@@ -233,26 +235,23 @@ export async function runPreDispatch(
|
|
|
233
235
|
loopState.stuckRecoveryAttempts = 0;
|
|
234
236
|
|
|
235
237
|
// Worktree lifecycle on milestone transition — merge current, enter next
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
s.currentMilestoneId!,
|
|
245
|
-
`[GSD] ${s.currentMilestoneId} complete`,
|
|
246
|
-
`Milestone ${s.currentMilestoneId} completed by GSD auto-mode.\n\nSee .gsd/${s.currentMilestoneId}/ for details.`,
|
|
238
|
+
try {
|
|
239
|
+
deps.resolver.mergeAndExit(s.currentMilestoneId!, ctx.ui);
|
|
240
|
+
} catch (mergeErr) {
|
|
241
|
+
if (mergeErr instanceof MergeConflictError) {
|
|
242
|
+
// Real code conflicts — stop the loop instead of retrying forever (#2330)
|
|
243
|
+
ctx.ui.notify(
|
|
244
|
+
`Merge conflict: ${mergeErr.conflictedFiles.join(", ")}. Resolve conflicts manually and run /gsd auto to resume.`,
|
|
245
|
+
"error",
|
|
247
246
|
);
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
} catch {
|
|
252
|
-
// Non-fatal — PR creation is best-effort
|
|
247
|
+
await deps.stopAuto(ctx, pi, `Merge conflict on milestone ${s.currentMilestoneId}`);
|
|
248
|
+
return { action: "break", reason: "merge-conflict" };
|
|
253
249
|
}
|
|
250
|
+
// Non-conflict errors — log and continue
|
|
254
251
|
}
|
|
255
252
|
|
|
253
|
+
// PR creation (auto_pr) is handled inside mergeMilestoneToMain (#2302)
|
|
254
|
+
|
|
256
255
|
deps.invalidateAllCaches();
|
|
257
256
|
|
|
258
257
|
state = await deps.deriveState(s.basePath);
|
|
@@ -279,9 +278,17 @@ export async function runPreDispatch(
|
|
|
279
278
|
// Reset completed-units tracking for the new milestone — stale entries
|
|
280
279
|
// from the previous milestone cause the dispatch loop to skip units
|
|
281
280
|
// that haven't actually been completed in the new milestone's context.
|
|
281
|
+
// Archive the old completed-units.json instead of wiping it (#2313).
|
|
282
282
|
s.completedUnits = [];
|
|
283
283
|
try {
|
|
284
284
|
const completedKeysPath = join(gsdRoot(s.basePath), "completed-units.json");
|
|
285
|
+
if (existsSync(completedKeysPath) && s.currentMilestoneId) {
|
|
286
|
+
const archivePath = join(
|
|
287
|
+
gsdRoot(s.basePath),
|
|
288
|
+
`completed-units-${s.currentMilestoneId}.json`,
|
|
289
|
+
);
|
|
290
|
+
cpSync(completedKeysPath, archivePath);
|
|
291
|
+
}
|
|
285
292
|
atomicWriteSync(completedKeysPath, JSON.stringify([], null, 2));
|
|
286
293
|
} catch { /* non-fatal */ }
|
|
287
294
|
|
|
@@ -322,25 +329,20 @@ export async function runPreDispatch(
|
|
|
322
329
|
if (incomplete.length === 0 && state.registry.length > 0) {
|
|
323
330
|
// All milestones complete — merge milestone branch before stopping
|
|
324
331
|
if (s.currentMilestoneId) {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
s.basePath,
|
|
333
|
-
s.currentMilestoneId,
|
|
334
|
-
`[GSD] ${s.currentMilestoneId} complete`,
|
|
335
|
-
`Milestone ${s.currentMilestoneId} completed by GSD auto-mode.\n\nSee .gsd/${s.currentMilestoneId}/ for details.`,
|
|
332
|
+
try {
|
|
333
|
+
deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
|
|
334
|
+
} catch (mergeErr) {
|
|
335
|
+
if (mergeErr instanceof MergeConflictError) {
|
|
336
|
+
ctx.ui.notify(
|
|
337
|
+
`Merge conflict: ${mergeErr.conflictedFiles.join(", ")}. Resolve conflicts manually and run /gsd auto to resume.`,
|
|
338
|
+
"error",
|
|
336
339
|
);
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
}
|
|
340
|
-
} catch {
|
|
341
|
-
// Non-fatal — PR creation is best-effort
|
|
340
|
+
await deps.stopAuto(ctx, pi, `Merge conflict on milestone ${s.currentMilestoneId}`);
|
|
341
|
+
return { action: "break", reason: "merge-conflict" };
|
|
342
342
|
}
|
|
343
343
|
}
|
|
344
|
+
|
|
345
|
+
// PR creation (auto_pr) is handled inside mergeMilestoneToMain (#2302)
|
|
344
346
|
}
|
|
345
347
|
deps.sendDesktopNotification(
|
|
346
348
|
"GSD",
|
|
@@ -422,25 +424,20 @@ export async function runPreDispatch(
|
|
|
422
424
|
if (state.phase === "complete") {
|
|
423
425
|
// Milestone merge on complete (before closeout so branch state is clean)
|
|
424
426
|
if (s.currentMilestoneId) {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
s.basePath,
|
|
433
|
-
s.currentMilestoneId,
|
|
434
|
-
`[GSD] ${s.currentMilestoneId} complete`,
|
|
435
|
-
`Milestone ${s.currentMilestoneId} completed by GSD auto-mode.\n\nSee .gsd/${s.currentMilestoneId}/ for details.`,
|
|
427
|
+
try {
|
|
428
|
+
deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
|
|
429
|
+
} catch (mergeErr) {
|
|
430
|
+
if (mergeErr instanceof MergeConflictError) {
|
|
431
|
+
ctx.ui.notify(
|
|
432
|
+
`Merge conflict: ${mergeErr.conflictedFiles.join(", ")}. Resolve conflicts manually and run /gsd auto to resume.`,
|
|
433
|
+
"error",
|
|
436
434
|
);
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
}
|
|
440
|
-
} catch {
|
|
441
|
-
// Non-fatal — PR creation is best-effort
|
|
435
|
+
await deps.stopAuto(ctx, pi, `Merge conflict on milestone ${s.currentMilestoneId}`);
|
|
436
|
+
return { action: "break", reason: "merge-conflict" };
|
|
442
437
|
}
|
|
443
438
|
}
|
|
439
|
+
|
|
440
|
+
// PR creation (auto_pr) is handled inside mergeMilestoneToMain (#2302)
|
|
444
441
|
}
|
|
445
442
|
deps.sendDesktopNotification(
|
|
446
443
|
"GSD",
|