gsd-pi 2.41.0-dev.3557dc4 → 2.41.0-dev.5a170d0
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 +1 -1
- package/dist/resources/extensions/gsd/auto/loop.js +80 -0
- package/dist/resources/extensions/gsd/auto/phases.js +2 -2
- package/dist/resources/extensions/gsd/auto/session.js +6 -0
- package/dist/resources/extensions/gsd/auto-dashboard.js +2 -0
- package/dist/resources/extensions/gsd/auto.js +28 -1
- package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +7 -2
- package/dist/resources/extensions/gsd/commands/catalog.js +32 -0
- package/dist/resources/extensions/gsd/commands/handlers/workflow.js +146 -0
- package/dist/resources/extensions/gsd/context-injector.js +74 -0
- package/dist/resources/extensions/gsd/custom-execution-policy.js +47 -0
- package/dist/resources/extensions/gsd/custom-verification.js +145 -0
- package/dist/resources/extensions/gsd/custom-workflow-engine.js +164 -0
- package/dist/resources/extensions/gsd/dashboard-overlay.js +1 -0
- package/dist/resources/extensions/gsd/definition-loader.js +352 -0
- package/dist/resources/extensions/gsd/dev-execution-policy.js +24 -0
- package/dist/resources/extensions/gsd/dev-workflow-engine.js +82 -0
- package/dist/resources/extensions/gsd/engine-resolver.js +40 -0
- package/dist/resources/extensions/gsd/engine-types.js +8 -0
- package/dist/resources/extensions/gsd/execution-policy.js +8 -0
- package/dist/resources/extensions/gsd/graph.js +225 -0
- package/dist/resources/extensions/gsd/run-manager.js +134 -0
- package/dist/resources/extensions/gsd/workflow-engine.js +7 -0
- package/dist/resources/skills/create-workflow/SKILL.md +103 -0
- package/dist/resources/skills/create-workflow/references/feature-patterns.md +128 -0
- package/dist/resources/skills/create-workflow/references/verification-policies.md +76 -0
- package/dist/resources/skills/create-workflow/references/yaml-schema-v1.md +46 -0
- package/dist/resources/skills/create-workflow/templates/blog-post-pipeline.yaml +60 -0
- package/dist/resources/skills/create-workflow/templates/code-audit.yaml +60 -0
- package/dist/resources/skills/create-workflow/templates/release-checklist.yaml +66 -0
- package/dist/resources/skills/create-workflow/templates/workflow-definition.yaml +32 -0
- package/dist/resources/skills/create-workflow/workflows/create-from-scratch.md +104 -0
- package/dist/resources/skills/create-workflow/workflows/create-from-template.md +72 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +13 -13
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +13 -13
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +4 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +5 -0
- package/src/resources/extensions/gsd/auto/loop.ts +91 -0
- package/src/resources/extensions/gsd/auto/phases.ts +2 -2
- package/src/resources/extensions/gsd/auto/session.ts +6 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +2 -0
- package/src/resources/extensions/gsd/auto.ts +31 -1
- package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +9 -2
- package/src/resources/extensions/gsd/commands/catalog.ts +32 -0
- package/src/resources/extensions/gsd/commands/handlers/workflow.ts +164 -0
- package/src/resources/extensions/gsd/context-injector.ts +100 -0
- package/src/resources/extensions/gsd/custom-execution-policy.ts +73 -0
- package/src/resources/extensions/gsd/custom-verification.ts +180 -0
- package/src/resources/extensions/gsd/custom-workflow-engine.ts +216 -0
- package/src/resources/extensions/gsd/dashboard-overlay.ts +1 -0
- package/src/resources/extensions/gsd/definition-loader.ts +462 -0
- package/src/resources/extensions/gsd/dev-execution-policy.ts +51 -0
- package/src/resources/extensions/gsd/dev-workflow-engine.ts +110 -0
- package/src/resources/extensions/gsd/engine-resolver.ts +57 -0
- package/src/resources/extensions/gsd/engine-types.ts +71 -0
- package/src/resources/extensions/gsd/execution-policy.ts +43 -0
- package/src/resources/extensions/gsd/graph.ts +312 -0
- package/src/resources/extensions/gsd/run-manager.ts +180 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +100 -118
- package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/bundled-workflow-defs.test.ts +180 -0
- package/src/resources/extensions/gsd/tests/captures.test.ts +12 -1
- package/src/resources/extensions/gsd/tests/commands-workflow-custom.test.ts +283 -0
- package/src/resources/extensions/gsd/tests/context-injector.test.ts +313 -0
- package/src/resources/extensions/gsd/tests/continue-here.test.ts +20 -20
- package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +540 -0
- package/src/resources/extensions/gsd/tests/custom-verification.test.ts +382 -0
- package/src/resources/extensions/gsd/tests/custom-workflow-engine.test.ts +339 -0
- package/src/resources/extensions/gsd/tests/dashboard-custom-engine.test.ts +87 -0
- package/src/resources/extensions/gsd/tests/definition-loader.test.ts +778 -0
- package/src/resources/extensions/gsd/tests/dev-engine-wrapper.test.ts +318 -0
- package/src/resources/extensions/gsd/tests/e2e-workflow-pipeline-integration.test.ts +476 -0
- package/src/resources/extensions/gsd/tests/engine-interfaces-contract.test.ts +271 -0
- package/src/resources/extensions/gsd/tests/graph-operations.test.ts +599 -0
- package/src/resources/extensions/gsd/tests/iterate-engine-integration.test.ts +429 -0
- package/src/resources/extensions/gsd/tests/run-manager.test.ts +229 -0
- package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +45 -0
- package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +195 -105
- package/src/resources/extensions/gsd/workflow-engine.ts +38 -0
- package/src/resources/skills/create-workflow/SKILL.md +103 -0
- package/src/resources/skills/create-workflow/references/feature-patterns.md +128 -0
- package/src/resources/skills/create-workflow/references/verification-policies.md +76 -0
- package/src/resources/skills/create-workflow/references/yaml-schema-v1.md +46 -0
- package/src/resources/skills/create-workflow/templates/blog-post-pipeline.yaml +60 -0
- package/src/resources/skills/create-workflow/templates/code-audit.yaml +60 -0
- package/src/resources/skills/create-workflow/templates/release-checklist.yaml +66 -0
- package/src/resources/skills/create-workflow/templates/workflow-definition.yaml +32 -0
- package/src/resources/skills/create-workflow/workflows/create-from-scratch.md +104 -0
- package/src/resources/skills/create-workflow/workflows/create-from-template.md +72 -0
- /package/dist/web/standalone/.next/static/{JBSIr4fSfHXs5g5x2ZBSC → K7GYOOPvQWX6TKYEKhODM}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{JBSIr4fSfHXs5g5x2ZBSC → K7GYOOPvQWX6TKYEKhODM}/_ssgManifest.js +0 -0
|
@@ -118,6 +118,51 @@ console.log('\n── Loop guard: arg order is normalized ──');
|
|
|
118
118
|
assertEq(getToolCallLoopCount(), 2, 'Should detect as same call regardless of key order');
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
122
|
+
// Nested/array arguments produce distinct hashes
|
|
123
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
124
|
+
|
|
125
|
+
console.log('\n── Loop guard: nested args are not stripped ──');
|
|
126
|
+
|
|
127
|
+
{
|
|
128
|
+
resetToolCallLoopGuard();
|
|
129
|
+
|
|
130
|
+
// Simulate ask_user_questions-style calls with different nested content
|
|
131
|
+
for (let i = 1; i <= 5; i++) {
|
|
132
|
+
const result = checkToolCallLoop('ask_user_questions', {
|
|
133
|
+
questions: [{ id: `q${i}`, question: `Question ${i}?` }],
|
|
134
|
+
});
|
|
135
|
+
assertTrue(result.block === false, `Nested call ${i} with unique content should be allowed`);
|
|
136
|
+
assertEq(getToolCallLoopCount(), 1, `Each unique nested call should reset count to 1`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Truly identical nested calls should still be detected
|
|
140
|
+
resetToolCallLoopGuard();
|
|
141
|
+
for (let i = 1; i <= 4; i++) {
|
|
142
|
+
checkToolCallLoop('ask_user_questions', {
|
|
143
|
+
questions: [{ id: 'same', question: 'Same?' }],
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
const blocked = checkToolCallLoop('ask_user_questions', {
|
|
147
|
+
questions: [{ id: 'same', question: 'Same?' }],
|
|
148
|
+
});
|
|
149
|
+
assertTrue(blocked.block === true, 'Identical nested calls should still be blocked');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
153
|
+
// Nested object key order is normalized
|
|
154
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
155
|
+
|
|
156
|
+
console.log('\n── Loop guard: nested key order is normalized ──');
|
|
157
|
+
|
|
158
|
+
{
|
|
159
|
+
resetToolCallLoopGuard();
|
|
160
|
+
|
|
161
|
+
checkToolCallLoop('tool', { outer: { b: 2, a: 1 } });
|
|
162
|
+
const result = checkToolCallLoop('tool', { outer: { a: 1, b: 2 } });
|
|
163
|
+
assertEq(getToolCallLoopCount(), 2, 'Same nested args in different key order should match');
|
|
164
|
+
}
|
|
165
|
+
|
|
121
166
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
122
167
|
|
|
123
168
|
report();
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync } from "node:fs";
|
|
2
4
|
import { join } from "node:path";
|
|
3
5
|
import { tmpdir } from "node:os";
|
|
4
6
|
import { execSync } from "node:child_process";
|
|
@@ -13,82 +15,42 @@ import {
|
|
|
13
15
|
worktreeBranchName,
|
|
14
16
|
worktreePath,
|
|
15
17
|
} from "../worktree-manager.ts";
|
|
16
|
-
import { createTestContext } from './test-helpers.ts';
|
|
17
18
|
|
|
18
|
-
const { assertEq, assertTrue, report } = createTestContext();
|
|
19
19
|
function run(command: string, cwd: string): string {
|
|
20
20
|
return execSync(command, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
const base = mkdtempSync(join(tmpdir(), "gsd-
|
|
25
|
-
run("git init -b main", base);
|
|
26
|
-
run('git config user.name "
|
|
27
|
-
run('git config user.email "
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
async function main(): Promise<void> {
|
|
41
|
-
console.log("\n=== worktreeBranchName ===");
|
|
42
|
-
assertEq(worktreeBranchName("feature-x"), "worktree/feature-x", "branch name format");
|
|
43
|
-
|
|
44
|
-
console.log("\n=== createWorktree ===");
|
|
45
|
-
const info = createWorktree(base, "feature-x");
|
|
46
|
-
assertTrue(info.name === "feature-x", "name matches");
|
|
47
|
-
assertTrue(info.branch === "worktree/feature-x", "branch matches");
|
|
48
|
-
assertTrue(info.exists, "worktree exists");
|
|
49
|
-
assertTrue(existsSync(info.path), "worktree path exists on disk");
|
|
50
|
-
assertTrue(existsSync(join(info.path, "README.md")), "README.md copied to worktree");
|
|
51
|
-
assertTrue(existsSync(join(info.path, ".gsd", "milestones", "M001", "M001-ROADMAP.md")), ".gsd files copied");
|
|
52
|
-
|
|
53
|
-
// Branch was created
|
|
54
|
-
const branches = run("git branch", base);
|
|
55
|
-
assertTrue(branches.includes("worktree/feature-x"), "branch was created");
|
|
56
|
-
|
|
57
|
-
console.log("\n=== createWorktree — duplicate ===");
|
|
58
|
-
let duplicateError = "";
|
|
59
|
-
try {
|
|
60
|
-
createWorktree(base, "feature-x");
|
|
61
|
-
} catch (e) {
|
|
62
|
-
duplicateError = (e as Error).message;
|
|
63
|
-
}
|
|
64
|
-
assertTrue(duplicateError.includes("already exists"), "duplicate creation fails");
|
|
23
|
+
function makeBaseRepo(): string {
|
|
24
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-wt-test-"));
|
|
25
|
+
run("git init -b main", base);
|
|
26
|
+
run('git config user.name "Test User"', base);
|
|
27
|
+
run('git config user.email "test@example.com"', base);
|
|
28
|
+
mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
|
|
29
|
+
writeFileSync(join(base, "README.md"), "# Test Project\n", "utf-8");
|
|
30
|
+
writeFileSync(
|
|
31
|
+
join(base, ".gsd", "milestones", "M001", "M001-ROADMAP.md"),
|
|
32
|
+
"# M001: Demo\n\n## Slices\n- [ ] **S01: First** `risk:low` `depends:[]`\n > After this: it works\n",
|
|
33
|
+
"utf-8",
|
|
34
|
+
);
|
|
35
|
+
run("git add .", base);
|
|
36
|
+
run('git commit -m "chore: init"', base);
|
|
37
|
+
return base;
|
|
38
|
+
}
|
|
65
39
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
console.log("\n=== listWorktrees ===");
|
|
76
|
-
const list = listWorktrees(base);
|
|
77
|
-
assertEq(list.length, 1, "one worktree listed");
|
|
78
|
-
assertEq(list[0]!.name, "feature-x", "correct name");
|
|
79
|
-
assertEq(list[0]!.branch, "worktree/feature-x", "correct branch");
|
|
80
|
-
assertTrue(list[0]!.exists, "exists flag is true");
|
|
81
|
-
|
|
82
|
-
console.log("\n=== make changes in worktree ===");
|
|
83
|
-
const wtPath = worktreePath(base, "feature-x");
|
|
84
|
-
// Add a new GSD artifact in the worktree
|
|
40
|
+
function makeRepoWithWorktree(worktreeName: string): { base: string; wtPath: string } {
|
|
41
|
+
const base = makeBaseRepo();
|
|
42
|
+
createWorktree(base, worktreeName);
|
|
43
|
+
return { base, wtPath: worktreePath(base, worktreeName) };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function makeRepoWithChanges(worktreeName: string): { base: string; wtPath: string } {
|
|
47
|
+
const { base, wtPath } = makeRepoWithWorktree(worktreeName);
|
|
85
48
|
mkdirSync(join(wtPath, ".gsd", "milestones", "M002"), { recursive: true });
|
|
86
49
|
writeFileSync(
|
|
87
50
|
join(wtPath, ".gsd", "milestones", "M002", "M002-ROADMAP.md"),
|
|
88
51
|
"# M002: New Feature\n\n## Slices\n- [ ] **S01: Setup** `risk:low` `depends:[]`\n > After this: new feature ready\n",
|
|
89
52
|
"utf-8",
|
|
90
53
|
);
|
|
91
|
-
// Modify an existing artifact
|
|
92
54
|
writeFileSync(
|
|
93
55
|
join(wtPath, ".gsd", "milestones", "M001", "M001-ROADMAP.md"),
|
|
94
56
|
"# M001: Demo (updated)\n\n## Slices\n- [x] **S01: First** `risk:low` `depends:[]`\n > Done\n",
|
|
@@ -96,46 +58,174 @@ async function main(): Promise<void> {
|
|
|
96
58
|
);
|
|
97
59
|
run("git add .", wtPath);
|
|
98
60
|
run('git commit -m "feat: add M002 and update M001"', wtPath);
|
|
99
|
-
|
|
100
|
-
console.log("\n=== diffWorktreeGSD ===");
|
|
101
|
-
const diff = diffWorktreeGSD(base, "feature-x");
|
|
102
|
-
assertTrue(diff.added.length > 0, "has added files");
|
|
103
|
-
assertTrue(diff.added.some(f => f.includes("M002")), "M002 roadmap is in added");
|
|
104
|
-
assertTrue(diff.modified.length > 0, "has modified files");
|
|
105
|
-
assertTrue(diff.modified.some(f => f.includes("M001")), "M001 roadmap is in modified");
|
|
106
|
-
assertEq(diff.removed.length, 0, "no removed files");
|
|
107
|
-
|
|
108
|
-
console.log("\n=== getWorktreeGSDDiff ===");
|
|
109
|
-
const fullDiff = getWorktreeGSDDiff(base, "feature-x");
|
|
110
|
-
assertTrue(fullDiff.includes("M002"), "full diff mentions M002");
|
|
111
|
-
assertTrue(fullDiff.includes("updated"), "full diff mentions update");
|
|
112
|
-
|
|
113
|
-
console.log("\n=== getWorktreeLog ===");
|
|
114
|
-
const log = getWorktreeLog(base, "feature-x");
|
|
115
|
-
assertTrue(log.includes("add M002"), "log shows commit message");
|
|
116
|
-
|
|
117
|
-
console.log("\n=== removeWorktree ===");
|
|
118
|
-
removeWorktree(base, "feature-x", { deleteBranch: true });
|
|
119
|
-
assertTrue(!existsSync(wtPath), "worktree directory removed");
|
|
120
|
-
const branchesAfter = run("git branch", base);
|
|
121
|
-
assertTrue(!branchesAfter.includes("worktree/feature-x"), "branch deleted");
|
|
122
|
-
|
|
123
|
-
console.log("\n=== listWorktrees after removal ===");
|
|
124
|
-
const listAfter = listWorktrees(base);
|
|
125
|
-
assertEq(listAfter.length, 0, "no worktrees after removal");
|
|
126
|
-
|
|
127
|
-
console.log("\n=== removeWorktree — already gone ===");
|
|
128
|
-
// Should not throw
|
|
129
|
-
removeWorktree(base, "feature-x", { deleteBranch: true });
|
|
130
|
-
assertTrue(true, "removeWorktree on missing worktree does not throw");
|
|
131
|
-
|
|
132
|
-
// Cleanup
|
|
133
|
-
rmSync(base, { recursive: true, force: true });
|
|
134
|
-
|
|
135
|
-
report();
|
|
61
|
+
return { base, wtPath };
|
|
136
62
|
}
|
|
137
63
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
64
|
+
// ─── worktreeBranchName ───────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
test("worktreeBranchName formats branch name", () => {
|
|
67
|
+
assert.strictEqual(
|
|
68
|
+
worktreeBranchName("feature-x"),
|
|
69
|
+
"worktree/feature-x",
|
|
70
|
+
"should prefix with worktree/",
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// ─── createWorktree ───────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
test("createWorktree creates worktree with correct metadata", () => {
|
|
77
|
+
const base = makeBaseRepo();
|
|
78
|
+
try {
|
|
79
|
+
const info = createWorktree(base, "feature-x");
|
|
80
|
+
assert.strictEqual(info.name, "feature-x", "name should match");
|
|
81
|
+
assert.strictEqual(info.branch, "worktree/feature-x", "branch should be prefixed");
|
|
82
|
+
assert.ok(info.exists, "exists flag should be true");
|
|
83
|
+
assert.ok(existsSync(info.path), "worktree path should exist on disk");
|
|
84
|
+
assert.ok(existsSync(join(info.path, "README.md")), "README.md should be in worktree");
|
|
85
|
+
assert.ok(
|
|
86
|
+
existsSync(join(info.path, ".gsd", "milestones", "M001", "M001-ROADMAP.md")),
|
|
87
|
+
".gsd files should be in worktree",
|
|
88
|
+
);
|
|
89
|
+
const branches = run("git branch", base);
|
|
90
|
+
assert.ok(branches.includes("worktree/feature-x"), "branch should be created in base repo");
|
|
91
|
+
} finally {
|
|
92
|
+
rmSync(base, { recursive: true, force: true });
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("createWorktree rejects duplicate name", () => {
|
|
97
|
+
const { base } = makeRepoWithWorktree("feature-x");
|
|
98
|
+
try {
|
|
99
|
+
assert.throws(
|
|
100
|
+
() => createWorktree(base, "feature-x"),
|
|
101
|
+
(err: Error) => {
|
|
102
|
+
assert.ok(
|
|
103
|
+
err.message.includes("already exists"),
|
|
104
|
+
`expected "already exists" in error, got: ${err.message}`,
|
|
105
|
+
);
|
|
106
|
+
return true;
|
|
107
|
+
},
|
|
108
|
+
"should throw on duplicate worktree name",
|
|
109
|
+
);
|
|
110
|
+
} finally {
|
|
111
|
+
rmSync(base, { recursive: true, force: true });
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("createWorktree rejects invalid name", () => {
|
|
116
|
+
const base = makeBaseRepo();
|
|
117
|
+
try {
|
|
118
|
+
assert.throws(
|
|
119
|
+
() => createWorktree(base, "bad name!"),
|
|
120
|
+
(err: Error) => {
|
|
121
|
+
assert.ok(
|
|
122
|
+
err.message.includes("Invalid worktree name"),
|
|
123
|
+
`expected "Invalid worktree name" in error, got: ${err.message}`,
|
|
124
|
+
);
|
|
125
|
+
return true;
|
|
126
|
+
},
|
|
127
|
+
"should throw on invalid worktree name",
|
|
128
|
+
);
|
|
129
|
+
} finally {
|
|
130
|
+
rmSync(base, { recursive: true, force: true });
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ─── listWorktrees ────────────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
test("listWorktrees returns active worktrees", () => {
|
|
137
|
+
const { base } = makeRepoWithWorktree("feature-x");
|
|
138
|
+
try {
|
|
139
|
+
const list = listWorktrees(base);
|
|
140
|
+
assert.strictEqual(list.length, 1, "should list exactly one worktree");
|
|
141
|
+
assert.strictEqual(list[0]!.name, "feature-x", "name should match");
|
|
142
|
+
assert.strictEqual(list[0]!.branch, "worktree/feature-x", "branch should match");
|
|
143
|
+
assert.ok(list[0]!.exists, "exists flag should be true");
|
|
144
|
+
} finally {
|
|
145
|
+
rmSync(base, { recursive: true, force: true });
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("listWorktrees returns empty after removal", () => {
|
|
150
|
+
const { base } = makeRepoWithWorktree("feature-x");
|
|
151
|
+
try {
|
|
152
|
+
removeWorktree(base, "feature-x");
|
|
153
|
+
const list = listWorktrees(base);
|
|
154
|
+
assert.strictEqual(list.length, 0, "should have no worktrees after removal");
|
|
155
|
+
} finally {
|
|
156
|
+
rmSync(base, { recursive: true, force: true });
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// ─── diffWorktreeGSD ─────────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
test("diffWorktreeGSD detects added and modified GSD files", () => {
|
|
163
|
+
const { base } = makeRepoWithChanges("feature-x");
|
|
164
|
+
try {
|
|
165
|
+
const diff = diffWorktreeGSD(base, "feature-x");
|
|
166
|
+
assert.ok(diff.added.length > 0, "should have added files");
|
|
167
|
+
assert.ok(
|
|
168
|
+
diff.added.some((f) => f.includes("M002")),
|
|
169
|
+
"M002 roadmap should be in added files",
|
|
170
|
+
);
|
|
171
|
+
assert.ok(diff.modified.length > 0, "should have modified files");
|
|
172
|
+
assert.ok(
|
|
173
|
+
diff.modified.some((f) => f.includes("M001")),
|
|
174
|
+
"M001 roadmap should be in modified files",
|
|
175
|
+
);
|
|
176
|
+
assert.strictEqual(diff.removed.length, 0, "should have no removed files");
|
|
177
|
+
} finally {
|
|
178
|
+
rmSync(base, { recursive: true, force: true });
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// ─── getWorktreeGSDDiff ───────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
test("getWorktreeGSDDiff returns patch content", () => {
|
|
185
|
+
const { base } = makeRepoWithChanges("feature-x");
|
|
186
|
+
try {
|
|
187
|
+
const fullDiff = getWorktreeGSDDiff(base, "feature-x");
|
|
188
|
+
assert.ok(fullDiff.includes("M002"), "diff should mention M002");
|
|
189
|
+
assert.ok(fullDiff.includes("updated"), "diff should mention the update");
|
|
190
|
+
} finally {
|
|
191
|
+
rmSync(base, { recursive: true, force: true });
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// ─── getWorktreeLog ───────────────────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
test("getWorktreeLog shows commits", () => {
|
|
198
|
+
const { base } = makeRepoWithChanges("feature-x");
|
|
199
|
+
try {
|
|
200
|
+
const log = getWorktreeLog(base, "feature-x");
|
|
201
|
+
assert.ok(log.includes("add M002"), "log should include the commit message");
|
|
202
|
+
} finally {
|
|
203
|
+
rmSync(base, { recursive: true, force: true });
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// ─── removeWorktree ───────────────────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
test("removeWorktree removes directory and branch", () => {
|
|
210
|
+
const { base, wtPath } = makeRepoWithWorktree("feature-x");
|
|
211
|
+
try {
|
|
212
|
+
removeWorktree(base, "feature-x", { deleteBranch: true });
|
|
213
|
+
assert.ok(!existsSync(wtPath), "worktree directory should be gone");
|
|
214
|
+
const branches = run("git branch", base);
|
|
215
|
+
assert.ok(!branches.includes("worktree/feature-x"), "branch should be deleted");
|
|
216
|
+
} finally {
|
|
217
|
+
rmSync(base, { recursive: true, force: true });
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("removeWorktree on missing worktree does not throw", () => {
|
|
222
|
+
const base = makeBaseRepo();
|
|
223
|
+
try {
|
|
224
|
+
assert.doesNotThrow(
|
|
225
|
+
() => removeWorktree(base, "nonexistent"),
|
|
226
|
+
"should not throw when worktree does not exist",
|
|
227
|
+
);
|
|
228
|
+
} finally {
|
|
229
|
+
rmSync(base, { recursive: true, force: true });
|
|
230
|
+
}
|
|
141
231
|
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* workflow-engine.ts — WorkflowEngine interface.
|
|
3
|
+
*
|
|
4
|
+
* Defines the contract every engine implementation must satisfy.
|
|
5
|
+
* Imports only from the leaf-node engine-types.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
EngineState,
|
|
10
|
+
EngineDispatchAction,
|
|
11
|
+
CompletedStep,
|
|
12
|
+
ReconcileResult,
|
|
13
|
+
DisplayMetadata,
|
|
14
|
+
} from "./engine-types.js";
|
|
15
|
+
|
|
16
|
+
/** A pluggable workflow engine that drives the auto-loop. */
|
|
17
|
+
export interface WorkflowEngine {
|
|
18
|
+
/** Unique identifier for this engine (e.g. "dev", "custom"). */
|
|
19
|
+
readonly engineId: string;
|
|
20
|
+
|
|
21
|
+
/** Derive the current engine state from the project on disk. */
|
|
22
|
+
deriveState(basePath: string): Promise<EngineState>;
|
|
23
|
+
|
|
24
|
+
/** Decide what the loop should do next given current state. */
|
|
25
|
+
resolveDispatch(
|
|
26
|
+
state: EngineState,
|
|
27
|
+
context: { basePath: string },
|
|
28
|
+
): Promise<EngineDispatchAction>;
|
|
29
|
+
|
|
30
|
+
/** Reconcile state after a step has been executed. */
|
|
31
|
+
reconcile(
|
|
32
|
+
state: EngineState,
|
|
33
|
+
completedStep: CompletedStep,
|
|
34
|
+
): Promise<ReconcileResult>;
|
|
35
|
+
|
|
36
|
+
/** Return UI-facing metadata for progress display. */
|
|
37
|
+
getDisplayMetadata(state: EngineState): DisplayMetadata;
|
|
38
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: create-workflow
|
|
3
|
+
description: Conversational guide for creating valid YAML workflow definitions. Use when asked to "create a workflow", "new workflow definition", "build a workflow", "workflow YAML", "define workflow steps", or "workflow from template".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<essential_principles>
|
|
7
|
+
You are a workflow definition author. You help users create valid V1 YAML workflow definitions that the GSD workflow engine can execute.
|
|
8
|
+
|
|
9
|
+
**V1 Schema Basics:**
|
|
10
|
+
|
|
11
|
+
- Every definition requires `version: 1`, a non-empty `name`, and at least one step in `steps[]`.
|
|
12
|
+
- Optional top-level fields: `description` (string), `params` (key-value defaults for `{{ key }}` substitution).
|
|
13
|
+
- Each step requires: `id` (unique string), `name` (non-empty string), `prompt` (non-empty string).
|
|
14
|
+
- Each step optionally has: `requires` or `depends_on` (array of step IDs), `produces` (array of artifact paths), `context_from` (array of step IDs), `verify` (verification policy object), `iterate` (fan-out config object).
|
|
15
|
+
- YAML uses **snake_case** keys: `depends_on`, `context_from`. The engine converts to camelCase internally.
|
|
16
|
+
|
|
17
|
+
**Validation Rules:**
|
|
18
|
+
|
|
19
|
+
- Step IDs must be unique across the workflow.
|
|
20
|
+
- Dependencies (`requires`/`depends_on`) must reference existing step IDs — no dangling refs.
|
|
21
|
+
- A step cannot depend on itself.
|
|
22
|
+
- The dependency graph must be acyclic (no circular dependencies).
|
|
23
|
+
- `produces` paths must not contain `..` (path traversal rejected).
|
|
24
|
+
- `iterate.source` must not contain `..` (path traversal rejected).
|
|
25
|
+
- `iterate.pattern` must be a valid regex with at least one capture group.
|
|
26
|
+
|
|
27
|
+
**Four Verification Policies:**
|
|
28
|
+
|
|
29
|
+
1. `content-heuristic` — Checks artifact content. Optional: `minSize` (number), `pattern` (string).
|
|
30
|
+
2. `shell-command` — Runs a shell command. Required: `command` (non-empty string).
|
|
31
|
+
3. `prompt-verify` — Asks an LLM to verify. Required: `prompt` (non-empty string).
|
|
32
|
+
4. `human-review` — Pauses for human approval. No extra fields required.
|
|
33
|
+
|
|
34
|
+
**Parameter Substitution:**
|
|
35
|
+
|
|
36
|
+
- Define defaults in top-level `params: { key: "default_value" }`.
|
|
37
|
+
- Use `{{ key }}` placeholders in step prompts — the engine replaces them at runtime.
|
|
38
|
+
- CLI overrides take precedence over definition defaults.
|
|
39
|
+
- Parameter values must not contain `..` (path traversal guard).
|
|
40
|
+
- Any unresolved `{{ key }}` after substitution causes an error.
|
|
41
|
+
|
|
42
|
+
**Path Traversal Guard:**
|
|
43
|
+
|
|
44
|
+
- The engine rejects any `produces` path or `iterate.source` containing `..`.
|
|
45
|
+
- Parameter values are also checked for `..` during substitution.
|
|
46
|
+
|
|
47
|
+
**Output Location:**
|
|
48
|
+
|
|
49
|
+
- Finished definitions go in `.gsd/workflow-defs/<name>.yaml`.
|
|
50
|
+
- After writing, tell the user to validate with `/gsd workflow validate <name>`.
|
|
51
|
+
</essential_principles>
|
|
52
|
+
|
|
53
|
+
<routing>
|
|
54
|
+
Determine the user's intent and route to the appropriate workflow:
|
|
55
|
+
|
|
56
|
+
**"I want to create a workflow from scratch" / "new workflow" / "build a workflow":**
|
|
57
|
+
→ Read `workflows/create-from-scratch.md` and follow it.
|
|
58
|
+
|
|
59
|
+
**"I want to start from a template" / "from an example" / "customize a template":**
|
|
60
|
+
→ Read `workflows/create-from-template.md` and follow it.
|
|
61
|
+
|
|
62
|
+
**"Help me understand the schema" / "what fields are available?":**
|
|
63
|
+
→ Read `references/yaml-schema-v1.md` and explain the relevant parts.
|
|
64
|
+
|
|
65
|
+
**"How does verification work?" / "verify policies":**
|
|
66
|
+
→ Read `references/verification-policies.md` and explain.
|
|
67
|
+
|
|
68
|
+
**"How do I use context_from / iterate / params?":**
|
|
69
|
+
→ Read `references/feature-patterns.md` and explain the relevant feature.
|
|
70
|
+
|
|
71
|
+
**If intent is unclear, ask one clarifying question:**
|
|
72
|
+
- "Do you want to create a workflow from scratch, or start from an existing template?"
|
|
73
|
+
- Then route based on the answer.
|
|
74
|
+
</routing>
|
|
75
|
+
|
|
76
|
+
<reference_index>
|
|
77
|
+
Read these files when you need detailed schema knowledge during workflow authoring:
|
|
78
|
+
|
|
79
|
+
- `references/yaml-schema-v1.md` — Complete field-by-field V1 schema reference. Read when you need to explain any field's type, constraints, or defaults.
|
|
80
|
+
- `references/verification-policies.md` — All four verify policies with complete YAML examples. Read when helping the user choose or configure verification for a step.
|
|
81
|
+
- `references/feature-patterns.md` — Usage patterns for `context_from`, `iterate`, and `params` with complete YAML examples. Read when the user wants context chaining, fan-out iteration, or parameterized workflows.
|
|
82
|
+
</reference_index>
|
|
83
|
+
|
|
84
|
+
<templates_index>
|
|
85
|
+
Available templates in `templates/`:
|
|
86
|
+
|
|
87
|
+
- `workflow-definition.yaml` — Blank scaffold with all fields shown as comments. Copy and fill for a quick start.
|
|
88
|
+
- `blog-post-pipeline.yaml` — Linear chain with params and content-heuristic verification.
|
|
89
|
+
- `code-audit.yaml` — Iterate-based fan-out with shell-command verification.
|
|
90
|
+
- `release-checklist.yaml` — Diamond dependency graph with human-review verification.
|
|
91
|
+
</templates_index>
|
|
92
|
+
|
|
93
|
+
<output_conventions>
|
|
94
|
+
When assembling the final YAML:
|
|
95
|
+
|
|
96
|
+
1. Use 2-space indentation consistently.
|
|
97
|
+
2. Quote string values that contain special YAML characters (`:`, `{`, `}`, `[`, `]`, `#`).
|
|
98
|
+
3. Always include `version: 1` as the first field.
|
|
99
|
+
4. Order top-level fields: `version`, `name`, `description`, `params`, `steps`.
|
|
100
|
+
5. Order step fields: `id`, `name`, `prompt`, `requires`, `produces`, `context_from`, `verify`, `iterate`.
|
|
101
|
+
6. Write the file to `.gsd/workflow-defs/<name>.yaml`.
|
|
102
|
+
7. After writing, tell the user: "Run `/gsd workflow validate <name>` to check the definition."
|
|
103
|
+
</output_conventions>
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
<feature_patterns>
|
|
2
|
+
Advanced workflow features: `context_from`, `iterate`, and `params`. Each section includes a complete YAML example.
|
|
3
|
+
|
|
4
|
+
**Feature 1: `context_from` — Context Chaining**
|
|
5
|
+
|
|
6
|
+
Injects artifacts from prior steps as context when the current step runs. The value is an array of step IDs.
|
|
7
|
+
|
|
8
|
+
```yaml
|
|
9
|
+
version: 1
|
|
10
|
+
name: research-and-synthesize
|
|
11
|
+
steps:
|
|
12
|
+
- id: gather
|
|
13
|
+
name: Gather sources
|
|
14
|
+
prompt: "Find and summarize the top 5 sources on the topic."
|
|
15
|
+
produces:
|
|
16
|
+
- sources.md
|
|
17
|
+
|
|
18
|
+
- id: analyze
|
|
19
|
+
name: Analyze sources
|
|
20
|
+
prompt: "Analyze the gathered sources for key themes."
|
|
21
|
+
requires:
|
|
22
|
+
- gather
|
|
23
|
+
context_from:
|
|
24
|
+
- gather
|
|
25
|
+
produces:
|
|
26
|
+
- analysis.md
|
|
27
|
+
|
|
28
|
+
- id: synthesize
|
|
29
|
+
name: Write synthesis
|
|
30
|
+
prompt: "Synthesize the analysis into a coherent report."
|
|
31
|
+
requires:
|
|
32
|
+
- analyze
|
|
33
|
+
context_from:
|
|
34
|
+
- gather
|
|
35
|
+
- analyze
|
|
36
|
+
produces:
|
|
37
|
+
- report.md
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
How it works:
|
|
41
|
+
- `context_from: [gather]` means the engine includes artifacts from the `gather` step when executing `analyze`.
|
|
42
|
+
- You can reference multiple prior steps: `context_from: [gather, analyze]`.
|
|
43
|
+
- The referenced steps must exist in the workflow (they are validated as step IDs).
|
|
44
|
+
- `context_from` does not imply a dependency — if you want the step to wait, also add the ID to `requires`.
|
|
45
|
+
|
|
46
|
+
**Feature 2: `iterate` — Fan-Out Iteration**
|
|
47
|
+
|
|
48
|
+
Reads an artifact, applies a regex pattern, and creates one sub-execution per match. The capture group extracts the iteration variable.
|
|
49
|
+
|
|
50
|
+
```yaml
|
|
51
|
+
version: 1
|
|
52
|
+
name: file-by-file-review
|
|
53
|
+
steps:
|
|
54
|
+
- id: inventory
|
|
55
|
+
name: List files to review
|
|
56
|
+
prompt: "List all TypeScript files in src/ that need review, one per line."
|
|
57
|
+
produces:
|
|
58
|
+
- file-list.txt
|
|
59
|
+
|
|
60
|
+
- id: review
|
|
61
|
+
name: Review each file
|
|
62
|
+
prompt: "Review the file for code quality issues."
|
|
63
|
+
requires:
|
|
64
|
+
- inventory
|
|
65
|
+
iterate:
|
|
66
|
+
source: file-list.txt
|
|
67
|
+
pattern: "^(.+\\.ts)$"
|
|
68
|
+
produces:
|
|
69
|
+
- reviews/
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
How it works:
|
|
73
|
+
- `source`: Path to an artifact (relative to the run directory). Must not contain `..`.
|
|
74
|
+
- `pattern`: A regex string applied with the global flag. Must contain at least one capture group `(...)`.
|
|
75
|
+
- The engine reads the source artifact, applies the pattern, and creates one execution per match.
|
|
76
|
+
- Each capture group match becomes available as the iteration variable.
|
|
77
|
+
- The regex is validated at definition-load time — invalid regex or missing capture groups are rejected.
|
|
78
|
+
|
|
79
|
+
Pattern requirements:
|
|
80
|
+
- Must be a valid JavaScript regex.
|
|
81
|
+
- Must contain at least one non-lookahead capture group: `(...)` not `(?:...)`.
|
|
82
|
+
- Example valid patterns: `^(.+)$`, `- (.+\.ts)`, `\[(.+?)\]`.
|
|
83
|
+
|
|
84
|
+
**Feature 3: `params` — Parameterized Workflows**
|
|
85
|
+
|
|
86
|
+
Define default parameter values at the top level. Use `{{ key }}` placeholders in step prompts. CLI overrides take precedence.
|
|
87
|
+
|
|
88
|
+
```yaml
|
|
89
|
+
version: 1
|
|
90
|
+
name: blog-post
|
|
91
|
+
description: Generate a blog post on a configurable topic.
|
|
92
|
+
params:
|
|
93
|
+
topic: "AI in healthcare"
|
|
94
|
+
audience: "technical professionals"
|
|
95
|
+
word_count: "1500"
|
|
96
|
+
steps:
|
|
97
|
+
- id: outline
|
|
98
|
+
name: Create outline
|
|
99
|
+
prompt: "Create a detailed outline for a blog post about {{ topic }} targeting {{ audience }}."
|
|
100
|
+
produces:
|
|
101
|
+
- outline.md
|
|
102
|
+
|
|
103
|
+
- id: draft
|
|
104
|
+
name: Write draft
|
|
105
|
+
prompt: "Write a {{ word_count }}-word blog post about {{ topic }} for {{ audience }} based on the outline."
|
|
106
|
+
requires:
|
|
107
|
+
- outline
|
|
108
|
+
context_from:
|
|
109
|
+
- outline
|
|
110
|
+
produces:
|
|
111
|
+
- draft.md
|
|
112
|
+
verify:
|
|
113
|
+
policy: content-heuristic
|
|
114
|
+
minSize: 500
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
How it works:
|
|
118
|
+
- `params` is a top-level object mapping string keys to string default values.
|
|
119
|
+
- `{{ key }}` in any step prompt is replaced with the corresponding param value.
|
|
120
|
+
- Merge order: definition `params` (defaults) ← CLI overrides (win).
|
|
121
|
+
- After substitution, any remaining `{{ key }}` that has no value causes an error — all placeholders must resolve.
|
|
122
|
+
- Parameter values must not contain `..` (path traversal guard).
|
|
123
|
+
- Keys in `{{ }}` match `\w+` (letters, digits, underscore).
|
|
124
|
+
|
|
125
|
+
Common usage:
|
|
126
|
+
- Make workflows reusable across different topics, projects, or configurations.
|
|
127
|
+
- Users override defaults at run time: `/gsd workflow run blog-post topic="Rust performance"`.
|
|
128
|
+
</feature_patterns>
|