@grainulation/orchard 1.0.1 → 1.0.4

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/CONTRIBUTING.md CHANGED
@@ -15,16 +15,20 @@ No `npm install` needed -- orchard has zero dependencies.
15
15
  ## How to contribute
16
16
 
17
17
  ### Report a bug
18
+
18
19
  Open an issue with:
20
+
19
21
  - What you expected
20
22
  - What happened instead
21
23
  - Your Node version (`node --version`)
22
24
  - Steps to reproduce
23
25
 
24
26
  ### Suggest a feature
27
+
25
28
  Open an issue describing the use case, not just the solution. "I need X because Y" is more useful than "add X."
26
29
 
27
30
  ### Submit a PR
31
+
28
32
  1. Fork the repo
29
33
  2. Create a branch (`git checkout -b fix/description`)
30
34
  3. Make your changes
@@ -59,7 +63,7 @@ The key architectural principle: **orchard operates above individual sprints.**
59
63
 
60
64
  - Zero dependencies. If you need something, write it or use Node built-ins.
61
65
  - No transpilation. Ship what you write.
62
- - ESM imports (`import`/`export`). Node 18+ required.
66
+ - ESM imports (`import`/`export`). Node 20+ required.
63
67
  - Keep functions small. If a function needs a scroll, split it.
64
68
  - No emojis in code, CLI output, or dashboards.
65
69
 
@@ -74,11 +78,13 @@ Tests use Node's built-in test runner. No test framework dependencies.
74
78
  ## Commit messages
75
79
 
76
80
  Follow the existing pattern:
81
+
77
82
  ```
78
83
  orchard: <what changed>
79
84
  ```
80
85
 
81
86
  Examples:
87
+
82
88
  ```
83
89
  orchard: add cross-sprint dependency graph
84
90
  orchard: fix timeline calculation for overlapping sprints
package/README.md CHANGED
@@ -3,7 +3,8 @@
3
3
  </p>
4
4
 
5
5
  <p align="center">
6
- <a href="https://www.npmjs.com/package/@grainulation/orchard"><img src="https://img.shields.io/npm/v/@grainulation/orchard" alt="npm version"></a> <a href="https://www.npmjs.com/package/@grainulation/orchard"><img src="https://img.shields.io/npm/dm/@grainulation/orchard" alt="npm downloads"></a> <a href="https://github.com/grainulation/orchard/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/@grainulation/orchard" alt="license"></a> <a href="https://nodejs.org"><img src="https://img.shields.io/node/v/@grainulation/orchard" alt="node"></a> <a href="https://github.com/grainulation/orchard/actions"><img src="https://github.com/grainulation/orchard/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
6
+ <a href="https://www.npmjs.com/package/@grainulation/orchard"><img src="https://img.shields.io/npm/v/@grainulation/orchard?label=%40grainulation%2Forchard" alt="npm version"></a> <a href="https://www.npmjs.com/package/@grainulation/orchard"><img src="https://img.shields.io/npm/dm/@grainulation/orchard" alt="npm downloads"></a> <a href="https://github.com/grainulation/orchard/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="license"></a> <a href="https://nodejs.org"><img src="https://img.shields.io/node/v/@grainulation/orchard" alt="node"></a> <a href="https://github.com/grainulation/orchard/actions"><img src="https://github.com/grainulation/orchard/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
7
+ <a href="https://deepwiki.com/grainulation/orchard"><img src="https://deepwiki.com/badge.svg" alt="Explore on DeepWiki"></a>
7
8
  </p>
8
9
 
9
10
  <p align="center"><strong>Multi-sprint orchestration and dependency tracking.</strong></p>
@@ -69,15 +70,15 @@ orchard dashboard # Generate unified HTML dashboard
69
70
 
70
71
  ## CLI
71
72
 
72
- | Command | Description |
73
- |---------|-------------|
74
- | `orchard plan` | Show sprint dependency graph |
75
- | `orchard status` | Show status of all tracked sprints |
76
- | `orchard assign <path> <person>` | Assign a person to a sprint |
77
- | `orchard sync` | Sync sprint states from directories |
78
- | `orchard dashboard [outfile]` | Generate unified HTML dashboard |
79
- | `orchard init` | Initialize orchard.json |
80
- | `orchard serve` | Start the portfolio dashboard web server |
73
+ | Command | Description |
74
+ | -------------------------------- | ---------------------------------------- |
75
+ | `orchard plan` | Show sprint dependency graph |
76
+ | `orchard status` | Show status of all tracked sprints |
77
+ | `orchard assign <path> <person>` | Assign a person to a sprint |
78
+ | `orchard sync` | Sync sprint states from directories |
79
+ | `orchard dashboard [outfile]` | Generate unified HTML dashboard |
80
+ | `orchard init` | Initialize orchard.json |
81
+ | `orchard serve` | Start the portfolio dashboard web server |
81
82
 
82
83
  ## Conflict detection
83
84
 
@@ -92,16 +93,16 @@ Node built-in modules only.
92
93
 
93
94
  ## Part of the grainulation ecosystem
94
95
 
95
- | Tool | Role |
96
- |------|------|
97
- | [wheat](https://github.com/grainulation/wheat) | Research engine -- grow structured evidence |
98
- | [farmer](https://github.com/grainulation/farmer) | Permission dashboard -- approve AI actions in real time |
99
- | [barn](https://github.com/grainulation/barn) | Shared tools -- templates, validators, sprint detection |
100
- | [mill](https://github.com/grainulation/mill) | Format conversion -- export to PDF, CSV, slides, 24 formats |
101
- | [silo](https://github.com/grainulation/silo) | Knowledge storage -- reusable claim libraries and packs |
102
- | [harvest](https://github.com/grainulation/harvest) | Analytics -- cross-sprint patterns and prediction scoring |
103
- | **orchard** | Orchestration -- multi-sprint coordination and dependencies |
104
- | [grainulation](https://github.com/grainulation/grainulation) | Unified CLI -- single entry point to the ecosystem |
96
+ | Tool | Role |
97
+ | ------------------------------------------------------------ | ----------------------------------------------------------- |
98
+ | [wheat](https://github.com/grainulation/wheat) | Research engine -- grow structured evidence |
99
+ | [farmer](https://github.com/grainulation/farmer) | Permission dashboard -- approve AI actions in real time |
100
+ | [barn](https://github.com/grainulation/barn) | Shared tools -- templates, validators, sprint detection |
101
+ | [mill](https://github.com/grainulation/mill) | Format conversion -- export to PDF, CSV, slides, 24 formats |
102
+ | [silo](https://github.com/grainulation/silo) | Knowledge storage -- reusable claim libraries and packs |
103
+ | [harvest](https://github.com/grainulation/harvest) | Analytics -- cross-sprint patterns and prediction scoring |
104
+ | **orchard** | Orchestration -- multi-sprint coordination and dependencies |
105
+ | [grainulation](https://github.com/grainulation/grainulation) | Unified CLI -- single entry point to the ecosystem |
105
106
 
106
107
  ## License
107
108
 
package/bin/orchard.js CHANGED
@@ -1,74 +1,79 @@
1
1
  #!/usr/bin/env node
2
- 'use strict';
2
+ "use strict";
3
3
 
4
- const path = require('node:path');
5
- const fs = require('node:fs');
4
+ const path = require("node:path");
5
+ const fs = require("node:fs");
6
6
 
7
- const verbose = process.argv.includes('--verbose') || process.argv.includes('-v');
7
+ const verbose =
8
+ process.argv.includes("--verbose") || process.argv.includes("-v");
8
9
  function vlog(...a) {
9
10
  if (!verbose) return;
10
11
  const ts = new Date().toISOString();
11
- process.stderr.write(`[${ts}] orchard: ${a.join(' ')}\n`);
12
+ process.stderr.write(`[${ts}] orchard: ${a.join(" ")}\n`);
12
13
  }
13
14
 
14
15
  const COMMANDS = {
15
- init: 'Initialize orchard.json in the current directory',
16
- plan: 'Show sprint dependency graph as ASCII',
17
- status: 'Show status of all tracked sprints',
18
- assign: 'Assign a person to a sprint',
19
- sync: 'Sync sprint states from their directories',
20
- dashboard: 'Generate unified HTML dashboard',
21
- serve: 'Start the portfolio dashboard web server',
22
- connect: 'Connect to a farmer instance',
23
- doctor: 'Check health of orchard setup',
24
- help: 'Show this help message',
16
+ init: "Initialize orchard.json in the current directory",
17
+ plan: "Show sprint dependency graph (--format mermaid|ascii, --mermaid)",
18
+ status: "Show status of all tracked sprints",
19
+ conflicts: "Show cross-sprint conflicts (--severity critical|warning|info)",
20
+ assign: "Assign a person to a sprint",
21
+ sync: "Sync sprint states from their directories",
22
+ decompose: "Auto-decompose a question into sub-sprints",
23
+ hackathon: "Hackathon coordinator (init|team|status|end)",
24
+ dashboard: "Generate unified HTML dashboard",
25
+ serve: "Start the portfolio dashboard web server",
26
+ connect: "Connect to a farmer instance",
27
+ next: "Show sprints ready for grainulator execution",
28
+ doctor: "Check health of orchard setup",
29
+ help: "Show this help message",
25
30
  };
26
31
 
27
32
  function loadConfig(dir) {
28
- const configPath = path.join(dir, 'orchard.json');
33
+ const configPath = path.join(dir, "orchard.json");
29
34
  if (!fs.existsSync(configPath)) {
30
35
  return null;
31
36
  }
32
- return JSON.parse(fs.readFileSync(configPath, 'utf8'));
37
+ return JSON.parse(fs.readFileSync(configPath, "utf8"));
33
38
  }
34
39
 
35
40
  function findOrchardRoot() {
36
41
  let dir = process.cwd();
37
42
  while (dir !== path.dirname(dir)) {
38
- if (fs.existsSync(path.join(dir, 'orchard.json'))) return dir;
43
+ if (fs.existsSync(path.join(dir, "orchard.json"))) return dir;
39
44
  dir = path.dirname(dir);
40
45
  }
41
46
  return null;
42
47
  }
43
48
 
44
49
  function printHelp() {
45
- console.log('');
46
- console.log(' orchard - Multi-sprint research orchestrator');
47
- console.log('');
48
- console.log(' Usage: orchard <command> [options]');
49
- console.log('');
50
- console.log(' Commands:');
50
+ console.log("");
51
+ console.log(" orchard - Multi-sprint research orchestrator");
52
+ console.log("");
53
+ console.log(" Usage: orchard <command> [options]");
54
+ console.log("");
55
+ console.log(" Commands:");
51
56
  for (const [cmd, desc] of Object.entries(COMMANDS)) {
52
57
  console.log(` ${cmd.padEnd(12)} ${desc}`);
53
58
  }
54
- console.log('');
55
- console.log(' Serve options:');
56
- console.log(' --port 9097 Port for the web server (default: 9097)');
57
- console.log(' --root <dir> Root directory to scan for sprints');
58
- console.log('');
59
- console.log(' Connect:');
60
- console.log(' orchard connect farmer --url http://localhost:9090');
61
- console.log('');
62
- console.log(' Config: orchard.json in project root');
63
- console.log('');
59
+ console.log("");
60
+ console.log(" Serve options:");
61
+ console.log(" --port 9097 Port for the web server (default: 9097)");
62
+ console.log(" --root <dir> Root directory to scan for sprints");
63
+ console.log("");
64
+ console.log(" Connect:");
65
+ console.log(" orchard connect farmer --url http://localhost:9090");
66
+ console.log("");
67
+ console.log(" Config: orchard.json in project root");
68
+ console.log("");
64
69
  }
65
70
 
66
71
  async function main() {
67
72
  const args = process.argv.slice(2);
68
- const command = args[0] || 'help';
69
- vlog('startup', `command=${command}`, `cwd=${process.cwd()}`);
73
+ const command = args[0] || "help";
74
+ vlog("startup", `command=${command}`, `cwd=${process.cwd()}`);
70
75
 
71
- if (command === 'help' || command === '--help' || command === '-h') {
76
+ if (command === "help" || command === "--help" || command === "-h") {
72
77
  printHelp();
73
78
  process.exit(0);
74
79
  }
@@ -80,30 +85,30 @@ async function main() {
80
85
  }
81
86
 
82
87
  // Serve command — start the HTTP server (ESM module)
83
- if (command === 'serve') {
84
- const serverPath = path.join(__dirname, '..', 'lib', 'server.js');
85
- const { spawn } = require('node:child_process');
88
+ if (command === "serve") {
89
+ const serverPath = path.join(__dirname, "..", "lib", "server.js");
90
+ const { spawn } = require("node:child_process");
86
91
 
87
92
  // Forward remaining args to the server
88
93
  const serverArgs = args.slice(1);
89
94
  const child = spawn(process.execPath, [serverPath, ...serverArgs], {
90
- stdio: 'inherit',
95
+ stdio: "inherit",
91
96
  });
92
97
 
93
- child.on('close', (code) => process.exit(code ?? 0));
94
- child.on('error', (err) => {
98
+ child.on("close", (code) => process.exit(code ?? 0));
99
+ child.on("error", (err) => {
95
100
  console.error(`orchard: failed to start server: ${err.message}`);
96
101
  process.exit(1);
97
102
  });
98
- process.on('SIGTERM', () => child.kill('SIGTERM'));
99
- process.on('SIGINT', () => child.kill('SIGINT'));
103
+ process.on("SIGTERM", () => child.kill("SIGTERM"));
104
+ process.on("SIGINT", () => child.kill("SIGINT"));
100
105
  return;
101
106
  }
102
107
 
103
- if (command === 'connect') {
104
- const { connect: farmerConnect } = require('../lib/farmer.js');
105
- const connectArgs = process.argv.slice(process.argv.indexOf('connect') + 1);
106
- const rootIdx = connectArgs.indexOf('--root');
108
+ if (command === "connect") {
109
+ const { connect: farmerConnect } = require("../lib/farmer.js");
110
+ const connectArgs = process.argv.slice(process.argv.indexOf("connect") + 1);
111
+ const rootIdx = connectArgs.indexOf("--root");
107
112
  let targetDir = process.cwd();
108
113
  if (rootIdx !== -1 && connectArgs[rootIdx + 1]) {
109
114
  targetDir = path.resolve(connectArgs[rootIdx + 1]);
@@ -112,85 +117,238 @@ async function main() {
112
117
  return;
113
118
  }
114
119
 
115
- if (command === 'init') {
116
- const { parseArgs } = require('node:util');
120
+ if (command === "init") {
121
+ const { parseArgs } = require("node:util");
117
122
  let rootDir = process.cwd();
118
123
  try {
119
- const { values } = parseArgs({ args: process.argv.slice(3), options: { root: { type: 'string' } }, allowPositionals: true });
124
+ const { values } = parseArgs({
125
+ args: process.argv.slice(3),
126
+ options: { root: { type: "string" } },
127
+ allowPositionals: true,
128
+ });
120
129
  if (values.root) rootDir = path.resolve(values.root);
121
- } catch (_) { /* ignore parse errors for init */ }
122
- const configPath = path.join(rootDir, 'orchard.json');
130
+ } catch (_) {
131
+ /* ignore parse errors for init */
132
+ }
133
+ const configPath = path.join(rootDir, "orchard.json");
123
134
  if (fs.existsSync(configPath)) {
124
- console.error('orchard: orchard.json already exists');
135
+ console.error("orchard: orchard.json already exists");
125
136
  process.exit(1);
126
137
  }
127
- const defaultConfig = { sprints: [], settings: { sync_interval: 'manual' } };
128
- fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2) + '\n', 'utf8');
129
- console.log('Initialized orchard.json add sprints with `orchard plan`');
138
+ const defaultConfig = {
139
+ sprints: [],
140
+ settings: { sync_interval: "manual" },
141
+ };
142
+ fs.writeFileSync(
143
+ configPath,
144
+ JSON.stringify(defaultConfig, null, 2) + "\n",
145
+ "utf8",
146
+ );
147
+ console.log("Initialized orchard.json — add sprints with `orchard plan`");
130
148
  process.exit(0);
131
149
  }
132
150
 
133
151
  const root = findOrchardRoot();
134
- if (!root && command !== 'help' && command !== 'doctor') {
135
- console.error('orchard: no orchard.json found. Run from a directory with orchard.json or a subdirectory.');
136
- console.error('');
137
- console.error('Create one:');
152
+ if (!root && command !== "help" && command !== "doctor") {
153
+ console.error(
154
+ "orchard: no orchard.json found. Run from a directory with orchard.json or a subdirectory.",
155
+ );
156
+ console.error("");
157
+ console.error("Create one:");
138
158
  console.error(' { "sprints": [] }');
139
159
  process.exit(1);
140
160
  }
141
161
 
142
162
  const config = root ? loadConfig(root) : { sprints: [] };
143
- const jsonMode = args.includes('--json');
163
+ const jsonMode = args.includes("--json");
144
164
 
145
165
  switch (command) {
146
- case 'plan': {
166
+ case "plan": {
147
167
  if (jsonMode) {
148
- const { buildGraph, topoSort, detectCycles } = require('../lib/planner.js');
168
+ const {
169
+ buildGraph,
170
+ topoSort,
171
+ detectCycles,
172
+ } = require("../lib/planner.js");
149
173
  const graph = buildGraph(config);
150
174
  const order = topoSort(config);
151
175
  const cycles = detectCycles(config);
152
176
  console.log(JSON.stringify({ graph, order, cycles }, null, 2));
153
177
  break;
154
178
  }
155
- const { printDependencyGraph } = require('../lib/planner.js');
156
- printDependencyGraph(config, root);
179
+ const fmtIdx = args.indexOf("--format");
180
+ const planFormat =
181
+ fmtIdx !== -1 && args[fmtIdx + 1] ? args[fmtIdx + 1] : null;
182
+ if (args.includes("--mermaid") || planFormat === "mermaid") {
183
+ const { generateMermaid } = require("../lib/planner.js");
184
+ console.log(generateMermaid(config));
185
+ break;
186
+ }
187
+ if (planFormat === "ascii" || !planFormat) {
188
+ const { printDependencyGraph } = require("../lib/planner.js");
189
+ printDependencyGraph(config, root);
190
+ break;
191
+ }
192
+ console.error(
193
+ `orchard: unknown plan format: ${planFormat}. Supported: ascii, mermaid`,
194
+ );
195
+ process.exit(1);
157
196
  break;
158
197
  }
159
- case 'status': {
198
+ case "status": {
160
199
  if (jsonMode) {
161
- const { getStatusData } = require('../lib/tracker.js');
200
+ const { getStatusData } = require("../lib/tracker.js");
162
201
  const data = getStatusData(config, root);
163
202
  console.log(JSON.stringify(data, null, 2));
164
203
  break;
165
204
  }
166
- const { printStatus } = require('../lib/tracker.js');
205
+ const { printStatus } = require("../lib/tracker.js");
167
206
  printStatus(config, root);
168
207
  break;
169
208
  }
170
- case 'assign': {
209
+ case "assign": {
171
210
  const sprintPath = args[1];
172
211
  const person = args[2];
173
212
  if (!sprintPath || !person) {
174
- console.error('orchard: usage: orchard assign <sprint-path> <person>');
213
+ console.error("orchard: usage: orchard assign <sprint-path> <person>");
175
214
  process.exit(1);
176
215
  }
177
- const { assignSprint } = require('../lib/assignments.js');
216
+ const { assignSprint } = require("../lib/assignments.js");
178
217
  assignSprint(config, root, sprintPath, person);
179
218
  break;
180
219
  }
181
- case 'sync': {
182
- const { syncAll } = require('../lib/sync.js');
220
+ case "sync": {
221
+ const { syncAll } = require("../lib/sync.js");
183
222
  syncAll(config, root);
184
223
  break;
185
224
  }
186
- case 'dashboard': {
187
- const { generateDashboard } = require('../lib/dashboard.js');
188
- const outPath = args[1] || path.join(root, 'orchard-dashboard.html');
225
+ case "dashboard": {
226
+ const { generateDashboard } = require("../lib/dashboard.js");
227
+ const outPath = args[1] || path.join(root, "orchard-dashboard.html");
189
228
  generateDashboard(config, root, outPath);
190
229
  break;
191
230
  }
192
- case 'doctor': {
193
- const { runChecks, printReport } = require('../lib/doctor.js');
231
+ case "conflicts": {
232
+ const {
233
+ detectConflicts,
234
+ filterBySeverity,
235
+ printConflicts,
236
+ } = require("../lib/conflicts.js");
237
+ const sevIdx = args.indexOf("--severity");
238
+ const severity =
239
+ sevIdx !== -1 && args[sevIdx + 1] ? args[sevIdx + 1] : "info";
240
+ if (jsonMode) {
241
+ const all = detectConflicts(config, root);
242
+ const filtered = filterBySeverity(all, severity);
243
+ console.log(
244
+ JSON.stringify(
245
+ { conflicts: filtered, count: filtered.length },
246
+ null,
247
+ 2,
248
+ ),
249
+ );
250
+ break;
251
+ }
252
+ printConflicts(config, root, { severity });
253
+ break;
254
+ }
255
+ case "decompose": {
256
+ const question = args
257
+ .filter((a) => !a.startsWith("--"))
258
+ .slice(1)
259
+ .join(" ");
260
+ if (!question) {
261
+ console.error(
262
+ 'orchard: usage: orchard decompose "<question>" [--apply] [--max <n>]',
263
+ );
264
+ process.exit(1);
265
+ }
266
+ const maxIdx = args.indexOf("--max");
267
+ const maxSprints =
268
+ maxIdx !== -1 && args[maxIdx + 1] ? parseInt(args[maxIdx + 1], 10) : 5;
269
+ if (args.includes("--apply")) {
270
+ const { applyDecomposition } = require("../lib/decompose.js");
271
+ const sprints = applyDecomposition(root, question, { maxSprints });
272
+ console.log(`Created ${sprints.length} sub-sprints for: "${question}"`);
273
+ for (const s of sprints) {
274
+ console.log(` ${s.path}`);
275
+ }
276
+ } else {
277
+ const { printDecomposition } = require("../lib/decompose.js");
278
+ printDecomposition(question, { maxSprints });
279
+ }
280
+ break;
281
+ }
282
+ case "hackathon": {
283
+ const sub = args[1];
284
+ const hack = require("../lib/hackathon.js");
285
+ switch (sub) {
286
+ case "init": {
287
+ const nameIdx = args.indexOf("--name");
288
+ const durIdx = args.indexOf("--duration");
289
+ const name =
290
+ nameIdx !== -1 && args[nameIdx + 1] ? args[nameIdx + 1] : undefined;
291
+ const duration =
292
+ durIdx !== -1 && args[durIdx + 1]
293
+ ? parseInt(args[durIdx + 1], 10)
294
+ : undefined;
295
+ const h = hack.initHackathon(root, { name, duration });
296
+ console.log(`Hackathon "${h.name}" started — ends at ${h.endTime}`);
297
+ break;
298
+ }
299
+ case "team": {
300
+ const teamName = args[2];
301
+ const qIdx = args.indexOf("--question");
302
+ const question =
303
+ qIdx !== -1 ? args.slice(qIdx + 1).join(" ") : undefined;
304
+ if (!teamName) {
305
+ console.error(
306
+ 'orchard: usage: orchard hackathon team <name> [--question "..."]',
307
+ );
308
+ process.exit(1);
309
+ }
310
+ const result = hack.addTeam(root, teamName, question);
311
+ console.log(
312
+ `Team "${result.teamName}" added — sprint: ${result.sprintPath}`,
313
+ );
314
+ break;
315
+ }
316
+ case "end": {
317
+ const board = hack.endHackathon(root);
318
+ console.log("Hackathon ended! Final leaderboard:");
319
+ for (let i = 0; i < board.length; i++) {
320
+ const t = board[i];
321
+ console.log(
322
+ ` ${i + 1}. ${t.team} — ${t.score}pts (${t.claimCount} claims)`,
323
+ );
324
+ }
325
+ break;
326
+ }
327
+ default: {
328
+ if (jsonMode) {
329
+ const timer = hack.timerStatus(root);
330
+ const board = hack.leaderboard(root);
331
+ console.log(JSON.stringify({ timer, leaderboard: board }, null, 2));
332
+ } else {
333
+ hack.printHackathon(root);
334
+ }
335
+ break;
336
+ }
337
+ }
338
+ break;
339
+ }
340
+ case "next": {
341
+ const { emitInstructions, printNext } = require("../lib/emit.js");
342
+ if (jsonMode) {
343
+ const instructions = emitInstructions(config, root);
344
+ console.log(JSON.stringify(instructions, null, 2));
345
+ } else {
346
+ printNext(config, root);
347
+ }
348
+ break;
349
+ }
350
+ case "doctor": {
351
+ const { runChecks, printReport } = require("../lib/doctor.js");
194
352
  const result = runChecks(root || process.cwd());
195
353
  if (jsonMode) {
196
354
  console.log(JSON.stringify(result, null, 2));
@@ -1,7 +1,7 @@
1
- 'use strict';
1
+ "use strict";
2
2
 
3
- const fs = require('node:fs');
4
- const path = require('node:path');
3
+ const fs = require("node:fs");
4
+ const path = require("node:path");
5
5
 
6
6
  /**
7
7
  * Assign a person to a sprint. Updates orchard.json.
@@ -11,7 +11,7 @@ function assignSprint(config, root, sprintPath, person) {
11
11
 
12
12
  if (!sprint) {
13
13
  console.error(`Sprint not found: ${sprintPath}`);
14
- console.error('Available sprints:');
14
+ console.error("Available sprints:");
15
15
  for (const s of config.sprints || []) {
16
16
  console.error(` ${s.path}`);
17
17
  }
@@ -21,13 +21,15 @@ function assignSprint(config, root, sprintPath, person) {
21
21
  const prev = sprint.assigned_to;
22
22
  sprint.assigned_to = person;
23
23
 
24
- const configPath = path.join(root, 'orchard.json');
25
- const tmp = configPath + '.tmp.' + process.pid;
26
- fs.writeFileSync(tmp, JSON.stringify(config, null, 2) + '\n');
24
+ const configPath = path.join(root, "orchard.json");
25
+ const tmp = configPath + ".tmp." + process.pid;
26
+ fs.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n");
27
27
  fs.renameSync(tmp, configPath);
28
28
 
29
29
  if (prev) {
30
- console.log(`Reassigned ${path.basename(sprintPath)}: ${prev} -> ${person}`);
30
+ console.log(
31
+ `Reassigned ${path.basename(sprintPath)}: ${prev} -> ${person}`,
32
+ );
31
33
  } else {
32
34
  console.log(`Assigned ${path.basename(sprintPath)} to ${person}`);
33
35
  }
@@ -40,7 +42,7 @@ function getWorkload(config) {
40
42
  const workload = new Map();
41
43
 
42
44
  for (const sprint of config.sprints || []) {
43
- const person = sprint.assigned_to || 'unassigned';
45
+ const person = sprint.assigned_to || "unassigned";
44
46
  if (!workload.has(person)) {
45
47
  workload.set(person, []);
46
48
  }
@@ -56,20 +58,20 @@ function getWorkload(config) {
56
58
  function printWorkload(config) {
57
59
  const workload = getWorkload(config);
58
60
 
59
- console.log('');
60
- console.log(' Workload Distribution');
61
- console.log(' ' + '-'.repeat(40));
61
+ console.log("");
62
+ console.log(" Workload Distribution");
63
+ console.log(" " + "-".repeat(40));
62
64
 
63
65
  for (const [person, sprints] of workload) {
64
- const active = sprints.filter((s) => s.status !== 'done').length;
66
+ const active = sprints.filter((s) => s.status !== "done").length;
65
67
  console.log(` ${person}: ${sprints.length} sprints (${active} active)`);
66
68
  for (const s of sprints) {
67
- const status = s.status || 'unknown';
69
+ const status = s.status || "unknown";
68
70
  console.log(` - ${path.basename(s.path)} [${status}]`);
69
71
  }
70
72
  }
71
73
 
72
- console.log('');
74
+ console.log("");
73
75
  }
74
76
 
75
77
  /**
@@ -80,8 +82,8 @@ function findOverloaded(config, maxLoad = 3) {
80
82
  const overloaded = [];
81
83
 
82
84
  for (const [person, sprints] of workload) {
83
- if (person === 'unassigned') continue;
84
- const active = sprints.filter((s) => s.status !== 'done').length;
85
+ if (person === "unassigned") continue;
86
+ const active = sprints.filter((s) => s.status !== "done").length;
85
87
  if (active > maxLoad) {
86
88
  overloaded.push({ person, active, sprints });
87
89
  }