@grainulation/orchard 1.0.0 → 1.0.2

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/bin/orchard.js CHANGED
@@ -1,74 +1,78 @@
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
+ doctor: "Check health of orchard setup",
28
+ help: "Show this help message",
25
29
  };
26
30
 
27
31
  function loadConfig(dir) {
28
- const configPath = path.join(dir, 'orchard.json');
32
+ const configPath = path.join(dir, "orchard.json");
29
33
  if (!fs.existsSync(configPath)) {
30
34
  return null;
31
35
  }
32
- return JSON.parse(fs.readFileSync(configPath, 'utf8'));
36
+ return JSON.parse(fs.readFileSync(configPath, "utf8"));
33
37
  }
34
38
 
35
39
  function findOrchardRoot() {
36
40
  let dir = process.cwd();
37
41
  while (dir !== path.dirname(dir)) {
38
- if (fs.existsSync(path.join(dir, 'orchard.json'))) return dir;
42
+ if (fs.existsSync(path.join(dir, "orchard.json"))) return dir;
39
43
  dir = path.dirname(dir);
40
44
  }
41
45
  return null;
42
46
  }
43
47
 
44
48
  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:');
49
+ console.log("");
50
+ console.log(" orchard - Multi-sprint research orchestrator");
51
+ console.log("");
52
+ console.log(" Usage: orchard <command> [options]");
53
+ console.log("");
54
+ console.log(" Commands:");
51
55
  for (const [cmd, desc] of Object.entries(COMMANDS)) {
52
56
  console.log(` ${cmd.padEnd(12)} ${desc}`);
53
57
  }
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('');
58
+ console.log("");
59
+ console.log(" Serve options:");
60
+ console.log(" --port 9097 Port for the web server (default: 9097)");
61
+ console.log(" --root <dir> Root directory to scan for sprints");
62
+ console.log("");
63
+ console.log(" Connect:");
64
+ console.log(" orchard connect farmer --url http://localhost:9090");
65
+ console.log("");
66
+ console.log(" Config: orchard.json in project root");
67
+ console.log("");
64
68
  }
65
69
 
66
70
  async function main() {
67
71
  const args = process.argv.slice(2);
68
- const command = args[0] || 'help';
69
- vlog('startup', `command=${command}`, `cwd=${process.cwd()}`);
72
+ const command = args[0] || "help";
73
+ vlog("startup", `command=${command}`, `cwd=${process.cwd()}`);
70
74
 
71
- if (command === 'help' || command === '--help' || command === '-h') {
75
+ if (command === "help" || command === "--help" || command === "-h") {
72
76
  printHelp();
73
77
  process.exit(0);
74
78
  }
@@ -80,30 +84,30 @@ async function main() {
80
84
  }
81
85
 
82
86
  // 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');
87
+ if (command === "serve") {
88
+ const serverPath = path.join(__dirname, "..", "lib", "server.js");
89
+ const { spawn } = require("node:child_process");
86
90
 
87
91
  // Forward remaining args to the server
88
92
  const serverArgs = args.slice(1);
89
93
  const child = spawn(process.execPath, [serverPath, ...serverArgs], {
90
- stdio: 'inherit',
94
+ stdio: "inherit",
91
95
  });
92
96
 
93
- child.on('close', (code) => process.exit(code ?? 0));
94
- child.on('error', (err) => {
97
+ child.on("close", (code) => process.exit(code ?? 0));
98
+ child.on("error", (err) => {
95
99
  console.error(`orchard: failed to start server: ${err.message}`);
96
100
  process.exit(1);
97
101
  });
98
- process.on('SIGTERM', () => child.kill('SIGTERM'));
99
- process.on('SIGINT', () => child.kill('SIGINT'));
102
+ process.on("SIGTERM", () => child.kill("SIGTERM"));
103
+ process.on("SIGINT", () => child.kill("SIGINT"));
100
104
  return;
101
105
  }
102
106
 
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');
107
+ if (command === "connect") {
108
+ const { connect: farmerConnect } = require("../lib/farmer.js");
109
+ const connectArgs = process.argv.slice(process.argv.indexOf("connect") + 1);
110
+ const rootIdx = connectArgs.indexOf("--root");
107
111
  let targetDir = process.cwd();
108
112
  if (rootIdx !== -1 && connectArgs[rootIdx + 1]) {
109
113
  targetDir = path.resolve(connectArgs[rootIdx + 1]);
@@ -112,85 +116,228 @@ async function main() {
112
116
  return;
113
117
  }
114
118
 
115
- if (command === 'init') {
116
- const { parseArgs } = require('node:util');
119
+ if (command === "init") {
120
+ const { parseArgs } = require("node:util");
117
121
  let rootDir = process.cwd();
118
122
  try {
119
- const { values } = parseArgs({ args: process.argv.slice(3), options: { root: { type: 'string' } }, allowPositionals: true });
123
+ const { values } = parseArgs({
124
+ args: process.argv.slice(3),
125
+ options: { root: { type: "string" } },
126
+ allowPositionals: true,
127
+ });
120
128
  if (values.root) rootDir = path.resolve(values.root);
121
- } catch (_) { /* ignore parse errors for init */ }
122
- const configPath = path.join(rootDir, 'orchard.json');
129
+ } catch (_) {
130
+ /* ignore parse errors for init */
131
+ }
132
+ const configPath = path.join(rootDir, "orchard.json");
123
133
  if (fs.existsSync(configPath)) {
124
- console.error('orchard: orchard.json already exists');
134
+ console.error("orchard: orchard.json already exists");
125
135
  process.exit(1);
126
136
  }
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`');
137
+ const defaultConfig = {
138
+ sprints: [],
139
+ settings: { sync_interval: "manual" },
140
+ };
141
+ fs.writeFileSync(
142
+ configPath,
143
+ JSON.stringify(defaultConfig, null, 2) + "\n",
144
+ "utf8",
145
+ );
146
+ console.log("Initialized orchard.json — add sprints with `orchard plan`");
130
147
  process.exit(0);
131
148
  }
132
149
 
133
150
  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:');
151
+ if (!root && command !== "help" && command !== "doctor") {
152
+ console.error(
153
+ "orchard: no orchard.json found. Run from a directory with orchard.json or a subdirectory.",
154
+ );
155
+ console.error("");
156
+ console.error("Create one:");
138
157
  console.error(' { "sprints": [] }');
139
158
  process.exit(1);
140
159
  }
141
160
 
142
161
  const config = root ? loadConfig(root) : { sprints: [] };
143
- const jsonMode = args.includes('--json');
162
+ const jsonMode = args.includes("--json");
144
163
 
145
164
  switch (command) {
146
- case 'plan': {
165
+ case "plan": {
147
166
  if (jsonMode) {
148
- const { buildGraph, topoSort, detectCycles } = require('../lib/planner.js');
167
+ const {
168
+ buildGraph,
169
+ topoSort,
170
+ detectCycles,
171
+ } = require("../lib/planner.js");
149
172
  const graph = buildGraph(config);
150
173
  const order = topoSort(config);
151
174
  const cycles = detectCycles(config);
152
175
  console.log(JSON.stringify({ graph, order, cycles }, null, 2));
153
176
  break;
154
177
  }
155
- const { printDependencyGraph } = require('../lib/planner.js');
156
- printDependencyGraph(config, root);
178
+ const fmtIdx = args.indexOf("--format");
179
+ const planFormat =
180
+ fmtIdx !== -1 && args[fmtIdx + 1] ? args[fmtIdx + 1] : null;
181
+ if (args.includes("--mermaid") || planFormat === "mermaid") {
182
+ const { generateMermaid } = require("../lib/planner.js");
183
+ console.log(generateMermaid(config));
184
+ break;
185
+ }
186
+ if (planFormat === "ascii" || !planFormat) {
187
+ const { printDependencyGraph } = require("../lib/planner.js");
188
+ printDependencyGraph(config, root);
189
+ break;
190
+ }
191
+ console.error(
192
+ `orchard: unknown plan format: ${planFormat}. Supported: ascii, mermaid`,
193
+ );
194
+ process.exit(1);
157
195
  break;
158
196
  }
159
- case 'status': {
197
+ case "status": {
160
198
  if (jsonMode) {
161
- const { getStatusData } = require('../lib/tracker.js');
199
+ const { getStatusData } = require("../lib/tracker.js");
162
200
  const data = getStatusData(config, root);
163
201
  console.log(JSON.stringify(data, null, 2));
164
202
  break;
165
203
  }
166
- const { printStatus } = require('../lib/tracker.js');
204
+ const { printStatus } = require("../lib/tracker.js");
167
205
  printStatus(config, root);
168
206
  break;
169
207
  }
170
- case 'assign': {
208
+ case "assign": {
171
209
  const sprintPath = args[1];
172
210
  const person = args[2];
173
211
  if (!sprintPath || !person) {
174
- console.error('orchard: usage: orchard assign <sprint-path> <person>');
212
+ console.error("orchard: usage: orchard assign <sprint-path> <person>");
175
213
  process.exit(1);
176
214
  }
177
- const { assignSprint } = require('../lib/assignments.js');
215
+ const { assignSprint } = require("../lib/assignments.js");
178
216
  assignSprint(config, root, sprintPath, person);
179
217
  break;
180
218
  }
181
- case 'sync': {
182
- const { syncAll } = require('../lib/sync.js');
219
+ case "sync": {
220
+ const { syncAll } = require("../lib/sync.js");
183
221
  syncAll(config, root);
184
222
  break;
185
223
  }
186
- case 'dashboard': {
187
- const { generateDashboard } = require('../lib/dashboard.js');
188
- const outPath = args[1] || path.join(root, 'orchard-dashboard.html');
224
+ case "dashboard": {
225
+ const { generateDashboard } = require("../lib/dashboard.js");
226
+ const outPath = args[1] || path.join(root, "orchard-dashboard.html");
189
227
  generateDashboard(config, root, outPath);
190
228
  break;
191
229
  }
192
- case 'doctor': {
193
- const { runChecks, printReport } = require('../lib/doctor.js');
230
+ case "conflicts": {
231
+ const {
232
+ detectConflicts,
233
+ filterBySeverity,
234
+ printConflicts,
235
+ } = require("../lib/conflicts.js");
236
+ const sevIdx = args.indexOf("--severity");
237
+ const severity =
238
+ sevIdx !== -1 && args[sevIdx + 1] ? args[sevIdx + 1] : "info";
239
+ if (jsonMode) {
240
+ const all = detectConflicts(config, root);
241
+ const filtered = filterBySeverity(all, severity);
242
+ console.log(
243
+ JSON.stringify(
244
+ { conflicts: filtered, count: filtered.length },
245
+ null,
246
+ 2,
247
+ ),
248
+ );
249
+ break;
250
+ }
251
+ printConflicts(config, root, { severity });
252
+ break;
253
+ }
254
+ case "decompose": {
255
+ const question = args
256
+ .filter((a) => !a.startsWith("--"))
257
+ .slice(1)
258
+ .join(" ");
259
+ if (!question) {
260
+ console.error(
261
+ 'orchard: usage: orchard decompose "<question>" [--apply] [--max <n>]',
262
+ );
263
+ process.exit(1);
264
+ }
265
+ const maxIdx = args.indexOf("--max");
266
+ const maxSprints =
267
+ maxIdx !== -1 && args[maxIdx + 1] ? parseInt(args[maxIdx + 1], 10) : 5;
268
+ if (args.includes("--apply")) {
269
+ const { applyDecomposition } = require("../lib/decompose.js");
270
+ const sprints = applyDecomposition(root, question, { maxSprints });
271
+ console.log(`Created ${sprints.length} sub-sprints for: "${question}"`);
272
+ for (const s of sprints) {
273
+ console.log(` ${s.path}`);
274
+ }
275
+ } else {
276
+ const { printDecomposition } = require("../lib/decompose.js");
277
+ printDecomposition(question, { maxSprints });
278
+ }
279
+ break;
280
+ }
281
+ case "hackathon": {
282
+ const sub = args[1];
283
+ const hack = require("../lib/hackathon.js");
284
+ switch (sub) {
285
+ case "init": {
286
+ const nameIdx = args.indexOf("--name");
287
+ const durIdx = args.indexOf("--duration");
288
+ const name =
289
+ nameIdx !== -1 && args[nameIdx + 1] ? args[nameIdx + 1] : undefined;
290
+ const duration =
291
+ durIdx !== -1 && args[durIdx + 1]
292
+ ? parseInt(args[durIdx + 1], 10)
293
+ : undefined;
294
+ const h = hack.initHackathon(root, { name, duration });
295
+ console.log(`Hackathon "${h.name}" started — ends at ${h.endTime}`);
296
+ break;
297
+ }
298
+ case "team": {
299
+ const teamName = args[2];
300
+ const qIdx = args.indexOf("--question");
301
+ const question =
302
+ qIdx !== -1 ? args.slice(qIdx + 1).join(" ") : undefined;
303
+ if (!teamName) {
304
+ console.error(
305
+ 'orchard: usage: orchard hackathon team <name> [--question "..."]',
306
+ );
307
+ process.exit(1);
308
+ }
309
+ const result = hack.addTeam(root, teamName, question);
310
+ console.log(
311
+ `Team "${result.teamName}" added — sprint: ${result.sprintPath}`,
312
+ );
313
+ break;
314
+ }
315
+ case "end": {
316
+ const board = hack.endHackathon(root);
317
+ console.log("Hackathon ended! Final leaderboard:");
318
+ for (let i = 0; i < board.length; i++) {
319
+ const t = board[i];
320
+ console.log(
321
+ ` ${i + 1}. ${t.team} — ${t.score}pts (${t.claimCount} claims)`,
322
+ );
323
+ }
324
+ break;
325
+ }
326
+ default: {
327
+ if (jsonMode) {
328
+ const timer = hack.timerStatus(root);
329
+ const board = hack.leaderboard(root);
330
+ console.log(JSON.stringify({ timer, leaderboard: board }, null, 2));
331
+ } else {
332
+ hack.printHackathon(root);
333
+ }
334
+ break;
335
+ }
336
+ }
337
+ break;
338
+ }
339
+ case "doctor": {
340
+ const { runChecks, printReport } = require("../lib/doctor.js");
194
341
  const result = runChecks(root || process.cwd());
195
342
  if (jsonMode) {
196
343
  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
  }