@tuan_son.dinh/gsd 2.6.0
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/LICENSE +21 -0
- package/README.md +453 -0
- package/dist/app-paths.d.ts +4 -0
- package/dist/app-paths.js +6 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +269 -0
- package/dist/loader.d.ts +2 -0
- package/dist/loader.js +70 -0
- package/dist/logo.d.ts +16 -0
- package/dist/logo.js +25 -0
- package/dist/onboarding.d.ts +43 -0
- package/dist/onboarding.js +418 -0
- package/dist/pi-migration.d.ts +14 -0
- package/dist/pi-migration.js +57 -0
- package/dist/resource-loader.d.ts +22 -0
- package/dist/resource-loader.js +60 -0
- package/dist/tool-bootstrap.d.ts +4 -0
- package/dist/tool-bootstrap.js +74 -0
- package/dist/wizard.d.ts +7 -0
- package/dist/wizard.js +25 -0
- package/package.json +60 -0
- package/patches/@mariozechner+pi-coding-agent+0.57.1.patch +108 -0
- package/patches/@mariozechner+pi-tui+0.57.1.patch +47 -0
- package/pkg/dist/modes/interactive/theme/dark.json +85 -0
- package/pkg/dist/modes/interactive/theme/light.json +84 -0
- package/pkg/dist/modes/interactive/theme/theme-schema.json +335 -0
- package/pkg/dist/modes/interactive/theme/theme.d.ts +78 -0
- package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -0
- package/pkg/dist/modes/interactive/theme/theme.js +949 -0
- package/pkg/dist/modes/interactive/theme/theme.js.map +1 -0
- package/pkg/package.json +8 -0
- package/scripts/postinstall.js +127 -0
- package/src/resources/GSD-WORKFLOW.md +661 -0
- package/src/resources/agents/researcher.md +29 -0
- package/src/resources/agents/scout.md +56 -0
- package/src/resources/agents/worker.md +31 -0
- package/src/resources/extensions/ask-user-questions.ts +249 -0
- package/src/resources/extensions/bg-shell/index.ts +2808 -0
- package/src/resources/extensions/browser-tools/BROWSER-TOOLS-V2-PROPOSAL.md +1277 -0
- package/src/resources/extensions/browser-tools/core.js +1057 -0
- package/src/resources/extensions/browser-tools/index.ts +4989 -0
- package/src/resources/extensions/browser-tools/package.json +20 -0
- package/src/resources/extensions/context7/index.ts +428 -0
- package/src/resources/extensions/context7/package.json +11 -0
- package/src/resources/extensions/get-secrets-from-user.ts +352 -0
- package/src/resources/extensions/google-search/index.ts +323 -0
- package/src/resources/extensions/google-search/package.json +9 -0
- package/src/resources/extensions/gsd/activity-log.ts +69 -0
- package/src/resources/extensions/gsd/auto.ts +2744 -0
- package/src/resources/extensions/gsd/commands.ts +313 -0
- package/src/resources/extensions/gsd/crash-recovery.ts +85 -0
- package/src/resources/extensions/gsd/dashboard-overlay.ts +521 -0
- package/src/resources/extensions/gsd/docs/preferences-reference.md +176 -0
- package/src/resources/extensions/gsd/doctor.ts +690 -0
- package/src/resources/extensions/gsd/files.ts +732 -0
- package/src/resources/extensions/gsd/git-service.ts +597 -0
- package/src/resources/extensions/gsd/gitignore.ts +168 -0
- package/src/resources/extensions/gsd/guided-flow.ts +817 -0
- package/src/resources/extensions/gsd/index.ts +558 -0
- package/src/resources/extensions/gsd/metrics.ts +374 -0
- package/src/resources/extensions/gsd/migrate/command.ts +218 -0
- package/src/resources/extensions/gsd/migrate/index.ts +42 -0
- package/src/resources/extensions/gsd/migrate/parser.ts +323 -0
- package/src/resources/extensions/gsd/migrate/parsers.ts +624 -0
- package/src/resources/extensions/gsd/migrate/preview.ts +48 -0
- package/src/resources/extensions/gsd/migrate/transformer.ts +346 -0
- package/src/resources/extensions/gsd/migrate/types.ts +370 -0
- package/src/resources/extensions/gsd/migrate/validator.ts +55 -0
- package/src/resources/extensions/gsd/migrate/writer.ts +539 -0
- package/src/resources/extensions/gsd/observability-validator.ts +408 -0
- package/src/resources/extensions/gsd/package.json +11 -0
- package/src/resources/extensions/gsd/paths.ts +308 -0
- package/src/resources/extensions/gsd/preferences.ts +757 -0
- package/src/resources/extensions/gsd/prompt-loader.ts +50 -0
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +25 -0
- package/src/resources/extensions/gsd/prompts/complete-slice.md +29 -0
- package/src/resources/extensions/gsd/prompts/discuss.md +189 -0
- package/src/resources/extensions/gsd/prompts/doctor-heal.md +29 -0
- package/src/resources/extensions/gsd/prompts/execute-task.md +61 -0
- package/src/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -0
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +3 -0
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +59 -0
- package/src/resources/extensions/gsd/prompts/guided-execute-task.md +1 -0
- package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +23 -0
- package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -0
- package/src/resources/extensions/gsd/prompts/guided-research-slice.md +11 -0
- package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -0
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +65 -0
- package/src/resources/extensions/gsd/prompts/plan-slice.md +51 -0
- package/src/resources/extensions/gsd/prompts/queue.md +85 -0
- package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +48 -0
- package/src/resources/extensions/gsd/prompts/replan-slice.md +39 -0
- package/src/resources/extensions/gsd/prompts/research-milestone.md +37 -0
- package/src/resources/extensions/gsd/prompts/research-slice.md +28 -0
- package/src/resources/extensions/gsd/prompts/review-migration.md +66 -0
- package/src/resources/extensions/gsd/prompts/run-uat.md +109 -0
- package/src/resources/extensions/gsd/prompts/system.md +187 -0
- package/src/resources/extensions/gsd/prompts/worktree-merge.md +123 -0
- package/src/resources/extensions/gsd/session-forensics.ts +487 -0
- package/src/resources/extensions/gsd/skill-discovery.ts +137 -0
- package/src/resources/extensions/gsd/state.ts +460 -0
- package/src/resources/extensions/gsd/templates/context.md +76 -0
- package/src/resources/extensions/gsd/templates/decisions.md +8 -0
- package/src/resources/extensions/gsd/templates/milestone-summary.md +73 -0
- package/src/resources/extensions/gsd/templates/plan.md +131 -0
- package/src/resources/extensions/gsd/templates/preferences.md +24 -0
- package/src/resources/extensions/gsd/templates/project.md +31 -0
- package/src/resources/extensions/gsd/templates/reassessment.md +28 -0
- package/src/resources/extensions/gsd/templates/requirements.md +81 -0
- package/src/resources/extensions/gsd/templates/research.md +46 -0
- package/src/resources/extensions/gsd/templates/roadmap.md +118 -0
- package/src/resources/extensions/gsd/templates/slice-context.md +58 -0
- package/src/resources/extensions/gsd/templates/slice-summary.md +99 -0
- package/src/resources/extensions/gsd/templates/state.md +19 -0
- package/src/resources/extensions/gsd/templates/task-plan.md +52 -0
- package/src/resources/extensions/gsd/templates/task-summary.md +57 -0
- package/src/resources/extensions/gsd/templates/uat.md +54 -0
- package/src/resources/extensions/gsd/tests/activity-log-prune.test.ts +327 -0
- package/src/resources/extensions/gsd/tests/auto-preflight.test.ts +56 -0
- package/src/resources/extensions/gsd/tests/auto-supervisor.test.mjs +53 -0
- package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +225 -0
- package/src/resources/extensions/gsd/tests/cost-projection.test.ts +160 -0
- package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +341 -0
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +689 -0
- package/src/resources/extensions/gsd/tests/discuss-prompt.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/doctor.test.ts +505 -0
- package/src/resources/extensions/gsd/tests/git-service.test.ts +1313 -0
- package/src/resources/extensions/gsd/tests/idle-recovery.test.ts +308 -0
- package/src/resources/extensions/gsd/tests/metrics-io.test.ts +201 -0
- package/src/resources/extensions/gsd/tests/metrics.test.ts +217 -0
- package/src/resources/extensions/gsd/tests/migrate-command.test.ts +390 -0
- package/src/resources/extensions/gsd/tests/migrate-parser.test.ts +786 -0
- package/src/resources/extensions/gsd/tests/migrate-transformer.test.ts +657 -0
- package/src/resources/extensions/gsd/tests/migrate-validator-parsers.test.ts +443 -0
- package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +318 -0
- package/src/resources/extensions/gsd/tests/migrate-writer.test.ts +420 -0
- package/src/resources/extensions/gsd/tests/must-have-parser.test.ts +309 -0
- package/src/resources/extensions/gsd/tests/parsers.test.ts +1351 -0
- package/src/resources/extensions/gsd/tests/plan-milestone.test.ts +163 -0
- package/src/resources/extensions/gsd/tests/plan-quality-validator.test.ts +386 -0
- package/src/resources/extensions/gsd/tests/reassess-prompt.test.ts +171 -0
- package/src/resources/extensions/gsd/tests/remote-questions.test.ts +155 -0
- package/src/resources/extensions/gsd/tests/remote-status.test.ts +99 -0
- package/src/resources/extensions/gsd/tests/replan-slice.test.ts +521 -0
- package/src/resources/extensions/gsd/tests/requirements.test.ts +125 -0
- package/src/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +34 -0
- package/src/resources/extensions/gsd/tests/resolve-ts.mjs +11 -0
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +348 -0
- package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +247 -0
- package/src/resources/extensions/gsd/tests/workflow-config.test.mjs +53 -0
- package/src/resources/extensions/gsd/tests/workspace-index.test.ts +94 -0
- package/src/resources/extensions/gsd/tests/worktree-integration.test.ts +253 -0
- package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +160 -0
- package/src/resources/extensions/gsd/tests/worktree.test.ts +264 -0
- package/src/resources/extensions/gsd/types.ts +159 -0
- package/src/resources/extensions/gsd/unit-runtime.ts +184 -0
- package/src/resources/extensions/gsd/workspace-index.ts +203 -0
- package/src/resources/extensions/gsd/worktree-command.ts +845 -0
- package/src/resources/extensions/gsd/worktree-manager.ts +392 -0
- package/src/resources/extensions/gsd/worktree.ts +183 -0
- package/src/resources/extensions/mac-tools/index.ts +852 -0
- package/src/resources/extensions/mac-tools/swift-cli/Package.swift +22 -0
- package/src/resources/extensions/mac-tools/swift-cli/Sources/main.swift +1318 -0
- package/src/resources/extensions/mcporter/index.ts +429 -0
- package/src/resources/extensions/remote-questions/config.ts +81 -0
- package/src/resources/extensions/remote-questions/discord-adapter.ts +128 -0
- package/src/resources/extensions/remote-questions/format.ts +163 -0
- package/src/resources/extensions/remote-questions/manager.ts +192 -0
- package/src/resources/extensions/remote-questions/remote-command.ts +307 -0
- package/src/resources/extensions/remote-questions/slack-adapter.ts +92 -0
- package/src/resources/extensions/remote-questions/status.ts +31 -0
- package/src/resources/extensions/remote-questions/store.ts +77 -0
- package/src/resources/extensions/remote-questions/types.ts +75 -0
- package/src/resources/extensions/search-the-web/cache.ts +78 -0
- package/src/resources/extensions/search-the-web/command-search-provider.ts +95 -0
- package/src/resources/extensions/search-the-web/format.ts +258 -0
- package/src/resources/extensions/search-the-web/http.ts +238 -0
- package/src/resources/extensions/search-the-web/index.ts +65 -0
- package/src/resources/extensions/search-the-web/native-search.ts +157 -0
- package/src/resources/extensions/search-the-web/provider.ts +118 -0
- package/src/resources/extensions/search-the-web/tavily.ts +116 -0
- package/src/resources/extensions/search-the-web/tool-fetch-page.ts +519 -0
- package/src/resources/extensions/search-the-web/tool-llm-context.ts +561 -0
- package/src/resources/extensions/search-the-web/tool-search.ts +576 -0
- package/src/resources/extensions/search-the-web/url-utils.ts +91 -0
- package/src/resources/extensions/shared/confirm-ui.ts +126 -0
- package/src/resources/extensions/shared/interview-ui.ts +613 -0
- package/src/resources/extensions/shared/next-action-ui.ts +197 -0
- package/src/resources/extensions/shared/progress-widget.ts +282 -0
- package/src/resources/extensions/shared/terminal.ts +23 -0
- package/src/resources/extensions/shared/thinking-widget.ts +107 -0
- package/src/resources/extensions/shared/ui.ts +400 -0
- package/src/resources/extensions/shared/wizard-ui.ts +551 -0
- package/src/resources/extensions/slash-commands/audit.ts +88 -0
- package/src/resources/extensions/slash-commands/clear.ts +10 -0
- package/src/resources/extensions/slash-commands/create-extension.ts +297 -0
- package/src/resources/extensions/slash-commands/create-slash-command.ts +234 -0
- package/src/resources/extensions/slash-commands/index.ts +12 -0
- package/src/resources/extensions/subagent/agents.ts +126 -0
- package/src/resources/extensions/subagent/index.ts +1020 -0
- package/src/resources/extensions/voice/index.ts +195 -0
- package/src/resources/extensions/voice/speech-recognizer.swift +154 -0
- package/src/resources/skills/debug-like-expert/SKILL.md +231 -0
- package/src/resources/skills/debug-like-expert/references/debugging-mindset.md +253 -0
- package/src/resources/skills/debug-like-expert/references/hypothesis-testing.md +373 -0
- package/src/resources/skills/debug-like-expert/references/investigation-techniques.md +337 -0
- package/src/resources/skills/debug-like-expert/references/verification-patterns.md +425 -0
- package/src/resources/skills/debug-like-expert/references/when-to-research.md +361 -0
- package/src/resources/skills/frontend-design/SKILL.md +45 -0
- package/src/resources/skills/swiftui/SKILL.md +208 -0
- package/src/resources/skills/swiftui/references/animations.md +921 -0
- package/src/resources/skills/swiftui/references/architecture.md +1561 -0
- package/src/resources/skills/swiftui/references/layout-system.md +1186 -0
- package/src/resources/skills/swiftui/references/navigation.md +1492 -0
- package/src/resources/skills/swiftui/references/networking-async.md +214 -0
- package/src/resources/skills/swiftui/references/performance.md +1706 -0
- package/src/resources/skills/swiftui/references/platform-integration.md +204 -0
- package/src/resources/skills/swiftui/references/state-management.md +1443 -0
- package/src/resources/skills/swiftui/references/swiftdata.md +297 -0
- package/src/resources/skills/swiftui/references/testing-debugging.md +247 -0
- package/src/resources/skills/swiftui/references/uikit-appkit-interop.md +218 -0
- package/src/resources/skills/swiftui/workflows/add-feature.md +191 -0
- package/src/resources/skills/swiftui/workflows/build-new-app.md +311 -0
- package/src/resources/skills/swiftui/workflows/debug-swiftui.md +192 -0
- package/src/resources/skills/swiftui/workflows/optimize-performance.md +197 -0
- package/src/resources/skills/swiftui/workflows/ship-app.md +203 -0
- package/src/resources/skills/swiftui/workflows/write-tests.md +235 -0
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSD Extension — /gsd
|
|
3
|
+
*
|
|
4
|
+
* One command, one wizard. Reads state from disk, shows contextual options,
|
|
5
|
+
* dispatches through GSD-WORKFLOW.md. The LLM does the rest.
|
|
6
|
+
*
|
|
7
|
+
* Auto-mode: /gsd auto loops fresh sessions until milestone complete.
|
|
8
|
+
*
|
|
9
|
+
* Commands:
|
|
10
|
+
* /gsd — contextual wizard (smart entry point)
|
|
11
|
+
* /gsd auto — start auto-mode (fresh session per unit)
|
|
12
|
+
* /gsd stop — stop auto-mode gracefully
|
|
13
|
+
* /gsd status — progress dashboard
|
|
14
|
+
*
|
|
15
|
+
* Hooks:
|
|
16
|
+
* before_agent_start — inject GSD system context for GSD projects
|
|
17
|
+
* agent_end — auto-mode advancement
|
|
18
|
+
* session_before_compact — save continue.md OR block during auto
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type {
|
|
22
|
+
ExtensionAPI,
|
|
23
|
+
ExtensionContext,
|
|
24
|
+
} from "@mariozechner/pi-coding-agent";
|
|
25
|
+
import { createBashTool, createWriteTool, createReadTool, createEditTool } from "@mariozechner/pi-coding-agent";
|
|
26
|
+
|
|
27
|
+
import { registerGSDCommand } from "./commands.js";
|
|
28
|
+
import { registerWorktreeCommand, getWorktreeOriginalCwd, getActiveWorktreeName } from "./worktree-command.js";
|
|
29
|
+
import { saveFile, formatContinue, loadFile, parseContinue, parseSummary } from "./files.js";
|
|
30
|
+
import { loadPrompt } from "./prompt-loader.js";
|
|
31
|
+
import { deriveState } from "./state.js";
|
|
32
|
+
import { isAutoActive, isAutoPaused, handleAgentEnd, pauseAuto, getAutoDashboardData } from "./auto.js";
|
|
33
|
+
import { saveActivityLog } from "./activity-log.js";
|
|
34
|
+
import { checkAutoStartAfterDiscuss } from "./guided-flow.js";
|
|
35
|
+
import { GSDDashboardOverlay } from "./dashboard-overlay.js";
|
|
36
|
+
import {
|
|
37
|
+
loadEffectiveGSDPreferences,
|
|
38
|
+
renderPreferencesForSystemPrompt,
|
|
39
|
+
resolveAllSkillReferences,
|
|
40
|
+
} from "./preferences.js";
|
|
41
|
+
import { hasSkillSnapshot, detectNewSkills, formatSkillsXml } from "./skill-discovery.js";
|
|
42
|
+
import {
|
|
43
|
+
resolveSlicePath, resolveSliceFile, resolveTaskFile, resolveTaskFiles, resolveTasksDir,
|
|
44
|
+
relSliceFile, relSlicePath, relTaskFile,
|
|
45
|
+
buildSliceFileName, gsdRoot,
|
|
46
|
+
} from "./paths.js";
|
|
47
|
+
import { Key } from "@mariozechner/pi-tui";
|
|
48
|
+
import { join } from "node:path";
|
|
49
|
+
import { existsSync } from "node:fs";
|
|
50
|
+
import { shortcutDesc } from "../shared/terminal.js";
|
|
51
|
+
import { Text } from "@mariozechner/pi-tui";
|
|
52
|
+
|
|
53
|
+
// ── ASCII logo ────────────────────────────────────────────────────────────
|
|
54
|
+
const GSD_LOGO_LINES = [
|
|
55
|
+
" ██████╗ ███████╗██████╗ ",
|
|
56
|
+
" ██╔════╝ ██╔════╝██╔══██╗",
|
|
57
|
+
" ██║ ███╗███████╗██║ ██║",
|
|
58
|
+
" ██║ ██║╚════██║██║ ██║",
|
|
59
|
+
" ╚██████╔╝███████║██████╔╝",
|
|
60
|
+
" ╚═════╝ ╚══════╝╚═════╝ ",
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
export default function (pi: ExtensionAPI) {
|
|
64
|
+
registerGSDCommand(pi);
|
|
65
|
+
registerWorktreeCommand(pi);
|
|
66
|
+
|
|
67
|
+
// ── /exit — kill the process immediately ──────────────────────────────
|
|
68
|
+
pi.registerCommand("exit", {
|
|
69
|
+
description: "Exit GSD immediately",
|
|
70
|
+
handler: async (_ctx) => {
|
|
71
|
+
process.exit(0);
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// ── Dynamic-cwd bash tool with default timeout ────────────────────────
|
|
76
|
+
// The built-in bash tool captures cwd at startup. This replacement uses
|
|
77
|
+
// a spawnHook to read process.cwd() dynamically so that process.chdir()
|
|
78
|
+
// (used by /worktree switch) propagates to shell commands.
|
|
79
|
+
//
|
|
80
|
+
// The upstream SDK's bash tool has no default timeout — if the LLM omits
|
|
81
|
+
// the timeout parameter, commands run indefinitely, causing hangs on
|
|
82
|
+
// Windows where process killing is unreliable (see #40). We wrap execute
|
|
83
|
+
// to inject a 120-second default when no timeout is provided.
|
|
84
|
+
const DEFAULT_BASH_TIMEOUT_SECS = 120;
|
|
85
|
+
const baseBash = createBashTool(process.cwd(), {
|
|
86
|
+
spawnHook: (ctx) => ({ ...ctx, cwd: process.cwd() }),
|
|
87
|
+
});
|
|
88
|
+
const dynamicBash = {
|
|
89
|
+
...baseBash,
|
|
90
|
+
execute: async (
|
|
91
|
+
toolCallId: string,
|
|
92
|
+
params: { command: string; timeout?: number },
|
|
93
|
+
signal?: AbortSignal,
|
|
94
|
+
onUpdate?: any,
|
|
95
|
+
ctx?: any,
|
|
96
|
+
) => {
|
|
97
|
+
const paramsWithTimeout = {
|
|
98
|
+
...params,
|
|
99
|
+
timeout: params.timeout ?? DEFAULT_BASH_TIMEOUT_SECS,
|
|
100
|
+
};
|
|
101
|
+
return baseBash.execute(toolCallId, paramsWithTimeout, signal, onUpdate, ctx);
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
pi.registerTool(dynamicBash as any);
|
|
105
|
+
|
|
106
|
+
// ── Dynamic-cwd file tools (write, read, edit) ────────────────────────
|
|
107
|
+
// The built-in file tools capture cwd at startup. When process.chdir()
|
|
108
|
+
// moves us into a worktree, relative paths still resolve against the
|
|
109
|
+
// original launch directory. These replacements delegate to freshly-
|
|
110
|
+
// created tools on each call so that process.cwd() is read dynamically.
|
|
111
|
+
const baseWrite = createWriteTool(process.cwd());
|
|
112
|
+
const dynamicWrite = {
|
|
113
|
+
...baseWrite,
|
|
114
|
+
execute: async (
|
|
115
|
+
toolCallId: string,
|
|
116
|
+
params: { path: string; content: string },
|
|
117
|
+
signal?: AbortSignal,
|
|
118
|
+
onUpdate?: any,
|
|
119
|
+
ctx?: any,
|
|
120
|
+
) => {
|
|
121
|
+
const fresh = createWriteTool(process.cwd());
|
|
122
|
+
return fresh.execute(toolCallId, params, signal, onUpdate, ctx);
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
pi.registerTool(dynamicWrite as any);
|
|
126
|
+
|
|
127
|
+
const baseRead = createReadTool(process.cwd());
|
|
128
|
+
const dynamicRead = {
|
|
129
|
+
...baseRead,
|
|
130
|
+
execute: async (
|
|
131
|
+
toolCallId: string,
|
|
132
|
+
params: { path: string; offset?: number; limit?: number },
|
|
133
|
+
signal?: AbortSignal,
|
|
134
|
+
onUpdate?: any,
|
|
135
|
+
ctx?: any,
|
|
136
|
+
) => {
|
|
137
|
+
const fresh = createReadTool(process.cwd());
|
|
138
|
+
return fresh.execute(toolCallId, params, signal, onUpdate, ctx);
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
pi.registerTool(dynamicRead as any);
|
|
142
|
+
|
|
143
|
+
const baseEdit = createEditTool(process.cwd());
|
|
144
|
+
const dynamicEdit = {
|
|
145
|
+
...baseEdit,
|
|
146
|
+
execute: async (
|
|
147
|
+
toolCallId: string,
|
|
148
|
+
params: { path: string; oldText: string; newText: string },
|
|
149
|
+
signal?: AbortSignal,
|
|
150
|
+
onUpdate?: any,
|
|
151
|
+
ctx?: any,
|
|
152
|
+
) => {
|
|
153
|
+
const fresh = createEditTool(process.cwd());
|
|
154
|
+
return fresh.execute(toolCallId, params, signal, onUpdate, ctx);
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
pi.registerTool(dynamicEdit as any);
|
|
158
|
+
|
|
159
|
+
// ── session_start: render branded GSD header + remote channel status ──
|
|
160
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
161
|
+
// Theme access throws in RPC mode (no TUI) — header is decorative, skip it
|
|
162
|
+
try {
|
|
163
|
+
const theme = ctx.ui.theme;
|
|
164
|
+
const version = process.env.GSD_VERSION || "0.0.0";
|
|
165
|
+
|
|
166
|
+
const logoText = GSD_LOGO_LINES.map((line) => theme.fg("accent", line)).join("\n");
|
|
167
|
+
const titleLine = ` ${theme.bold("Get Shit Done")} ${theme.fg("dim", `v${version}`)}`;
|
|
168
|
+
|
|
169
|
+
const headerContent = `${logoText}\n${titleLine}`;
|
|
170
|
+
ctx.ui.setHeader((_ui, _theme) => new Text(headerContent, 1, 0));
|
|
171
|
+
} catch {
|
|
172
|
+
// RPC mode — no TUI, skip header rendering
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Notify remote questions status if configured
|
|
176
|
+
try {
|
|
177
|
+
const [{ getRemoteConfigStatus }, { getLatestPromptSummary }] = await Promise.all([
|
|
178
|
+
import("../remote-questions/config.js"),
|
|
179
|
+
import("../remote-questions/status.js"),
|
|
180
|
+
]);
|
|
181
|
+
const status = getRemoteConfigStatus();
|
|
182
|
+
const latest = getLatestPromptSummary();
|
|
183
|
+
if (!status.includes("not configured")) {
|
|
184
|
+
const suffix = latest ? `\nLast remote prompt: ${latest.id} (${latest.status})` : "";
|
|
185
|
+
ctx.ui.notify(`${status}${suffix}`, status.includes("disabled") ? "warning" : "info");
|
|
186
|
+
}
|
|
187
|
+
} catch {
|
|
188
|
+
// Remote questions module not available — ignore
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// ── Ctrl+Alt+G shortcut — GSD dashboard overlay ────────────────────────
|
|
193
|
+
pi.registerShortcut(Key.ctrlAlt("g"), {
|
|
194
|
+
description: shortcutDesc("Open GSD dashboard", "/gsd status"),
|
|
195
|
+
handler: async (ctx) => {
|
|
196
|
+
// Only show if .gsd/ exists
|
|
197
|
+
if (!existsSync(join(process.cwd(), ".gsd"))) {
|
|
198
|
+
ctx.ui.notify("No .gsd/ directory found. Run /gsd to start.", "info");
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
await ctx.ui.custom<void>(
|
|
203
|
+
(tui, theme, _kb, done) => {
|
|
204
|
+
return new GSDDashboardOverlay(tui, theme, () => done());
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
overlay: true,
|
|
208
|
+
overlayOptions: {
|
|
209
|
+
width: "90%",
|
|
210
|
+
minWidth: 80,
|
|
211
|
+
maxHeight: "92%",
|
|
212
|
+
anchor: "center",
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
);
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// ── before_agent_start: inject GSD contract into true system prompt ─────
|
|
220
|
+
pi.on("before_agent_start", async (event, ctx: ExtensionContext) => {
|
|
221
|
+
if (!existsSync(join(process.cwd(), ".gsd"))) return;
|
|
222
|
+
|
|
223
|
+
const systemContent = loadPrompt("system");
|
|
224
|
+
const loadedPreferences = loadEffectiveGSDPreferences();
|
|
225
|
+
let preferenceBlock = "";
|
|
226
|
+
if (loadedPreferences) {
|
|
227
|
+
const cwd = process.cwd();
|
|
228
|
+
const report = resolveAllSkillReferences(loadedPreferences.preferences, cwd);
|
|
229
|
+
preferenceBlock = `\n\n${renderPreferencesForSystemPrompt(loadedPreferences.preferences, report.resolutions)}`;
|
|
230
|
+
|
|
231
|
+
// Emit warnings for unresolved skill references
|
|
232
|
+
if (report.warnings.length > 0) {
|
|
233
|
+
ctx.ui.notify(
|
|
234
|
+
`GSD skill preferences: ${report.warnings.length} unresolved skill${report.warnings.length === 1 ? "" : "s"}: ${report.warnings.join(", ")}`,
|
|
235
|
+
"warning",
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Detect skills installed during this auto-mode session
|
|
241
|
+
let newSkillsBlock = "";
|
|
242
|
+
if (hasSkillSnapshot()) {
|
|
243
|
+
const newSkills = detectNewSkills();
|
|
244
|
+
if (newSkills.length > 0) {
|
|
245
|
+
newSkillsBlock = formatSkillsXml(newSkills);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const injection = await buildGuidedExecuteContextInjection(event.prompt, process.cwd());
|
|
250
|
+
|
|
251
|
+
// Worktree context — override the static CWD in the system prompt
|
|
252
|
+
let worktreeBlock = "";
|
|
253
|
+
const worktreeName = getActiveWorktreeName();
|
|
254
|
+
const worktreeMainCwd = getWorktreeOriginalCwd();
|
|
255
|
+
if (worktreeName && worktreeMainCwd) {
|
|
256
|
+
worktreeBlock = [
|
|
257
|
+
"",
|
|
258
|
+
"",
|
|
259
|
+
"[WORKTREE CONTEXT — OVERRIDES CURRENT WORKING DIRECTORY ABOVE]",
|
|
260
|
+
`IMPORTANT: Ignore the "Current working directory" shown earlier in this prompt.`,
|
|
261
|
+
`The actual current working directory is: ${process.cwd()}`,
|
|
262
|
+
"",
|
|
263
|
+
`You are working inside a GSD worktree.`,
|
|
264
|
+
`- Worktree name: ${worktreeName}`,
|
|
265
|
+
`- Worktree path (this is the real cwd): ${process.cwd()}`,
|
|
266
|
+
`- Main project: ${worktreeMainCwd}`,
|
|
267
|
+
`- Branch: worktree/${worktreeName}`,
|
|
268
|
+
"",
|
|
269
|
+
"All file operations, bash commands, and GSD state resolve against the worktree path above.",
|
|
270
|
+
"Use /worktree merge to merge changes back. Use /worktree return to switch back to the main tree.",
|
|
271
|
+
].join("\n");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
systemPrompt: `${event.systemPrompt}\n\n[SYSTEM CONTEXT — GSD]\n\n${systemContent}${preferenceBlock}${newSkillsBlock}${worktreeBlock}`,
|
|
276
|
+
...(injection
|
|
277
|
+
? {
|
|
278
|
+
message: {
|
|
279
|
+
customType: "gsd-guided-context",
|
|
280
|
+
content: injection,
|
|
281
|
+
display: false,
|
|
282
|
+
},
|
|
283
|
+
}
|
|
284
|
+
: {}),
|
|
285
|
+
};
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// ── agent_end: auto-mode advancement or auto-start after discuss ───────────
|
|
289
|
+
pi.on("agent_end", async (event, ctx: ExtensionContext) => {
|
|
290
|
+
// If discuss phase just finished, start auto-mode
|
|
291
|
+
if (checkAutoStartAfterDiscuss()) return;
|
|
292
|
+
|
|
293
|
+
// If auto-mode is already running, advance to next unit
|
|
294
|
+
if (!isAutoActive()) return;
|
|
295
|
+
|
|
296
|
+
// If the agent was aborted (user pressed Escape), pause auto-mode
|
|
297
|
+
// instead of advancing. This preserves the conversation so the user
|
|
298
|
+
// can inspect what happened, interact with the agent, or resume.
|
|
299
|
+
const lastMsg = event.messages[event.messages.length - 1];
|
|
300
|
+
if (lastMsg && "stopReason" in lastMsg && lastMsg.stopReason === "aborted") {
|
|
301
|
+
await pauseAuto(ctx, pi);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
await handleAgentEnd(ctx, pi);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// ── session_before_compact ────────────────────────────────────────────────
|
|
309
|
+
pi.on("session_before_compact", async (_event, _ctx: ExtensionContext) => {
|
|
310
|
+
// Block compaction during auto-mode — each unit is a fresh session
|
|
311
|
+
// Also block during paused state — context is valuable for the user
|
|
312
|
+
if (isAutoActive() || isAutoPaused()) {
|
|
313
|
+
return { cancel: true };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const basePath = process.cwd();
|
|
317
|
+
const state = await deriveState(basePath);
|
|
318
|
+
|
|
319
|
+
// Only save continue.md if we're actively executing a task
|
|
320
|
+
if (!state.activeMilestone || !state.activeSlice || !state.activeTask) return;
|
|
321
|
+
if (state.phase !== "executing") return;
|
|
322
|
+
|
|
323
|
+
const sDir = resolveSlicePath(basePath, state.activeMilestone.id, state.activeSlice.id);
|
|
324
|
+
if (!sDir) return;
|
|
325
|
+
|
|
326
|
+
// Check for existing continue file (new naming or legacy)
|
|
327
|
+
const existingFile = resolveSliceFile(basePath, state.activeMilestone.id, state.activeSlice.id, "CONTINUE");
|
|
328
|
+
if (existingFile && await loadFile(existingFile)) return;
|
|
329
|
+
const legacyContinue = join(sDir, "continue.md");
|
|
330
|
+
if (await loadFile(legacyContinue)) return;
|
|
331
|
+
|
|
332
|
+
const continuePath = join(sDir, buildSliceFileName(state.activeSlice.id, "CONTINUE"));
|
|
333
|
+
|
|
334
|
+
const continueData = {
|
|
335
|
+
frontmatter: {
|
|
336
|
+
milestone: state.activeMilestone.id,
|
|
337
|
+
slice: state.activeSlice.id,
|
|
338
|
+
task: state.activeTask.id,
|
|
339
|
+
step: 0,
|
|
340
|
+
totalSteps: 0,
|
|
341
|
+
status: "compacted" as const,
|
|
342
|
+
savedAt: new Date().toISOString(),
|
|
343
|
+
},
|
|
344
|
+
completedWork: `Task ${state.activeTask.id} (${state.activeTask.title}) was in progress when compaction occurred.`,
|
|
345
|
+
remainingWork: "Check the task plan for remaining steps.",
|
|
346
|
+
decisions: "Check task summary files for prior decisions.",
|
|
347
|
+
context: "Session was auto-compacted by Pi. Resume with /gsd.",
|
|
348
|
+
nextAction: `Resume task ${state.activeTask.id}: ${state.activeTask.title}.`,
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
await saveFile(continuePath, formatContinue(continueData));
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// ── session_shutdown: save activity log on Ctrl+C / SIGTERM ─────────────
|
|
355
|
+
pi.on("session_shutdown", async (_event, ctx: ExtensionContext) => {
|
|
356
|
+
if (!isAutoActive() && !isAutoPaused()) return;
|
|
357
|
+
|
|
358
|
+
// Save the current session — the lock file stays on disk
|
|
359
|
+
// so the next /gsd auto knows it was interrupted
|
|
360
|
+
const dash = getAutoDashboardData();
|
|
361
|
+
if (dash.currentUnit) {
|
|
362
|
+
saveActivityLog(ctx, dash.basePath, dash.currentUnit.type, dash.currentUnit.id);
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function buildGuidedExecuteContextInjection(prompt: string, basePath: string): Promise<string | null> {
|
|
368
|
+
const executeMatch = prompt.match(/Execute the next task:\s+(T\d+)\s+\("([^"]+)"\)\s+in slice\s+(S\d+)\s+of milestone\s+(M\d+)/i);
|
|
369
|
+
if (executeMatch) {
|
|
370
|
+
const [, taskId, taskTitle, sliceId, milestoneId] = executeMatch;
|
|
371
|
+
return buildTaskExecutionContextInjection(basePath, milestoneId, sliceId, taskId, taskTitle);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const resumeMatch = prompt.match(/Resume interrupted work\.[\s\S]*?slice\s+(S\d+)\s+of milestone\s+(M\d+)/i);
|
|
375
|
+
if (resumeMatch) {
|
|
376
|
+
const [, sliceId, milestoneId] = resumeMatch;
|
|
377
|
+
const state = await deriveState(basePath);
|
|
378
|
+
if (
|
|
379
|
+
state.activeMilestone?.id === milestoneId &&
|
|
380
|
+
state.activeSlice?.id === sliceId &&
|
|
381
|
+
state.activeTask
|
|
382
|
+
) {
|
|
383
|
+
return buildTaskExecutionContextInjection(
|
|
384
|
+
basePath,
|
|
385
|
+
milestoneId,
|
|
386
|
+
sliceId,
|
|
387
|
+
state.activeTask.id,
|
|
388
|
+
state.activeTask.title,
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function buildTaskExecutionContextInjection(
|
|
397
|
+
basePath: string,
|
|
398
|
+
milestoneId: string,
|
|
399
|
+
sliceId: string,
|
|
400
|
+
taskId: string,
|
|
401
|
+
taskTitle: string,
|
|
402
|
+
): Promise<string> {
|
|
403
|
+
const taskPlanPath = resolveTaskFile(basePath, milestoneId, sliceId, taskId, "PLAN");
|
|
404
|
+
const taskPlanRelPath = relTaskFile(basePath, milestoneId, sliceId, taskId, "PLAN");
|
|
405
|
+
const taskPlanContent = taskPlanPath ? await loadFile(taskPlanPath) : null;
|
|
406
|
+
const taskPlanInline = taskPlanContent
|
|
407
|
+
? [
|
|
408
|
+
"## Inlined Task Plan (authoritative local execution contract)",
|
|
409
|
+
`Source: \`${taskPlanRelPath}\``,
|
|
410
|
+
"",
|
|
411
|
+
taskPlanContent.trim(),
|
|
412
|
+
].join("\n")
|
|
413
|
+
: [
|
|
414
|
+
"## Inlined Task Plan (authoritative local execution contract)",
|
|
415
|
+
`Task plan not found at dispatch time. Read \`${taskPlanRelPath}\` before executing.`,
|
|
416
|
+
].join("\n");
|
|
417
|
+
|
|
418
|
+
const slicePlanPath = resolveSliceFile(basePath, milestoneId, sliceId, "PLAN");
|
|
419
|
+
const slicePlanRelPath = relSliceFile(basePath, milestoneId, sliceId, "PLAN");
|
|
420
|
+
const slicePlanContent = slicePlanPath ? await loadFile(slicePlanPath) : null;
|
|
421
|
+
const slicePlanExcerpt = extractSliceExecutionExcerpt(slicePlanContent, slicePlanRelPath);
|
|
422
|
+
|
|
423
|
+
const priorTaskLines = await buildCarryForwardLines(basePath, milestoneId, sliceId, taskId);
|
|
424
|
+
const resumeSection = await buildResumeSection(basePath, milestoneId, sliceId);
|
|
425
|
+
|
|
426
|
+
return [
|
|
427
|
+
"[GSD Guided Execute Context]",
|
|
428
|
+
"Use this injected context as startup context for guided task execution. Treat the inlined task plan as the authoritative local execution contract. Use source artifacts to verify details and run checks.",
|
|
429
|
+
"",
|
|
430
|
+
resumeSection,
|
|
431
|
+
"",
|
|
432
|
+
"## Carry-Forward Context",
|
|
433
|
+
...priorTaskLines,
|
|
434
|
+
"",
|
|
435
|
+
taskPlanInline,
|
|
436
|
+
"",
|
|
437
|
+
slicePlanExcerpt,
|
|
438
|
+
"",
|
|
439
|
+
"## Backing Source Artifacts",
|
|
440
|
+
`- Slice plan: \`${slicePlanRelPath}\``,
|
|
441
|
+
`- Task plan source: \`${taskPlanRelPath}\``,
|
|
442
|
+
].join("\n");
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async function buildCarryForwardLines(
|
|
446
|
+
basePath: string,
|
|
447
|
+
milestoneId: string,
|
|
448
|
+
sliceId: string,
|
|
449
|
+
taskId: string,
|
|
450
|
+
): Promise<string[]> {
|
|
451
|
+
const tDir = resolveTasksDir(basePath, milestoneId, sliceId);
|
|
452
|
+
if (!tDir) return ["- No prior task summaries in this slice."];
|
|
453
|
+
|
|
454
|
+
const currentNum = parseInt(taskId.replace(/^T/, ""), 10);
|
|
455
|
+
const sRel = relSlicePath(basePath, milestoneId, sliceId);
|
|
456
|
+
const summaryFiles = resolveTaskFiles(tDir, "SUMMARY")
|
|
457
|
+
.filter((file) => parseInt(file.replace(/^T/, ""), 10) < currentNum)
|
|
458
|
+
.sort();
|
|
459
|
+
|
|
460
|
+
if (summaryFiles.length === 0) return ["- No prior task summaries in this slice."];
|
|
461
|
+
|
|
462
|
+
const lines = await Promise.all(summaryFiles.map(async (file) => {
|
|
463
|
+
const absPath = join(tDir, file);
|
|
464
|
+
const content = await loadFile(absPath);
|
|
465
|
+
const relPath = `${sRel}/tasks/${file}`;
|
|
466
|
+
if (!content) return `- \`${relPath}\``;
|
|
467
|
+
|
|
468
|
+
const summary = parseSummary(content);
|
|
469
|
+
const provided = summary.frontmatter.provides.slice(0, 2).join("; ");
|
|
470
|
+
const decisions = summary.frontmatter.key_decisions.slice(0, 2).join("; ");
|
|
471
|
+
const patterns = summary.frontmatter.patterns_established.slice(0, 2).join("; ");
|
|
472
|
+
const diagnostics = extractMarkdownSection(content, "Diagnostics");
|
|
473
|
+
|
|
474
|
+
const parts = [summary.title || relPath];
|
|
475
|
+
if (summary.oneLiner) parts.push(summary.oneLiner);
|
|
476
|
+
if (provided) parts.push(`provides: ${provided}`);
|
|
477
|
+
if (decisions) parts.push(`decisions: ${decisions}`);
|
|
478
|
+
if (patterns) parts.push(`patterns: ${patterns}`);
|
|
479
|
+
if (diagnostics) parts.push(`diagnostics: ${oneLine(diagnostics)}`);
|
|
480
|
+
|
|
481
|
+
return `- \`${relPath}\` — ${parts.join(" | ")}`;
|
|
482
|
+
}));
|
|
483
|
+
|
|
484
|
+
return lines;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async function buildResumeSection(basePath: string, milestoneId: string, sliceId: string): Promise<string> {
|
|
488
|
+
const continueFile = resolveSliceFile(basePath, milestoneId, sliceId, "CONTINUE");
|
|
489
|
+
const legacyDir = resolveSlicePath(basePath, milestoneId, sliceId);
|
|
490
|
+
const legacyPath = legacyDir ? join(legacyDir, "continue.md") : null;
|
|
491
|
+
const continueContent = continueFile ? await loadFile(continueFile) : null;
|
|
492
|
+
const legacyContent = !continueContent && legacyPath ? await loadFile(legacyPath) : null;
|
|
493
|
+
const resolvedContent = continueContent ?? legacyContent;
|
|
494
|
+
const resolvedRelPath = continueContent
|
|
495
|
+
? relSliceFile(basePath, milestoneId, sliceId, "CONTINUE")
|
|
496
|
+
: (legacyPath ? `${relSlicePath(basePath, milestoneId, sliceId)}/continue.md` : null);
|
|
497
|
+
|
|
498
|
+
if (!resolvedContent || !resolvedRelPath) {
|
|
499
|
+
return ["## Resume State", "- No continue file present. Start from the top of the task plan."].join("\n");
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const cont = parseContinue(resolvedContent);
|
|
503
|
+
const lines = [
|
|
504
|
+
"## Resume State",
|
|
505
|
+
`Source: \`${resolvedRelPath}\``,
|
|
506
|
+
`- Status: ${cont.frontmatter.status || "in_progress"}`,
|
|
507
|
+
];
|
|
508
|
+
|
|
509
|
+
if (cont.frontmatter.step && cont.frontmatter.totalSteps) {
|
|
510
|
+
lines.push(`- Progress: step ${cont.frontmatter.step} of ${cont.frontmatter.totalSteps}`);
|
|
511
|
+
}
|
|
512
|
+
if (cont.completedWork) lines.push(`- Completed: ${oneLine(cont.completedWork)}`);
|
|
513
|
+
if (cont.remainingWork) lines.push(`- Remaining: ${oneLine(cont.remainingWork)}`);
|
|
514
|
+
if (cont.decisions) lines.push(`- Decisions: ${oneLine(cont.decisions)}`);
|
|
515
|
+
if (cont.nextAction) lines.push(`- Next action: ${oneLine(cont.nextAction)}`);
|
|
516
|
+
|
|
517
|
+
return lines.join("\n");
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function extractSliceExecutionExcerpt(content: string | null, relPath: string): string {
|
|
521
|
+
if (!content) {
|
|
522
|
+
return [
|
|
523
|
+
"## Slice Plan Excerpt",
|
|
524
|
+
`Slice plan not found at dispatch time. Read \`${relPath}\` before running slice-level verification.`,
|
|
525
|
+
].join("\n");
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const lines = content.split("\n");
|
|
529
|
+
const goalLine = lines.find((line) => line.startsWith("**Goal:**"))?.trim();
|
|
530
|
+
const demoLine = lines.find((line) => line.startsWith("**Demo:**"))?.trim();
|
|
531
|
+
const verification = extractMarkdownSection(content, "Verification");
|
|
532
|
+
const observability = extractMarkdownSection(content, "Observability / Diagnostics");
|
|
533
|
+
|
|
534
|
+
const parts = ["## Slice Plan Excerpt", `Source: \`${relPath}\``];
|
|
535
|
+
if (goalLine) parts.push(goalLine);
|
|
536
|
+
if (demoLine) parts.push(demoLine);
|
|
537
|
+
if (verification) parts.push("", "### Slice Verification", verification.trim());
|
|
538
|
+
if (observability) parts.push("", "### Slice Observability / Diagnostics", observability.trim());
|
|
539
|
+
return parts.join("\n");
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function extractMarkdownSection(content: string, heading: string): string | null {
|
|
543
|
+
const match = new RegExp(`^## ${escapeRegExp(heading)}\\s*$`, "m").exec(content);
|
|
544
|
+
if (!match) return null;
|
|
545
|
+
const start = match.index + match[0].length;
|
|
546
|
+
const rest = content.slice(start);
|
|
547
|
+
const nextHeading = rest.match(/^##\s+/m);
|
|
548
|
+
const end = nextHeading?.index ?? rest.length;
|
|
549
|
+
return rest.slice(0, end).trim();
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function escapeRegExp(value: string): string {
|
|
553
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function oneLine(text: string): string {
|
|
557
|
+
return text.replace(/\s+/g, " ").trim();
|
|
558
|
+
}
|