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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jared Geller
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,104 @@
1
+ # Great Northern Diver
2
+
3
+ A small installer for a planning, execution, and critique prompt set. Writes managed files into `.agents` inside the target repo so the workflow lives next to the code it operates on.
4
+
5
+ ## Install
6
+
7
+ One-shot scaffold. No global install or project dependency required.
8
+
9
+ ```bash
10
+ npx gnd-workflow@latest install
11
+ # or
12
+ pnpm dlx gnd-workflow@latest install
13
+ ```
14
+
15
+ Target another repo or pin a version:
16
+
17
+ ```bash
18
+ npx gnd-workflow@latest install ../my-repo
19
+ npx gnd-workflow@0.1.0 install
20
+ ```
21
+
22
+ ## What It Writes
23
+
24
+ The installer writes managed files into `.agents/`. The workflow itself creates
25
+ `.planning/` at runtime for structured plans:
26
+
27
+ ```text
28
+ .agents/ ← managed by the installer
29
+ .gnd-version.json
30
+ agents/
31
+ gnd-diver.agent.md
32
+ gnd-navigator.agent.md
33
+ skills/
34
+ gnd-chart/SKILL.md
35
+ gnd-critique/SKILL.md
36
+ .planning/ ← created by gnd-chart and gnd-navigator
37
+ active-plan-*.md
38
+ archive/ ← completed plans moved here by gnd-critique
39
+ ```
40
+
41
+ - `.gnd-version.json` records which gnd-workflow version scaffolded the files.
42
+ - `gnd-chart` is the planning skill.
43
+ - `gnd-navigator` dispatches approved plan legs.
44
+ - `gnd-diver` executes one leg.
45
+ - `gnd-critique` reviews delivered work and feeds corrections back into the process.
46
+
47
+ All of these are plain text. Whether to track them in git is up to you:
48
+
49
+ - **`.agents/`** — tracking lets collaborators (or yourself on another machine)
50
+ see the workflow files without re-running the installer. Ignoring keeps
51
+ generated files out of your repo; `npx gnd-workflow@latest install`
52
+ re-creates them.
53
+ - **`.planning/`** — tracking preserves plan history alongside code. Ignoring
54
+ treats plans as ephemeral working state.
55
+
56
+ Neither directory needs to be tracked or ignored for the workflow to function.
57
+
58
+ ## Philosophy
59
+
60
+ This is an agentic-development-first workflow. The navigator commits and pushes
61
+ directly to the default branch — no feature branches, no pull requests, no
62
+ staging area. Plan → execute → critique → push, in a tight loop.
63
+
64
+ That model works well for solo and hobby projects where you're the only
65
+ collaborator and velocity matters more than ceremony
66
+ ([peninsular-reveries](https://github.com/ironloon/peninsular-reveries) is
67
+ the project it was built around). It's a poor fit for teams that rely on branch
68
+ protection, code review gates, or CI pipelines that run before merge.
69
+
70
+ If your project needs those guardrails, you can still use the planning and
71
+ critique skills on their own — just override the navigator's landing step to
72
+ target a branch instead of pushing directly.
73
+
74
+ ## Updating
75
+
76
+ ```bash
77
+ npx gnd-workflow@latest install
78
+ ```
79
+
80
+ If a managed file already exists:
81
+
82
+ - unchanged files are left alone
83
+ - changed files trigger an interactive prompt before overwrite
84
+ - non-interactive runs fail closed unless you pass `--force`
85
+
86
+ Flags:
87
+
88
+ - `--dry-run` shows what would change.
89
+ - `--force` replaces differing managed files without prompting.
90
+ - `--version` shows the installed version.
91
+ - `-C, --cwd <path>` resolves the target project root from a specific working directory.
92
+
93
+ The target project root must be a real directory; symlinked and junctioned roots are rejected.
94
+
95
+ ## Development
96
+
97
+ ```bash
98
+ npm test
99
+ node ./bin/gnd-workflow.js help
100
+ node ./bin/gnd-workflow.js install --dry-run
101
+ npm pack --dry-run
102
+ ```
103
+
104
+ Maintainer release steps live in [RELEASING.md](RELEASING.md).
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { main } from "../src/cli.js";
4
+
5
+ const exitCode = await main(process.argv.slice(2));
6
+ process.exit(exitCode);
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "gnd-workflow",
3
+ "version": "0.1.0",
4
+ "description": "Installable agent and skill files for coding projects.",
5
+ "type": "module",
6
+ "bin": {
7
+ "gnd-workflow": "bin/gnd-workflow.js"
8
+ },
9
+ "exports": {
10
+ ".": "./src/install.js"
11
+ },
12
+ "files": [
13
+ "bin",
14
+ "src/!(*.test|*-test-helpers).js",
15
+ "templates",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "scripts": {
20
+ "test": "node --test src/**/*.test.js package-smoke.test.js",
21
+ "prepublishOnly": "npm test"
22
+ },
23
+ "keywords": [
24
+ "workflow",
25
+ "planning",
26
+ "orchestration",
27
+ "critique",
28
+ "project-planning",
29
+ "agent-workflow",
30
+ "developer-workflow",
31
+ "great-northern-diver"
32
+ ],
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/ironloon/the-great-northern-diver.git"
36
+ },
37
+ "homepage": "https://github.com/ironloon/the-great-northern-diver#readme",
38
+ "bugs": {
39
+ "url": "https://github.com/ironloon/the-great-northern-diver/issues"
40
+ },
41
+ "license": "MIT",
42
+ "engines": {
43
+ "node": ">=22"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ }
48
+ }
package/src/cli.js ADDED
@@ -0,0 +1,319 @@
1
+ import path from "node:path";
2
+ import { createInterface } from "node:readline/promises";
3
+ import { parseArgs } from "node:util";
4
+ import { DEFAULT_INSTALL_DIR, installWorkflow, readPackageVersion } from "./install.js";
5
+ import { normalizePathForContent } from "./path-policy.js";
6
+
7
+ function printHelp(stream) {
8
+ stream.write(
9
+ [
10
+ "Great Northern Diver",
11
+ "",
12
+ "Usage:",
13
+ " gnd-workflow install [project-root] [options]",
14
+ " gnd-workflow help",
15
+ "",
16
+ `Writes managed agent files into ${normalizePathForContent(DEFAULT_INSTALL_DIR)} in the target project.`,
17
+ "",
18
+ "Options:",
19
+ " --force Replace differing managed files without prompting",
20
+ " --dry-run Print the install plan without writing files",
21
+ " -C, --cwd <path> Resolve paths from a specific working directory",
22
+ " --version Show version number",
23
+ " --help Show this help text",
24
+ ""
25
+ ].join("\n")
26
+ );
27
+ }
28
+
29
+ function formatPath(projectRoot, filePath) {
30
+ return path.relative(projectRoot, filePath).replaceAll("\\", "/") || ".";
31
+ }
32
+
33
+ function printFileGroup(stream, title, projectRoot, entries) {
34
+ if (entries.length === 0) {
35
+ return;
36
+ }
37
+
38
+ stream.write(`${title}:\n`);
39
+
40
+ for (const entry of entries) {
41
+ stream.write(` ${entry.status.padEnd(9)} ${formatPath(projectRoot, entry.path)}\n`);
42
+ }
43
+ }
44
+
45
+ function printConflicts(stream, conflicts) {
46
+ if (conflicts.length === 0) {
47
+ return;
48
+ }
49
+
50
+ stream.write("Conflicts requiring confirmation without --force:\n");
51
+
52
+ for (const conflict of conflicts) {
53
+ stream.write(` overwrite ${conflict.relativePath}\n`);
54
+ }
55
+ }
56
+
57
+ function parseInstallArgs(argv, cwd) {
58
+ const { values, positionals } = parseArgs({
59
+ args: argv,
60
+ allowPositionals: true,
61
+ strict: true,
62
+ options: {
63
+ "dry-run": {
64
+ type: "boolean"
65
+ },
66
+ force: {
67
+ type: "boolean"
68
+ },
69
+ cwd: {
70
+ type: "string",
71
+ short: "C"
72
+ },
73
+ help: {
74
+ type: "boolean"
75
+ },
76
+ version: {
77
+ type: "boolean"
78
+ },
79
+ }
80
+ });
81
+
82
+ if (positionals.length > 1) {
83
+ throw new Error(`Unexpected positional argument '${positionals[1]}'.`);
84
+ }
85
+
86
+ const baseProjectRoot = values.cwd ? path.resolve(cwd, values.cwd) : cwd;
87
+ const options = {
88
+ projectRoot: baseProjectRoot,
89
+ dryRun: values["dry-run"] ?? false,
90
+ force: values.force ?? false,
91
+ version: values.version ?? false,
92
+ help: values.help ?? false
93
+ };
94
+
95
+ if (positionals.length === 1) {
96
+ options.projectRoot = path.resolve(baseProjectRoot, positionals[0]);
97
+ }
98
+
99
+ return options;
100
+ }
101
+
102
+ function formatErrorMessage(error) {
103
+ return error?.message !== undefined ? String(error.message) : String(error);
104
+ }
105
+
106
+ function formatConflictPrompt(conflict) {
107
+ return [
108
+ "Managed file differs from the packaged version:",
109
+ ` ${conflict.relativePath}`,
110
+ "Replace it? [y]es/[n]o/[a]ll: "
111
+ ].join("\n");
112
+ }
113
+
114
+ const DEFAULT_PROMPT_TIMEOUT_MS = 300_000;
115
+
116
+ function questionWithTtyLifecycle(prompt, input, message, timeoutMs) {
117
+ if (input.readableEnded || input.destroyed) {
118
+ return Promise.resolve(null);
119
+ }
120
+
121
+ return new Promise((resolve, reject) => {
122
+ let settled = false;
123
+ let timer = null;
124
+
125
+ const cleanup = () => {
126
+ if (timer !== null) clearTimeout(timer);
127
+ input.off("end", handleInputEnded);
128
+ input.off("close", handleInputClosed);
129
+ input.off("error", handleInputError);
130
+ };
131
+
132
+ const settle = (callback) => (value) => {
133
+ if (settled) {
134
+ return;
135
+ }
136
+
137
+ settled = true;
138
+ cleanup();
139
+ callback(value);
140
+ };
141
+
142
+ const handleInputEnded = settle(() => {
143
+ prompt.close();
144
+ resolve(null);
145
+ });
146
+ const handleInputClosed = settle(() => {
147
+ prompt.close();
148
+ resolve(null);
149
+ });
150
+ const handleInputError = settle(reject);
151
+ const handleTimeout = settle(() => {
152
+ prompt.close();
153
+ resolve(null);
154
+ });
155
+
156
+ input.once("end", handleInputEnded);
157
+ input.once("close", handleInputClosed);
158
+ input.once("error", handleInputError);
159
+
160
+ if (timeoutMs != null && timeoutMs > 0) {
161
+ timer = setTimeout(handleTimeout, timeoutMs);
162
+ }
163
+
164
+ prompt.question(message).then(
165
+ settle(resolve),
166
+ settle(reject)
167
+ );
168
+ });
169
+ }
170
+
171
+ function createInteractiveConflictPrompter(input, output, options = {}) {
172
+ if (!input?.isTTY || !output?.isTTY) {
173
+ return null;
174
+ }
175
+
176
+ const promptTimeoutMs = options.promptTimeoutMs ?? DEFAULT_PROMPT_TIMEOUT_MS;
177
+ let acceptAll = false;
178
+ let prompt = null;
179
+
180
+ return {
181
+ async confirmManagedFileConflict(conflict) {
182
+ if (acceptAll) {
183
+ return true;
184
+ }
185
+
186
+ prompt ??= createInterface({
187
+ input,
188
+ output
189
+ });
190
+
191
+ while (true) {
192
+ const answer = await questionWithTtyLifecycle(prompt, input, formatConflictPrompt(conflict), promptTimeoutMs);
193
+
194
+ if (answer === null) {
195
+ return false;
196
+ }
197
+
198
+ const normalizedAnswer = answer.trim().toLowerCase();
199
+
200
+ if (normalizedAnswer === "y" || normalizedAnswer === "yes") {
201
+ return true;
202
+ }
203
+
204
+ if (normalizedAnswer === "a" || normalizedAnswer === "all") {
205
+ acceptAll = true;
206
+ return true;
207
+ }
208
+
209
+ if (normalizedAnswer === "" || normalizedAnswer === "n" || normalizedAnswer === "no") {
210
+ return false;
211
+ }
212
+
213
+ output.write("Please answer y, n, or a.\n");
214
+ }
215
+ },
216
+ close() {
217
+ prompt?.close();
218
+ }
219
+ };
220
+ }
221
+
222
+ function printInstallResult(stream, result) {
223
+ stream.write("Great Northern Diver install summary\n\n");
224
+ stream.write(`Project root: ${result.projectRoot}\n`);
225
+ stream.write(`Install root: ${normalizePathForContent(result.installDir)}\n`);
226
+
227
+ if (result.dryRun) {
228
+ stream.write("Mode: dry-run\n");
229
+ }
230
+
231
+ stream.write("\n");
232
+ printFileGroup(stream, "Managed files", result.projectRoot, result.managedFiles);
233
+ printConflicts(stream, result.conflicts ?? []);
234
+ }
235
+
236
+ async function runInstallCommand(rest, io, promptController) {
237
+ const stdout = io.stdout ?? process.stdout;
238
+ const stderr = io.stderr ?? process.stderr;
239
+ const cwd = io.cwd ?? process.cwd();
240
+ const runInstallWorkflow = io.installWorkflow ?? installWorkflow;
241
+
242
+ let options;
243
+
244
+ try {
245
+ options = parseInstallArgs(rest, cwd);
246
+ } catch (error) {
247
+ stderr.write(`${formatErrorMessage(error)}\n\n`);
248
+ printHelp(stderr);
249
+ return 1;
250
+ }
251
+
252
+ if (options.help) {
253
+ printHelp(stdout);
254
+ return 0;
255
+ }
256
+
257
+ if (options.version) {
258
+ stdout.write(`${await readPackageVersion()}\n`);
259
+ return 0;
260
+ }
261
+
262
+ if (options.dryRun && options.force) {
263
+ stderr.write("Warning: --force has no effect in --dry-run mode.\n");
264
+ }
265
+
266
+ const result = await runInstallWorkflow(
267
+ promptController !== null && !options.force && !options.dryRun
268
+ ? {
269
+ ...options,
270
+ confirmManagedFileConflict: promptController.confirmManagedFileConflict
271
+ }
272
+ : options
273
+ );
274
+
275
+ printInstallResult(stdout, result);
276
+ return 0;
277
+ }
278
+
279
+ export async function main(argv, io = {}) {
280
+ const stdout = io.stdout ?? process.stdout;
281
+ const stderr = io.stderr ?? process.stderr;
282
+ const stdin = io.stdin ?? process.stdin;
283
+ const [command = "install", ...rest] = argv;
284
+ const promptController = io.confirmManagedFileConflict
285
+ ? {
286
+ confirmManagedFileConflict: io.confirmManagedFileConflict,
287
+ close() {}
288
+ }
289
+ : createInteractiveConflictPrompter(stdin, stderr, { promptTimeoutMs: io.promptTimeoutMs });
290
+
291
+ if (command === "version" || command === "--version" || command === "-v") {
292
+ stdout.write(`${await readPackageVersion()}\n`);
293
+ promptController?.close();
294
+ return 0;
295
+ }
296
+
297
+ if (command === "help" || command === "--help" || command === "-h") {
298
+ printHelp(stdout);
299
+ promptController?.close();
300
+ return 0;
301
+ }
302
+
303
+ if (command !== "install") {
304
+ stderr.write(`Unknown command '${command}'.\n\n`);
305
+ printHelp(stderr);
306
+ promptController?.close();
307
+ return 1;
308
+ }
309
+
310
+ try {
311
+ return await runInstallCommand(rest, io, promptController);
312
+ } catch (error) {
313
+ stderr.write(`${formatErrorMessage(error)}\n`);
314
+ return 1;
315
+ } finally {
316
+ promptController?.close();
317
+ }
318
+ }
319
+