gsd-pi 2.38.0-dev.add4f78 → 2.38.0-dev.d533afb
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/resource-loader.js +34 -1
- package/dist/resources/extensions/github-sync/cli.js +284 -0
- package/dist/resources/extensions/github-sync/index.js +73 -0
- package/dist/resources/extensions/github-sync/mapping.js +67 -0
- package/dist/resources/extensions/github-sync/sync.js +424 -0
- package/dist/resources/extensions/github-sync/templates.js +118 -0
- package/dist/resources/extensions/github-sync/types.js +7 -0
- package/dist/resources/extensions/gsd/auto/session.js +3 -23
- package/dist/resources/extensions/gsd/auto-dispatch.js +1 -1
- package/dist/resources/extensions/gsd/auto-loop.js +292 -263
- package/dist/resources/extensions/gsd/auto-post-unit.js +28 -3
- package/dist/resources/extensions/gsd/auto-prompts.js +23 -43
- package/dist/resources/extensions/gsd/auto-start.js +7 -1
- package/dist/resources/extensions/gsd/auto-worktree.js +3 -3
- package/dist/resources/extensions/gsd/auto.js +143 -80
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
- package/dist/resources/extensions/gsd/commands.js +2 -1
- package/dist/resources/extensions/gsd/context-budget.js +2 -10
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +0 -2
- package/dist/resources/extensions/gsd/doctor-providers.js +27 -11
- package/dist/resources/extensions/gsd/doctor.js +20 -1
- package/dist/resources/extensions/gsd/exit-command.js +2 -1
- package/dist/resources/extensions/gsd/files.js +4 -0
- package/dist/resources/extensions/gsd/git-service.js +15 -12
- package/dist/resources/extensions/gsd/guided-flow.js +82 -32
- package/dist/resources/extensions/gsd/index.js +22 -19
- package/dist/resources/extensions/gsd/native-git-bridge.js +37 -0
- package/dist/resources/extensions/gsd/preferences-models.js +0 -12
- package/dist/resources/extensions/gsd/preferences-types.js +1 -1
- package/dist/resources/extensions/gsd/preferences-validation.js +58 -10
- package/dist/resources/extensions/gsd/preferences.js +4 -2
- package/dist/resources/extensions/gsd/prompts/discuss.md +11 -14
- package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -2
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
- package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
- package/dist/resources/extensions/gsd/prompts/queue.md +4 -8
- package/dist/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
- package/dist/resources/extensions/gsd/prompts/run-uat.md +27 -10
- package/dist/resources/extensions/gsd/prompts/workflow-start.md +2 -2
- package/dist/resources/extensions/gsd/repo-identity.js +19 -3
- package/dist/resources/extensions/gsd/roadmap-mutations.js +24 -0
- package/dist/resources/extensions/mcp-client/index.js +14 -1
- package/package.json +1 -1
- package/packages/pi-ai/dist/utils/oauth/anthropic.js +2 -2
- package/packages/pi-ai/dist/utils/oauth/anthropic.js.map +1 -1
- package/packages/pi-ai/src/utils/oauth/anthropic.ts +2 -2
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +205 -7
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +223 -7
- package/src/resources/extensions/github-sync/cli.ts +364 -0
- package/src/resources/extensions/github-sync/index.ts +93 -0
- package/src/resources/extensions/github-sync/mapping.ts +81 -0
- package/src/resources/extensions/github-sync/sync.ts +556 -0
- package/src/resources/extensions/github-sync/templates.ts +183 -0
- package/src/resources/extensions/github-sync/tests/cli.test.ts +20 -0
- package/src/resources/extensions/github-sync/tests/commit-linking.test.ts +39 -0
- package/src/resources/extensions/github-sync/tests/mapping.test.ts +104 -0
- package/src/resources/extensions/github-sync/tests/templates.test.ts +110 -0
- package/src/resources/extensions/github-sync/types.ts +47 -0
- package/src/resources/extensions/gsd/auto/session.ts +3 -25
- package/src/resources/extensions/gsd/auto-dispatch.ts +1 -1
- package/src/resources/extensions/gsd/auto-loop.ts +382 -360
- package/src/resources/extensions/gsd/auto-post-unit.ts +29 -3
- package/src/resources/extensions/gsd/auto-prompts.ts +25 -45
- package/src/resources/extensions/gsd/auto-start.ts +11 -1
- package/src/resources/extensions/gsd/auto-worktree.ts +3 -3
- package/src/resources/extensions/gsd/auto.ts +139 -86
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
- package/src/resources/extensions/gsd/commands.ts +2 -2
- package/src/resources/extensions/gsd/context-budget.ts +2 -12
- package/src/resources/extensions/gsd/docs/preferences-reference.md +0 -2
- package/src/resources/extensions/gsd/doctor-providers.ts +26 -9
- package/src/resources/extensions/gsd/doctor.ts +22 -1
- package/src/resources/extensions/gsd/exit-command.ts +2 -2
- package/src/resources/extensions/gsd/files.ts +3 -1
- package/src/resources/extensions/gsd/git-service.ts +20 -10
- package/src/resources/extensions/gsd/guided-flow.ts +110 -38
- package/src/resources/extensions/gsd/index.ts +21 -16
- package/src/resources/extensions/gsd/native-git-bridge.ts +37 -0
- package/src/resources/extensions/gsd/preferences-models.ts +0 -12
- package/src/resources/extensions/gsd/preferences-types.ts +4 -4
- package/src/resources/extensions/gsd/preferences-validation.ts +50 -10
- package/src/resources/extensions/gsd/preferences.ts +3 -2
- package/src/resources/extensions/gsd/prompts/discuss.md +11 -14
- package/src/resources/extensions/gsd/prompts/execute-task.md +2 -2
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
- package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
- package/src/resources/extensions/gsd/prompts/queue.md +4 -8
- package/src/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
- package/src/resources/extensions/gsd/prompts/run-uat.md +27 -10
- package/src/resources/extensions/gsd/prompts/workflow-start.md +2 -2
- package/src/resources/extensions/gsd/repo-identity.ts +20 -3
- package/src/resources/extensions/gsd/roadmap-mutations.ts +29 -0
- package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +21 -18
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +122 -68
- package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +86 -3
- package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -7
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +59 -0
- package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +21 -1
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +11 -3
- package/src/resources/extensions/gsd/types.ts +0 -1
- package/src/resources/extensions/mcp-client/index.ts +17 -1
- package/dist/resources/extensions/gsd/prompt-compressor.js +0 -393
- package/dist/resources/extensions/gsd/semantic-chunker.js +0 -254
- package/dist/resources/extensions/gsd/summary-distiller.js +0 -212
- package/src/resources/extensions/gsd/prompt-compressor.ts +0 -508
- package/src/resources/extensions/gsd/semantic-chunker.ts +0 -336
- package/src/resources/extensions/gsd/summary-distiller.ts +0 -258
- package/src/resources/extensions/gsd/tests/context-compression.test.ts +0 -193
- package/src/resources/extensions/gsd/tests/prompt-compressor.test.ts +0 -529
- package/src/resources/extensions/gsd/tests/semantic-chunker.test.ts +0 -426
- package/src/resources/extensions/gsd/tests/summary-distiller.test.ts +0 -323
- package/src/resources/extensions/gsd/tests/token-optimization-benchmark.test.ts +0 -1272
- 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
|
+
}
|
|
@@ -137,27 +137,8 @@ export class AutoSession {
|
|
|
137
137
|
sigtermHandler: (() => void) | null = null;
|
|
138
138
|
|
|
139
139
|
// ── Loop promise state ──────────────────────────────────────────────────
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
* emitted from the previous session's abort during this window must be
|
|
143
|
-
* ignored; they do not belong to the new unit.
|
|
144
|
-
*/
|
|
145
|
-
sessionSwitchInFlight = false;
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* One-shot resolver for the current unit's agent_end promise.
|
|
149
|
-
* Non-null only while a unit is in-flight (between sendMessage and agent_end).
|
|
150
|
-
* Scoped to the session to prevent concurrent session corruption.
|
|
151
|
-
*/
|
|
152
|
-
pendingResolve: ((result: { status: "completed" | "cancelled" | "error"; event?: { messages: unknown[] } }) => void) | null = null;
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Queue for agent_end events that arrive when no pendingResolve exists.
|
|
156
|
-
* This happens when error-recovery sendMessage retries produce agent_end
|
|
157
|
-
* events between loop iterations. The next runUnit drains this queue
|
|
158
|
-
* instead of waiting for a new event.
|
|
159
|
-
*/
|
|
160
|
-
pendingAgentEndQueue: Array<{ messages: unknown[]; unitId?: string }> = [];
|
|
140
|
+
// Per-unit resolve function and session-switch guard live at module level
|
|
141
|
+
// in auto-loop.ts (_currentResolve, _sessionSwitchInFlight).
|
|
161
142
|
|
|
162
143
|
// ── Methods ──────────────────────────────────────────────────────────────
|
|
163
144
|
|
|
@@ -236,10 +217,7 @@ export class AutoSession {
|
|
|
236
217
|
// Signal handler
|
|
237
218
|
this.sigtermHandler = null;
|
|
238
219
|
|
|
239
|
-
// Loop promise state
|
|
240
|
-
this.sessionSwitchInFlight = false;
|
|
241
|
-
this.pendingResolve = null;
|
|
242
|
-
this.pendingAgentEndQueue = [];
|
|
220
|
+
// Loop promise state lives in auto-loop.ts module scope
|
|
243
221
|
}
|
|
244
222
|
|
|
245
223
|
toJSON(): Record<string, unknown> {
|
|
@@ -155,7 +155,7 @@ const DISPATCH_RULES: DispatchRule[] = [
|
|
|
155
155
|
uatContent ?? "",
|
|
156
156
|
basePath,
|
|
157
157
|
),
|
|
158
|
-
pauseAfterDispatch: uatType !== "artifact-driven",
|
|
158
|
+
pauseAfterDispatch: uatType !== "artifact-driven" && uatType !== "browser-executable" && uatType !== "runtime-executable",
|
|
159
159
|
};
|
|
160
160
|
},
|
|
161
161
|
},
|