gsd-pi 2.37.1-dev.d3ace49 β†’ 2.38.0-dev.29edcdc

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.
Files changed (188) hide show
  1. package/dist/app-paths.js +1 -1
  2. package/dist/cli.js +9 -0
  3. package/dist/extension-discovery.d.ts +5 -3
  4. package/dist/extension-discovery.js +14 -9
  5. package/dist/extension-registry.js +2 -2
  6. package/dist/remote-questions-config.js +2 -2
  7. package/dist/resource-loader.js +34 -1
  8. package/dist/resources/extensions/browser-tools/package.json +3 -1
  9. package/dist/resources/extensions/cmux/index.js +55 -1
  10. package/dist/resources/extensions/context7/package.json +1 -1
  11. package/dist/resources/extensions/env-utils.js +29 -0
  12. package/dist/resources/extensions/get-secrets-from-user.js +5 -24
  13. package/dist/resources/extensions/github-sync/cli.js +284 -0
  14. package/dist/resources/extensions/github-sync/index.js +73 -0
  15. package/dist/resources/extensions/github-sync/mapping.js +67 -0
  16. package/dist/resources/extensions/github-sync/sync.js +424 -0
  17. package/dist/resources/extensions/github-sync/templates.js +118 -0
  18. package/dist/resources/extensions/github-sync/types.js +7 -0
  19. package/dist/resources/extensions/google-search/package.json +3 -1
  20. package/dist/resources/extensions/gsd/auto/session.js +6 -23
  21. package/dist/resources/extensions/gsd/auto-dispatch.js +8 -9
  22. package/dist/resources/extensions/gsd/auto-loop.js +597 -588
  23. package/dist/resources/extensions/gsd/auto-post-unit.js +99 -70
  24. package/dist/resources/extensions/gsd/auto-prompts.js +23 -43
  25. package/dist/resources/extensions/gsd/auto-start.js +13 -2
  26. package/dist/resources/extensions/gsd/auto-worktree-sync.js +13 -5
  27. package/dist/resources/extensions/gsd/auto-worktree.js +3 -3
  28. package/dist/resources/extensions/gsd/auto.js +143 -96
  29. package/dist/resources/extensions/gsd/captures.js +9 -1
  30. package/dist/resources/extensions/gsd/commands-extensions.js +3 -2
  31. package/dist/resources/extensions/gsd/commands-handlers.js +16 -3
  32. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
  33. package/dist/resources/extensions/gsd/commands.js +24 -3
  34. package/dist/resources/extensions/gsd/context-budget.js +2 -10
  35. package/dist/resources/extensions/gsd/detection.js +1 -2
  36. package/dist/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  37. package/dist/resources/extensions/gsd/doctor-checks.js +82 -0
  38. package/dist/resources/extensions/gsd/doctor-environment.js +78 -0
  39. package/dist/resources/extensions/gsd/doctor-format.js +15 -0
  40. package/dist/resources/extensions/gsd/doctor-providers.js +27 -11
  41. package/dist/resources/extensions/gsd/doctor.js +204 -12
  42. package/dist/resources/extensions/gsd/exit-command.js +2 -1
  43. package/dist/resources/extensions/gsd/export.js +1 -1
  44. package/dist/resources/extensions/gsd/files.js +6 -2
  45. package/dist/resources/extensions/gsd/forensics.js +1 -1
  46. package/dist/resources/extensions/gsd/git-service.js +15 -12
  47. package/dist/resources/extensions/gsd/guided-flow.js +82 -32
  48. package/dist/resources/extensions/gsd/index.js +24 -20
  49. package/dist/resources/extensions/gsd/migrate/parsers.js +1 -1
  50. package/dist/resources/extensions/gsd/native-git-bridge.js +37 -0
  51. package/dist/resources/extensions/gsd/package.json +1 -1
  52. package/dist/resources/extensions/gsd/preferences-models.js +0 -12
  53. package/dist/resources/extensions/gsd/preferences-types.js +1 -1
  54. package/dist/resources/extensions/gsd/preferences-validation.js +59 -11
  55. package/dist/resources/extensions/gsd/preferences.js +8 -5
  56. package/dist/resources/extensions/gsd/prompts/discuss.md +11 -14
  57. package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -2
  58. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  59. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  60. package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  61. package/dist/resources/extensions/gsd/prompts/queue.md +4 -8
  62. package/dist/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
  63. package/dist/resources/extensions/gsd/prompts/run-uat.md +27 -10
  64. package/dist/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  65. package/dist/resources/extensions/gsd/repo-identity.js +21 -4
  66. package/dist/resources/extensions/gsd/resource-version.js +2 -1
  67. package/dist/resources/extensions/gsd/roadmap-mutations.js +24 -0
  68. package/dist/resources/extensions/gsd/state.js +1 -1
  69. package/dist/resources/extensions/gsd/visualizer-data.js +1 -1
  70. package/dist/resources/extensions/gsd/worktree.js +35 -16
  71. package/dist/resources/extensions/mcp-client/index.js +14 -1
  72. package/dist/resources/extensions/remote-questions/status.js +2 -1
  73. package/dist/resources/extensions/remote-questions/store.js +2 -1
  74. package/dist/resources/extensions/search-the-web/provider.js +2 -1
  75. package/dist/resources/extensions/subagent/index.js +12 -3
  76. package/dist/resources/extensions/subagent/isolation.js +2 -1
  77. package/dist/resources/extensions/ttsr/rule-loader.js +2 -1
  78. package/dist/resources/extensions/universal-config/package.json +1 -1
  79. package/dist/welcome-screen.d.ts +12 -0
  80. package/dist/welcome-screen.js +53 -0
  81. package/package.json +1 -1
  82. package/packages/pi-ai/dist/utils/oauth/anthropic.js +2 -2
  83. package/packages/pi-ai/dist/utils/oauth/anthropic.js.map +1 -1
  84. package/packages/pi-ai/src/utils/oauth/anthropic.ts +2 -2
  85. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  86. package/packages/pi-coding-agent/dist/core/extensions/loader.js +205 -7
  87. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  88. package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
  89. package/packages/pi-coding-agent/dist/core/package-manager.js +8 -4
  90. package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
  91. package/packages/pi-coding-agent/package.json +1 -1
  92. package/packages/pi-coding-agent/src/core/extensions/loader.ts +223 -7
  93. package/packages/pi-coding-agent/src/core/package-manager.ts +8 -4
  94. package/pkg/package.json +1 -1
  95. package/src/resources/extensions/cmux/index.ts +57 -1
  96. package/src/resources/extensions/env-utils.ts +31 -0
  97. package/src/resources/extensions/get-secrets-from-user.ts +5 -24
  98. package/src/resources/extensions/github-sync/cli.ts +364 -0
  99. package/src/resources/extensions/github-sync/index.ts +93 -0
  100. package/src/resources/extensions/github-sync/mapping.ts +81 -0
  101. package/src/resources/extensions/github-sync/sync.ts +556 -0
  102. package/src/resources/extensions/github-sync/templates.ts +183 -0
  103. package/src/resources/extensions/github-sync/tests/cli.test.ts +20 -0
  104. package/src/resources/extensions/github-sync/tests/commit-linking.test.ts +39 -0
  105. package/src/resources/extensions/github-sync/tests/mapping.test.ts +104 -0
  106. package/src/resources/extensions/github-sync/tests/templates.test.ts +110 -0
  107. package/src/resources/extensions/github-sync/types.ts +47 -0
  108. package/src/resources/extensions/gsd/auto/session.ts +7 -25
  109. package/src/resources/extensions/gsd/auto-dispatch.ts +7 -9
  110. package/src/resources/extensions/gsd/auto-loop.ts +484 -546
  111. package/src/resources/extensions/gsd/auto-post-unit.ts +80 -44
  112. package/src/resources/extensions/gsd/auto-prompts.ts +25 -45
  113. package/src/resources/extensions/gsd/auto-start.ts +18 -2
  114. package/src/resources/extensions/gsd/auto-worktree-sync.ts +15 -4
  115. package/src/resources/extensions/gsd/auto-worktree.ts +3 -3
  116. package/src/resources/extensions/gsd/auto.ts +139 -101
  117. package/src/resources/extensions/gsd/captures.ts +10 -1
  118. package/src/resources/extensions/gsd/commands-extensions.ts +4 -2
  119. package/src/resources/extensions/gsd/commands-handlers.ts +17 -2
  120. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
  121. package/src/resources/extensions/gsd/commands.ts +26 -4
  122. package/src/resources/extensions/gsd/context-budget.ts +2 -12
  123. package/src/resources/extensions/gsd/detection.ts +2 -2
  124. package/src/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  125. package/src/resources/extensions/gsd/doctor-checks.ts +75 -0
  126. package/src/resources/extensions/gsd/doctor-environment.ts +82 -1
  127. package/src/resources/extensions/gsd/doctor-format.ts +20 -0
  128. package/src/resources/extensions/gsd/doctor-providers.ts +26 -9
  129. package/src/resources/extensions/gsd/doctor-types.ts +16 -1
  130. package/src/resources/extensions/gsd/doctor.ts +199 -14
  131. package/src/resources/extensions/gsd/exit-command.ts +2 -2
  132. package/src/resources/extensions/gsd/export.ts +1 -1
  133. package/src/resources/extensions/gsd/files.ts +5 -3
  134. package/src/resources/extensions/gsd/forensics.ts +1 -1
  135. package/src/resources/extensions/gsd/git-service.ts +20 -10
  136. package/src/resources/extensions/gsd/guided-flow.ts +110 -38
  137. package/src/resources/extensions/gsd/index.ts +24 -17
  138. package/src/resources/extensions/gsd/migrate/parsers.ts +1 -1
  139. package/src/resources/extensions/gsd/native-git-bridge.ts +37 -0
  140. package/src/resources/extensions/gsd/preferences-models.ts +0 -12
  141. package/src/resources/extensions/gsd/preferences-types.ts +4 -4
  142. package/src/resources/extensions/gsd/preferences-validation.ts +51 -11
  143. package/src/resources/extensions/gsd/preferences.ts +8 -5
  144. package/src/resources/extensions/gsd/prompts/discuss.md +11 -14
  145. package/src/resources/extensions/gsd/prompts/execute-task.md +2 -2
  146. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  147. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  148. package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  149. package/src/resources/extensions/gsd/prompts/queue.md +4 -8
  150. package/src/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
  151. package/src/resources/extensions/gsd/prompts/run-uat.md +27 -10
  152. package/src/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  153. package/src/resources/extensions/gsd/repo-identity.ts +23 -4
  154. package/src/resources/extensions/gsd/resource-version.ts +3 -1
  155. package/src/resources/extensions/gsd/roadmap-mutations.ts +29 -0
  156. package/src/resources/extensions/gsd/state.ts +1 -1
  157. package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +21 -18
  158. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +122 -68
  159. package/src/resources/extensions/gsd/tests/cmux.test.ts +93 -0
  160. package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +266 -0
  161. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +86 -3
  162. package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -7
  163. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +59 -0
  164. package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +21 -1
  165. package/src/resources/extensions/gsd/tests/run-uat.test.ts +11 -3
  166. package/src/resources/extensions/gsd/tests/worktree.test.ts +47 -0
  167. package/src/resources/extensions/gsd/types.ts +0 -1
  168. package/src/resources/extensions/gsd/visualizer-data.ts +1 -1
  169. package/src/resources/extensions/gsd/worktree.ts +35 -15
  170. package/src/resources/extensions/mcp-client/index.ts +17 -1
  171. package/src/resources/extensions/remote-questions/status.ts +3 -1
  172. package/src/resources/extensions/remote-questions/store.ts +3 -1
  173. package/src/resources/extensions/search-the-web/provider.ts +2 -1
  174. package/src/resources/extensions/subagent/index.ts +12 -3
  175. package/src/resources/extensions/subagent/isolation.ts +3 -1
  176. package/src/resources/extensions/ttsr/rule-loader.ts +3 -1
  177. package/dist/resources/extensions/gsd/prompt-compressor.js +0 -393
  178. package/dist/resources/extensions/gsd/semantic-chunker.js +0 -254
  179. package/dist/resources/extensions/gsd/summary-distiller.js +0 -212
  180. package/src/resources/extensions/gsd/prompt-compressor.ts +0 -508
  181. package/src/resources/extensions/gsd/semantic-chunker.ts +0 -336
  182. package/src/resources/extensions/gsd/summary-distiller.ts +0 -258
  183. package/src/resources/extensions/gsd/tests/context-compression.test.ts +0 -193
  184. package/src/resources/extensions/gsd/tests/prompt-compressor.test.ts +0 -529
  185. package/src/resources/extensions/gsd/tests/semantic-chunker.test.ts +0 -426
  186. package/src/resources/extensions/gsd/tests/summary-distiller.test.ts +0 -323
  187. package/src/resources/extensions/gsd/tests/token-optimization-benchmark.test.ts +0 -1272
  188. package/src/resources/extensions/gsd/tests/token-optimization-prefs.test.ts +0 -164
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Markdown formatters for GitHub issue bodies, PR descriptions,
3
+ * and summary comments.
4
+ *
5
+ * All functions produce GitHub-flavored markdown strings ready
6
+ * for the `gh` CLI body parameters.
7
+ */
8
+
9
+ // ─── Milestone Issue Body ───────────────────────────────────────────────────
10
+
11
+ export interface MilestoneData {
12
+ id: string;
13
+ title: string;
14
+ vision?: string;
15
+ successCriteria?: string[];
16
+ slices?: Array<{ id: string; title: string; taskCount?: number }>;
17
+ }
18
+
19
+ export function formatMilestoneIssueBody(data: MilestoneData): string {
20
+ const lines: string[] = [];
21
+
22
+ lines.push(`# ${data.id}: ${data.title}`);
23
+ lines.push("");
24
+
25
+ if (data.vision) {
26
+ lines.push("## Vision");
27
+ lines.push(data.vision);
28
+ lines.push("");
29
+ }
30
+
31
+ if (data.successCriteria?.length) {
32
+ lines.push("## Success Criteria");
33
+ for (const criterion of data.successCriteria) {
34
+ lines.push(`- [ ] ${criterion}`);
35
+ }
36
+ lines.push("");
37
+ }
38
+
39
+ if (data.slices?.length) {
40
+ lines.push("## Slices");
41
+ lines.push("");
42
+ lines.push("| Slice | Title | Tasks |");
43
+ lines.push("|-------|-------|-------|");
44
+ for (const slice of data.slices) {
45
+ lines.push(`| ${slice.id} | ${slice.title} | ${slice.taskCount ?? "β€”"} |`);
46
+ }
47
+ lines.push("");
48
+ }
49
+
50
+ lines.push("---");
51
+ lines.push("*Auto-generated by GSD GitHub Sync*");
52
+
53
+ return lines.join("\n");
54
+ }
55
+
56
+ // ─── Slice PR Body ──────────────────────────────────────────────────────────
57
+
58
+ export interface SliceData {
59
+ id: string;
60
+ title: string;
61
+ goal?: string;
62
+ mustHaves?: string[];
63
+ demoCriterion?: string;
64
+ tasks?: Array<{ id: string; title: string; issueNumber?: number }>;
65
+ }
66
+
67
+ export function formatSlicePRBody(data: SliceData): string {
68
+ const lines: string[] = [];
69
+
70
+ lines.push(`## ${data.id}: ${data.title}`);
71
+ lines.push("");
72
+
73
+ if (data.goal) {
74
+ lines.push("### Goal");
75
+ lines.push(data.goal);
76
+ lines.push("");
77
+ }
78
+
79
+ if (data.mustHaves?.length) {
80
+ lines.push("### Must-Haves");
81
+ for (const item of data.mustHaves) {
82
+ lines.push(`- ${item}`);
83
+ }
84
+ lines.push("");
85
+ }
86
+
87
+ if (data.demoCriterion) {
88
+ lines.push("### Demo Criterion");
89
+ lines.push(data.demoCriterion);
90
+ lines.push("");
91
+ }
92
+
93
+ if (data.tasks?.length) {
94
+ lines.push("### Tasks");
95
+ for (const task of data.tasks) {
96
+ const ref = task.issueNumber ? ` (#${task.issueNumber})` : "";
97
+ lines.push(`- [ ] ${task.id}: ${task.title}${ref}`);
98
+ }
99
+ lines.push("");
100
+ }
101
+
102
+ lines.push("---");
103
+ lines.push("*Auto-generated by GSD GitHub Sync*");
104
+
105
+ return lines.join("\n");
106
+ }
107
+
108
+ // ─── Task Issue Body ────────────────────────────────────────────────────────
109
+
110
+ export interface TaskData {
111
+ id: string;
112
+ title: string;
113
+ description?: string;
114
+ files?: string[];
115
+ verifyCriteria?: string[];
116
+ }
117
+
118
+ export function formatTaskIssueBody(data: TaskData): string {
119
+ const lines: string[] = [];
120
+
121
+ lines.push(`## ${data.id}: ${data.title}`);
122
+ lines.push("");
123
+
124
+ if (data.description) {
125
+ lines.push(data.description);
126
+ lines.push("");
127
+ }
128
+
129
+ if (data.files?.length) {
130
+ lines.push("### Files");
131
+ for (const file of data.files) {
132
+ lines.push(`- \`${file}\``);
133
+ }
134
+ lines.push("");
135
+ }
136
+
137
+ if (data.verifyCriteria?.length) {
138
+ lines.push("### Verification");
139
+ for (const criterion of data.verifyCriteria) {
140
+ lines.push(`- [ ] ${criterion}`);
141
+ }
142
+ lines.push("");
143
+ }
144
+
145
+ return lines.join("\n");
146
+ }
147
+
148
+ // ─── Summary Comment ────────────────────────────────────────────────────────
149
+
150
+ export interface SummaryData {
151
+ oneLiner?: string;
152
+ body?: string;
153
+ frontmatter?: Record<string, unknown>;
154
+ }
155
+
156
+ export function formatSummaryComment(data: SummaryData): string {
157
+ const lines: string[] = [];
158
+
159
+ if (data.oneLiner) {
160
+ lines.push(`**Summary:** ${data.oneLiner}`);
161
+ lines.push("");
162
+ }
163
+
164
+ if (data.body) {
165
+ lines.push(data.body);
166
+ lines.push("");
167
+ }
168
+
169
+ if (data.frontmatter && Object.keys(data.frontmatter).length > 0) {
170
+ lines.push("<details>");
171
+ lines.push("<summary>Metadata</summary>");
172
+ lines.push("");
173
+ lines.push("```yaml");
174
+ for (const [key, value] of Object.entries(data.frontmatter)) {
175
+ lines.push(`${key}: ${JSON.stringify(value)}`);
176
+ }
177
+ lines.push("```");
178
+ lines.push("");
179
+ lines.push("</details>");
180
+ }
181
+
182
+ return lines.join("\n");
183
+ }
@@ -0,0 +1,20 @@
1
+ import test, { describe, it, beforeEach } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { ghIsAvailable, _resetGhCache } from "../cli.ts";
4
+
5
+ describe("cli", () => {
6
+ beforeEach(() => {
7
+ _resetGhCache();
8
+ });
9
+
10
+ it("ghIsAvailable returns boolean", () => {
11
+ const result = ghIsAvailable();
12
+ assert.equal(typeof result, "boolean");
13
+ });
14
+
15
+ it("ghIsAvailable caches result", () => {
16
+ const first = ghIsAvailable();
17
+ const second = ghIsAvailable();
18
+ assert.equal(first, second);
19
+ });
20
+ });
@@ -0,0 +1,39 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { buildTaskCommitMessage } from "../../gsd/git-service.ts";
4
+
5
+ describe("commit linking", () => {
6
+ it("appends Resolves #N when issueNumber is set", () => {
7
+ const msg = buildTaskCommitMessage({
8
+ taskId: "S01/T02",
9
+ taskTitle: "implement auth",
10
+ issueNumber: 43,
11
+ });
12
+ assert.ok(msg.includes("Resolves #43"), "should include Resolves trailer");
13
+ assert.ok(msg.startsWith("feat(S01/T02):"), "subject line unchanged");
14
+ });
15
+
16
+ it("includes both key files and Resolves #N", () => {
17
+ const msg = buildTaskCommitMessage({
18
+ taskId: "S01/T02",
19
+ taskTitle: "implement auth",
20
+ keyFiles: ["src/auth.ts"],
21
+ issueNumber: 43,
22
+ });
23
+ assert.ok(msg.includes("- src/auth.ts"), "key files present");
24
+ assert.ok(msg.includes("Resolves #43"), "Resolves trailer present");
25
+ // Resolves should come after key files
26
+ const keyFilesIdx = msg.indexOf("- src/auth.ts");
27
+ const resolvesIdx = msg.indexOf("Resolves #43");
28
+ assert.ok(resolvesIdx > keyFilesIdx, "Resolves after key files");
29
+ });
30
+
31
+ it("no Resolves trailer when issueNumber is not set", () => {
32
+ const msg = buildTaskCommitMessage({
33
+ taskId: "S01/T02",
34
+ taskTitle: "implement auth",
35
+ });
36
+ assert.ok(!msg.includes("Resolves"), "no Resolves when no issueNumber");
37
+ assert.ok(!msg.includes("\n"), "no body when no issueNumber or keyFiles");
38
+ });
39
+ });
@@ -0,0 +1,104 @@
1
+ import { describe, it, beforeEach, afterEach } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import {
7
+ loadSyncMapping,
8
+ saveSyncMapping,
9
+ createEmptyMapping,
10
+ getMilestoneRecord,
11
+ getSliceRecord,
12
+ getTaskRecord,
13
+ getTaskIssueNumber,
14
+ setMilestoneRecord,
15
+ setSliceRecord,
16
+ setTaskRecord,
17
+ } from "../mapping.ts";
18
+ import type { SyncMapping, MilestoneSyncRecord, SliceSyncRecord, SyncEntityRecord } from "../types.ts";
19
+
20
+ describe("mapping", () => {
21
+ let tmpDir: string;
22
+
23
+ beforeEach(() => {
24
+ tmpDir = mkdtempSync(join(tmpdir(), "gsd-sync-test-"));
25
+ mkdirSync(join(tmpDir, ".gsd"), { recursive: true });
26
+ });
27
+
28
+ afterEach(() => {
29
+ rmSync(tmpDir, { recursive: true, force: true });
30
+ });
31
+
32
+ it("loadSyncMapping returns null when no file exists", () => {
33
+ const result = loadSyncMapping(tmpDir);
34
+ assert.equal(result, null);
35
+ });
36
+
37
+ it("round-trips save/load", () => {
38
+ const mapping = createEmptyMapping("owner/repo");
39
+ saveSyncMapping(tmpDir, mapping);
40
+ const loaded = loadSyncMapping(tmpDir);
41
+ assert.deepEqual(loaded, mapping);
42
+ });
43
+
44
+ it("createEmptyMapping has correct structure", () => {
45
+ const mapping = createEmptyMapping("owner/repo");
46
+ assert.equal(mapping.version, 1);
47
+ assert.equal(mapping.repo, "owner/repo");
48
+ assert.deepEqual(mapping.milestones, {});
49
+ assert.deepEqual(mapping.slices, {});
50
+ assert.deepEqual(mapping.tasks, {});
51
+ });
52
+
53
+ it("milestone record accessors work", () => {
54
+ const mapping = createEmptyMapping("owner/repo");
55
+ assert.equal(getMilestoneRecord(mapping, "M001"), null);
56
+
57
+ const record: MilestoneSyncRecord = {
58
+ issueNumber: 42,
59
+ ghMilestoneNumber: 1,
60
+ lastSyncedAt: "2025-01-01T00:00:00Z",
61
+ state: "open",
62
+ };
63
+ setMilestoneRecord(mapping, "M001", record);
64
+ assert.deepEqual(getMilestoneRecord(mapping, "M001"), record);
65
+ });
66
+
67
+ it("slice record accessors work", () => {
68
+ const mapping = createEmptyMapping("owner/repo");
69
+ assert.equal(getSliceRecord(mapping, "M001", "S01"), null);
70
+
71
+ const record: SliceSyncRecord = {
72
+ issueNumber: 0,
73
+ prNumber: 50,
74
+ branch: "milestone/M001/S01",
75
+ lastSyncedAt: "2025-01-01T00:00:00Z",
76
+ state: "open",
77
+ };
78
+ setSliceRecord(mapping, "M001", "S01", record);
79
+ assert.deepEqual(getSliceRecord(mapping, "M001", "S01"), record);
80
+ });
81
+
82
+ it("task record accessors work", () => {
83
+ const mapping = createEmptyMapping("owner/repo");
84
+ assert.equal(getTaskRecord(mapping, "M001", "S01", "T01"), null);
85
+ assert.equal(getTaskIssueNumber(mapping, "M001", "S01", "T01"), null);
86
+
87
+ const record: SyncEntityRecord = {
88
+ issueNumber: 43,
89
+ lastSyncedAt: "2025-01-01T00:00:00Z",
90
+ state: "open",
91
+ };
92
+ setTaskRecord(mapping, "M001", "S01", "T01", record);
93
+ assert.deepEqual(getTaskRecord(mapping, "M001", "S01", "T01"), record);
94
+ assert.equal(getTaskIssueNumber(mapping, "M001", "S01", "T01"), 43);
95
+ });
96
+
97
+ it("rejects mapping with wrong version", () => {
98
+ const mapping = createEmptyMapping("owner/repo");
99
+ (mapping as any).version = 2;
100
+ saveSyncMapping(tmpDir, mapping);
101
+ const loaded = loadSyncMapping(tmpDir);
102
+ assert.equal(loaded, null);
103
+ });
104
+ });
@@ -0,0 +1,110 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import {
4
+ formatMilestoneIssueBody,
5
+ formatSlicePRBody,
6
+ formatTaskIssueBody,
7
+ formatSummaryComment,
8
+ } from "../templates.ts";
9
+
10
+ describe("templates", () => {
11
+ describe("formatMilestoneIssueBody", () => {
12
+ it("includes title and vision", () => {
13
+ const body = formatMilestoneIssueBody({
14
+ id: "M001",
15
+ title: "Build Auth",
16
+ vision: "Secure authentication for all users",
17
+ });
18
+ assert.ok(body.includes("M001: Build Auth"));
19
+ assert.ok(body.includes("Secure authentication"));
20
+ });
21
+
22
+ it("renders success criteria as checkboxes", () => {
23
+ const body = formatMilestoneIssueBody({
24
+ id: "M001",
25
+ title: "Auth",
26
+ successCriteria: ["Users can log in", "OAuth works"],
27
+ });
28
+ assert.ok(body.includes("- [ ] Users can log in"));
29
+ assert.ok(body.includes("- [ ] OAuth works"));
30
+ });
31
+
32
+ it("renders slice table", () => {
33
+ const body = formatMilestoneIssueBody({
34
+ id: "M001",
35
+ title: "Auth",
36
+ slices: [
37
+ { id: "S01", title: "Core types", taskCount: 3 },
38
+ { id: "S02", title: "OAuth", taskCount: 5 },
39
+ ],
40
+ });
41
+ assert.ok(body.includes("| S01 | Core types | 3 |"));
42
+ assert.ok(body.includes("| S02 | OAuth | 5 |"));
43
+ });
44
+ });
45
+
46
+ describe("formatSlicePRBody", () => {
47
+ it("includes goal and must-haves", () => {
48
+ const body = formatSlicePRBody({
49
+ id: "S01",
50
+ title: "Core Auth Types",
51
+ goal: "Define all auth types",
52
+ mustHaves: ["User type", "Session type"],
53
+ });
54
+ assert.ok(body.includes("Define all auth types"));
55
+ assert.ok(body.includes("- User type"));
56
+ assert.ok(body.includes("- Session type"));
57
+ });
58
+
59
+ it("renders task checklist with issue links", () => {
60
+ const body = formatSlicePRBody({
61
+ id: "S01",
62
+ title: "Auth",
63
+ tasks: [
64
+ { id: "T01", title: "Types", issueNumber: 43 },
65
+ { id: "T02", title: "Schema" },
66
+ ],
67
+ });
68
+ assert.ok(body.includes("- [ ] T01: Types (#43)"));
69
+ assert.ok(body.includes("- [ ] T02: Schema"));
70
+ assert.ok(!body.includes("T02: Schema (#"));
71
+ });
72
+ });
73
+
74
+ describe("formatTaskIssueBody", () => {
75
+ it("includes files and verification", () => {
76
+ const body = formatTaskIssueBody({
77
+ id: "T01",
78
+ title: "Add types",
79
+ files: ["src/types.ts"],
80
+ verifyCriteria: ["Types compile"],
81
+ });
82
+ assert.ok(body.includes("`src/types.ts`"));
83
+ assert.ok(body.includes("- [ ] Types compile"));
84
+ });
85
+ });
86
+
87
+ describe("formatSummaryComment", () => {
88
+ it("includes one-liner and body", () => {
89
+ const comment = formatSummaryComment({
90
+ oneLiner: "Added retry logic",
91
+ body: "Implemented exponential backoff",
92
+ });
93
+ assert.ok(comment.includes("**Summary:** Added retry logic"));
94
+ assert.ok(comment.includes("Implemented exponential backoff"));
95
+ });
96
+
97
+ it("wraps frontmatter in details block", () => {
98
+ const comment = formatSummaryComment({
99
+ frontmatter: { duration: "45m", key_files: ["a.ts"] },
100
+ });
101
+ assert.ok(comment.includes("<details>"));
102
+ assert.ok(comment.includes("duration:"));
103
+ });
104
+
105
+ it("handles empty data gracefully", () => {
106
+ const comment = formatSummaryComment({});
107
+ assert.equal(typeof comment, "string");
108
+ });
109
+ });
110
+ });
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Type definitions for the GitHub Sync extension.
3
+ *
4
+ * Config shape (stored in GSD preferences under `github` key) and
5
+ * sync mapping records (stored in `.gsd/github-sync.json`).
6
+ */
7
+
8
+ // ─── Configuration ──────────────────────────────────────────────────────────
9
+
10
+ export interface GitHubSyncConfig {
11
+ enabled: boolean;
12
+ /** "owner/repo" β€” auto-detected from git remote if omitted. */
13
+ repo?: string;
14
+ /** GitHub Projects v2 number (optional). */
15
+ project?: number;
16
+ /** Labels applied to all created issues. */
17
+ labels?: string[];
18
+ /** Append "Resolves #N" to task commits. Default: true. */
19
+ auto_link_commits?: boolean;
20
+ /** Create per-slice draft PRs. Default: true. */
21
+ slice_prs?: boolean;
22
+ }
23
+
24
+ // ─── Sync Mapping ───────────────────────────────────────────────────────────
25
+
26
+ export interface SyncEntityRecord {
27
+ issueNumber: number;
28
+ lastSyncedAt: string;
29
+ state: "open" | "closed";
30
+ }
31
+
32
+ export interface MilestoneSyncRecord extends SyncEntityRecord {
33
+ ghMilestoneNumber: number;
34
+ }
35
+
36
+ export interface SliceSyncRecord extends SyncEntityRecord {
37
+ prNumber: number;
38
+ branch: string;
39
+ }
40
+
41
+ export interface SyncMapping {
42
+ version: 1;
43
+ repo: string;
44
+ milestones: Record<string, MilestoneSyncRecord>;
45
+ slices: Record<string, SliceSyncRecord>;
46
+ tasks: Record<string, SyncEntityRecord>;
47
+ }
@@ -124,6 +124,9 @@ export class AutoSession {
124
124
  // ── Sidecar queue ─────────────────────────────────────────────────────
125
125
  sidecarQueue: SidecarItem[] = [];
126
126
 
127
+ // ── Dispatch circuit breakers ──────────────────────────────────────
128
+ rewriteAttemptCount = 0;
129
+
127
130
  // ── Metrics ──────────────────────────────────────────────────────────────
128
131
  autoStartTime = 0;
129
132
  lastPromptCharCount: number | undefined;
@@ -134,27 +137,8 @@ export class AutoSession {
134
137
  sigtermHandler: (() => void) | null = null;
135
138
 
136
139
  // ── Loop promise state ──────────────────────────────────────────────────
137
- /**
138
- * True only while runUnit is rotating into a fresh session. agent_end events
139
- * emitted from the previous session's abort during this window must be
140
- * ignored; they do not belong to the new unit.
141
- */
142
- sessionSwitchInFlight = false;
143
-
144
- /**
145
- * One-shot resolver for the current unit's agent_end promise.
146
- * Non-null only while a unit is in-flight (between sendMessage and agent_end).
147
- * Scoped to the session to prevent concurrent session corruption.
148
- */
149
- pendingResolve: ((result: { status: "completed" | "cancelled" | "error"; event?: { messages: unknown[] } }) => void) | null = null;
150
-
151
- /**
152
- * Queue for agent_end events that arrive when no pendingResolve exists.
153
- * This happens when error-recovery sendMessage retries produce agent_end
154
- * events between loop iterations. The next runUnit drains this queue
155
- * instead of waiting for a new event.
156
- */
157
- pendingAgentEndQueue: Array<{ messages: unknown[] }> = [];
140
+ // Per-unit resolve function and session-switch guard live at module level
141
+ // in auto-loop.ts (_currentResolve, _sessionSwitchInFlight).
158
142
 
159
143
  // ── Methods ──────────────────────────────────────────────────────────────
160
144
 
@@ -228,14 +212,12 @@ export class AutoSession {
228
212
  this.lastBaselineCharCount = undefined;
229
213
  this.pendingQuickTasks = [];
230
214
  this.sidecarQueue = [];
215
+ this.rewriteAttemptCount = 0;
231
216
 
232
217
  // Signal handler
233
218
  this.sigtermHandler = null;
234
219
 
235
- // Loop promise state
236
- this.sessionSwitchInFlight = false;
237
- this.pendingResolve = null;
238
- this.pendingAgentEndQueue = [];
220
+ // Loop promise state lives in auto-loop.ts module scope
239
221
  }
240
222
 
241
223
  toJSON(): Record<string, unknown> {
@@ -62,6 +62,7 @@ export interface DispatchContext {
62
62
  midTitle: string;
63
63
  state: GSDState;
64
64
  prefs: GSDPreferences | undefined;
65
+ session?: import("./auto/session.js").AutoSession;
65
66
  }
66
67
 
67
68
  interface DispatchRule {
@@ -82,26 +83,23 @@ function missingSliceStop(mid: string, phase: string): DispatchAction {
82
83
  // ─── Rewrite Circuit Breaker ──────────────────────────────────────────────
83
84
 
84
85
  const MAX_REWRITE_ATTEMPTS = 3;
85
- let rewriteAttemptCount = 0;
86
- export function resetRewriteCircuitBreaker(): void {
87
- rewriteAttemptCount = 0;
88
- }
89
86
 
90
87
  // ─── Rules ────────────────────────────────────────────────────────────────
91
88
 
92
89
  const DISPATCH_RULES: DispatchRule[] = [
93
90
  {
94
91
  name: "rewrite-docs (override gate)",
95
- match: async ({ mid, midTitle, state, basePath }) => {
92
+ match: async ({ mid, midTitle, state, basePath, session }) => {
96
93
  const pendingOverrides = await loadActiveOverrides(basePath);
97
94
  if (pendingOverrides.length === 0) return null;
98
- if (rewriteAttemptCount >= MAX_REWRITE_ATTEMPTS) {
95
+ const count = session?.rewriteAttemptCount ?? 0;
96
+ if (count >= MAX_REWRITE_ATTEMPTS) {
99
97
  const { resolveAllOverrides } = await import("./files.js");
100
98
  await resolveAllOverrides(basePath);
101
- rewriteAttemptCount = 0;
99
+ if (session) session.rewriteAttemptCount = 0;
102
100
  return null;
103
101
  }
104
- rewriteAttemptCount++;
102
+ if (session) session.rewriteAttemptCount++;
105
103
  const unitId = state.activeSlice ? `${mid}/${state.activeSlice.id}` : mid;
106
104
  return {
107
105
  action: "dispatch",
@@ -157,7 +155,7 @@ const DISPATCH_RULES: DispatchRule[] = [
157
155
  uatContent ?? "",
158
156
  basePath,
159
157
  ),
160
- pauseAfterDispatch: uatType !== "artifact-driven",
158
+ pauseAfterDispatch: uatType !== "artifact-driven" && uatType !== "browser-executable" && uatType !== "runtime-executable",
161
159
  };
162
160
  },
163
161
  },