gnd-workflow 0.1.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 +104 -0
- package/bin/gnd-workflow.js +6 -0
- package/package.json +48 -0
- package/src/cli.js +319 -0
- package/src/install.js +268 -0
- package/src/path-policy.js +190 -0
- package/templates/workflow/agents/gnd-diver.agent.md +34 -0
- package/templates/workflow/agents/gnd-navigator.agent.md +164 -0
- package/templates/workflow/skills/gnd-chart/SKILL.md +184 -0
- package/templates/workflow/skills/gnd-critique/SKILL.md +200 -0
package/src/install.js
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import {
|
|
4
|
+
describePathKind,
|
|
5
|
+
ensureManagedPathWithinProjectRoot,
|
|
6
|
+
ensureProjectRootCanBeCreated,
|
|
7
|
+
getExistingPathKind,
|
|
8
|
+
resolveManagedRoot,
|
|
9
|
+
toProjectRelativePath
|
|
10
|
+
} from "./path-policy.js";
|
|
11
|
+
|
|
12
|
+
function isPermissionError(error) {
|
|
13
|
+
return error && (error.code === "EACCES" || error.code === "EPERM" || error.code === "EROFS");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const moduleDir = import.meta.dirname;
|
|
17
|
+
const TEMPLATE_ROOT = path.resolve(moduleDir, "..", "templates", "workflow");
|
|
18
|
+
const packageJsonPath = path.resolve(moduleDir, "..", "package.json");
|
|
19
|
+
|
|
20
|
+
export async function readPackageVersion() {
|
|
21
|
+
const raw = await readFile(packageJsonPath, "utf8");
|
|
22
|
+
return JSON.parse(raw).version;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const DEFAULT_INSTALL_DIR = ".agents";
|
|
26
|
+
|
|
27
|
+
export const VERSION_FILE = ".gnd-version.json";
|
|
28
|
+
|
|
29
|
+
export const MANAGED_FILES = Object.freeze([
|
|
30
|
+
"agents/gnd-diver.agent.md",
|
|
31
|
+
"agents/gnd-navigator.agent.md",
|
|
32
|
+
"skills/gnd-chart/SKILL.md",
|
|
33
|
+
"skills/gnd-critique/SKILL.md"
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {Object} ManagedFileConflict
|
|
38
|
+
* @property {"overwrite"} action
|
|
39
|
+
* @property {string} path
|
|
40
|
+
* @property {string} relativePath
|
|
41
|
+
* @property {string} existingContent
|
|
42
|
+
* @property {string} nextContent
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @callback ConfirmManagedFileConflict
|
|
47
|
+
* @param {ManagedFileConflict} conflict
|
|
48
|
+
* @returns {boolean|Promise<boolean>}
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @typedef {Object} InstallOptions
|
|
53
|
+
* @property {string} [projectRoot]
|
|
54
|
+
* @property {boolean} [dryRun]
|
|
55
|
+
* @property {boolean} [force]
|
|
56
|
+
* @property {ConfirmManagedFileConflict} [confirmManagedFileConflict]
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
async function ensureRegularFileOrMissing(filePath, displayPath) {
|
|
60
|
+
const kind = await getExistingPathKind(filePath);
|
|
61
|
+
|
|
62
|
+
if (kind !== null && kind !== "file") {
|
|
63
|
+
throw new Error(`Refusing to manage ${displayPath}. Managed file paths must be regular files, not ${describePathKind(kind)}.`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function readTextIfExists(filePath, displayPath) {
|
|
68
|
+
await ensureRegularFileOrMissing(filePath, displayPath);
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
return await readFile(filePath, "utf8");
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (error && error.code === "ENOENT") {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function resolveInstallRoot(projectRoot) {
|
|
82
|
+
await ensureProjectRootCanBeCreated(projectRoot);
|
|
83
|
+
|
|
84
|
+
const installRoot = await resolveManagedRoot(projectRoot, DEFAULT_INSTALL_DIR, "installDir");
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
absolutePath: installRoot,
|
|
88
|
+
contentPath: toProjectRelativePath(projectRoot, installRoot)
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function createInstallPlan() {
|
|
93
|
+
const files = [];
|
|
94
|
+
|
|
95
|
+
for (const relativePath of MANAGED_FILES) {
|
|
96
|
+
const templatePath = path.join(TEMPLATE_ROOT, relativePath);
|
|
97
|
+
let raw;
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
raw = await readFile(templatePath, "utf8");
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (error && error.code === "ENOENT") {
|
|
103
|
+
throw new Error(`Missing template '${relativePath}'. The gnd-workflow package may be corrupted; try reinstalling.`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
files.push({
|
|
110
|
+
relativePath,
|
|
111
|
+
content: raw.replaceAll("\r\n", "\n")
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return files;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function prepareManagedFileWrite(filePath, content, options) {
|
|
119
|
+
const displayPath = toProjectRelativePath(options.projectRoot, filePath);
|
|
120
|
+
|
|
121
|
+
await ensureManagedPathWithinProjectRoot(options.projectRoot, filePath, `Refusing to manage ${displayPath}. Managed files must stay within the project root.`);
|
|
122
|
+
|
|
123
|
+
const existingContent = await readTextIfExists(filePath, displayPath);
|
|
124
|
+
|
|
125
|
+
if (existingContent === content) {
|
|
126
|
+
return { path: filePath, status: "unchanged", content };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (existingContent !== null && !options.force) {
|
|
130
|
+
const conflict = {
|
|
131
|
+
action: "overwrite",
|
|
132
|
+
path: filePath,
|
|
133
|
+
relativePath: displayPath,
|
|
134
|
+
existingContent,
|
|
135
|
+
nextContent: content
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
if (options.dryRun) {
|
|
139
|
+
return {
|
|
140
|
+
path: filePath,
|
|
141
|
+
status: "updated",
|
|
142
|
+
content,
|
|
143
|
+
conflict
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const confirmed = typeof options.confirmManagedFileConflict === "function"
|
|
148
|
+
? (await options.confirmManagedFileConflict(conflict)) === true
|
|
149
|
+
: false;
|
|
150
|
+
|
|
151
|
+
if (!confirmed) {
|
|
152
|
+
if (typeof options.confirmManagedFileConflict === "function") {
|
|
153
|
+
throw new Error(`Install canceled after declining to overwrite ${displayPath}. No files were changed.`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
throw new Error(`Refusing to overwrite ${displayPath}. Re-run with --force to replace it.`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
path: filePath,
|
|
162
|
+
status: existingContent === null ? "created" : "updated",
|
|
163
|
+
content,
|
|
164
|
+
...(existingContent !== null ? { previousContent: existingContent } : {})
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function installWorkflow(options = {}) {
|
|
169
|
+
const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
|
|
170
|
+
const dryRun = options.dryRun ?? false;
|
|
171
|
+
const force = options.force ?? false;
|
|
172
|
+
|
|
173
|
+
const installRoot = await resolveInstallRoot(projectRoot);
|
|
174
|
+
const plan = await createInstallPlan();
|
|
175
|
+
const writeOptions = {
|
|
176
|
+
projectRoot,
|
|
177
|
+
dryRun,
|
|
178
|
+
force,
|
|
179
|
+
confirmManagedFileConflict: options.confirmManagedFileConflict
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const managedFiles = [];
|
|
183
|
+
|
|
184
|
+
for (const entry of plan) {
|
|
185
|
+
const filePath = path.join(installRoot.absolutePath, entry.relativePath);
|
|
186
|
+
managedFiles.push(await prepareManagedFileWrite(filePath, entry.content, writeOptions));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const packageVersion = await readPackageVersion();
|
|
190
|
+
const versionFilePath = path.join(installRoot.absolutePath, VERSION_FILE);
|
|
191
|
+
const versionDisplayPath = toProjectRelativePath(projectRoot, versionFilePath);
|
|
192
|
+
const versionContent = JSON.stringify({ installedFrom: packageVersion }, null, 2) + "\n";
|
|
193
|
+
const existingVersionContent = await readTextIfExists(versionFilePath, versionDisplayPath);
|
|
194
|
+
|
|
195
|
+
managedFiles.push({
|
|
196
|
+
path: versionFilePath,
|
|
197
|
+
status: existingVersionContent === versionContent ? "unchanged"
|
|
198
|
+
: existingVersionContent === null ? "created" : "updated",
|
|
199
|
+
content: versionContent,
|
|
200
|
+
...(existingVersionContent !== null && existingVersionContent !== versionContent
|
|
201
|
+
? { previousContent: existingVersionContent } : {})
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
if (!dryRun) {
|
|
205
|
+
const writtenEntries = [];
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
for (const entry of managedFiles) {
|
|
209
|
+
if (entry.status !== "unchanged") {
|
|
210
|
+
const dir = path.dirname(entry.path);
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
await mkdir(dir, { recursive: true });
|
|
214
|
+
} catch (cause) {
|
|
215
|
+
const hint = isPermissionError(cause) ? " Check that you have write access to the project directory." : "";
|
|
216
|
+
throw new Error(`Failed to create directory '${toProjectRelativePath(projectRoot, dir)}': ${cause?.message ?? cause}${hint}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
await writeFile(entry.path, entry.content, "utf8");
|
|
221
|
+
} catch (cause) {
|
|
222
|
+
const hint = isPermissionError(cause) ? " Check that you have write access to the project directory." : "";
|
|
223
|
+
throw new Error(`Failed to write '${toProjectRelativePath(projectRoot, entry.path)}': ${cause?.message ?? cause}${hint}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
writtenEntries.push(entry);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
} catch (writeError) {
|
|
230
|
+
const rollbackErrors = [];
|
|
231
|
+
|
|
232
|
+
for (const written of writtenEntries) {
|
|
233
|
+
try {
|
|
234
|
+
if (written.status === "created") {
|
|
235
|
+
await unlink(written.path);
|
|
236
|
+
} else if (written.previousContent !== undefined) {
|
|
237
|
+
await writeFile(written.path, written.previousContent, "utf8");
|
|
238
|
+
}
|
|
239
|
+
} catch (rollbackError) {
|
|
240
|
+
rollbackErrors.push({ path: written.path, status: written.status, cause: rollbackError });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (rollbackErrors.length > 0) {
|
|
245
|
+
const unremoved = rollbackErrors
|
|
246
|
+
.filter((e) => e.status === "created")
|
|
247
|
+
.map((e) => toProjectRelativePath(projectRoot, e.path));
|
|
248
|
+
const unrestored = rollbackErrors
|
|
249
|
+
.filter((e) => e.status !== "created")
|
|
250
|
+
.map((e) => toProjectRelativePath(projectRoot, e.path));
|
|
251
|
+
const parts = [];
|
|
252
|
+
if (unremoved.length > 0) parts.push(`could not remove newly created: ${unremoved.join(", ")}`);
|
|
253
|
+
if (unrestored.length > 0) parts.push(`could not restore previous content: ${unrestored.join(", ")}`);
|
|
254
|
+
writeError.message += ` Rollback incomplete \u2014 ${parts.join("; ")}.`;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
throw writeError;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
projectRoot,
|
|
263
|
+
installDir: installRoot.contentPath,
|
|
264
|
+
dryRun,
|
|
265
|
+
managedFiles: managedFiles.map(({ path: filePath, status }) => ({ path: filePath, status })),
|
|
266
|
+
conflicts: managedFiles.flatMap((entry) => entry.conflict ? [entry.conflict] : [])
|
|
267
|
+
};
|
|
268
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { lstat, realpath } from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
export function normalizePathForContent(inputPath) {
|
|
5
|
+
return inputPath.replaceAll("\\", "/");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function isWithinProjectRoot(projectRoot, candidatePath) {
|
|
9
|
+
const relativePath = path.relative(projectRoot, candidatePath);
|
|
10
|
+
|
|
11
|
+
return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getPathKindFromStats(stats) {
|
|
15
|
+
if (stats.isSymbolicLink()) {
|
|
16
|
+
return "symlink";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (stats.isDirectory()) {
|
|
20
|
+
return "directory";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (stats.isFile()) {
|
|
24
|
+
return "file";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return "other";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function describePathKind(kind) {
|
|
31
|
+
if (kind === "file") {
|
|
32
|
+
return "a file";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (kind === "symlink") {
|
|
36
|
+
return "a symlink or junction";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (kind === "directory") {
|
|
40
|
+
return "a directory";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return "a special filesystem entry";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function tryLstat(filePath) {
|
|
47
|
+
try {
|
|
48
|
+
return await lstat(filePath);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (error && error.code === "ENOENT") {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function tryRealpath(filePath) {
|
|
59
|
+
try {
|
|
60
|
+
return await realpath(filePath);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (error && error.code === "ENOENT") {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function hasSymbolicLinkAncestor(filePath) {
|
|
71
|
+
let currentPath = path.resolve(filePath);
|
|
72
|
+
|
|
73
|
+
while (true) {
|
|
74
|
+
const stats = await tryLstat(currentPath);
|
|
75
|
+
|
|
76
|
+
if (stats !== null && stats.isSymbolicLink()) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const parentPath = path.dirname(currentPath);
|
|
81
|
+
|
|
82
|
+
if (parentPath === currentPath) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
currentPath = parentPath;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function getExistingPathKind(filePath) {
|
|
91
|
+
const stats = await tryLstat(filePath);
|
|
92
|
+
|
|
93
|
+
return stats === null ? null : getPathKindFromStats(stats);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function findNearestExistingAncestor(filePath) {
|
|
97
|
+
let currentPath = filePath;
|
|
98
|
+
|
|
99
|
+
while (true) {
|
|
100
|
+
const stats = await tryLstat(currentPath);
|
|
101
|
+
|
|
102
|
+
if (stats !== null) {
|
|
103
|
+
return {
|
|
104
|
+
path: currentPath,
|
|
105
|
+
realpath: await realpath(currentPath),
|
|
106
|
+
kind: getPathKindFromStats(stats)
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const parentPath = path.dirname(currentPath);
|
|
111
|
+
|
|
112
|
+
if (parentPath === currentPath) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
currentPath = parentPath;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function ensureManagedPathWithinProjectRoot(projectRoot, candidatePath, errorMessage) {
|
|
121
|
+
if (!isWithinProjectRoot(projectRoot, candidatePath)) {
|
|
122
|
+
throw new Error(errorMessage);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const realProjectRoot = await tryRealpath(projectRoot);
|
|
126
|
+
|
|
127
|
+
if (realProjectRoot === null) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const nearestExistingAncestor = await findNearestExistingAncestor(candidatePath);
|
|
132
|
+
|
|
133
|
+
if (nearestExistingAncestor !== null) {
|
|
134
|
+
if (nearestExistingAncestor.path !== candidatePath && nearestExistingAncestor.kind === "symlink") {
|
|
135
|
+
throw new Error(errorMessage);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!isWithinProjectRoot(realProjectRoot, nearestExistingAncestor.realpath)) {
|
|
139
|
+
throw new Error(errorMessage);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function ensureProjectRootCanBeCreated(projectRoot) {
|
|
145
|
+
const projectRootKind = await getExistingPathKind(projectRoot);
|
|
146
|
+
|
|
147
|
+
if (projectRootKind !== null) {
|
|
148
|
+
if (projectRootKind === "symlink") {
|
|
149
|
+
throw new Error(`Project root '${projectRoot}' cannot be a symlinked or junctioned directory. Use its real path.`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (projectRootKind !== "directory") {
|
|
153
|
+
throw new Error(`Project root '${projectRoot}' must be a directory, not ${describePathKind(projectRootKind)}.`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (await hasSymbolicLinkAncestor(projectRoot)) {
|
|
157
|
+
throw new Error(`Project root '${projectRoot}' cannot be a symlinked or junctioned directory. Use its real path.`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const nearestExistingAncestor = await findNearestExistingAncestor(projectRoot);
|
|
164
|
+
|
|
165
|
+
if (nearestExistingAncestor !== null && await hasSymbolicLinkAncestor(nearestExistingAncestor.path)) {
|
|
166
|
+
throw new Error(`Project root '${projectRoot}' cannot be created through a symlinked or junctioned ancestor. Create the directory first or use its real path.`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function resolveManagedRoot(projectRoot, requestedPath, label) {
|
|
171
|
+
const managedRoot = path.resolve(projectRoot, requestedPath);
|
|
172
|
+
|
|
173
|
+
await ensureManagedPathWithinProjectRoot(projectRoot, managedRoot, `${label} path '${requestedPath}' must stay within the project root.`);
|
|
174
|
+
|
|
175
|
+
const managedRootKind = await getExistingPathKind(managedRoot);
|
|
176
|
+
|
|
177
|
+
if (managedRootKind === "symlink") {
|
|
178
|
+
throw new Error(`${label} path '${requestedPath}' cannot be a symlinked or junctioned directory. Use a real directory within the project root.`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (managedRootKind !== null && managedRootKind !== "directory") {
|
|
182
|
+
throw new Error(`${label} path '${requestedPath}' must point to a directory within the project root.`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return managedRoot;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function toProjectRelativePath(projectRoot, targetPath) {
|
|
189
|
+
return normalizePathForContent(path.relative(projectRoot, targetPath) || ".");
|
|
190
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Implementation agent. Executes a single plan leg by reading code, making changes, and running verification."
|
|
3
|
+
user-invocable: false
|
|
4
|
+
agents: []
|
|
5
|
+
---
|
|
6
|
+
# Diver
|
|
7
|
+
|
|
8
|
+
You are `gnd-diver`. You receive a single plan leg with intent, owned files, and a verification command. Execute the leg and report results.
|
|
9
|
+
|
|
10
|
+
## Approach
|
|
11
|
+
|
|
12
|
+
1. Read owned files and any read-only context listed in the task.
|
|
13
|
+
2. Understand the code structure and any project constraints before changing anything.
|
|
14
|
+
3. Implement changes that fulfill the intent — no more, no less.
|
|
15
|
+
4. Run the verification command and capture its output.
|
|
16
|
+
5. Report results in the format below.
|
|
17
|
+
|
|
18
|
+
## Constraints
|
|
19
|
+
|
|
20
|
+
- ONLY create or modify files in your owned_files set.
|
|
21
|
+
- Do NOT modify shared infrastructure (`package.json`, `build.ts`, routers, routes, shared styles, etc.) even if helpful. Report needed changes as deferred edits.
|
|
22
|
+
- Honor project constraints embedded in the leg intent or referenced guidance files.
|
|
23
|
+
- Complete ALL steps. Do not stop partway or ask whether to continue.
|
|
24
|
+
- Do not offer alternatives unless the requested approach is impossible.
|
|
25
|
+
- No suggestions, follow-ups, or "you might also want to..." commentary.
|
|
26
|
+
|
|
27
|
+
## Output Format
|
|
28
|
+
|
|
29
|
+
Report ONLY:
|
|
30
|
+
|
|
31
|
+
1. **Files created/modified** — each path
|
|
32
|
+
2. **Deferred edit requests** — needed shared-file changes and why (if any)
|
|
33
|
+
3. **Verification outcome** — pass/fail and error output
|
|
34
|
+
4. **Blockers** — specific explanation (omit if none)
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Dispatch agent. Reads a structured plan from memory, dispatches legs to gnd-diver via runSubagent, reviews results, and runs the project-defined integration gate."
|
|
3
|
+
argument-hint: "Open a fresh chat and send a short bootstrap like 'start' or 'dispatch'; include a plan title or file if multiple live plans exist."
|
|
4
|
+
agents: ["gnd-diver"]
|
|
5
|
+
contracts:
|
|
6
|
+
- confirmed-legs-required
|
|
7
|
+
- mark-in-progress-before-dispatch
|
|
8
|
+
- resume-in-progress-legs
|
|
9
|
+
---
|
|
10
|
+
# Navigator
|
|
11
|
+
|
|
12
|
+
You are `gnd-navigator`. Your ONLY job is to dispatch plan legs to sub-agents via `runSubagent`, review results, and run the integration gate.
|
|
13
|
+
|
|
14
|
+
**You are a dispatcher, not an implementer.** Use `runSubagent` for every leg. Do NOT edit source files yourself except small post-review corrections. If you catch yourself writing implementation code, STOP — use `runSubagent`.
|
|
15
|
+
|
|
16
|
+
**Process ALL legs in a single session.** After each sub-agent returns and review completes, immediately dispatch the next pending leg. Do NOT stop, summarize, or prompt the user between legs. You are done only when every leg is `done` or `failed` and the gate has run.
|
|
17
|
+
|
|
18
|
+
**Bootstrap messages are not plan input.** Ignore short startup phrases (`cue`, `start`, `run`, `dispatch`, `go`, `begin`, `active plan`). Resolve the live plan from memory. Treat only substantive user text as plan-selection context.
|
|
19
|
+
|
|
20
|
+
**Project guidance is layered.** Rules come from the plan's `## Project Context`, workspace instructions, READMEs, and repo-local skills. When they conflict, the narrower and more explicit source wins.
|
|
21
|
+
|
|
22
|
+
### Quick Reference
|
|
23
|
+
|
|
24
|
+
| Phase | Steps | Key rule |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| **Start** | 1–3 | Resolve plan → validate structure → find dispatchable legs |
|
|
27
|
+
| **Dispatch loop** | 4–10 | Staleness → compose → mark in-progress → dispatch → review (scope-check!) → mark done → next leg |
|
|
28
|
+
| **Wrap-up** | 11–17 | Integration gate → finalize → land → record → verify delivery |
|
|
29
|
+
|
|
30
|
+
## Protocol
|
|
31
|
+
|
|
32
|
+
1. **Resolve the plan.** List `.planning/` (exclude `archive/`). Supports `active-plan-*.md`.
|
|
33
|
+
- User-selected plan (by title/slug/file) takes priority.
|
|
34
|
+
- Prefer a plan with an `in-progress` leg (resume), then one with `done` legs but no `## Implementation` (interrupted), then one with no implementation yet.
|
|
35
|
+
- Multiple ambiguous candidates → ask the user.
|
|
36
|
+
- Already has `## Implementation` → tell user it needs critique, not re-dispatch.
|
|
37
|
+
|
|
38
|
+
2. **Read the plan.** Parse `## Project Context`, `## User Intent`, legs, dispatch order, and any `## Implementation` / `## Critique` sections.
|
|
39
|
+
|
|
40
|
+
3. **Validate plan structure.** Before dispatching, confirm the plan contains:
|
|
41
|
+
- `## Project Context` with at least a `Full validation` entry
|
|
42
|
+
- `## User Intent` (non-empty)
|
|
43
|
+
- Every leg has `Status`, `Confirmed`, `Owned files`, `Verification`, and `Intent`. A leg missing any field is not dispatchable — report the gap.
|
|
44
|
+
- `## Dispatch Order`
|
|
45
|
+
|
|
46
|
+
If any required section is missing or empty, stop and tell the user what needs to be added. Do NOT guess or fill in missing sections yourself.
|
|
47
|
+
|
|
48
|
+
4. **Find dispatchable legs.** Status `pending`, `Confirmed: yes`, and dependencies are `none` or all `done`.
|
|
49
|
+
|
|
50
|
+
5. **Staleness check.** Before each dispatch, `grep_search` for 2–3 key identifiers (function names, class names, selectors, route paths) from the leg intent in the owned files. Go/no-go:
|
|
51
|
+
- **All found** in expected files → proceed.
|
|
52
|
+
- **Renamed or moved** (found elsewhere, or a clear successor exists) → update the leg intent with current names, note the change, proceed.
|
|
53
|
+
- **Owned file missing entirely** → escalate to user; do not dispatch.
|
|
54
|
+
- **Identifier deleted with no successor** → escalate to user; the plan may need re-charting.
|
|
55
|
+
|
|
56
|
+
Do NOT read full files — just confirm the plan is not stale.
|
|
57
|
+
|
|
58
|
+
6. **Compose dispatch prompt.** Include:
|
|
59
|
+
- Leg intent verbatim from the plan
|
|
60
|
+
- Sub-agent contract (below)
|
|
61
|
+
- owned_files and read-only lists
|
|
62
|
+
- Verification command
|
|
63
|
+
- Brief anchoring context from the staleness check
|
|
64
|
+
- Do NOT rewrite intent into step-by-step instructions.
|
|
65
|
+
|
|
66
|
+
7. **Mark in-progress.** `memory str_replace` in the plan: `pending` → `in-progress`.
|
|
67
|
+
|
|
68
|
+
8. **Dispatch.** `runSubagent` with the prompt and `agentName: "gnd-diver"`.
|
|
69
|
+
|
|
70
|
+
9. **Post-dispatch review:**
|
|
71
|
+
a. **Scope check.** List changed files in the workspace and compare the set of modified files against the leg's `owned_files`. Any file modified outside that set is a boundary violation.
|
|
72
|
+
b. Read every file the sub-agent modified.
|
|
73
|
+
c. Verify changes match intent — no scope creep, no skipped requirements.
|
|
74
|
+
d. Check numeric targets (counts, pool sizes, etc.) if the intent specifies them.
|
|
75
|
+
e. Check text quality — irregular plurals, dynamic formatters — if intent involves copy.
|
|
76
|
+
f. Run the leg's verification command.
|
|
77
|
+
g. **Boundary violations:** If the scope check (a) found out-of-scope modifications, revert them to their previous state and either re-dispatch with narrower instructions or record needed changes as deferred edits. Note violations in a `## Boundary Notes` section.
|
|
78
|
+
h. Small corrections → fix directly. Larger problems → re-dispatch with specifics.
|
|
79
|
+
i. Genuine product-direction blockers → escalate to user.
|
|
80
|
+
j. Mark `done` only after review AND verification pass.
|
|
81
|
+
|
|
82
|
+
10. **Update status.** `in-progress` → `done` or `failed`.
|
|
83
|
+
|
|
84
|
+
11. **Loop.** Check for newly dispatchable legs. Repeat from step 4. Do NOT stop after one leg.
|
|
85
|
+
|
|
86
|
+
12. **Integration gate.** When all legs are `done`:
|
|
87
|
+
- Apply deferred shared-file edits.
|
|
88
|
+
- Run sync, build, lint, test, packaging, or release steps from `## Project Context`, workspace instructions, READMEs, or relevant skills.
|
|
89
|
+
- Run the full-validation command or checklist.
|
|
90
|
+
|
|
91
|
+
13. **Finalize statuses.** Every leg must be `done` or `failed`. No `in-progress` or `pending` left.
|
|
92
|
+
|
|
93
|
+
14. **Land the work.** If the plan or project expects landing: compare changed files against the union of all leg owned-file lists + deferred edits, resolve out-of-scope questions, stage, commit with a summary message, push. If explicitly local-only, skip and report local state.
|
|
94
|
+
|
|
95
|
+
15. **Record outcome.** Append to the plan:
|
|
96
|
+
```markdown
|
|
97
|
+
## Implementation
|
|
98
|
+
Commit: <short-sha> | none (local-only)
|
|
99
|
+
Pushed: <date>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
16. **Verify delivery.** Perform the delivery verification from `## Project Context` or project guidance (deployed URL, package version, artifact check, etc.). Re-check as needed for cached surfaces. If it fails, diagnose and fix. If not applicable, say so.
|
|
103
|
+
|
|
104
|
+
17. **Handle failures.** Diagnose, fix, re-run. Escalate only if genuinely stuck.
|
|
105
|
+
|
|
106
|
+
18. **Resumption.** On re-invocation: resolve the plan again, prefer one with `in-progress` legs, complete post-dispatch review or recovery for that leg before dispatching anything new, skip `done` legs, and otherwise resume from the first dispatchable confirmed pending leg.
|
|
107
|
+
|
|
108
|
+
## Plan Format
|
|
109
|
+
|
|
110
|
+
Plans live in `.planning/` as `active-plan-YYYY-MM-DD-HHmm-<slug>.md`.
|
|
111
|
+
|
|
112
|
+
```markdown
|
|
113
|
+
# Plan: [title]
|
|
114
|
+
|
|
115
|
+
## Project Context
|
|
116
|
+
- Sources:
|
|
117
|
+
- `path/to/doc.md` - why it matters
|
|
118
|
+
- Constraints:
|
|
119
|
+
- [Derived rules the plan relies on]
|
|
120
|
+
- Full validation:
|
|
121
|
+
- `command` or checklist
|
|
122
|
+
- Delivery verification:
|
|
123
|
+
- deployed URL, package check, artifact check, local-only, or `none`
|
|
124
|
+
|
|
125
|
+
## User Intent
|
|
126
|
+
[2-4 sentences: what the user wants and why.]
|
|
127
|
+
|
|
128
|
+
## Legs
|
|
129
|
+
|
|
130
|
+
### LEG-[N]: [short title]
|
|
131
|
+
- Status: pending | in-progress | done | failed
|
|
132
|
+
- Depends on: none | LEG-[X], LEG-[Y]
|
|
133
|
+
- Owned files:
|
|
134
|
+
- `path/to/file.ts`
|
|
135
|
+
- Read-only:
|
|
136
|
+
- `path/to/context-file.ts`
|
|
137
|
+
- Deferred shared edits:
|
|
138
|
+
- `package.json` - description
|
|
139
|
+
- Verification: `command`
|
|
140
|
+
- Intent: [what and why]
|
|
141
|
+
|
|
142
|
+
## Dispatch Order
|
|
143
|
+
1. LEG-1 (label) - no dependencies
|
|
144
|
+
2. LEG-2 (label) - depends on LEG-1
|
|
145
|
+
After all complete: deferred edits → full validation → delivery verification → commit → push.
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Sub-Agent Contract
|
|
149
|
+
|
|
150
|
+
Prepend to every dispatch prompt:
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
**CONTRACT — Read before doing anything.**
|
|
155
|
+
|
|
156
|
+
- ONLY create or modify files in your owned_files set.
|
|
157
|
+
- Do NOT modify shared infrastructure (`package.json`, `build.ts`, routers, routes, shared styles, etc.). Report needed changes as deferred edits.
|
|
158
|
+
- Run your verification command. Report pass/fail and error output.
|
|
159
|
+
- Hit a blocker → explain specifically. Do not silently skip.
|
|
160
|
+
- Complete ALL steps. Do not stop partway or ask whether to continue.
|
|
161
|
+
- Report ONLY: (a) files created/modified, (b) deferred edits if any, (c) verification outcome, (d) blockers.
|
|
162
|
+
- No suggestions, follow-ups, or optional improvements.
|
|
163
|
+
|
|
164
|
+
---
|