gsd-pi 2.41.0-dev.cac69f9 → 2.42.0-dev.1df898f
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 +23 -0
- package/dist/cli.js +18 -3
- package/dist/loader.js +3 -1
- package/dist/resource-loader.js +39 -6
- package/dist/resources/extensions/async-jobs/async-bash-tool.js +52 -4
- package/dist/resources/extensions/async-jobs/await-tool.js +5 -0
- package/dist/resources/extensions/async-jobs/index.js +2 -0
- package/dist/resources/extensions/gsd/auto/loop.js +80 -0
- package/dist/resources/extensions/gsd/auto/phases.js +3 -5
- package/dist/resources/extensions/gsd/auto/session.js +6 -0
- package/dist/resources/extensions/gsd/auto-dashboard.js +2 -0
- package/dist/resources/extensions/gsd/auto-prompts.js +3 -16
- package/dist/resources/extensions/gsd/auto-start.js +8 -11
- package/dist/resources/extensions/gsd/auto.js +28 -1
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +11 -5
- package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +7 -2
- package/dist/resources/extensions/gsd/commands/catalog.js +32 -0
- package/dist/resources/extensions/gsd/commands/handlers/workflow.js +146 -0
- package/dist/resources/extensions/gsd/context-injector.js +74 -0
- package/dist/resources/extensions/gsd/custom-execution-policy.js +47 -0
- package/dist/resources/extensions/gsd/custom-verification.js +145 -0
- package/dist/resources/extensions/gsd/custom-workflow-engine.js +164 -0
- package/dist/resources/extensions/gsd/dashboard-overlay.js +1 -0
- package/dist/resources/extensions/gsd/definition-loader.js +352 -0
- package/dist/resources/extensions/gsd/detection.js +19 -0
- package/dist/resources/extensions/gsd/dev-execution-policy.js +24 -0
- package/dist/resources/extensions/gsd/dev-workflow-engine.js +82 -0
- package/dist/resources/extensions/gsd/doctor-checks.js +31 -1
- package/dist/resources/extensions/gsd/doctor-providers.js +10 -0
- package/dist/resources/extensions/gsd/engine-resolver.js +40 -0
- package/dist/resources/extensions/gsd/engine-types.js +8 -0
- package/dist/resources/extensions/gsd/execution-policy.js +8 -0
- package/dist/resources/extensions/gsd/forensics.js +84 -0
- package/dist/resources/extensions/gsd/git-constants.js +1 -0
- package/dist/resources/extensions/gsd/git-service.js +1 -1
- package/dist/resources/extensions/gsd/graph.js +225 -0
- package/dist/resources/extensions/gsd/native-git-bridge.js +1 -0
- package/dist/resources/extensions/gsd/preferences-types.js +1 -0
- package/dist/resources/extensions/gsd/preferences.js +59 -8
- package/dist/resources/extensions/gsd/prompts/forensics.md +12 -5
- package/dist/resources/extensions/gsd/repo-identity.js +46 -5
- package/dist/resources/extensions/gsd/run-manager.js +134 -0
- package/dist/resources/extensions/gsd/service-tier.js +13 -4
- package/dist/resources/extensions/gsd/session-lock.js +2 -2
- package/dist/resources/extensions/gsd/workflow-engine.js +7 -0
- package/dist/resources/extensions/gsd/worktree-resolver.js +2 -2
- package/dist/resources/extensions/gsd/worktree.js +2 -2
- package/dist/resources/extensions/mcp-client/index.js +2 -1
- package/dist/resources/extensions/search-the-web/tool-search.js +3 -3
- package/dist/resources/skills/create-workflow/SKILL.md +103 -0
- package/dist/resources/skills/create-workflow/references/feature-patterns.md +128 -0
- package/dist/resources/skills/create-workflow/references/verification-policies.md +76 -0
- package/dist/resources/skills/create-workflow/references/yaml-schema-v1.md +46 -0
- package/dist/resources/skills/create-workflow/templates/blog-post-pipeline.yaml +60 -0
- package/dist/resources/skills/create-workflow/templates/code-audit.yaml +60 -0
- package/dist/resources/skills/create-workflow/templates/release-checklist.yaml +66 -0
- package/dist/resources/skills/create-workflow/templates/workflow-definition.yaml +32 -0
- package/dist/resources/skills/create-workflow/workflows/create-from-scratch.md +104 -0
- package/dist/resources/skills/create-workflow/workflows/create-from-template.md +72 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +15 -15
- 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/api/git/route.js +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 +15 -15
- package/dist/web/standalone/.next/server/chunks/229.js +2 -2
- 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/web-mode.d.ts +2 -0
- package/dist/web-mode.js +40 -4
- package/package.json +1 -1
- package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/agent.js +2 -0
- package/packages/pi-agent-core/dist/agent.js.map +1 -1
- package/packages/pi-agent-core/dist/types.d.ts +6 -0
- package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/types.js.map +1 -1
- package/packages/pi-agent-core/src/agent.test.ts +53 -0
- package/packages/pi-agent-core/src/agent.ts +3 -0
- package/packages/pi-agent-core/src/types.ts +6 -0
- package/packages/pi-agent-core/tsconfig.json +1 -1
- package/packages/pi-ai/dist/models.d.ts +5 -3
- package/packages/pi-ai/dist/models.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +801 -1468
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +1135 -1588
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/dist/models.js.map +1 -1
- package/packages/pi-ai/dist/utils/oauth/github-copilot.d.ts.map +1 -1
- package/packages/pi-ai/dist/utils/oauth/github-copilot.js +60 -2
- package/packages/pi-ai/dist/utils/oauth/github-copilot.js.map +1 -1
- package/packages/pi-ai/scripts/generate-models.ts +1543 -0
- package/packages/pi-ai/src/models.generated.ts +1140 -1593
- package/packages/pi-ai/src/models.ts +7 -4
- package/packages/pi-ai/src/utils/oauth/github-copilot.ts +74 -2
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +8 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.d.ts +7 -0
- package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.js +29 -2
- package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.test.js +60 -0
- package/packages/pi-coding-agent/dist/core/auth-storage.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +18 -0
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.js +23 -0
- package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.js +2 -0
- package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/package-manager.d.ts +6 -0
- package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/package-manager.js +63 -11
- package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/resource-loader.d.ts +9 -0
- package/packages/pi-coding-agent/dist/core/resource-loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/resource-loader.js +20 -6
- package/packages/pi-coding-agent/dist/core/resource-loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.js +6 -5
- package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-editor.js +3 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-editor.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 +9 -6
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.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 +30 -10
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/agent-session.ts +7 -1
- package/packages/pi-coding-agent/src/core/auth-storage.test.ts +68 -0
- package/packages/pi-coding-agent/src/core/auth-storage.ts +30 -2
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +18 -0
- package/packages/pi-coding-agent/src/core/lsp/client.ts +29 -0
- package/packages/pi-coding-agent/src/core/model-registry.ts +3 -0
- package/packages/pi-coding-agent/src/core/package-manager.ts +99 -58
- package/packages/pi-coding-agent/src/core/resource-loader.ts +24 -6
- package/packages/pi-coding-agent/src/core/system-prompt.ts +6 -5
- package/packages/pi-coding-agent/src/modes/interactive/components/extension-editor.ts +3 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +10 -6
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +31 -11
- package/pkg/package.json +1 -1
- package/src/resources/extensions/async-jobs/async-bash-timeout.test.ts +122 -0
- package/src/resources/extensions/async-jobs/async-bash-tool.ts +40 -4
- package/src/resources/extensions/async-jobs/await-tool.test.ts +47 -0
- package/src/resources/extensions/async-jobs/await-tool.ts +5 -0
- package/src/resources/extensions/async-jobs/index.ts +1 -0
- package/src/resources/extensions/async-jobs/job-manager.ts +2 -0
- package/src/resources/extensions/gsd/auto/loop-deps.ts +0 -1
- package/src/resources/extensions/gsd/auto/loop.ts +91 -0
- package/src/resources/extensions/gsd/auto/phases.ts +3 -5
- package/src/resources/extensions/gsd/auto/session.ts +6 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +2 -0
- package/src/resources/extensions/gsd/auto-prompts.ts +2 -18
- package/src/resources/extensions/gsd/auto-start.ts +7 -10
- package/src/resources/extensions/gsd/auto.ts +31 -1
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +13 -5
- package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +9 -2
- package/src/resources/extensions/gsd/commands/catalog.ts +32 -0
- package/src/resources/extensions/gsd/commands/handlers/workflow.ts +164 -0
- package/src/resources/extensions/gsd/context-injector.ts +100 -0
- package/src/resources/extensions/gsd/custom-execution-policy.ts +73 -0
- package/src/resources/extensions/gsd/custom-verification.ts +180 -0
- package/src/resources/extensions/gsd/custom-workflow-engine.ts +216 -0
- package/src/resources/extensions/gsd/dashboard-overlay.ts +1 -0
- package/src/resources/extensions/gsd/definition-loader.ts +462 -0
- package/src/resources/extensions/gsd/detection.ts +19 -0
- package/src/resources/extensions/gsd/dev-execution-policy.ts +51 -0
- package/src/resources/extensions/gsd/dev-workflow-engine.ts +110 -0
- package/src/resources/extensions/gsd/doctor-checks.ts +32 -1
- package/src/resources/extensions/gsd/doctor-providers.ts +13 -0
- package/src/resources/extensions/gsd/doctor-types.ts +1 -0
- package/src/resources/extensions/gsd/engine-resolver.ts +57 -0
- package/src/resources/extensions/gsd/engine-types.ts +71 -0
- package/src/resources/extensions/gsd/execution-policy.ts +43 -0
- package/src/resources/extensions/gsd/forensics.ts +92 -0
- package/src/resources/extensions/gsd/git-constants.ts +1 -0
- package/src/resources/extensions/gsd/git-service.ts +0 -1
- package/src/resources/extensions/gsd/gitignore.ts +1 -1
- package/src/resources/extensions/gsd/graph.ts +312 -0
- package/src/resources/extensions/gsd/native-git-bridge.ts +1 -0
- package/src/resources/extensions/gsd/preferences-types.ts +3 -0
- package/src/resources/extensions/gsd/preferences.ts +62 -6
- package/src/resources/extensions/gsd/prompts/forensics.md +12 -5
- package/src/resources/extensions/gsd/repo-identity.ts +48 -5
- package/src/resources/extensions/gsd/run-manager.ts +180 -0
- package/src/resources/extensions/gsd/service-tier.ts +17 -4
- package/src/resources/extensions/gsd/session-lock.ts +2 -2
- package/src/resources/extensions/gsd/tests/activity-log.test.ts +31 -69
- package/src/resources/extensions/gsd/tests/bundled-workflow-defs.test.ts +180 -0
- package/src/resources/extensions/gsd/tests/commands-workflow-custom.test.ts +283 -0
- package/src/resources/extensions/gsd/tests/context-injector.test.ts +313 -0
- package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +540 -0
- package/src/resources/extensions/gsd/tests/custom-verification.test.ts +382 -0
- package/src/resources/extensions/gsd/tests/custom-workflow-engine.test.ts +339 -0
- package/src/resources/extensions/gsd/tests/dashboard-custom-engine.test.ts +87 -0
- package/src/resources/extensions/gsd/tests/definition-loader.test.ts +778 -0
- package/src/resources/extensions/gsd/tests/dev-engine-wrapper.test.ts +318 -0
- package/src/resources/extensions/gsd/tests/e2e-workflow-pipeline-integration.test.ts +476 -0
- package/src/resources/extensions/gsd/tests/engine-interfaces-contract.test.ts +271 -0
- package/src/resources/extensions/gsd/tests/forensics-dedup.test.ts +48 -0
- package/src/resources/extensions/gsd/tests/forensics-issue-routing.test.ts +43 -0
- package/src/resources/extensions/gsd/tests/git-locale.test.ts +133 -0
- package/src/resources/extensions/gsd/tests/git-service.test.ts +44 -0
- package/src/resources/extensions/gsd/tests/graph-operations.test.ts +599 -0
- package/src/resources/extensions/gsd/tests/iterate-engine-integration.test.ts +429 -0
- package/src/resources/extensions/gsd/tests/journal.test.ts +82 -127
- package/src/resources/extensions/gsd/tests/manifest-status.test.ts +73 -82
- package/src/resources/extensions/gsd/tests/run-manager.test.ts +229 -0
- package/src/resources/extensions/gsd/tests/service-tier.test.ts +30 -1
- package/src/resources/extensions/gsd/tests/skill-activation.test.ts +56 -3
- package/src/resources/extensions/gsd/tests/symlink-numbered-variants.test.ts +151 -0
- package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +45 -0
- package/src/resources/extensions/gsd/tests/verification-gate.test.ts +156 -263
- package/src/resources/extensions/gsd/tests/worktree-health-dispatch.test.ts +35 -78
- package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +81 -74
- package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +1 -2
- package/src/resources/extensions/gsd/workflow-engine.ts +38 -0
- package/src/resources/extensions/gsd/worktree-resolver.ts +2 -3
- package/src/resources/extensions/gsd/worktree.ts +2 -2
- package/src/resources/extensions/mcp-client/index.ts +5 -1
- package/src/resources/extensions/search-the-web/tool-search.ts +3 -3
- package/src/resources/skills/create-workflow/SKILL.md +103 -0
- package/src/resources/skills/create-workflow/references/feature-patterns.md +128 -0
- package/src/resources/skills/create-workflow/references/verification-policies.md +76 -0
- package/src/resources/skills/create-workflow/references/yaml-schema-v1.md +46 -0
- package/src/resources/skills/create-workflow/templates/blog-post-pipeline.yaml +60 -0
- package/src/resources/skills/create-workflow/templates/code-audit.yaml +60 -0
- package/src/resources/skills/create-workflow/templates/release-checklist.yaml +66 -0
- package/src/resources/skills/create-workflow/templates/workflow-definition.yaml +32 -0
- package/src/resources/skills/create-workflow/workflows/create-from-scratch.md +104 -0
- package/src/resources/skills/create-workflow/workflows/create-from-template.md +72 -0
- /package/dist/web/standalone/.next/static/{EnGUNqHeGbE0tuuUkTJVA → qw8qDHXOTLUXBq1vEknSz}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{EnGUNqHeGbE0tuuUkTJVA → qw8qDHXOTLUXBq1vEknSz}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* graph.ts — Pure data module for GRAPH.yaml workflow step tracking.
|
|
3
|
+
*
|
|
4
|
+
* Provides types and functions for reading, writing, and querying the
|
|
5
|
+
* step graph that drives CustomWorkflowEngine. Zero engine dependencies.
|
|
6
|
+
*
|
|
7
|
+
* GRAPH.yaml lives in a run directory and tracks step statuses
|
|
8
|
+
* (pending → active → complete) with optional dependency edges.
|
|
9
|
+
*
|
|
10
|
+
* Observability:
|
|
11
|
+
* - readGraph/writeGraph use YAML on disk — human-readable, diffable,
|
|
12
|
+
* inspectable with `cat` or any YAML viewer.
|
|
13
|
+
* - Each GraphStep has status, startedAt, finishedAt fields visible in GRAPH.yaml.
|
|
14
|
+
* - writeGraph uses atomic write (tmp + rename) for crash safety.
|
|
15
|
+
* - All operations are immutable — callers always get a new graph object.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { parse, stringify } from "yaml";
|
|
19
|
+
import { readFileSync, writeFileSync, renameSync, existsSync, mkdirSync } from "node:fs";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import type { WorkflowDefinition } from "./definition-loader.js";
|
|
22
|
+
|
|
23
|
+
// ─── Types ───────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
export interface GraphStep {
|
|
26
|
+
/** Unique step identifier within the workflow. */
|
|
27
|
+
id: string;
|
|
28
|
+
/** Human-readable step title. */
|
|
29
|
+
title: string;
|
|
30
|
+
/** Current status: pending → active → complete → expanded (iterate parent). */
|
|
31
|
+
status: "pending" | "active" | "complete" | "expanded";
|
|
32
|
+
/** The prompt to dispatch for this step. */
|
|
33
|
+
prompt: string;
|
|
34
|
+
/** IDs of steps that must be "complete" before this step can run. */
|
|
35
|
+
dependsOn: string[];
|
|
36
|
+
/** For iteration instances: ID of the parent step that was expanded. */
|
|
37
|
+
parentStepId?: string;
|
|
38
|
+
/** ISO timestamp when the step started executing. */
|
|
39
|
+
startedAt?: string;
|
|
40
|
+
/** ISO timestamp when the step finished executing. */
|
|
41
|
+
finishedAt?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface WorkflowGraph {
|
|
45
|
+
/** Ordered list of steps in the workflow. */
|
|
46
|
+
steps: GraphStep[];
|
|
47
|
+
/** Workflow metadata. */
|
|
48
|
+
metadata: {
|
|
49
|
+
name: string;
|
|
50
|
+
createdAt: string;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─── YAML schema mapping ─────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
const GRAPH_FILENAME = "GRAPH.yaml";
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Internal YAML shape — uses snake_case for YAML keys.
|
|
60
|
+
* Converted to/from the camelCase TypeScript types on read/write.
|
|
61
|
+
*/
|
|
62
|
+
interface YamlStep {
|
|
63
|
+
id: string;
|
|
64
|
+
title: string;
|
|
65
|
+
status: string;
|
|
66
|
+
prompt: string;
|
|
67
|
+
depends_on?: string[];
|
|
68
|
+
parent_step_id?: string;
|
|
69
|
+
started_at?: string;
|
|
70
|
+
finished_at?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface YamlGraph {
|
|
74
|
+
steps: YamlStep[];
|
|
75
|
+
metadata: { name: string; created_at: string };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── Functions ───────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Read and parse GRAPH.yaml from a run directory.
|
|
82
|
+
*
|
|
83
|
+
* @param runDir — directory containing GRAPH.yaml
|
|
84
|
+
* @returns Parsed workflow graph
|
|
85
|
+
* @throws Error if file doesn't exist or YAML is malformed
|
|
86
|
+
*/
|
|
87
|
+
export function readGraph(runDir: string): WorkflowGraph {
|
|
88
|
+
const filePath = join(runDir, GRAPH_FILENAME);
|
|
89
|
+
if (!existsSync(filePath)) {
|
|
90
|
+
throw new Error(`GRAPH.yaml not found: ${filePath}`);
|
|
91
|
+
}
|
|
92
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
93
|
+
const yaml = parse(raw) as YamlGraph;
|
|
94
|
+
|
|
95
|
+
if (!yaml?.steps || !Array.isArray(yaml.steps)) {
|
|
96
|
+
throw new Error(`Invalid GRAPH.yaml: missing or invalid 'steps' array in ${filePath}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
steps: yaml.steps.map((s) => ({
|
|
101
|
+
id: s.id,
|
|
102
|
+
title: s.title,
|
|
103
|
+
status: s.status as GraphStep["status"],
|
|
104
|
+
prompt: s.prompt,
|
|
105
|
+
dependsOn: s.depends_on ?? [],
|
|
106
|
+
...(s.parent_step_id != null ? { parentStepId: s.parent_step_id } : {}),
|
|
107
|
+
...(s.started_at != null ? { startedAt: s.started_at } : {}),
|
|
108
|
+
...(s.finished_at != null ? { finishedAt: s.finished_at } : {}),
|
|
109
|
+
})),
|
|
110
|
+
metadata: {
|
|
111
|
+
name: yaml.metadata?.name ?? "unnamed",
|
|
112
|
+
createdAt: yaml.metadata?.created_at ?? new Date().toISOString(),
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Write a workflow graph to GRAPH.yaml in a run directory.
|
|
119
|
+
* Creates the directory if it doesn't exist. Write is atomic (write + rename).
|
|
120
|
+
*
|
|
121
|
+
* @param runDir — directory to write GRAPH.yaml into
|
|
122
|
+
* @param graph — the workflow graph to serialize
|
|
123
|
+
*/
|
|
124
|
+
export function writeGraph(runDir: string, graph: WorkflowGraph): void {
|
|
125
|
+
if (!existsSync(runDir)) {
|
|
126
|
+
mkdirSync(runDir, { recursive: true });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const yamlData: YamlGraph = {
|
|
130
|
+
steps: graph.steps.map((s) => ({
|
|
131
|
+
id: s.id,
|
|
132
|
+
title: s.title,
|
|
133
|
+
status: s.status,
|
|
134
|
+
prompt: s.prompt,
|
|
135
|
+
depends_on: s.dependsOn.length > 0 ? s.dependsOn : undefined,
|
|
136
|
+
parent_step_id: s.parentStepId ?? undefined,
|
|
137
|
+
started_at: s.startedAt ?? undefined,
|
|
138
|
+
finished_at: s.finishedAt ?? undefined,
|
|
139
|
+
})) as YamlStep[],
|
|
140
|
+
metadata: {
|
|
141
|
+
name: graph.metadata.name,
|
|
142
|
+
created_at: graph.metadata.createdAt,
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const filePath = join(runDir, GRAPH_FILENAME);
|
|
147
|
+
const tmpPath = filePath + ".tmp";
|
|
148
|
+
const content = stringify(yamlData);
|
|
149
|
+
writeFileSync(tmpPath, content, "utf-8");
|
|
150
|
+
// Atomic rename for crash safety
|
|
151
|
+
renameSync(tmpPath, filePath);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Get the next pending step whose dependencies are all complete.
|
|
156
|
+
*
|
|
157
|
+
* Returns the first step (in array order) with status "pending" where
|
|
158
|
+
* every step in its `dependsOn` list has status "complete".
|
|
159
|
+
*
|
|
160
|
+
* @param graph — the workflow graph to query
|
|
161
|
+
* @returns The next dispatchable step, or null if none available
|
|
162
|
+
*/
|
|
163
|
+
export function getNextPendingStep(graph: WorkflowGraph): GraphStep | null {
|
|
164
|
+
const statusMap = new Map(graph.steps.map((s) => [s.id, s.status]));
|
|
165
|
+
|
|
166
|
+
for (const step of graph.steps) {
|
|
167
|
+
if (step.status !== "pending") continue;
|
|
168
|
+
const depsComplete = step.dependsOn.every(
|
|
169
|
+
(depId) => statusMap.get(depId) === "complete",
|
|
170
|
+
);
|
|
171
|
+
if (depsComplete) return step;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Return a new graph with the specified step marked as "complete".
|
|
179
|
+
* Immutable — does not mutate the input graph.
|
|
180
|
+
*
|
|
181
|
+
* @param graph — the current workflow graph
|
|
182
|
+
* @param stepId — ID of the step to mark complete
|
|
183
|
+
* @returns New graph with the step's status set to "complete"
|
|
184
|
+
* @throws Error if stepId is not found in the graph
|
|
185
|
+
*/
|
|
186
|
+
export function markStepComplete(
|
|
187
|
+
graph: WorkflowGraph,
|
|
188
|
+
stepId: string,
|
|
189
|
+
): WorkflowGraph {
|
|
190
|
+
const found = graph.steps.some((s) => s.id === stepId);
|
|
191
|
+
if (!found) {
|
|
192
|
+
throw new Error(`Step not found: ${stepId}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
...graph,
|
|
197
|
+
steps: graph.steps.map((s) =>
|
|
198
|
+
s.id === stepId
|
|
199
|
+
? { ...s, status: "complete" as const, finishedAt: new Date().toISOString() }
|
|
200
|
+
: s,
|
|
201
|
+
),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ─── Iteration expansion ─────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Expand an iterate step into concrete instances. Pure and deterministic —
|
|
209
|
+
* identical inputs always produce identical output.
|
|
210
|
+
*
|
|
211
|
+
* Given a parent step with status "pending" and an array of matched items,
|
|
212
|
+
* creates one instance step per item, marks the parent as "expanded", and
|
|
213
|
+
* rewrites any downstream dependsOn references from the parent ID to the
|
|
214
|
+
* full set of instance IDs.
|
|
215
|
+
*
|
|
216
|
+
* @param graph — the current workflow graph (not mutated)
|
|
217
|
+
* @param stepId — ID of the iterate step to expand
|
|
218
|
+
* @param items — matched items from the source artifact
|
|
219
|
+
* @param promptTemplate — template with {{item}} placeholders
|
|
220
|
+
* @returns New WorkflowGraph with instances inserted and deps rewritten
|
|
221
|
+
* @throws Error if stepId not found or step is not pending
|
|
222
|
+
*/
|
|
223
|
+
export function expandIteration(
|
|
224
|
+
graph: WorkflowGraph,
|
|
225
|
+
stepId: string,
|
|
226
|
+
items: string[],
|
|
227
|
+
promptTemplate: string,
|
|
228
|
+
): WorkflowGraph {
|
|
229
|
+
const parentIndex = graph.steps.findIndex((s) => s.id === stepId);
|
|
230
|
+
if (parentIndex === -1) {
|
|
231
|
+
throw new Error(`expandIteration: step not found: ${stepId}`);
|
|
232
|
+
}
|
|
233
|
+
const parentStep = graph.steps[parentIndex];
|
|
234
|
+
if (parentStep.status !== "pending") {
|
|
235
|
+
throw new Error(
|
|
236
|
+
`expandIteration: step "${stepId}" has status "${parentStep.status}", expected "pending"`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Create instance steps
|
|
241
|
+
const instanceIds: string[] = [];
|
|
242
|
+
const instances: GraphStep[] = items.map((item, i) => {
|
|
243
|
+
const instanceId = `${stepId}--${String(i + 1).padStart(3, "0")}`;
|
|
244
|
+
instanceIds.push(instanceId);
|
|
245
|
+
return {
|
|
246
|
+
id: instanceId,
|
|
247
|
+
title: `${parentStep.title}: ${item}`,
|
|
248
|
+
status: "pending" as const,
|
|
249
|
+
prompt: promptTemplate.replace(/\{\{item\}\}/g, () => item),
|
|
250
|
+
dependsOn: [...parentStep.dependsOn],
|
|
251
|
+
parentStepId: stepId,
|
|
252
|
+
};
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Build new steps array: copy everything, mark parent as expanded,
|
|
256
|
+
// insert instances right after the parent, rewrite downstream deps.
|
|
257
|
+
const newSteps: GraphStep[] = [];
|
|
258
|
+
for (let i = 0; i < graph.steps.length; i++) {
|
|
259
|
+
if (i === parentIndex) {
|
|
260
|
+
// Mark parent as expanded
|
|
261
|
+
newSteps.push({ ...parentStep, status: "expanded" as const });
|
|
262
|
+
// Insert instances immediately after parent
|
|
263
|
+
newSteps.push(...instances);
|
|
264
|
+
} else {
|
|
265
|
+
const step = graph.steps[i];
|
|
266
|
+
// Rewrite dependsOn: replace parent ID with all instance IDs
|
|
267
|
+
const hasDep = step.dependsOn.includes(stepId);
|
|
268
|
+
if (hasDep) {
|
|
269
|
+
const rewritten = step.dependsOn.flatMap((dep) =>
|
|
270
|
+
dep === stepId ? instanceIds : [dep],
|
|
271
|
+
);
|
|
272
|
+
newSteps.push({ ...step, dependsOn: rewritten });
|
|
273
|
+
} else {
|
|
274
|
+
newSteps.push(step);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
...graph,
|
|
281
|
+
steps: newSteps,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ─── Definition → Graph conversion ──────────────────────────────────────
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Convert a parsed WorkflowDefinition into a WorkflowGraph with all
|
|
289
|
+
* steps in "pending" status. Used by run-manager to generate the initial
|
|
290
|
+
* GRAPH.yaml for a new run.
|
|
291
|
+
*
|
|
292
|
+
* @param def — a validated WorkflowDefinition from definition-loader
|
|
293
|
+
* @returns WorkflowGraph with pending steps and metadata from the definition
|
|
294
|
+
*/
|
|
295
|
+
export function initializeGraph(def: WorkflowDefinition): WorkflowGraph {
|
|
296
|
+
return {
|
|
297
|
+
steps: def.steps.map((s) => ({
|
|
298
|
+
id: s.id,
|
|
299
|
+
title: s.name,
|
|
300
|
+
status: "pending" as const,
|
|
301
|
+
prompt: s.prompt,
|
|
302
|
+
dependsOn: s.requires ?? [],
|
|
303
|
+
})),
|
|
304
|
+
metadata: {
|
|
305
|
+
name: def.name,
|
|
306
|
+
createdAt: new Date().toISOString(),
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** @deprecated Use initializeGraph instead. Kept for backward compatibility. */
|
|
312
|
+
export { initializeGraph as graphFromDefinition };
|
|
@@ -847,6 +847,7 @@ export function nativeMergeSquash(basePath: string, branch: string): GitMergeRes
|
|
|
847
847
|
cwd: basePath,
|
|
848
848
|
stdio: ["ignore", "pipe", "pipe"],
|
|
849
849
|
encoding: "utf-8",
|
|
850
|
+
env: GIT_NO_PROMPT_ENV,
|
|
850
851
|
});
|
|
851
852
|
return { success: true, conflicts: [] };
|
|
852
853
|
} catch (err: unknown) {
|
|
@@ -89,6 +89,7 @@ export const KNOWN_PREFERENCE_KEYS = new Set<string>([
|
|
|
89
89
|
"reactive_execution",
|
|
90
90
|
"github",
|
|
91
91
|
"service_tier",
|
|
92
|
+
"forensics_dedup",
|
|
92
93
|
]);
|
|
93
94
|
|
|
94
95
|
/** Canonical list of all dispatch unit types. */
|
|
@@ -223,6 +224,8 @@ export interface GSDPreferences {
|
|
|
223
224
|
github?: GitHubSyncConfig;
|
|
224
225
|
/** OpenAI service tier preference. "priority" = 2x cost, faster. "flex" = 0.5x cost, slower. Only affects gpt-5.4 models. */
|
|
225
226
|
service_tier?: "priority" | "flex";
|
|
227
|
+
/** Opt-in: search existing issues and PRs before filing from /gsd forensics. Uses additional AI tokens. */
|
|
228
|
+
forensics_dedup?: boolean;
|
|
226
229
|
}
|
|
227
230
|
|
|
228
231
|
export interface LoadedGSDPreferences {
|
|
@@ -200,12 +200,22 @@ function loadPreferencesFile(path: string, scope: "global" | "project"): LoadedG
|
|
|
200
200
|
export function parsePreferencesMarkdown(content: string): GSDPreferences | null {
|
|
201
201
|
// Use indexOf instead of [\s\S]*? regex to avoid backtracking (#468)
|
|
202
202
|
const startMarker = content.startsWith('---\r\n') ? '---\r\n' : '---\n';
|
|
203
|
-
if (
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
203
|
+
if (content.startsWith(startMarker)) {
|
|
204
|
+
const searchStart = startMarker.length;
|
|
205
|
+
const endIdx = content.indexOf('\n---', searchStart);
|
|
206
|
+
if (endIdx === -1) return null;
|
|
207
|
+
const block = content.slice(searchStart, endIdx);
|
|
208
|
+
return parseFrontmatterBlock(block.replace(/\r/g, ''));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Fallback: heading+list format (e.g. "## Git\n- isolation: none") (#2036)
|
|
212
|
+
// GSD agents may write preferences files without frontmatter delimiters.
|
|
213
|
+
if (/^##\s+\w/m.test(content)) {
|
|
214
|
+
return parseHeadingListFormat(content);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
console.warn("[parsePreferencesMarkdown] preferences.md exists but uses an unrecognized format — skipping.");
|
|
218
|
+
return null;
|
|
209
219
|
}
|
|
210
220
|
|
|
211
221
|
function parseFrontmatterBlock(frontmatter: string): GSDPreferences {
|
|
@@ -221,6 +231,51 @@ function parseFrontmatterBlock(frontmatter: string): GSDPreferences {
|
|
|
221
231
|
}
|
|
222
232
|
}
|
|
223
233
|
|
|
234
|
+
/**
|
|
235
|
+
* Parse heading+list format into a nested object, then cast to GSDPreferences.
|
|
236
|
+
* Handles markdown like:
|
|
237
|
+
* ## Git
|
|
238
|
+
* - isolation: none
|
|
239
|
+
* - commit_docs: true
|
|
240
|
+
* ## Models
|
|
241
|
+
* - planner: sonnet
|
|
242
|
+
*/
|
|
243
|
+
function parseHeadingListFormat(content: string): GSDPreferences {
|
|
244
|
+
const result: Record<string, Record<string, string>> = {};
|
|
245
|
+
let currentSection: string | null = null;
|
|
246
|
+
|
|
247
|
+
for (const rawLine of content.split('\n')) {
|
|
248
|
+
const line = rawLine.replace(/\r$/, '');
|
|
249
|
+
const headingMatch = line.match(/^##\s+(.+)$/);
|
|
250
|
+
if (headingMatch) {
|
|
251
|
+
currentSection = headingMatch[1].trim().toLowerCase().replace(/\s+/g, '_');
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (currentSection) {
|
|
255
|
+
const itemMatch = line.match(/^-\s+([^:]+):\s*(.*)$/);
|
|
256
|
+
if (itemMatch) {
|
|
257
|
+
if (!result[currentSection]) result[currentSection] = {};
|
|
258
|
+
const value = itemMatch[2].trim();
|
|
259
|
+
// Coerce "true"/"false" strings and numbers
|
|
260
|
+
result[currentSection][itemMatch[1].trim()] = value;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Convert string values to appropriate types via YAML parser for each section
|
|
266
|
+
const typed: Record<string, unknown> = {};
|
|
267
|
+
for (const [section, entries] of Object.entries(result)) {
|
|
268
|
+
const yamlLines = Object.entries(entries).map(([k, v]) => `${k}: ${v}`).join('\n');
|
|
269
|
+
try {
|
|
270
|
+
typed[section] = parseYaml(yamlLines);
|
|
271
|
+
} catch {
|
|
272
|
+
typed[section] = entries;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return typed as GSDPreferences;
|
|
277
|
+
}
|
|
278
|
+
|
|
224
279
|
// ─── Merging ────────────────────────────────────────────────────────────────
|
|
225
280
|
|
|
226
281
|
/**
|
|
@@ -286,6 +341,7 @@ function mergePreferences(base: GSDPreferences, override: GSDPreferences): GSDPr
|
|
|
286
341
|
? { ...(base.github ?? {}), ...(override.github ?? {}) } as import("../github-sync/types.js").GitHubSyncConfig
|
|
287
342
|
: undefined,
|
|
288
343
|
service_tier: override.service_tier ?? base.service_tier,
|
|
344
|
+
forensics_dedup: override.forensics_dedup ?? base.forensics_dedup,
|
|
289
345
|
};
|
|
290
346
|
}
|
|
291
347
|
|
|
@@ -101,11 +101,19 @@ Explain your findings:
|
|
|
101
101
|
- **Code snippet** — the problematic code and what it should do instead
|
|
102
102
|
- **Recovery** — what the user can do right now to get unstuck
|
|
103
103
|
|
|
104
|
+
{{dedupSection}}
|
|
105
|
+
|
|
104
106
|
Then **offer GitHub issue creation**: "Would you like me to create a GitHub issue for this on gsd-build/gsd-2?"
|
|
105
107
|
|
|
106
|
-
|
|
108
|
+
**CRITICAL: The `github_issues` tool ONLY targets the current user's repository — it has no `repo` parameter. You MUST use `gh issue create --repo gsd-build/gsd-2` via the `bash` tool to file on the correct repo. Do NOT use the `github_issues` tool for this.**
|
|
107
109
|
|
|
108
|
-
|
|
110
|
+
If yes, create using the `bash` tool:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
gh issue create --repo gsd-build/gsd-2 \
|
|
114
|
+
--title "..." \
|
|
115
|
+
--label "bug" --label "auto-generated" \
|
|
116
|
+
--body "$(cat <<'EOF'
|
|
109
117
|
## Problem
|
|
110
118
|
[1-2 sentence summary]
|
|
111
119
|
|
|
@@ -128,11 +136,10 @@ If yes, create using `gh issue create` with this format:
|
|
|
128
136
|
|
|
129
137
|
---
|
|
130
138
|
*Auto-generated by `/gsd forensics`*
|
|
139
|
+
EOF
|
|
140
|
+
)"
|
|
131
141
|
```
|
|
132
142
|
|
|
133
|
-
**Repository:** gsd-build/gsd-2
|
|
134
|
-
**Labels:** bug, auto-generated
|
|
135
|
-
|
|
136
143
|
### Redaction Rules (CRITICAL)
|
|
137
144
|
|
|
138
145
|
Before creating the issue, you MUST:
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { createHash } from "node:crypto";
|
|
10
10
|
import { execFileSync } from "node:child_process";
|
|
11
|
-
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
12
12
|
import { homedir } from "node:os";
|
|
13
13
|
import { basename, dirname, join, resolve } from "node:path";
|
|
14
14
|
|
|
@@ -271,15 +271,54 @@ export function externalProjectsRoot(): string {
|
|
|
271
271
|
return join(base, "projects");
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
+
// ─── Numbered Variant Cleanup ────────────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* macOS collision pattern: `.gsd 2`, `.gsd 3`, `.gsd 4`, etc.
|
|
278
|
+
*
|
|
279
|
+
* When `symlinkSync` (or Finder) tries to create `.gsd` but a real directory
|
|
280
|
+
* already exists at that path, macOS APFS silently renames the new entry to
|
|
281
|
+
* `.gsd 2`, then `.gsd 3`, and so on. These numbered variants confuse GSD
|
|
282
|
+
* because the canonical `.gsd` path no longer resolves to the external state
|
|
283
|
+
* directory, making tracked planning files appear deleted.
|
|
284
|
+
*
|
|
285
|
+
* This helper scans the project root for entries matching `.gsd <digits>` and
|
|
286
|
+
* removes them. It is called early in `ensureGsdSymlink()` so that the
|
|
287
|
+
* canonical `.gsd` path is always the one in use.
|
|
288
|
+
*/
|
|
289
|
+
const GSD_NUMBERED_VARIANT_RE = /^\.gsd \d+$/;
|
|
290
|
+
|
|
291
|
+
export function cleanNumberedGsdVariants(projectPath: string): string[] {
|
|
292
|
+
const removed: string[] = [];
|
|
293
|
+
try {
|
|
294
|
+
const entries = readdirSync(projectPath);
|
|
295
|
+
for (const entry of entries) {
|
|
296
|
+
if (GSD_NUMBERED_VARIANT_RE.test(entry)) {
|
|
297
|
+
const fullPath = join(projectPath, entry);
|
|
298
|
+
try {
|
|
299
|
+
rmSync(fullPath, { recursive: true, force: true });
|
|
300
|
+
removed.push(entry);
|
|
301
|
+
} catch {
|
|
302
|
+
// Best-effort: if removal fails (e.g. permissions), continue with next
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
} catch {
|
|
307
|
+
// Non-fatal: readdir failure should not block symlink creation
|
|
308
|
+
}
|
|
309
|
+
return removed;
|
|
310
|
+
}
|
|
311
|
+
|
|
274
312
|
// ─── Symlink Management ─────────────────────────────────────────────────────
|
|
275
313
|
|
|
276
314
|
/**
|
|
277
315
|
* Ensure the `<project>/.gsd` symlink points to the external state directory.
|
|
278
316
|
*
|
|
279
|
-
* 1.
|
|
280
|
-
* 2.
|
|
281
|
-
* 3. If `<project>/.gsd`
|
|
282
|
-
* 4. If `<project>/.gsd` is
|
|
317
|
+
* 1. Clean up any macOS numbered collision variants (`.gsd 2`, `.gsd 3`, etc.)
|
|
318
|
+
* 2. mkdir -p the external dir
|
|
319
|
+
* 3. If `<project>/.gsd` doesn't exist → create symlink
|
|
320
|
+
* 4. If `<project>/.gsd` is already the correct symlink → no-op
|
|
321
|
+
* 5. If `<project>/.gsd` is a real directory → return as-is (migration handles later)
|
|
283
322
|
*
|
|
284
323
|
* Returns the resolved external path.
|
|
285
324
|
*/
|
|
@@ -297,6 +336,10 @@ export function ensureGsdSymlink(projectPath: string): string {
|
|
|
297
336
|
return localGsd;
|
|
298
337
|
}
|
|
299
338
|
|
|
339
|
+
// Clean up macOS numbered collision variants (.gsd 2, .gsd 3, etc.) before
|
|
340
|
+
// any existence checks — otherwise they accumulate and confuse state (#2205).
|
|
341
|
+
cleanNumberedGsdVariants(projectPath);
|
|
342
|
+
|
|
300
343
|
// Ensure external directory exists
|
|
301
344
|
mkdirSync(externalPath, { recursive: true });
|
|
302
345
|
|