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.
- package/dist/app-paths.js +1 -1
- package/dist/cli.js +9 -0
- package/dist/extension-discovery.d.ts +5 -3
- package/dist/extension-discovery.js +14 -9
- package/dist/extension-registry.js +2 -2
- package/dist/remote-questions-config.js +2 -2
- package/dist/resource-loader.js +34 -1
- package/dist/resources/extensions/browser-tools/package.json +3 -1
- package/dist/resources/extensions/cmux/index.js +55 -1
- package/dist/resources/extensions/context7/package.json +1 -1
- package/dist/resources/extensions/env-utils.js +29 -0
- package/dist/resources/extensions/get-secrets-from-user.js +5 -24
- 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/google-search/package.json +3 -1
- package/dist/resources/extensions/gsd/auto/session.js +6 -23
- package/dist/resources/extensions/gsd/auto-dispatch.js +8 -9
- package/dist/resources/extensions/gsd/auto-loop.js +597 -588
- package/dist/resources/extensions/gsd/auto-post-unit.js +99 -70
- package/dist/resources/extensions/gsd/auto-prompts.js +23 -43
- package/dist/resources/extensions/gsd/auto-start.js +13 -2
- package/dist/resources/extensions/gsd/auto-worktree-sync.js +13 -5
- package/dist/resources/extensions/gsd/auto-worktree.js +3 -3
- package/dist/resources/extensions/gsd/auto.js +143 -96
- package/dist/resources/extensions/gsd/captures.js +9 -1
- package/dist/resources/extensions/gsd/commands-extensions.js +3 -2
- package/dist/resources/extensions/gsd/commands-handlers.js +16 -3
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
- package/dist/resources/extensions/gsd/commands.js +24 -3
- package/dist/resources/extensions/gsd/context-budget.js +2 -10
- package/dist/resources/extensions/gsd/detection.js +1 -2
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +0 -2
- package/dist/resources/extensions/gsd/doctor-checks.js +82 -0
- package/dist/resources/extensions/gsd/doctor-environment.js +78 -0
- package/dist/resources/extensions/gsd/doctor-format.js +15 -0
- package/dist/resources/extensions/gsd/doctor-providers.js +27 -11
- package/dist/resources/extensions/gsd/doctor.js +204 -12
- package/dist/resources/extensions/gsd/exit-command.js +2 -1
- package/dist/resources/extensions/gsd/export.js +1 -1
- package/dist/resources/extensions/gsd/files.js +6 -2
- package/dist/resources/extensions/gsd/forensics.js +1 -1
- 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 +24 -20
- package/dist/resources/extensions/gsd/migrate/parsers.js +1 -1
- package/dist/resources/extensions/gsd/native-git-bridge.js +37 -0
- package/dist/resources/extensions/gsd/package.json +1 -1
- 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 +59 -11
- package/dist/resources/extensions/gsd/preferences.js +8 -5
- 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 +21 -4
- package/dist/resources/extensions/gsd/resource-version.js +2 -1
- package/dist/resources/extensions/gsd/roadmap-mutations.js +24 -0
- package/dist/resources/extensions/gsd/state.js +1 -1
- package/dist/resources/extensions/gsd/visualizer-data.js +1 -1
- package/dist/resources/extensions/gsd/worktree.js +35 -16
- package/dist/resources/extensions/mcp-client/index.js +14 -1
- package/dist/resources/extensions/remote-questions/status.js +2 -1
- package/dist/resources/extensions/remote-questions/store.js +2 -1
- package/dist/resources/extensions/search-the-web/provider.js +2 -1
- package/dist/resources/extensions/subagent/index.js +12 -3
- package/dist/resources/extensions/subagent/isolation.js +2 -1
- package/dist/resources/extensions/ttsr/rule-loader.js +2 -1
- package/dist/resources/extensions/universal-config/package.json +1 -1
- package/dist/welcome-screen.d.ts +12 -0
- package/dist/welcome-screen.js +53 -0
- 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/dist/core/package-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/package-manager.js +8 -4
- package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +223 -7
- package/packages/pi-coding-agent/src/core/package-manager.ts +8 -4
- package/pkg/package.json +1 -1
- package/src/resources/extensions/cmux/index.ts +57 -1
- package/src/resources/extensions/env-utils.ts +31 -0
- package/src/resources/extensions/get-secrets-from-user.ts +5 -24
- 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 +7 -25
- package/src/resources/extensions/gsd/auto-dispatch.ts +7 -9
- package/src/resources/extensions/gsd/auto-loop.ts +484 -546
- package/src/resources/extensions/gsd/auto-post-unit.ts +80 -44
- package/src/resources/extensions/gsd/auto-prompts.ts +25 -45
- package/src/resources/extensions/gsd/auto-start.ts +18 -2
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +15 -4
- package/src/resources/extensions/gsd/auto-worktree.ts +3 -3
- package/src/resources/extensions/gsd/auto.ts +139 -101
- package/src/resources/extensions/gsd/captures.ts +10 -1
- package/src/resources/extensions/gsd/commands-extensions.ts +4 -2
- package/src/resources/extensions/gsd/commands-handlers.ts +17 -2
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
- package/src/resources/extensions/gsd/commands.ts +26 -4
- package/src/resources/extensions/gsd/context-budget.ts +2 -12
- package/src/resources/extensions/gsd/detection.ts +2 -2
- package/src/resources/extensions/gsd/docs/preferences-reference.md +0 -2
- package/src/resources/extensions/gsd/doctor-checks.ts +75 -0
- package/src/resources/extensions/gsd/doctor-environment.ts +82 -1
- package/src/resources/extensions/gsd/doctor-format.ts +20 -0
- package/src/resources/extensions/gsd/doctor-providers.ts +26 -9
- package/src/resources/extensions/gsd/doctor-types.ts +16 -1
- package/src/resources/extensions/gsd/doctor.ts +199 -14
- package/src/resources/extensions/gsd/exit-command.ts +2 -2
- package/src/resources/extensions/gsd/export.ts +1 -1
- package/src/resources/extensions/gsd/files.ts +5 -3
- package/src/resources/extensions/gsd/forensics.ts +1 -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 +24 -17
- package/src/resources/extensions/gsd/migrate/parsers.ts +1 -1
- 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 +51 -11
- package/src/resources/extensions/gsd/preferences.ts +8 -5
- 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 +23 -4
- package/src/resources/extensions/gsd/resource-version.ts +3 -1
- package/src/resources/extensions/gsd/roadmap-mutations.ts +29 -0
- package/src/resources/extensions/gsd/state.ts +1 -1
- 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/cmux.test.ts +93 -0
- package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +266 -0
- 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/tests/worktree.test.ts +47 -0
- package/src/resources/extensions/gsd/types.ts +0 -1
- package/src/resources/extensions/gsd/visualizer-data.ts +1 -1
- package/src/resources/extensions/gsd/worktree.ts +35 -15
- package/src/resources/extensions/mcp-client/index.ts +17 -1
- package/src/resources/extensions/remote-questions/status.ts +3 -1
- package/src/resources/extensions/remote-questions/store.ts +3 -1
- package/src/resources/extensions/search-the-web/provider.ts +2 -1
- package/src/resources/extensions/subagent/index.ts +12 -3
- package/src/resources/extensions/subagent/isolation.ts +3 -1
- package/src/resources/extensions/ttsr/rule-loader.ts +3 -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,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Sync extension for GSD.
|
|
3
|
+
*
|
|
4
|
+
* Opt-in extension that syncs GSD lifecycle events to GitHub:
|
|
5
|
+
* milestones → GH Milestones + tracking issues, slices → draft PRs,
|
|
6
|
+
* tasks → sub-issues with auto-close on commit.
|
|
7
|
+
*
|
|
8
|
+
* Integration happens via a single dynamic import in auto-post-unit.ts.
|
|
9
|
+
* This index registers a `/github-sync` command for manual bootstrap
|
|
10
|
+
* and status display.
|
|
11
|
+
*/
|
|
12
|
+
import { bootstrapSync } from "./sync.js";
|
|
13
|
+
import { loadSyncMapping } from "./mapping.js";
|
|
14
|
+
import { ghIsAvailable } from "./cli.js";
|
|
15
|
+
export default function (pi) {
|
|
16
|
+
pi.registerCommand("github-sync", {
|
|
17
|
+
description: "Bootstrap GitHub sync or show sync status",
|
|
18
|
+
handler: async (args, ctx) => {
|
|
19
|
+
const subcommand = args.trim().toLowerCase();
|
|
20
|
+
if (subcommand === "status") {
|
|
21
|
+
await showStatus(ctx);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (subcommand === "bootstrap" || subcommand === "") {
|
|
25
|
+
await runBootstrap(ctx);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
ctx.ui.notify("Usage: /github-sync [bootstrap|status]", "info");
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
async function showStatus(ctx) {
|
|
33
|
+
if (!ghIsAvailable()) {
|
|
34
|
+
ctx.ui.notify("GitHub sync: `gh` CLI not installed or not authenticated.", "warning");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const mapping = loadSyncMapping(ctx.cwd);
|
|
38
|
+
if (!mapping) {
|
|
39
|
+
ctx.ui.notify("GitHub sync: No sync mapping found. Run `/github-sync bootstrap` to initialize.", "info");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const milestoneCount = Object.keys(mapping.milestones).length;
|
|
43
|
+
const sliceCount = Object.keys(mapping.slices).length;
|
|
44
|
+
const taskCount = Object.keys(mapping.tasks).length;
|
|
45
|
+
const openMilestones = Object.values(mapping.milestones).filter(m => m.state === "open").length;
|
|
46
|
+
const openSlices = Object.values(mapping.slices).filter(s => s.state === "open").length;
|
|
47
|
+
const openTasks = Object.values(mapping.tasks).filter(t => t.state === "open").length;
|
|
48
|
+
ctx.ui.notify([
|
|
49
|
+
`GitHub sync: repo=${mapping.repo}`,
|
|
50
|
+
` Milestones: ${milestoneCount} (${openMilestones} open)`,
|
|
51
|
+
` Slices: ${sliceCount} (${openSlices} open)`,
|
|
52
|
+
` Tasks: ${taskCount} (${openTasks} open)`,
|
|
53
|
+
].join("\n"), "info");
|
|
54
|
+
}
|
|
55
|
+
async function runBootstrap(ctx) {
|
|
56
|
+
if (!ghIsAvailable()) {
|
|
57
|
+
ctx.ui.notify("GitHub sync: `gh` CLI not installed or not authenticated.", "warning");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
ctx.ui.notify("GitHub sync: bootstrapping...", "info");
|
|
61
|
+
try {
|
|
62
|
+
const counts = await bootstrapSync(ctx.cwd);
|
|
63
|
+
if (counts.milestones === 0 && counts.slices === 0 && counts.tasks === 0) {
|
|
64
|
+
ctx.ui.notify("GitHub sync: everything already synced (or no milestones found).", "info");
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
ctx.ui.notify(`GitHub sync: created ${counts.milestones} milestone(s), ${counts.slices} slice(s), ${counts.tasks} task(s).`, "info");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
ctx.ui.notify(`GitHub sync bootstrap failed: ${err}`, "error");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistence layer for the GitHub sync mapping.
|
|
3
|
+
*
|
|
4
|
+
* The mapping lives at `.gsd/github-sync.json` and tracks which GSD
|
|
5
|
+
* entities have been synced to which GitHub entities (issues, PRs,
|
|
6
|
+
* milestones) along with their numbers and sync timestamps.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { atomicWriteSync } from "../gsd/atomic-write.js";
|
|
11
|
+
const MAPPING_FILENAME = "github-sync.json";
|
|
12
|
+
function mappingPath(basePath) {
|
|
13
|
+
return join(basePath, ".gsd", MAPPING_FILENAME);
|
|
14
|
+
}
|
|
15
|
+
// ─── Load / Save ────────────────────────────────────────────────────────────
|
|
16
|
+
export function loadSyncMapping(basePath) {
|
|
17
|
+
const path = mappingPath(basePath);
|
|
18
|
+
if (!existsSync(path))
|
|
19
|
+
return null;
|
|
20
|
+
try {
|
|
21
|
+
const raw = readFileSync(path, "utf-8");
|
|
22
|
+
const parsed = JSON.parse(raw);
|
|
23
|
+
if (parsed?.version !== 1)
|
|
24
|
+
return null;
|
|
25
|
+
return parsed;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function saveSyncMapping(basePath, mapping) {
|
|
32
|
+
const path = mappingPath(basePath);
|
|
33
|
+
atomicWriteSync(path, JSON.stringify(mapping, null, 2) + "\n");
|
|
34
|
+
}
|
|
35
|
+
export function createEmptyMapping(repo) {
|
|
36
|
+
return {
|
|
37
|
+
version: 1,
|
|
38
|
+
repo,
|
|
39
|
+
milestones: {},
|
|
40
|
+
slices: {},
|
|
41
|
+
tasks: {},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
// ─── Accessors ──────────────────────────────────────────────────────────────
|
|
45
|
+
export function getMilestoneRecord(mapping, mid) {
|
|
46
|
+
return mapping.milestones[mid] ?? null;
|
|
47
|
+
}
|
|
48
|
+
export function getSliceRecord(mapping, mid, sid) {
|
|
49
|
+
return mapping.slices[`${mid}/${sid}`] ?? null;
|
|
50
|
+
}
|
|
51
|
+
export function getTaskRecord(mapping, mid, sid, tid) {
|
|
52
|
+
return mapping.tasks[`${mid}/${sid}/${tid}`] ?? null;
|
|
53
|
+
}
|
|
54
|
+
export function getTaskIssueNumber(mapping, mid, sid, tid) {
|
|
55
|
+
const record = getTaskRecord(mapping, mid, sid, tid);
|
|
56
|
+
return record?.issueNumber ?? null;
|
|
57
|
+
}
|
|
58
|
+
// ─── Mutators ───────────────────────────────────────────────────────────────
|
|
59
|
+
export function setMilestoneRecord(mapping, mid, record) {
|
|
60
|
+
mapping.milestones[mid] = record;
|
|
61
|
+
}
|
|
62
|
+
export function setSliceRecord(mapping, mid, sid, record) {
|
|
63
|
+
mapping.slices[`${mid}/${sid}`] = record;
|
|
64
|
+
}
|
|
65
|
+
export function setTaskRecord(mapping, mid, sid, tid, record) {
|
|
66
|
+
mapping.tasks[`${mid}/${sid}/${tid}`] = record;
|
|
67
|
+
}
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core GitHub sync engine.
|
|
3
|
+
*
|
|
4
|
+
* Entry point: `runGitHubSync()` — called from the GSD post-unit pipeline.
|
|
5
|
+
* Routes to per-event sync functions based on the unit type, reads GSD
|
|
6
|
+
* files to build GitHub entities, and persists the sync mapping.
|
|
7
|
+
*
|
|
8
|
+
* All errors are caught internally — sync failures never block execution.
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { loadFile, parseRoadmap, parsePlan, parseSummary } from "../gsd/files.js";
|
|
13
|
+
import { resolveMilestoneFile, resolveSliceFile, resolveTaskFile, } from "../gsd/paths.js";
|
|
14
|
+
import { debugLog } from "../gsd/debug-logger.js";
|
|
15
|
+
import { loadEffectiveGSDPreferences } from "../gsd/preferences.js";
|
|
16
|
+
import { loadSyncMapping, saveSyncMapping, createEmptyMapping, getMilestoneRecord, getSliceRecord, getTaskRecord, setMilestoneRecord, setSliceRecord, setTaskRecord, } from "./mapping.js";
|
|
17
|
+
import { ghIsAvailable, ghHasRateLimit, ghDetectRepo, ghCreateIssue, ghCloseIssue, ghAddComment, ghCreateMilestone, ghCloseMilestone, ghCreatePR, ghMarkPRReady, ghMergePR, ghCreateBranch, ghPushBranch, ghAddToProject, } from "./cli.js";
|
|
18
|
+
import { formatMilestoneIssueBody, formatSlicePRBody, formatTaskIssueBody, formatSummaryComment, } from "./templates.js";
|
|
19
|
+
// ─── Entry Point ────────────────────────────────────────────────────────────
|
|
20
|
+
/**
|
|
21
|
+
* Main sync entry point — called from GSD post-unit pipeline.
|
|
22
|
+
* Routes to the appropriate sync function based on unit type.
|
|
23
|
+
*/
|
|
24
|
+
export async function runGitHubSync(basePath, unitType, unitId) {
|
|
25
|
+
try {
|
|
26
|
+
const config = loadGitHubSyncConfig(basePath);
|
|
27
|
+
if (!config?.enabled)
|
|
28
|
+
return;
|
|
29
|
+
if (!ghIsAvailable()) {
|
|
30
|
+
debugLog("github-sync", { skip: "gh CLI not available" });
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
// Resolve repo
|
|
34
|
+
const repo = config.repo ?? resolveRepo(basePath);
|
|
35
|
+
if (!repo) {
|
|
36
|
+
debugLog("github-sync", { skip: "could not detect repo" });
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
// Rate limit check
|
|
40
|
+
if (!ghHasRateLimit(basePath)) {
|
|
41
|
+
debugLog("github-sync", { skip: "rate limit low" });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// Load or init mapping
|
|
45
|
+
let mapping = loadSyncMapping(basePath) ?? createEmptyMapping(repo);
|
|
46
|
+
mapping.repo = repo;
|
|
47
|
+
// Parse unit ID parts
|
|
48
|
+
const parts = unitId.split("/");
|
|
49
|
+
const [mid, sid, tid] = parts;
|
|
50
|
+
// Route by unit type
|
|
51
|
+
switch (unitType) {
|
|
52
|
+
case "plan-milestone":
|
|
53
|
+
if (mid)
|
|
54
|
+
await syncMilestonePlan(basePath, mapping, config, mid);
|
|
55
|
+
break;
|
|
56
|
+
case "plan-slice":
|
|
57
|
+
case "research-slice":
|
|
58
|
+
if (mid && sid)
|
|
59
|
+
await syncSlicePlan(basePath, mapping, config, mid, sid);
|
|
60
|
+
break;
|
|
61
|
+
case "execute-task":
|
|
62
|
+
case "reactive-execute":
|
|
63
|
+
if (mid && sid && tid)
|
|
64
|
+
await syncTaskComplete(basePath, mapping, config, mid, sid, tid);
|
|
65
|
+
break;
|
|
66
|
+
case "complete-slice":
|
|
67
|
+
if (mid && sid)
|
|
68
|
+
await syncSliceComplete(basePath, mapping, config, mid, sid);
|
|
69
|
+
break;
|
|
70
|
+
case "complete-milestone":
|
|
71
|
+
if (mid)
|
|
72
|
+
await syncMilestoneComplete(basePath, mapping, config, mid);
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
saveSyncMapping(basePath, mapping);
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
debugLog("github-sync", { error: String(err) });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// ─── Per-Event Sync Functions ───────────────────────────────────────────────
|
|
82
|
+
async function syncMilestonePlan(basePath, mapping, config, mid) {
|
|
83
|
+
// Skip if already synced
|
|
84
|
+
if (getMilestoneRecord(mapping, mid))
|
|
85
|
+
return;
|
|
86
|
+
// Load roadmap data
|
|
87
|
+
const roadmapPath = resolveMilestoneFile(basePath, mid, "ROADMAP");
|
|
88
|
+
if (!roadmapPath)
|
|
89
|
+
return;
|
|
90
|
+
const content = await loadFile(roadmapPath);
|
|
91
|
+
if (!content)
|
|
92
|
+
return;
|
|
93
|
+
const roadmap = parseRoadmap(content);
|
|
94
|
+
const title = `${mid}: ${roadmap.title || "Milestone"}`;
|
|
95
|
+
// Create GitHub Milestone
|
|
96
|
+
const milestoneResult = ghCreateMilestone(basePath, mapping.repo, title, roadmap.vision || "");
|
|
97
|
+
if (!milestoneResult.ok) {
|
|
98
|
+
debugLog("github-sync", { phase: "create-milestone", error: milestoneResult.error });
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const ghMilestoneNumber = milestoneResult.data;
|
|
102
|
+
// Create tracking issue
|
|
103
|
+
const issueBody = formatMilestoneIssueBody({
|
|
104
|
+
id: mid,
|
|
105
|
+
title: roadmap.title || "Milestone",
|
|
106
|
+
vision: roadmap.vision,
|
|
107
|
+
successCriteria: roadmap.successCriteria,
|
|
108
|
+
slices: roadmap.slices?.map(s => ({
|
|
109
|
+
id: s.id,
|
|
110
|
+
title: s.title,
|
|
111
|
+
})),
|
|
112
|
+
});
|
|
113
|
+
const issueResult = ghCreateIssue(basePath, {
|
|
114
|
+
repo: mapping.repo,
|
|
115
|
+
title: `${mid}: ${roadmap.title || "Milestone"} — Tracking`,
|
|
116
|
+
body: issueBody,
|
|
117
|
+
labels: config.labels,
|
|
118
|
+
milestone: ghMilestoneNumber,
|
|
119
|
+
});
|
|
120
|
+
if (!issueResult.ok) {
|
|
121
|
+
debugLog("github-sync", { phase: "create-tracking-issue", error: issueResult.error });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
// Add to project if configured
|
|
125
|
+
if (config.project) {
|
|
126
|
+
ghAddToProject(basePath, mapping.repo, config.project, issueResult.data);
|
|
127
|
+
}
|
|
128
|
+
setMilestoneRecord(mapping, mid, {
|
|
129
|
+
issueNumber: issueResult.data,
|
|
130
|
+
ghMilestoneNumber,
|
|
131
|
+
lastSyncedAt: new Date().toISOString(),
|
|
132
|
+
state: "open",
|
|
133
|
+
});
|
|
134
|
+
debugLog("github-sync", {
|
|
135
|
+
phase: "milestone-synced",
|
|
136
|
+
mid,
|
|
137
|
+
milestone: ghMilestoneNumber,
|
|
138
|
+
issue: issueResult.data,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
async function syncSlicePlan(basePath, mapping, config, mid, sid) {
|
|
142
|
+
// Skip if already synced
|
|
143
|
+
if (getSliceRecord(mapping, mid, sid))
|
|
144
|
+
return;
|
|
145
|
+
// Ensure milestone is synced first
|
|
146
|
+
if (!getMilestoneRecord(mapping, mid)) {
|
|
147
|
+
await syncMilestonePlan(basePath, mapping, config, mid);
|
|
148
|
+
}
|
|
149
|
+
const milestoneRecord = getMilestoneRecord(mapping, mid);
|
|
150
|
+
// Load slice plan
|
|
151
|
+
const planPath = resolveSliceFile(basePath, mid, sid, "PLAN");
|
|
152
|
+
if (!planPath)
|
|
153
|
+
return;
|
|
154
|
+
const content = await loadFile(planPath);
|
|
155
|
+
if (!content)
|
|
156
|
+
return;
|
|
157
|
+
const plan = parsePlan(content);
|
|
158
|
+
const sliceBranch = `milestone/${mid}/${sid}`;
|
|
159
|
+
const milestoneBranch = `milestone/${mid}`;
|
|
160
|
+
// Create task sub-issues first (so we can link them in the PR body)
|
|
161
|
+
const taskIssueNumbers = [];
|
|
162
|
+
if (plan.tasks) {
|
|
163
|
+
for (const task of plan.tasks) {
|
|
164
|
+
// Skip if already synced
|
|
165
|
+
if (getTaskRecord(mapping, mid, sid, task.id)) {
|
|
166
|
+
const existing = getTaskRecord(mapping, mid, sid, task.id);
|
|
167
|
+
taskIssueNumbers.push({ id: task.id, title: task.title, issueNumber: existing.issueNumber });
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const taskBody = formatTaskIssueBody({
|
|
171
|
+
id: task.id,
|
|
172
|
+
title: task.title,
|
|
173
|
+
description: task.description,
|
|
174
|
+
files: task.files,
|
|
175
|
+
verifyCriteria: task.verify ? [task.verify] : undefined,
|
|
176
|
+
});
|
|
177
|
+
const taskResult = ghCreateIssue(basePath, {
|
|
178
|
+
repo: mapping.repo,
|
|
179
|
+
title: `${mid}/${sid}/${task.id}: ${task.title}`,
|
|
180
|
+
body: taskBody,
|
|
181
|
+
labels: config.labels,
|
|
182
|
+
milestone: milestoneRecord?.ghMilestoneNumber,
|
|
183
|
+
parentIssue: milestoneRecord?.issueNumber,
|
|
184
|
+
});
|
|
185
|
+
if (taskResult.ok) {
|
|
186
|
+
setTaskRecord(mapping, mid, sid, task.id, {
|
|
187
|
+
issueNumber: taskResult.data,
|
|
188
|
+
lastSyncedAt: new Date().toISOString(),
|
|
189
|
+
state: "open",
|
|
190
|
+
});
|
|
191
|
+
taskIssueNumbers.push({ id: task.id, title: task.title, issueNumber: taskResult.data });
|
|
192
|
+
if (config.project) {
|
|
193
|
+
ghAddToProject(basePath, mapping.repo, config.project, taskResult.data);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
taskIssueNumbers.push({ id: task.id, title: task.title });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (config.slice_prs === false) {
|
|
202
|
+
// Slice PRs disabled — just record without PR
|
|
203
|
+
setSliceRecord(mapping, mid, sid, {
|
|
204
|
+
issueNumber: 0,
|
|
205
|
+
prNumber: 0,
|
|
206
|
+
branch: sliceBranch,
|
|
207
|
+
lastSyncedAt: new Date().toISOString(),
|
|
208
|
+
state: "open",
|
|
209
|
+
});
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
// Create slice branch from milestone branch
|
|
213
|
+
const branchResult = ghCreateBranch(basePath, sliceBranch, milestoneBranch);
|
|
214
|
+
if (!branchResult.ok) {
|
|
215
|
+
debugLog("github-sync", { phase: "create-slice-branch", error: branchResult.error });
|
|
216
|
+
// Branch might already exist — continue anyway
|
|
217
|
+
}
|
|
218
|
+
// Push the slice branch
|
|
219
|
+
const pushResult = ghPushBranch(basePath, sliceBranch);
|
|
220
|
+
if (!pushResult.ok) {
|
|
221
|
+
debugLog("github-sync", { phase: "push-slice-branch", error: pushResult.error });
|
|
222
|
+
}
|
|
223
|
+
// Create draft PR
|
|
224
|
+
const prBody = formatSlicePRBody({
|
|
225
|
+
id: sid,
|
|
226
|
+
title: plan.title || sid,
|
|
227
|
+
goal: plan.goal,
|
|
228
|
+
mustHaves: plan.mustHaves,
|
|
229
|
+
demoCriterion: plan.demo,
|
|
230
|
+
tasks: taskIssueNumbers,
|
|
231
|
+
});
|
|
232
|
+
const prResult = ghCreatePR(basePath, {
|
|
233
|
+
repo: mapping.repo,
|
|
234
|
+
base: milestoneBranch,
|
|
235
|
+
head: sliceBranch,
|
|
236
|
+
title: `${sid}: ${plan.title || sid}`,
|
|
237
|
+
body: prBody,
|
|
238
|
+
draft: true,
|
|
239
|
+
});
|
|
240
|
+
const prNumber = prResult.ok ? prResult.data : 0;
|
|
241
|
+
if (!prResult.ok) {
|
|
242
|
+
debugLog("github-sync", { phase: "create-slice-pr", error: prResult.error });
|
|
243
|
+
}
|
|
244
|
+
setSliceRecord(mapping, mid, sid, {
|
|
245
|
+
issueNumber: 0, // Slice doesn't get its own issue — tracked via PR
|
|
246
|
+
prNumber,
|
|
247
|
+
branch: sliceBranch,
|
|
248
|
+
lastSyncedAt: new Date().toISOString(),
|
|
249
|
+
state: "open",
|
|
250
|
+
});
|
|
251
|
+
debugLog("github-sync", {
|
|
252
|
+
phase: "slice-synced",
|
|
253
|
+
mid,
|
|
254
|
+
sid,
|
|
255
|
+
pr: prNumber,
|
|
256
|
+
taskIssues: taskIssueNumbers.filter(t => t.issueNumber).length,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
async function syncTaskComplete(basePath, mapping, config, mid, sid, tid) {
|
|
260
|
+
const taskRecord = getTaskRecord(mapping, mid, sid, tid);
|
|
261
|
+
if (!taskRecord || taskRecord.state === "closed")
|
|
262
|
+
return;
|
|
263
|
+
// Load task summary
|
|
264
|
+
const summaryPath = resolveTaskFile(basePath, mid, sid, tid, "SUMMARY");
|
|
265
|
+
if (summaryPath) {
|
|
266
|
+
const content = await loadFile(summaryPath);
|
|
267
|
+
if (content) {
|
|
268
|
+
const summary = parseSummary(content);
|
|
269
|
+
const comment = formatSummaryComment({
|
|
270
|
+
oneLiner: summary.oneLiner,
|
|
271
|
+
body: summary.whatHappened,
|
|
272
|
+
frontmatter: summary.frontmatter,
|
|
273
|
+
});
|
|
274
|
+
ghAddComment(basePath, mapping.repo, taskRecord.issueNumber, comment);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// Close the task issue
|
|
278
|
+
ghCloseIssue(basePath, mapping.repo, taskRecord.issueNumber);
|
|
279
|
+
taskRecord.state = "closed";
|
|
280
|
+
taskRecord.lastSyncedAt = new Date().toISOString();
|
|
281
|
+
setTaskRecord(mapping, mid, sid, tid, taskRecord);
|
|
282
|
+
debugLog("github-sync", { phase: "task-closed", mid, sid, tid, issue: taskRecord.issueNumber });
|
|
283
|
+
}
|
|
284
|
+
async function syncSliceComplete(basePath, mapping, config, mid, sid) {
|
|
285
|
+
const sliceRecord = getSliceRecord(mapping, mid, sid);
|
|
286
|
+
if (!sliceRecord || sliceRecord.state === "closed")
|
|
287
|
+
return;
|
|
288
|
+
// Post slice summary as PR comment
|
|
289
|
+
const summaryPath = resolveSliceFile(basePath, mid, sid, "SUMMARY");
|
|
290
|
+
if (summaryPath && sliceRecord.prNumber) {
|
|
291
|
+
const content = await loadFile(summaryPath);
|
|
292
|
+
if (content) {
|
|
293
|
+
const summary = parseSummary(content);
|
|
294
|
+
const comment = formatSummaryComment({
|
|
295
|
+
oneLiner: summary.oneLiner,
|
|
296
|
+
body: summary.whatHappened,
|
|
297
|
+
frontmatter: summary.frontmatter,
|
|
298
|
+
});
|
|
299
|
+
ghAddComment(basePath, mapping.repo, sliceRecord.prNumber, comment);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// Mark PR ready for review, then merge
|
|
303
|
+
if (sliceRecord.prNumber) {
|
|
304
|
+
ghMarkPRReady(basePath, mapping.repo, sliceRecord.prNumber);
|
|
305
|
+
// Squash-merge into milestone branch
|
|
306
|
+
ghMergePR(basePath, mapping.repo, sliceRecord.prNumber, "squash");
|
|
307
|
+
}
|
|
308
|
+
sliceRecord.state = "closed";
|
|
309
|
+
sliceRecord.lastSyncedAt = new Date().toISOString();
|
|
310
|
+
setSliceRecord(mapping, mid, sid, sliceRecord);
|
|
311
|
+
debugLog("github-sync", { phase: "slice-completed", mid, sid, pr: sliceRecord.prNumber });
|
|
312
|
+
}
|
|
313
|
+
async function syncMilestoneComplete(basePath, mapping, config, mid) {
|
|
314
|
+
const record = getMilestoneRecord(mapping, mid);
|
|
315
|
+
if (!record || record.state === "closed")
|
|
316
|
+
return;
|
|
317
|
+
// Close tracking issue
|
|
318
|
+
ghCloseIssue(basePath, mapping.repo, record.issueNumber, `Milestone ${mid} completed.`);
|
|
319
|
+
// Close GitHub milestone
|
|
320
|
+
ghCloseMilestone(basePath, mapping.repo, record.ghMilestoneNumber);
|
|
321
|
+
record.state = "closed";
|
|
322
|
+
record.lastSyncedAt = new Date().toISOString();
|
|
323
|
+
setMilestoneRecord(mapping, mid, record);
|
|
324
|
+
debugLog("github-sync", { phase: "milestone-completed", mid });
|
|
325
|
+
}
|
|
326
|
+
// ─── Bootstrap ──────────────────────────────────────────────────────────────
|
|
327
|
+
/**
|
|
328
|
+
* Walk the `.gsd/milestones/` tree and create GitHub entities for any
|
|
329
|
+
* that are missing from the sync mapping. Safe to run multiple times.
|
|
330
|
+
*/
|
|
331
|
+
export async function bootstrapSync(basePath) {
|
|
332
|
+
const config = loadGitHubSyncConfig(basePath);
|
|
333
|
+
if (!config?.enabled)
|
|
334
|
+
return { milestones: 0, slices: 0, tasks: 0 };
|
|
335
|
+
if (!ghIsAvailable())
|
|
336
|
+
return { milestones: 0, slices: 0, tasks: 0 };
|
|
337
|
+
const repo = config.repo ?? resolveRepo(basePath);
|
|
338
|
+
if (!repo)
|
|
339
|
+
return { milestones: 0, slices: 0, tasks: 0 };
|
|
340
|
+
let mapping = loadSyncMapping(basePath) ?? createEmptyMapping(repo);
|
|
341
|
+
mapping.repo = repo;
|
|
342
|
+
const taskCountBefore = Object.keys(mapping.tasks).length;
|
|
343
|
+
const counts = { milestones: 0, slices: 0, tasks: 0 };
|
|
344
|
+
const milestonesDir = join(basePath, ".gsd", "milestones");
|
|
345
|
+
if (!existsSync(milestonesDir))
|
|
346
|
+
return counts;
|
|
347
|
+
const milestoneIds = readdirSync(milestonesDir, { withFileTypes: true })
|
|
348
|
+
.filter(d => d.isDirectory())
|
|
349
|
+
.map(d => d.name)
|
|
350
|
+
.sort();
|
|
351
|
+
for (const mid of milestoneIds) {
|
|
352
|
+
if (!getMilestoneRecord(mapping, mid)) {
|
|
353
|
+
await syncMilestonePlan(basePath, mapping, config, mid);
|
|
354
|
+
counts.milestones++;
|
|
355
|
+
}
|
|
356
|
+
// Find slices
|
|
357
|
+
const slicesDir = join(milestonesDir, mid, "slices");
|
|
358
|
+
if (!existsSync(slicesDir))
|
|
359
|
+
continue;
|
|
360
|
+
const sliceIds = readdirSync(slicesDir, { withFileTypes: true })
|
|
361
|
+
.filter(d => d.isDirectory())
|
|
362
|
+
.map(d => d.name)
|
|
363
|
+
.sort();
|
|
364
|
+
for (const sid of sliceIds) {
|
|
365
|
+
if (!getSliceRecord(mapping, mid, sid)) {
|
|
366
|
+
await syncSlicePlan(basePath, mapping, config, mid, sid);
|
|
367
|
+
counts.slices++;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
counts.tasks = Object.keys(mapping.tasks).length - taskCountBefore;
|
|
372
|
+
saveSyncMapping(basePath, mapping);
|
|
373
|
+
return counts;
|
|
374
|
+
}
|
|
375
|
+
// ─── Config Loading ─────────────────────────────────────────────────────────
|
|
376
|
+
let _cachedConfig;
|
|
377
|
+
function loadGitHubSyncConfig(_basePath) {
|
|
378
|
+
if (_cachedConfig !== undefined)
|
|
379
|
+
return _cachedConfig;
|
|
380
|
+
try {
|
|
381
|
+
const prefs = loadEffectiveGSDPreferences();
|
|
382
|
+
const github = prefs?.preferences?.github;
|
|
383
|
+
if (!github || typeof github !== "object") {
|
|
384
|
+
_cachedConfig = null;
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
_cachedConfig = github;
|
|
388
|
+
return _cachedConfig;
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
_cachedConfig = null;
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
/** Reset config cache (for testing). */
|
|
396
|
+
export function _resetConfigCache() {
|
|
397
|
+
_cachedConfig = undefined;
|
|
398
|
+
}
|
|
399
|
+
function resolveRepo(basePath) {
|
|
400
|
+
const result = ghDetectRepo(basePath);
|
|
401
|
+
return result.ok ? result.data : null;
|
|
402
|
+
}
|
|
403
|
+
// ─── Commit Linking ─────────────────────────────────────────────────────────
|
|
404
|
+
/**
|
|
405
|
+
* Look up the GitHub issue number for a task so the commit message
|
|
406
|
+
* can include `Resolves #N`. Called from git-service commit building.
|
|
407
|
+
*/
|
|
408
|
+
export function getTaskIssueNumberForCommit(basePath, mid, sid, tid) {
|
|
409
|
+
try {
|
|
410
|
+
const config = loadGitHubSyncConfig(basePath);
|
|
411
|
+
if (!config?.enabled)
|
|
412
|
+
return null;
|
|
413
|
+
if (config.auto_link_commits === false)
|
|
414
|
+
return null;
|
|
415
|
+
const mapping = loadSyncMapping(basePath);
|
|
416
|
+
if (!mapping)
|
|
417
|
+
return null;
|
|
418
|
+
const record = getTaskRecord(mapping, mid, sid, tid);
|
|
419
|
+
return record?.issueNumber ?? null;
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
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
|
+
export function formatMilestoneIssueBody(data) {
|
|
9
|
+
const lines = [];
|
|
10
|
+
lines.push(`# ${data.id}: ${data.title}`);
|
|
11
|
+
lines.push("");
|
|
12
|
+
if (data.vision) {
|
|
13
|
+
lines.push("## Vision");
|
|
14
|
+
lines.push(data.vision);
|
|
15
|
+
lines.push("");
|
|
16
|
+
}
|
|
17
|
+
if (data.successCriteria?.length) {
|
|
18
|
+
lines.push("## Success Criteria");
|
|
19
|
+
for (const criterion of data.successCriteria) {
|
|
20
|
+
lines.push(`- [ ] ${criterion}`);
|
|
21
|
+
}
|
|
22
|
+
lines.push("");
|
|
23
|
+
}
|
|
24
|
+
if (data.slices?.length) {
|
|
25
|
+
lines.push("## Slices");
|
|
26
|
+
lines.push("");
|
|
27
|
+
lines.push("| Slice | Title | Tasks |");
|
|
28
|
+
lines.push("|-------|-------|-------|");
|
|
29
|
+
for (const slice of data.slices) {
|
|
30
|
+
lines.push(`| ${slice.id} | ${slice.title} | ${slice.taskCount ?? "—"} |`);
|
|
31
|
+
}
|
|
32
|
+
lines.push("");
|
|
33
|
+
}
|
|
34
|
+
lines.push("---");
|
|
35
|
+
lines.push("*Auto-generated by GSD GitHub Sync*");
|
|
36
|
+
return lines.join("\n");
|
|
37
|
+
}
|
|
38
|
+
export function formatSlicePRBody(data) {
|
|
39
|
+
const lines = [];
|
|
40
|
+
lines.push(`## ${data.id}: ${data.title}`);
|
|
41
|
+
lines.push("");
|
|
42
|
+
if (data.goal) {
|
|
43
|
+
lines.push("### Goal");
|
|
44
|
+
lines.push(data.goal);
|
|
45
|
+
lines.push("");
|
|
46
|
+
}
|
|
47
|
+
if (data.mustHaves?.length) {
|
|
48
|
+
lines.push("### Must-Haves");
|
|
49
|
+
for (const item of data.mustHaves) {
|
|
50
|
+
lines.push(`- ${item}`);
|
|
51
|
+
}
|
|
52
|
+
lines.push("");
|
|
53
|
+
}
|
|
54
|
+
if (data.demoCriterion) {
|
|
55
|
+
lines.push("### Demo Criterion");
|
|
56
|
+
lines.push(data.demoCriterion);
|
|
57
|
+
lines.push("");
|
|
58
|
+
}
|
|
59
|
+
if (data.tasks?.length) {
|
|
60
|
+
lines.push("### Tasks");
|
|
61
|
+
for (const task of data.tasks) {
|
|
62
|
+
const ref = task.issueNumber ? ` (#${task.issueNumber})` : "";
|
|
63
|
+
lines.push(`- [ ] ${task.id}: ${task.title}${ref}`);
|
|
64
|
+
}
|
|
65
|
+
lines.push("");
|
|
66
|
+
}
|
|
67
|
+
lines.push("---");
|
|
68
|
+
lines.push("*Auto-generated by GSD GitHub Sync*");
|
|
69
|
+
return lines.join("\n");
|
|
70
|
+
}
|
|
71
|
+
export function formatTaskIssueBody(data) {
|
|
72
|
+
const lines = [];
|
|
73
|
+
lines.push(`## ${data.id}: ${data.title}`);
|
|
74
|
+
lines.push("");
|
|
75
|
+
if (data.description) {
|
|
76
|
+
lines.push(data.description);
|
|
77
|
+
lines.push("");
|
|
78
|
+
}
|
|
79
|
+
if (data.files?.length) {
|
|
80
|
+
lines.push("### Files");
|
|
81
|
+
for (const file of data.files) {
|
|
82
|
+
lines.push(`- \`${file}\``);
|
|
83
|
+
}
|
|
84
|
+
lines.push("");
|
|
85
|
+
}
|
|
86
|
+
if (data.verifyCriteria?.length) {
|
|
87
|
+
lines.push("### Verification");
|
|
88
|
+
for (const criterion of data.verifyCriteria) {
|
|
89
|
+
lines.push(`- [ ] ${criterion}`);
|
|
90
|
+
}
|
|
91
|
+
lines.push("");
|
|
92
|
+
}
|
|
93
|
+
return lines.join("\n");
|
|
94
|
+
}
|
|
95
|
+
export function formatSummaryComment(data) {
|
|
96
|
+
const lines = [];
|
|
97
|
+
if (data.oneLiner) {
|
|
98
|
+
lines.push(`**Summary:** ${data.oneLiner}`);
|
|
99
|
+
lines.push("");
|
|
100
|
+
}
|
|
101
|
+
if (data.body) {
|
|
102
|
+
lines.push(data.body);
|
|
103
|
+
lines.push("");
|
|
104
|
+
}
|
|
105
|
+
if (data.frontmatter && Object.keys(data.frontmatter).length > 0) {
|
|
106
|
+
lines.push("<details>");
|
|
107
|
+
lines.push("<summary>Metadata</summary>");
|
|
108
|
+
lines.push("");
|
|
109
|
+
lines.push("```yaml");
|
|
110
|
+
for (const [key, value] of Object.entries(data.frontmatter)) {
|
|
111
|
+
lines.push(`${key}: ${JSON.stringify(value)}`);
|
|
112
|
+
}
|
|
113
|
+
lines.push("```");
|
|
114
|
+
lines.push("");
|
|
115
|
+
lines.push("</details>");
|
|
116
|
+
}
|
|
117
|
+
return lines.join("\n");
|
|
118
|
+
}
|