@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 +7 -1
- package/README.md +21 -20
- package/bin/orchard.js +238 -80
- package/lib/assignments.js +19 -17
- package/lib/conflicts.js +177 -29
- package/lib/dashboard.js +100 -47
- package/lib/decompose.js +268 -0
- package/lib/doctor.js +48 -32
- package/lib/emit.js +72 -0
- package/lib/export.js +72 -44
- package/lib/farmer.js +126 -42
- package/lib/hackathon.js +349 -0
- package/lib/planner.js +150 -21
- package/lib/server.js +395 -165
- package/lib/sync.js +31 -25
- package/lib/tracker.js +52 -40
- package/package.json +7 -3
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
|
|
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/
|
|
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
|
|
73
|
-
|
|
74
|
-
| `orchard plan`
|
|
75
|
-
| `orchard status`
|
|
76
|
-
| `orchard assign <path> <person>` | Assign a person to a sprint
|
|
77
|
-
| `orchard sync`
|
|
78
|
-
| `orchard dashboard [outfile]`
|
|
79
|
-
| `orchard init`
|
|
80
|
-
| `orchard serve`
|
|
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
|
|
96
|
-
|
|
97
|
-
| [wheat](https://github.com/grainulation/wheat)
|
|
98
|
-
| [farmer](https://github.com/grainulation/farmer)
|
|
99
|
-
| [barn](https://github.com/grainulation/barn)
|
|
100
|
-
| [mill](https://github.com/grainulation/mill)
|
|
101
|
-
| [silo](https://github.com/grainulation/silo)
|
|
102
|
-
| [harvest](https://github.com/grainulation/harvest)
|
|
103
|
-
| **orchard**
|
|
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
|
-
|
|
2
|
+
"use strict";
|
|
3
3
|
|
|
4
|
-
const path = require(
|
|
5
|
-
const fs = require(
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const fs = require("node:fs");
|
|
6
6
|
|
|
7
|
-
const verbose =
|
|
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(
|
|
12
|
+
process.stderr.write(`[${ts}] orchard: ${a.join(" ")}\n`);
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
const COMMANDS = {
|
|
15
|
-
init:
|
|
16
|
-
plan:
|
|
17
|
-
status:
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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(
|
|
47
|
-
console.log(
|
|
48
|
-
console.log(
|
|
49
|
-
console.log(
|
|
50
|
-
console.log(
|
|
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(
|
|
56
|
-
console.log(
|
|
57
|
-
console.log(
|
|
58
|
-
console.log(
|
|
59
|
-
console.log(
|
|
60
|
-
console.log(
|
|
61
|
-
console.log(
|
|
62
|
-
console.log(
|
|
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] ||
|
|
69
|
-
vlog(
|
|
73
|
+
const command = args[0] || "help";
|
|
74
|
+
vlog("startup", `command=${command}`, `cwd=${process.cwd()}`);
|
|
70
75
|
|
|
71
|
-
if (command ===
|
|
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 ===
|
|
84
|
-
const serverPath = path.join(__dirname,
|
|
85
|
-
const { spawn } = require(
|
|
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:
|
|
95
|
+
stdio: "inherit",
|
|
91
96
|
});
|
|
92
97
|
|
|
93
|
-
child.on(
|
|
94
|
-
child.on(
|
|
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(
|
|
99
|
-
process.on(
|
|
103
|
+
process.on("SIGTERM", () => child.kill("SIGTERM"));
|
|
104
|
+
process.on("SIGINT", () => child.kill("SIGINT"));
|
|
100
105
|
return;
|
|
101
106
|
}
|
|
102
107
|
|
|
103
|
-
if (command ===
|
|
104
|
-
const { connect: farmerConnect } = require(
|
|
105
|
-
const connectArgs = process.argv.slice(process.argv.indexOf(
|
|
106
|
-
const rootIdx = connectArgs.indexOf(
|
|
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 ===
|
|
116
|
-
const { parseArgs } = require(
|
|
120
|
+
if (command === "init") {
|
|
121
|
+
const { parseArgs } = require("node:util");
|
|
117
122
|
let rootDir = process.cwd();
|
|
118
123
|
try {
|
|
119
|
-
const { values } = parseArgs({
|
|
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 (_) {
|
|
122
|
-
|
|
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(
|
|
135
|
+
console.error("orchard: orchard.json already exists");
|
|
125
136
|
process.exit(1);
|
|
126
137
|
}
|
|
127
|
-
const defaultConfig = {
|
|
128
|
-
|
|
129
|
-
|
|
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 !==
|
|
135
|
-
console.error(
|
|
136
|
-
|
|
137
|
-
|
|
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(
|
|
163
|
+
const jsonMode = args.includes("--json");
|
|
144
164
|
|
|
145
165
|
switch (command) {
|
|
146
|
-
case
|
|
166
|
+
case "plan": {
|
|
147
167
|
if (jsonMode) {
|
|
148
|
-
const {
|
|
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
|
|
156
|
-
|
|
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
|
|
198
|
+
case "status": {
|
|
160
199
|
if (jsonMode) {
|
|
161
|
-
const { getStatusData } = require(
|
|
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(
|
|
205
|
+
const { printStatus } = require("../lib/tracker.js");
|
|
167
206
|
printStatus(config, root);
|
|
168
207
|
break;
|
|
169
208
|
}
|
|
170
|
-
case
|
|
209
|
+
case "assign": {
|
|
171
210
|
const sprintPath = args[1];
|
|
172
211
|
const person = args[2];
|
|
173
212
|
if (!sprintPath || !person) {
|
|
174
|
-
console.error(
|
|
213
|
+
console.error("orchard: usage: orchard assign <sprint-path> <person>");
|
|
175
214
|
process.exit(1);
|
|
176
215
|
}
|
|
177
|
-
const { assignSprint } = require(
|
|
216
|
+
const { assignSprint } = require("../lib/assignments.js");
|
|
178
217
|
assignSprint(config, root, sprintPath, person);
|
|
179
218
|
break;
|
|
180
219
|
}
|
|
181
|
-
case
|
|
182
|
-
const { syncAll } = require(
|
|
220
|
+
case "sync": {
|
|
221
|
+
const { syncAll } = require("../lib/sync.js");
|
|
183
222
|
syncAll(config, root);
|
|
184
223
|
break;
|
|
185
224
|
}
|
|
186
|
-
case
|
|
187
|
-
const { generateDashboard } = require(
|
|
188
|
-
const outPath = args[1] || path.join(root,
|
|
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
|
|
193
|
-
const {
|
|
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));
|
package/lib/assignments.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
|
-
const fs = require(
|
|
4
|
-
const path = require(
|
|
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(
|
|
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,
|
|
25
|
-
const tmp = configPath +
|
|
26
|
-
fs.writeFileSync(tmp, JSON.stringify(config, null, 2) +
|
|
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(
|
|
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 ||
|
|
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(
|
|
61
|
-
console.log(
|
|
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 !==
|
|
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 ||
|
|
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 ===
|
|
84
|
-
const active = sprints.filter((s) => s.status !==
|
|
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
|
}
|