gsd-pi 2.29.0-dev.7612840 → 2.29.0-dev.77f06e2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -17
- package/dist/resources/extensions/bg-shell/process-manager.ts +13 -0
- package/dist/resources/extensions/gsd/auto-dashboard.ts +186 -65
- package/dist/resources/extensions/gsd/auto-post-unit.ts +6 -3
- package/dist/resources/extensions/gsd/auto-recovery.ts +16 -22
- package/dist/resources/extensions/gsd/auto-worktree-sync.ts +7 -6
- package/dist/resources/extensions/gsd/commands-handlers.ts +20 -1
- package/dist/resources/extensions/gsd/commands-logs.ts +13 -14
- package/dist/resources/extensions/gsd/commands-prefs-wizard.ts +44 -14
- package/dist/resources/extensions/gsd/commands-workflow-templates.ts +544 -0
- package/dist/resources/extensions/gsd/commands.ts +53 -21
- package/dist/resources/extensions/gsd/json-persistence.ts +16 -1
- package/dist/resources/extensions/gsd/prompts/workflow-start.md +28 -0
- package/dist/resources/extensions/gsd/queue-order.ts +10 -11
- package/dist/resources/extensions/gsd/session-status-io.ts +23 -41
- package/dist/resources/extensions/gsd/tests/extension-selector-separator.test.ts +60 -38
- package/dist/resources/extensions/gsd/tests/workflow-templates.test.ts +173 -0
- package/dist/resources/extensions/gsd/workflow-templates/bugfix.md +87 -0
- package/dist/resources/extensions/gsd/workflow-templates/dep-upgrade.md +74 -0
- package/dist/resources/extensions/gsd/workflow-templates/full-project.md +41 -0
- package/dist/resources/extensions/gsd/workflow-templates/hotfix.md +45 -0
- package/dist/resources/extensions/gsd/workflow-templates/refactor.md +83 -0
- package/dist/resources/extensions/gsd/workflow-templates/registry.json +85 -0
- package/dist/resources/extensions/gsd/workflow-templates/security-audit.md +73 -0
- package/dist/resources/extensions/gsd/workflow-templates/small-feature.md +81 -0
- package/dist/resources/extensions/gsd/workflow-templates/spike.md +69 -0
- package/dist/resources/extensions/gsd/workflow-templates.ts +241 -0
- package/dist/resources/extensions/mcp-client/index.ts +459 -0
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +13 -0
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +13 -0
- package/src/resources/extensions/bg-shell/process-manager.ts +13 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +186 -65
- package/src/resources/extensions/gsd/auto-post-unit.ts +6 -3
- package/src/resources/extensions/gsd/auto-recovery.ts +16 -22
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +7 -6
- package/src/resources/extensions/gsd/commands-handlers.ts +20 -1
- package/src/resources/extensions/gsd/commands-logs.ts +13 -14
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +44 -14
- package/src/resources/extensions/gsd/commands-workflow-templates.ts +544 -0
- package/src/resources/extensions/gsd/commands.ts +53 -21
- package/src/resources/extensions/gsd/json-persistence.ts +16 -1
- package/src/resources/extensions/gsd/prompts/workflow-start.md +28 -0
- package/src/resources/extensions/gsd/queue-order.ts +10 -11
- package/src/resources/extensions/gsd/session-status-io.ts +23 -41
- package/src/resources/extensions/gsd/tests/extension-selector-separator.test.ts +60 -38
- package/src/resources/extensions/gsd/tests/workflow-templates.test.ts +173 -0
- package/src/resources/extensions/gsd/workflow-templates/bugfix.md +87 -0
- package/src/resources/extensions/gsd/workflow-templates/dep-upgrade.md +74 -0
- package/src/resources/extensions/gsd/workflow-templates/full-project.md +41 -0
- package/src/resources/extensions/gsd/workflow-templates/hotfix.md +45 -0
- package/src/resources/extensions/gsd/workflow-templates/refactor.md +83 -0
- package/src/resources/extensions/gsd/workflow-templates/registry.json +85 -0
- package/src/resources/extensions/gsd/workflow-templates/security-audit.md +73 -0
- package/src/resources/extensions/gsd/workflow-templates/small-feature.md +81 -0
- package/src/resources/extensions/gsd/workflow-templates/spike.md +69 -0
- package/src/resources/extensions/gsd/workflow-templates.ts +241 -0
- package/src/resources/extensions/mcp-client/index.ts +459 -0
- package/dist/resources/extensions/mcporter/index.ts +0 -525
- package/src/resources/extensions/mcporter/index.ts +0 -525
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Workflow Template: {{templateName}}
|
|
2
|
+
|
|
3
|
+
You are executing a **{{templateName}}** workflow (template: `{{templateId}}`).
|
|
4
|
+
|
|
5
|
+
## Context
|
|
6
|
+
|
|
7
|
+
- **Description:** {{description}}
|
|
8
|
+
- **Issue reference:** {{issueRef}}
|
|
9
|
+
- **Date:** {{date}}
|
|
10
|
+
- **Branch:** {{branch}}
|
|
11
|
+
- **Artifact directory:** {{artifactDir}}
|
|
12
|
+
- **Phases:** {{phases}}
|
|
13
|
+
- **Complexity:** {{complexity}}
|
|
14
|
+
|
|
15
|
+
## Workflow Definition
|
|
16
|
+
|
|
17
|
+
Follow the workflow defined below. Execute each phase in order, completing one before moving to the next. At each phase gate, confirm with the user before proceeding.
|
|
18
|
+
|
|
19
|
+
{{workflowContent}}
|
|
20
|
+
|
|
21
|
+
## Execution Rules
|
|
22
|
+
|
|
23
|
+
1. **Follow the phases in order.** Do not skip phases unless the workflow explicitly allows it.
|
|
24
|
+
2. **Artifact discipline.** If an artifact directory is specified, write all planning/summary documents there.
|
|
25
|
+
3. **Atomic commits.** Commit working code after each meaningful change. Use conventional commit format: `<type>(<scope>): <description>`.
|
|
26
|
+
4. **Verify before shipping.** Run the project's test suite and build before marking the workflow complete.
|
|
27
|
+
5. **Gate between phases.** After each phase, summarize what was done and ask the user to confirm before moving to the next phase.
|
|
28
|
+
6. **Stay focused.** This is a {{complexity}}-complexity workflow. Match your ceremony level to the task — don't over-engineer or under-deliver.
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
* survives branch switches and is shared across sessions.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
13
12
|
import { join } from "node:path";
|
|
14
13
|
import { gsdRoot } from "./paths.js";
|
|
15
14
|
import { milestoneIdSort } from "./milestone-ids.js";
|
|
15
|
+
import { loadJsonFileOrNull, saveJsonFile } from "./json-persistence.js";
|
|
16
16
|
|
|
17
17
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
18
18
|
|
|
@@ -45,6 +45,12 @@ function queueOrderPath(basePath: string): string {
|
|
|
45
45
|
return join(gsdRoot(basePath), "QUEUE-ORDER.json");
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
// ─── Type Guards ─────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
function isQueueOrderFile(data: unknown): data is QueueOrderFile {
|
|
51
|
+
return data !== null && typeof data === "object" && "order" in data! && Array.isArray((data as QueueOrderFile).order);
|
|
52
|
+
}
|
|
53
|
+
|
|
48
54
|
// ─── Read / Write ────────────────────────────────────────────────────────────
|
|
49
55
|
|
|
50
56
|
/**
|
|
@@ -52,15 +58,8 @@ function queueOrderPath(basePath: string): string {
|
|
|
52
58
|
* the file is corrupt/unreadable.
|
|
53
59
|
*/
|
|
54
60
|
export function loadQueueOrder(basePath: string): string[] | null {
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
try {
|
|
58
|
-
const data: QueueOrderFile = JSON.parse(readFileSync(p, "utf-8"));
|
|
59
|
-
if (!Array.isArray(data.order)) return null;
|
|
60
|
-
return data.order;
|
|
61
|
-
} catch {
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
61
|
+
const data = loadJsonFileOrNull(queueOrderPath(basePath), isQueueOrderFile);
|
|
62
|
+
return data?.order ?? null;
|
|
64
63
|
}
|
|
65
64
|
|
|
66
65
|
/**
|
|
@@ -71,7 +70,7 @@ export function saveQueueOrder(basePath: string, order: string[]): void {
|
|
|
71
70
|
order,
|
|
72
71
|
updatedAt: new Date().toISOString(),
|
|
73
72
|
};
|
|
74
|
-
|
|
73
|
+
saveJsonFile(queueOrderPath(basePath), data);
|
|
75
74
|
}
|
|
76
75
|
|
|
77
76
|
// ─── Sorting ─────────────────────────────────────────────────────────────────
|
|
@@ -11,9 +11,6 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import {
|
|
14
|
-
writeFileSync,
|
|
15
|
-
readFileSync,
|
|
16
|
-
renameSync,
|
|
17
14
|
unlinkSync,
|
|
18
15
|
readdirSync,
|
|
19
16
|
mkdirSync,
|
|
@@ -21,6 +18,7 @@ import {
|
|
|
21
18
|
} from "node:fs";
|
|
22
19
|
import { join } from "node:path";
|
|
23
20
|
import { gsdRoot } from "./paths.js";
|
|
21
|
+
import { loadJsonFileOrNull, writeJsonFileAtomic } from "./json-persistence.js";
|
|
24
22
|
|
|
25
23
|
// ─── Types ─────────────────────────────────────────────────────────────────
|
|
26
24
|
|
|
@@ -49,9 +47,16 @@ export interface SignalMessage {
|
|
|
49
47
|
const PARALLEL_DIR = "parallel";
|
|
50
48
|
const STATUS_SUFFIX = ".status.json";
|
|
51
49
|
const SIGNAL_SUFFIX = ".signal.json";
|
|
52
|
-
const TMP_SUFFIX = ".tmp";
|
|
53
50
|
const DEFAULT_STALE_TIMEOUT_MS = 30_000;
|
|
54
51
|
|
|
52
|
+
function isSessionStatus(data: unknown): data is SessionStatus {
|
|
53
|
+
return data !== null && typeof data === "object" && "milestoneId" in data && "pid" in data;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isSignalMessage(data: unknown): data is SignalMessage {
|
|
57
|
+
return data !== null && typeof data === "object" && "signal" in data && "sentAt" in data;
|
|
58
|
+
}
|
|
59
|
+
|
|
55
60
|
// ─── Helpers ───────────────────────────────────────────────────────────────
|
|
56
61
|
|
|
57
62
|
function parallelDir(basePath: string): string {
|
|
@@ -86,25 +91,13 @@ function isPidAlive(pid: number): boolean {
|
|
|
86
91
|
|
|
87
92
|
/** Write session status atomically (write to .tmp, then rename). */
|
|
88
93
|
export function writeSessionStatus(basePath: string, status: SessionStatus): void {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const dest = statusPath(basePath, status.milestoneId);
|
|
92
|
-
const tmp = dest + TMP_SUFFIX;
|
|
93
|
-
writeFileSync(tmp, JSON.stringify(status, null, 2), "utf-8");
|
|
94
|
-
renameSync(tmp, dest);
|
|
95
|
-
} catch { /* non-fatal */ }
|
|
94
|
+
ensureParallelDir(basePath);
|
|
95
|
+
writeJsonFileAtomic(statusPath(basePath, status.milestoneId), status);
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
/** Read a specific milestone's session status. */
|
|
99
99
|
export function readSessionStatus(basePath: string, milestoneId: string): SessionStatus | null {
|
|
100
|
-
|
|
101
|
-
const p = statusPath(basePath, milestoneId);
|
|
102
|
-
if (!existsSync(p)) return null;
|
|
103
|
-
const raw = readFileSync(p, "utf-8");
|
|
104
|
-
return JSON.parse(raw) as SessionStatus;
|
|
105
|
-
} catch {
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
100
|
+
return loadJsonFileOrNull(statusPath(basePath, milestoneId), isSessionStatus);
|
|
108
101
|
}
|
|
109
102
|
|
|
110
103
|
/** Read all session status files from .gsd/parallel/. */
|
|
@@ -114,13 +107,10 @@ export function readAllSessionStatuses(basePath: string): SessionStatus[] {
|
|
|
114
107
|
|
|
115
108
|
const results: SessionStatus[] = [];
|
|
116
109
|
try {
|
|
117
|
-
const
|
|
118
|
-
for (const entry of entries) {
|
|
110
|
+
for (const entry of readdirSync(dir)) {
|
|
119
111
|
if (!entry.endsWith(STATUS_SUFFIX)) continue;
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
results.push(JSON.parse(raw) as SessionStatus);
|
|
123
|
-
} catch { /* skip corrupt files */ }
|
|
112
|
+
const status = loadJsonFileOrNull(join(dir, entry), isSessionStatus);
|
|
113
|
+
if (status) results.push(status);
|
|
124
114
|
}
|
|
125
115
|
} catch { /* non-fatal */ }
|
|
126
116
|
return results;
|
|
@@ -138,27 +128,19 @@ export function removeSessionStatus(basePath: string, milestoneId: string): void
|
|
|
138
128
|
|
|
139
129
|
/** Write a signal file for a worker to consume. */
|
|
140
130
|
export function sendSignal(basePath: string, milestoneId: string, signal: SessionSignal): void {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const tmp = dest + TMP_SUFFIX;
|
|
145
|
-
const msg: SignalMessage = { signal, sentAt: Date.now(), from: "coordinator" };
|
|
146
|
-
writeFileSync(tmp, JSON.stringify(msg, null, 2), "utf-8");
|
|
147
|
-
renameSync(tmp, dest);
|
|
148
|
-
} catch { /* non-fatal */ }
|
|
131
|
+
ensureParallelDir(basePath);
|
|
132
|
+
const msg: SignalMessage = { signal, sentAt: Date.now(), from: "coordinator" };
|
|
133
|
+
writeJsonFileAtomic(signalPath(basePath, milestoneId), msg);
|
|
149
134
|
}
|
|
150
135
|
|
|
151
136
|
/** Read and delete a signal file (atomic consume). Returns null if no signal pending. */
|
|
152
137
|
export function consumeSignal(basePath: string, milestoneId: string): SignalMessage | null {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
unlinkSync(p);
|
|
158
|
-
return JSON.parse(raw) as SignalMessage;
|
|
159
|
-
} catch {
|
|
160
|
-
return null;
|
|
138
|
+
const p = signalPath(basePath, milestoneId);
|
|
139
|
+
const msg = loadJsonFileOrNull(p, isSignalMessage);
|
|
140
|
+
if (msg) {
|
|
141
|
+
try { unlinkSync(p); } catch { /* non-fatal */ }
|
|
161
142
|
}
|
|
143
|
+
return msg;
|
|
162
144
|
}
|
|
163
145
|
|
|
164
146
|
// ─── Stale Detection ───────────────────────────────────────────────────────
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
// Tests for the SEPARATOR_PREFIX convention used by ExtensionSelectorComponent
|
|
1
|
+
// Tests for the SEPARATOR_PREFIX convention used by ExtensionSelectorComponent
|
|
2
|
+
// and the two-step provider→model picker in configureModels.
|
|
3
|
+
//
|
|
2
4
|
// We cannot import the component directly in node:test because its transitive
|
|
3
5
|
// dependency (countdown-timer.ts) uses TypeScript parameter properties which
|
|
4
6
|
// are unsupported under --experimental-strip-types. Instead we duplicate the
|
|
@@ -69,16 +71,17 @@ describe("separator detection", () => {
|
|
|
69
71
|
});
|
|
70
72
|
});
|
|
71
73
|
|
|
72
|
-
describe("model
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
74
|
+
describe("two-step provider→model picker", () => {
|
|
75
|
+
// Simulate the grouping logic from configureModels
|
|
76
|
+
const availableModels = [
|
|
77
|
+
{ id: "claude-opus-4-6", provider: "anthropic" },
|
|
78
|
+
{ id: "gpt-4o", provider: "openai" },
|
|
79
|
+
{ id: "claude-sonnet-4-5", provider: "anthropic" },
|
|
80
|
+
{ id: "o3-mini", provider: "openai" },
|
|
81
|
+
{ id: "claude-haiku-4-5", provider: "anthropic" },
|
|
82
|
+
];
|
|
81
83
|
|
|
84
|
+
function buildProviderGroups() {
|
|
82
85
|
const byProvider = new Map<string, typeof availableModels>();
|
|
83
86
|
for (const m of availableModels) {
|
|
84
87
|
let group = byProvider.get(m.provider);
|
|
@@ -89,34 +92,53 @@ describe("model grouping", () => {
|
|
|
89
92
|
group.push(m);
|
|
90
93
|
}
|
|
91
94
|
const providers = Array.from(byProvider.keys()).sort((a, b) => a.localeCompare(b));
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
for (const provider of providers) {
|
|
95
|
-
const group = byProvider.get(provider)!;
|
|
96
|
-
modelOptions.push(`${SEPARATOR_PREFIX} ${provider} (${group.length}) ${SEPARATOR_PREFIX}`);
|
|
97
|
-
for (const m of group) {
|
|
98
|
-
modelOptions.push(`${m.id} · ${m.provider}`);
|
|
99
|
-
}
|
|
95
|
+
for (const group of byProvider.values()) {
|
|
96
|
+
group.sort((a, b) => a.id.localeCompare(b.id));
|
|
100
97
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
assert.
|
|
115
|
-
assert.
|
|
116
|
-
assert.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
98
|
+
return { byProvider, providers };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
test("provider menu lists providers with model counts", () => {
|
|
102
|
+
const { providers, byProvider } = buildProviderGroups();
|
|
103
|
+
const providerOptions = providers.map(p => {
|
|
104
|
+
const count = byProvider.get(p)!.length;
|
|
105
|
+
return `${p} (${count} models)`;
|
|
106
|
+
});
|
|
107
|
+
providerOptions.push("(keep current)", "(clear)", "(type manually)");
|
|
108
|
+
|
|
109
|
+
assert.strictEqual(providerOptions[0], "anthropic (3 models)");
|
|
110
|
+
assert.strictEqual(providerOptions[1], "openai (2 models)");
|
|
111
|
+
assert.strictEqual(providerOptions[2], "(keep current)");
|
|
112
|
+
assert.strictEqual(providerOptions[3], "(clear)");
|
|
113
|
+
assert.strictEqual(providerOptions[4], "(type manually)");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("model menu for a provider is sorted alphabetically", () => {
|
|
117
|
+
const { byProvider } = buildProviderGroups();
|
|
118
|
+
const anthropicModels = byProvider.get("anthropic")!;
|
|
119
|
+
const modelOptions = anthropicModels.map(m => m.id);
|
|
120
|
+
|
|
121
|
+
assert.strictEqual(modelOptions[0], "claude-haiku-4-5");
|
|
122
|
+
assert.strictEqual(modelOptions[1], "claude-opus-4-6");
|
|
123
|
+
assert.strictEqual(modelOptions[2], "claude-sonnet-4-5");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("provider name is extracted correctly from choice string", () => {
|
|
127
|
+
const choice = "anthropic (3 models)";
|
|
128
|
+
const providerName = choice.replace(/ \(\d+ models?\)$/, "");
|
|
129
|
+
assert.strictEqual(providerName, "anthropic");
|
|
130
|
+
|
|
131
|
+
const singleChoice = "ollama (1 model)";
|
|
132
|
+
const singleProvider = singleChoice.replace(/ \(\d+ models?\)$/, "");
|
|
133
|
+
assert.strictEqual(singleProvider, "ollama");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("openai models are sorted within their group", () => {
|
|
137
|
+
const { byProvider } = buildProviderGroups();
|
|
138
|
+
const openaiModels = byProvider.get("openai")!;
|
|
139
|
+
const modelOptions = openaiModels.map(m => m.id);
|
|
140
|
+
|
|
141
|
+
assert.strictEqual(modelOptions[0], "gpt-4o");
|
|
142
|
+
assert.strictEqual(modelOptions[1], "o3-mini");
|
|
121
143
|
});
|
|
122
144
|
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// GSD Workflow Templates — Unit Tests
|
|
2
|
+
//
|
|
3
|
+
// Tests registry loading, template resolution, auto-detection, and listing.
|
|
4
|
+
|
|
5
|
+
import { createTestContext } from './test-helpers.ts';
|
|
6
|
+
import {
|
|
7
|
+
loadRegistry,
|
|
8
|
+
resolveByName,
|
|
9
|
+
autoDetect,
|
|
10
|
+
listTemplates,
|
|
11
|
+
getTemplateInfo,
|
|
12
|
+
loadWorkflowTemplate,
|
|
13
|
+
} from '../workflow-templates.ts';
|
|
14
|
+
|
|
15
|
+
const { assertEq, assertTrue, assertMatch, report } = createTestContext();
|
|
16
|
+
|
|
17
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
18
|
+
// Registry Loading
|
|
19
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
20
|
+
|
|
21
|
+
console.log('\n── Registry Loading ──');
|
|
22
|
+
|
|
23
|
+
{
|
|
24
|
+
const registry = loadRegistry();
|
|
25
|
+
assertTrue(registry !== null, 'Registry should load');
|
|
26
|
+
assertEq(registry.version, 1, 'Registry version should be 1');
|
|
27
|
+
assertTrue(Object.keys(registry.templates).length >= 8, 'Should have at least 8 templates');
|
|
28
|
+
|
|
29
|
+
// Verify required template keys exist
|
|
30
|
+
const expectedIds = ['full-project', 'bugfix', 'small-feature', 'refactor', 'spike', 'hotfix', 'security-audit', 'dep-upgrade'];
|
|
31
|
+
for (const id of expectedIds) {
|
|
32
|
+
assertTrue(id in registry.templates, `Template "${id}" should exist in registry`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Verify each template has required fields
|
|
36
|
+
for (const [id, entry] of Object.entries(registry.templates)) {
|
|
37
|
+
assertTrue(typeof entry.name === 'string' && entry.name.length > 0, `${id}: name should be non-empty string`);
|
|
38
|
+
assertTrue(typeof entry.description === 'string' && entry.description.length > 0, `${id}: description should be non-empty`);
|
|
39
|
+
assertTrue(typeof entry.file === 'string' && entry.file.endsWith('.md'), `${id}: file should be a .md path`);
|
|
40
|
+
assertTrue(Array.isArray(entry.phases) && entry.phases.length > 0, `${id}: phases should be non-empty array`);
|
|
41
|
+
assertTrue(Array.isArray(entry.triggers) && entry.triggers.length > 0, `${id}: triggers should be non-empty array`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
46
|
+
// Resolve by Name
|
|
47
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
48
|
+
|
|
49
|
+
console.log('\n── Resolve by Name ──');
|
|
50
|
+
|
|
51
|
+
{
|
|
52
|
+
// Exact match
|
|
53
|
+
const bugfix = resolveByName('bugfix');
|
|
54
|
+
assertTrue(bugfix !== null, 'Should resolve "bugfix"');
|
|
55
|
+
assertEq(bugfix!.id, 'bugfix', 'ID should be "bugfix"');
|
|
56
|
+
assertEq(bugfix!.confidence, 'exact', 'Exact name should have exact confidence');
|
|
57
|
+
|
|
58
|
+
// Case-insensitive name match
|
|
59
|
+
const spike = resolveByName('Research Spike');
|
|
60
|
+
assertTrue(spike !== null, 'Should resolve "Research Spike" by name');
|
|
61
|
+
assertEq(spike!.id, 'spike', 'Should resolve to spike');
|
|
62
|
+
|
|
63
|
+
// Alias match
|
|
64
|
+
const bug = resolveByName('bug');
|
|
65
|
+
assertTrue(bug !== null, 'Should resolve "bug" alias');
|
|
66
|
+
assertEq(bug!.id, 'bugfix', 'Alias "bug" should map to bugfix');
|
|
67
|
+
|
|
68
|
+
const feat = resolveByName('feat');
|
|
69
|
+
assertTrue(feat !== null, 'Should resolve "feat" alias');
|
|
70
|
+
assertEq(feat!.id, 'small-feature', 'Alias "feat" should map to small-feature');
|
|
71
|
+
|
|
72
|
+
const deps = resolveByName('deps');
|
|
73
|
+
assertTrue(deps !== null, 'Should resolve "deps" alias');
|
|
74
|
+
assertEq(deps!.id, 'dep-upgrade', 'Alias "deps" should map to dep-upgrade');
|
|
75
|
+
|
|
76
|
+
// No match
|
|
77
|
+
const missing = resolveByName('nonexistent-template');
|
|
78
|
+
assertTrue(missing === null, 'Should return null for unknown template');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
82
|
+
// Auto-Detection
|
|
83
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
84
|
+
|
|
85
|
+
console.log('\n── Auto-Detection ──');
|
|
86
|
+
|
|
87
|
+
{
|
|
88
|
+
// Should detect bugfix from "fix" keyword
|
|
89
|
+
const fixMatches = autoDetect('fix the login button');
|
|
90
|
+
assertTrue(fixMatches.length > 0, 'Should detect matches for "fix the login button"');
|
|
91
|
+
assertTrue(fixMatches.some(m => m.id === 'bugfix'), 'Should include bugfix in matches');
|
|
92
|
+
|
|
93
|
+
// Should detect spike from "research" keyword
|
|
94
|
+
const researchMatches = autoDetect('research authentication libraries');
|
|
95
|
+
assertTrue(researchMatches.length > 0, 'Should detect matches for "research"');
|
|
96
|
+
assertTrue(researchMatches.some(m => m.id === 'spike'), 'Should include spike in matches');
|
|
97
|
+
|
|
98
|
+
// Should detect hotfix from "urgent" keyword
|
|
99
|
+
const urgentMatches = autoDetect('urgent production is down');
|
|
100
|
+
assertTrue(urgentMatches.length > 0, 'Should detect matches for "urgent"');
|
|
101
|
+
assertTrue(urgentMatches.some(m => m.id === 'hotfix'), 'Should include hotfix in matches');
|
|
102
|
+
|
|
103
|
+
// Should detect dep-upgrade from "upgrade" keyword
|
|
104
|
+
const upgradeMatches = autoDetect('upgrade react to v19');
|
|
105
|
+
assertTrue(upgradeMatches.length > 0, 'Should detect matches for "upgrade"');
|
|
106
|
+
assertTrue(upgradeMatches.some(m => m.id === 'dep-upgrade'), 'Should include dep-upgrade in matches');
|
|
107
|
+
|
|
108
|
+
// Multi-word triggers should score higher
|
|
109
|
+
const projectMatches = autoDetect('create a new project from scratch');
|
|
110
|
+
const projectMatch = projectMatches.find(m => m.id === 'full-project');
|
|
111
|
+
assertTrue(projectMatch !== undefined, 'Should detect full-project for "from scratch"');
|
|
112
|
+
|
|
113
|
+
// Empty input should return no matches
|
|
114
|
+
const emptyMatches = autoDetect('');
|
|
115
|
+
assertEq(emptyMatches.length, 0, 'Empty input should return no matches');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
119
|
+
// List Templates
|
|
120
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
121
|
+
|
|
122
|
+
console.log('\n── List Templates ──');
|
|
123
|
+
|
|
124
|
+
{
|
|
125
|
+
const output = listTemplates();
|
|
126
|
+
assertTrue(output.includes('Workflow Templates'), 'Should have header');
|
|
127
|
+
assertTrue(output.includes('bugfix'), 'Should list bugfix');
|
|
128
|
+
assertTrue(output.includes('spike'), 'Should list spike');
|
|
129
|
+
assertTrue(output.includes('hotfix'), 'Should list hotfix');
|
|
130
|
+
assertTrue(output.includes('/gsd start'), 'Should include usage hint');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
134
|
+
// Template Info
|
|
135
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
136
|
+
|
|
137
|
+
console.log('\n── Template Info ──');
|
|
138
|
+
|
|
139
|
+
{
|
|
140
|
+
const info = getTemplateInfo('bugfix');
|
|
141
|
+
assertTrue(info !== null, 'Should return info for bugfix');
|
|
142
|
+
assertTrue(info!.includes('Bug Fix'), 'Should include template name');
|
|
143
|
+
assertTrue(info!.includes('triage'), 'Should include phase names');
|
|
144
|
+
assertTrue(info!.includes('Triggers'), 'Should include triggers section');
|
|
145
|
+
|
|
146
|
+
const missing = getTemplateInfo('nonexistent');
|
|
147
|
+
assertTrue(missing === null, 'Should return null for unknown template');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
151
|
+
// Load Workflow Template Content
|
|
152
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
153
|
+
|
|
154
|
+
console.log('\n── Load Workflow Template ──');
|
|
155
|
+
|
|
156
|
+
{
|
|
157
|
+
const content = loadWorkflowTemplate('bugfix');
|
|
158
|
+
assertTrue(content !== null, 'Should load bugfix template');
|
|
159
|
+
assertTrue(content!.includes('Bugfix Workflow'), 'Should contain workflow title');
|
|
160
|
+
assertTrue(content!.includes('Phase 1: Triage'), 'Should contain triage phase');
|
|
161
|
+
assertTrue(content!.includes('Phase 4: Ship'), 'Should contain ship phase');
|
|
162
|
+
|
|
163
|
+
const hotfixContent = loadWorkflowTemplate('hotfix');
|
|
164
|
+
assertTrue(hotfixContent !== null, 'Should load hotfix template');
|
|
165
|
+
assertTrue(hotfixContent!.includes('Hotfix Workflow'), 'Should contain hotfix title');
|
|
166
|
+
|
|
167
|
+
const missingContent = loadWorkflowTemplate('nonexistent');
|
|
168
|
+
assertTrue(missingContent === null, 'Should return null for unknown template');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
172
|
+
|
|
173
|
+
report();
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Bugfix Workflow
|
|
2
|
+
|
|
3
|
+
<template_meta>
|
|
4
|
+
name: bugfix
|
|
5
|
+
version: 1
|
|
6
|
+
requires_project: false
|
|
7
|
+
artifact_dir: .gsd/workflows/bugfixes/
|
|
8
|
+
</template_meta>
|
|
9
|
+
|
|
10
|
+
<purpose>
|
|
11
|
+
Fix a bug from identification through to PR submission. Designed for issues reported
|
|
12
|
+
via GitHub, user reports, or developer discovery. Emphasizes root cause analysis
|
|
13
|
+
before jumping to fixes.
|
|
14
|
+
</purpose>
|
|
15
|
+
|
|
16
|
+
<phases>
|
|
17
|
+
1. triage — Identify root cause, reproduce the bug
|
|
18
|
+
2. fix — Implement the fix with tests
|
|
19
|
+
3. verify — Run full test suite, check for regressions
|
|
20
|
+
4. ship — Create PR with detailed explanation
|
|
21
|
+
</phases>
|
|
22
|
+
|
|
23
|
+
<process>
|
|
24
|
+
|
|
25
|
+
## Phase 1: Triage
|
|
26
|
+
|
|
27
|
+
**Goal:** Understand the bug before touching any code.
|
|
28
|
+
|
|
29
|
+
1. **Gather context:**
|
|
30
|
+
- If a GitHub issue was referenced, read the issue description, labels, and comments
|
|
31
|
+
- Identify the expected behavior vs actual behavior
|
|
32
|
+
- Note any error messages, stack traces, or reproduction steps provided
|
|
33
|
+
|
|
34
|
+
2. **Reproduce:**
|
|
35
|
+
- Find the minimal reproduction path
|
|
36
|
+
- Identify the affected code paths (files, functions, lines)
|
|
37
|
+
- If the bug is intermittent, note the conditions that trigger it
|
|
38
|
+
|
|
39
|
+
3. **Root cause analysis:**
|
|
40
|
+
- Trace the bug to its root cause (not just the symptom)
|
|
41
|
+
- Identify when the bug was introduced if possible (git blame/log)
|
|
42
|
+
- Assess blast radius: what else could be affected?
|
|
43
|
+
|
|
44
|
+
4. **Produce:** Write a brief `TRIAGE.md` in the artifact directory with:
|
|
45
|
+
- Root cause explanation
|
|
46
|
+
- Reproduction steps
|
|
47
|
+
- Affected files/functions
|
|
48
|
+
- Proposed fix approach
|
|
49
|
+
|
|
50
|
+
5. **Gate:** Present the triage findings and proposed fix to the user for confirmation.
|
|
51
|
+
|
|
52
|
+
## Phase 2: Fix
|
|
53
|
+
|
|
54
|
+
**Goal:** Implement a clean, tested fix.
|
|
55
|
+
|
|
56
|
+
1. **Plan the fix:** Write a brief plan (1-3 tasks max)
|
|
57
|
+
2. **Write the fix:** Implement the code change
|
|
58
|
+
3. **Write tests:** Add or update tests that:
|
|
59
|
+
- Reproduce the original bug (test fails without fix)
|
|
60
|
+
- Verify the fix works
|
|
61
|
+
- Cover edge cases
|
|
62
|
+
4. **Commit:** Atomic commit with message: `fix(<scope>): <description>`
|
|
63
|
+
|
|
64
|
+
## Phase 3: Verify
|
|
65
|
+
|
|
66
|
+
**Goal:** Ensure the fix doesn't break anything else.
|
|
67
|
+
|
|
68
|
+
1. Run the project's full test suite
|
|
69
|
+
2. Run the build (if applicable)
|
|
70
|
+
3. Run the linter (if applicable)
|
|
71
|
+
4. Check for regressions in related functionality
|
|
72
|
+
5. If any failures, fix them before proceeding
|
|
73
|
+
|
|
74
|
+
## Phase 4: Ship
|
|
75
|
+
|
|
76
|
+
**Goal:** Create a well-documented PR.
|
|
77
|
+
|
|
78
|
+
1. Ensure all changes are committed on the workflow branch
|
|
79
|
+
2. Build the PR body:
|
|
80
|
+
- Link to the original issue (if applicable)
|
|
81
|
+
- Explain the root cause
|
|
82
|
+
- Describe the fix approach
|
|
83
|
+
- List the test coverage added
|
|
84
|
+
3. Present the PR details to the user for review
|
|
85
|
+
4. Create the PR via `gh pr create` (with user approval)
|
|
86
|
+
|
|
87
|
+
</process>
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Dependency Upgrade Workflow
|
|
2
|
+
|
|
3
|
+
<template_meta>
|
|
4
|
+
name: dep-upgrade
|
|
5
|
+
version: 1
|
|
6
|
+
requires_project: false
|
|
7
|
+
artifact_dir: .gsd/workflows/upgrades/
|
|
8
|
+
</template_meta>
|
|
9
|
+
|
|
10
|
+
<purpose>
|
|
11
|
+
Upgrade project dependencies safely. Assess breaking changes before upgrading,
|
|
12
|
+
fix issues incrementally, and verify everything works. Handles both single-package
|
|
13
|
+
upgrades and bulk dependency refresh.
|
|
14
|
+
</purpose>
|
|
15
|
+
|
|
16
|
+
<phases>
|
|
17
|
+
1. assess — Analyze what's outdated and what will break
|
|
18
|
+
2. upgrade — Apply upgrades incrementally
|
|
19
|
+
3. fix — Resolve breaking changes
|
|
20
|
+
4. verify — Full test suite and build validation
|
|
21
|
+
</phases>
|
|
22
|
+
|
|
23
|
+
<process>
|
|
24
|
+
|
|
25
|
+
## Phase 1: Assess
|
|
26
|
+
|
|
27
|
+
**Goal:** Know what you're getting into before changing versions.
|
|
28
|
+
|
|
29
|
+
1. **List outdated dependencies:** Run `npm outdated` / equivalent
|
|
30
|
+
2. **For each target upgrade:**
|
|
31
|
+
- Read the changelog / release notes
|
|
32
|
+
- Identify breaking changes
|
|
33
|
+
- Check for known migration guides
|
|
34
|
+
- Assess impact on the codebase (grep for affected APIs)
|
|
35
|
+
3. **Prioritize:** Which upgrades to do now, which to defer
|
|
36
|
+
4. **Produce:** Write `ASSESSMENT.md` with:
|
|
37
|
+
- Dependency list with current → target versions
|
|
38
|
+
- Breaking changes per dependency
|
|
39
|
+
- Upgrade order (dependencies before dependents)
|
|
40
|
+
- Risk assessment
|
|
41
|
+
|
|
42
|
+
5. **Gate:** Review assessment with user. Confirm upgrade scope.
|
|
43
|
+
|
|
44
|
+
## Phase 2: Upgrade
|
|
45
|
+
|
|
46
|
+
**Goal:** Apply version bumps incrementally.
|
|
47
|
+
|
|
48
|
+
1. Upgrade one dependency (or one group of related dependencies) at a time
|
|
49
|
+
2. Run tests after each upgrade
|
|
50
|
+
3. Commit each upgrade: `chore(deps): upgrade <package> to <version>`
|
|
51
|
+
4. If tests fail, move to Phase 3 for that dependency before continuing
|
|
52
|
+
|
|
53
|
+
## Phase 3: Fix
|
|
54
|
+
|
|
55
|
+
**Goal:** Resolve any breaking changes from upgrades.
|
|
56
|
+
|
|
57
|
+
1. Fix API changes, type errors, deprecations
|
|
58
|
+
2. Update configuration if needed
|
|
59
|
+
3. Commit fixes separately from the upgrade: `fix(deps): adapt to <package> v<version> changes`
|
|
60
|
+
|
|
61
|
+
## Phase 4: Verify
|
|
62
|
+
|
|
63
|
+
**Goal:** Ensure everything works together.
|
|
64
|
+
|
|
65
|
+
1. Run the full test suite
|
|
66
|
+
2. Run the build
|
|
67
|
+
3. Run the linter
|
|
68
|
+
4. Check for deprecation warnings in output
|
|
69
|
+
5. **Produce:** Write `SUMMARY.md` with:
|
|
70
|
+
- Dependencies upgraded (from → to)
|
|
71
|
+
- Breaking changes encountered and how they were resolved
|
|
72
|
+
- Any deferred upgrades and why
|
|
73
|
+
|
|
74
|
+
</process>
|