@grainulation/orchard 1.0.2 → 1.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/CONTRIBUTING.md CHANGED
@@ -63,7 +63,7 @@ The key architectural principle: **orchard operates above individual sprints.**
63
63
 
64
64
  - Zero dependencies. If you need something, write it or use Node built-ins.
65
65
  - No transpilation. Ship what you write.
66
- - ESM imports (`import`/`export`). Node 18+ required.
66
+ - ESM imports (`import`/`export`). Node 20+ required.
67
67
  - Keep functions small. If a function needs a scroll, split it.
68
68
  - No emojis in code, CLI output, or dashboards.
69
69
 
package/README.md CHANGED
@@ -3,7 +3,7 @@
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/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>
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
7
  <a href="https://deepwiki.com/grainulation/orchard"><img src="https://deepwiki.com/badge.svg" alt="Explore on DeepWiki"></a>
8
8
  </p>
9
9
 
package/bin/orchard.js CHANGED
@@ -24,6 +24,7 @@ const COMMANDS = {
24
24
  dashboard: "Generate unified HTML dashboard",
25
25
  serve: "Start the portfolio dashboard web server",
26
26
  connect: "Connect to a farmer instance",
27
+ next: "Show sprints ready for grainulator execution",
27
28
  doctor: "Check health of orchard setup",
28
29
  help: "Show this help message",
29
30
  };
@@ -336,6 +337,16 @@ async function main() {
336
337
  }
337
338
  break;
338
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
+ }
339
350
  case "doctor": {
340
351
  const { runChecks, printReport } = require("../lib/doctor.js");
341
352
  const result = runChecks(root || process.cwd());
package/lib/emit.js ADDED
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+
3
+ const fs = require("node:fs");
4
+ const path = require("node:path");
5
+ const { findReady } = require("./sync.js");
6
+
7
+ /**
8
+ * Emit grainulator-compatible instructions for all ready sprints.
9
+ *
10
+ * Reads findReady() output + each sprint's claims.json meta to build
11
+ * a structured instruction per sprint: { dir, question, tools, model }.
12
+ *
13
+ * @param {object} config - Parsed orchard.json
14
+ * @param {string} root - Orchard root directory
15
+ * @returns {object[]} Array of grainulator instructions
16
+ */
17
+ function emitInstructions(config, root) {
18
+ const ready = findReady(config, root);
19
+ const instructions = [];
20
+
21
+ for (const sprint of ready) {
22
+ const sprintDir = path.resolve(root, sprint.path);
23
+ const claimsPath = path.join(sprintDir, "claims.json");
24
+
25
+ let question = sprint.question || null;
26
+
27
+ // Try to extract question from claims.json meta if not in orchard config
28
+ if (!question) {
29
+ try {
30
+ const raw = fs.readFileSync(claimsPath, "utf8");
31
+ const data = JSON.parse(raw);
32
+ question = data.meta?.question || null;
33
+ } catch {
34
+ // No claims.json or unreadable -- skip this sprint
35
+ }
36
+ }
37
+
38
+ if (!question) continue;
39
+
40
+ instructions.push({
41
+ dir: sprintDir,
42
+ question,
43
+ tools: ["Read", "Write", "Edit", "Bash", "Glob", "Grep", "WebSearch", "WebFetch"],
44
+ model: sprint.model || "sonnet",
45
+ });
46
+ }
47
+
48
+ return instructions;
49
+ }
50
+
51
+ /**
52
+ * Print ready sprint instructions to stdout (CLI-friendly).
53
+ */
54
+ function printNext(config, root) {
55
+ const instructions = emitInstructions(config, root);
56
+
57
+ if (instructions.length === 0) {
58
+ console.log("\n No sprints are ready. Check dependencies with `orchard plan`.\n");
59
+ return instructions;
60
+ }
61
+
62
+ console.log(`\n ${instructions.length} sprint(s) ready:\n`);
63
+ for (const inst of instructions) {
64
+ console.log(` - ${path.basename(inst.dir)}`);
65
+ console.log(` Question: ${inst.question}`);
66
+ console.log(` Run: /grainulator:research "${inst.question}"\n`);
67
+ }
68
+
69
+ return instructions;
70
+ }
71
+
72
+ module.exports = { emitInstructions, printNext };
package/lib/farmer.js CHANGED
@@ -5,13 +5,27 @@ const path = require("node:path");
5
5
  const http = require("node:http");
6
6
  const https = require("node:https");
7
7
 
8
+ /** Track whether we have already warned about missing token */
9
+ let _warnedNoToken = false;
10
+
8
11
  /**
9
12
  * POST an activity event to farmer.
10
13
  * Graceful failure -- catch and warn, never crash.
11
14
  * @param {string} farmerUrl - Base URL of farmer (e.g. http://localhost:9090)
12
15
  * @param {object} event - Event object (e.g. { type: "scan", data: {...} })
16
+ * @param {object} [opts] - Options
17
+ * @param {string} [opts.token] - Bearer token for Authorization header
13
18
  */
14
- function notify(farmerUrl, event) {
19
+ function notify(farmerUrl, event, opts) {
20
+ const token = (opts && opts.token) || null;
21
+
22
+ if (!token && !_warnedNoToken) {
23
+ _warnedNoToken = true;
24
+ process.stderr.write(
25
+ "[orchard] no farmer token configured -- requests are unauthenticated\n",
26
+ );
27
+ }
28
+
15
29
  return new Promise((resolve) => {
16
30
  try {
17
31
  const payload = JSON.stringify({
@@ -23,16 +37,21 @@ function notify(farmerUrl, event) {
23
37
  const url = new URL(`${farmerUrl}/hooks/activity`);
24
38
  const transport = url.protocol === "https:" ? https : http;
25
39
 
40
+ const headers = {
41
+ "Content-Type": "application/json",
42
+ "Content-Length": Buffer.byteLength(payload),
43
+ };
44
+ if (token) {
45
+ headers["Authorization"] = `Bearer ${token}`;
46
+ }
47
+
26
48
  const req = transport.request(
27
49
  {
28
50
  hostname: url.hostname,
29
51
  port: url.port,
30
52
  path: url.pathname,
31
53
  method: "POST",
32
- headers: {
33
- "Content-Type": "application/json",
34
- "Content-Length": Buffer.byteLength(payload),
35
- },
54
+ headers,
36
55
  timeout: 5000,
37
56
  },
38
57
  (res) => {
@@ -76,7 +95,7 @@ async function connect(targetDir, args) {
76
95
  const subcommand = args[0];
77
96
  if (subcommand !== "farmer") {
78
97
  console.error(
79
- "Usage: orchard connect farmer [--url http://localhost:9090]",
98
+ "Usage: orchard connect farmer --url http://localhost:9090 [--token <t>]",
80
99
  );
81
100
  process.exit(1);
82
101
  }
@@ -86,16 +105,29 @@ async function connect(targetDir, args) {
86
105
  const urlIdx = args.indexOf("--url");
87
106
  if (urlIdx !== -1 && args[urlIdx + 1]) {
88
107
  const url = args[urlIdx + 1];
89
- const config = { url };
108
+
109
+ // Read optional --token flag
110
+ const tokenIdx = args.indexOf("--token");
111
+ const token = tokenIdx !== -1 && args[tokenIdx + 1] ? args[tokenIdx + 1] : null;
112
+
113
+ const config = token ? { url, token } : { url };
90
114
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf8");
91
115
  console.log(`Farmer connection saved to ${configPath}`);
92
116
  console.log(` URL: ${url}`);
117
+ if (token) {
118
+ console.log(" Token: configured");
119
+ } else {
120
+ console.log(
121
+ " Token: not configured (use --token <t> to enable authenticated requests)",
122
+ );
123
+ }
93
124
 
94
125
  // Test the connection
95
- const result = await notify(url, {
96
- type: "connect",
97
- data: { tool: "orchard" },
98
- });
126
+ const result = await notify(
127
+ url,
128
+ { type: "connect", data: { tool: "orchard" } },
129
+ { token },
130
+ );
99
131
  if (result.ok) {
100
132
  console.log(" Connection test: OK");
101
133
  } else {
@@ -113,11 +145,47 @@ async function connect(targetDir, args) {
113
145
  if (fs.existsSync(configPath)) {
114
146
  const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
115
147
  console.log(`Farmer connection: ${config.url}`);
148
+ if (config.token) {
149
+ console.log(" Token: configured");
150
+ } else {
151
+ console.log(" Token: not configured");
152
+ }
116
153
  console.log(`Config: ${configPath}`);
117
154
  } else {
118
155
  console.log("No farmer connection configured.");
119
- console.log("Usage: orchard connect farmer --url http://localhost:9090");
156
+ console.log(
157
+ "Usage: orchard connect farmer --url http://localhost:9090 [--token <t>]",
158
+ );
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Read .farmer.json from a directory.
164
+ * @param {string} dir - Directory containing .farmer.json
165
+ * @returns {{ url: string, token?: string } | null}
166
+ */
167
+ function loadConfig(dir) {
168
+ const configPath = path.join(dir, ".farmer.json");
169
+ if (!fs.existsSync(configPath)) return null;
170
+ try {
171
+ return JSON.parse(fs.readFileSync(configPath, "utf8"));
172
+ } catch {
173
+ return null;
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Convenience: read .farmer.json and notify with auth if token exists.
179
+ * @param {string} dir - Directory containing .farmer.json
180
+ * @param {object} event - Event object
181
+ * @returns {Promise<{ok: boolean, status?: number, body?: string, error?: string}>}
182
+ */
183
+ function notifyFromConfig(dir, event) {
184
+ const config = loadConfig(dir);
185
+ if (!config || !config.url) {
186
+ return Promise.resolve({ ok: false, error: "no farmer configured" });
120
187
  }
188
+ return notify(config.url, event, { token: config.token || null });
121
189
  }
122
190
 
123
- module.exports = { connect, notify };
191
+ module.exports = { connect, notify, loadConfig, notifyFromConfig };
package/lib/server.js CHANGED
@@ -790,6 +790,14 @@ ${ROUTES.map((r) => "<tr><td><code>" + r.method + "</code></td><td><code>" + r.p
790
790
  let filePath = url.pathname;
791
791
  filePath = join(PUBLIC_DIR, filePath);
792
792
 
793
+ // Prevent directory traversal
794
+ const resolved = resolve(filePath);
795
+ if (!resolved.startsWith(PUBLIC_DIR + "/") && resolved !== PUBLIC_DIR) {
796
+ res.writeHead(403);
797
+ res.end("Forbidden");
798
+ return;
799
+ }
800
+
793
801
  if (existsSync(filePath) && !statSync(filePath).isDirectory()) {
794
802
  const ext = extname(filePath);
795
803
  const mime = MIME[ext] || "application/octet-stream";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grainulation/orchard",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "Multi-sprint research orchestrator — coordinate parallel research across teams",
5
5
  "main": "lib/planner.js",
6
6
  "exports": {
@@ -11,6 +11,7 @@
11
11
  "./conflicts": "./lib/conflicts.js",
12
12
  "./hackathon": "./lib/hackathon.js",
13
13
  "./decompose": "./lib/decompose.js",
14
+ "./emit": "./lib/emit.js",
14
15
  "./doctor": "./lib/doctor.js",
15
16
  "./package.json": "./package.json"
16
17
  },
@@ -41,7 +42,6 @@
41
42
  ],
42
43
  "author": "grainulation contributors",
43
44
  "license": "MIT",
44
- "type": "module",
45
45
  "repository": {
46
46
  "type": "git",
47
47
  "url": "git+https://github.com/grainulation/orchard.git"