tryharness 0.5.1 → 0.6.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/README.md +8 -1
- package/cli.mjs +26 -1
- package/lib/daemon.mjs +69 -2
- package/lib/store.mjs +46 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -73,7 +73,14 @@ machine with `--run --workspace <dir>` (or an agent's `run_workspace` request, o
|
|
|
73
73
|
authorizes it with `tryharness board run <id>`) is claimed by the daemon in that agent's name,
|
|
74
74
|
run non-interactively through the same runner as a delegated run, and reported back on the
|
|
75
75
|
task with the result and the agent's session id; `tryharness task <id> --follow` works on it.
|
|
76
|
-
One run per agent at a time; a daemon restart marks a running task unknown.
|
|
76
|
+
One run per agent at a time; a daemon restart marks a running task unknown.
|
|
77
|
+
A coding agent also starts, without `board run`, the tasks you or Harness post for it with a workspace in a
|
|
78
|
+
folder it has already worked in (its indexed sessions; your home folder counts only as itself, links are
|
|
79
|
+
followed), up to 10 a day. That is on by default, and the daemon says so once when it first starts with it. A
|
|
80
|
+
task another agent wrote, or one Harness posts from a turn you did not start or while working another agent's
|
|
81
|
+
task, always waits for you. `tryharness board standing` shows it; `tryharness board standing <agent> <folder>...
|
|
82
|
+
[--cap <n>]` names the folders instead; `tryharness board standing <agent> off` and `on` turn it off or back to
|
|
83
|
+
the default. An interactive
|
|
77
84
|
claim carries a lease (default 60 minutes); a lapsed lease shows the claim as stale and only
|
|
78
85
|
the poster or the user releases it. `tryharness board agents` shows when each agent last read
|
|
79
86
|
the board. Every change goes up the bridge as identity and status only.
|
package/cli.mjs
CHANGED
|
@@ -171,6 +171,7 @@ async function connect() {
|
|
|
171
171
|
throw new Error(`setup is incomplete for ${failed.map((state) => state.agent).join(", ")}. Nothing was hidden; run npx tryharness doctor after fixing the reported CLI error, then rerun setup.`);
|
|
172
172
|
}
|
|
173
173
|
console.log("history: indexing locally in the background");
|
|
174
|
+
console.log("runs: your coding agents start the tasks you ask for in folders they have worked in, up to 10 a day each, without a Start; tasks another agent wrote wait for you. Change it: npx tryharness board standing");
|
|
174
175
|
console.log("Done. You can close this terminal.");
|
|
175
176
|
}
|
|
176
177
|
|
|
@@ -316,6 +317,30 @@ async function board() {
|
|
|
316
317
|
for (const entry of response.entries) console.log(`\n[${entry.kind} · ${entry.author}${entry.source ? ` · ${entry.source}` : ""} · ${entry.score}]\n${entry.text.slice(0, 400)}`);
|
|
317
318
|
return;
|
|
318
319
|
}
|
|
320
|
+
// Standing permission: a coding agent starts the tasks you or Harness post for it without a Start. On by default
|
|
321
|
+
// for the folders you have used it in; name folders and a daily cap instead, turn it off, or back on to the default.
|
|
322
|
+
// Tasks another agent wrote always wait for you.
|
|
323
|
+
if (sub === "standing") {
|
|
324
|
+
const rest = argv.slice(argv.indexOf(sub) + 1).filter((value, index, all) => !value.startsWith("--") && all[index - 1] !== "--cap" && all[index - 1] !== "--as");
|
|
325
|
+
const [target, ...dirs] = rest;
|
|
326
|
+
const usage = "usage: tryharness board standing [<agent> <folder> [<folder>...] [--cap <runs a day, default 10>] | <agent> off | <agent> on]";
|
|
327
|
+
if (target && !AGENTS.includes(target)) throw new Error(usage);
|
|
328
|
+
const toggle = dirs.length === 1 && ["on", "off"].includes(dirs[0]) ? dirs[0] : null;
|
|
329
|
+
const response = !target ? await daemonRequest({ command: "board.standing", agent })
|
|
330
|
+
: toggle ? await daemonRequest({ command: `board.standing.${toggle}`, agent, targetAgent: target })
|
|
331
|
+
: dirs.length ? await daemonRequest({ command: "board.standing.set", agent, targetAgent: target, workspaces: dirs.map((dir) => path.resolve(dir)), dailyCap: Number(arg("--cap", "10")) })
|
|
332
|
+
: (() => { throw new Error(usage); })();
|
|
333
|
+
if (!response.ok) throw new Error(response.error || "standing permission failed");
|
|
334
|
+
if (!response.standing.length) { console.log("No coding agent is installed on this machine."); return; }
|
|
335
|
+
for (const rule of response.standing) {
|
|
336
|
+
if (rule.mode === "off") { console.log(`${rule.agent} waits for your Start. Turn it on: npx tryharness board standing ${rule.agent} on`); continue; }
|
|
337
|
+
const where = rule.mode === "default"
|
|
338
|
+
? (rule.workspaces.length ? `in the ${rule.workspaces.length} folder${rule.workspaces.length === 1 ? "" : "s"} you have used it in (${rule.workspaces.slice(0, 5).join(", ")}${rule.workspaces.length > 5 ? ", ..." : ""})` : "in folders you have used it in (none yet)")
|
|
339
|
+
: `in ${rule.workspaces.join(", ")}`;
|
|
340
|
+
console.log(`${rule.agent} starts tasks from you or Harness without asking, ${where}; ${rule.runs_today} of ${rule.daily_cap} today. Turn it off: npx tryharness board standing ${rule.agent} off`);
|
|
341
|
+
}
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
319
344
|
if (sub === "run" || sub === "release") {
|
|
320
345
|
const id = String(argv[argv.indexOf(sub) + 1] || "");
|
|
321
346
|
const response = await daemonRequest({ command: sub === "run" ? "board.run" : "board.release", agent, id });
|
|
@@ -379,7 +404,7 @@ async function board() {
|
|
|
379
404
|
for (const wait of t.waiting_on) console.log(`waiting: ${wait.id.slice(0, 8)} is ${wait.status}${wait.holder ? ` with ${wait.holder}` : ""}`);
|
|
380
405
|
return;
|
|
381
406
|
}
|
|
382
|
-
if (sub !== "list") throw new Error("usage: tryharness board [list --all | show <id> | trace <id> | post --for <agent> --objective <text> [--file <brief.md>] [--run --workspace <dir>] | run <id> | release <id> | agents]");
|
|
407
|
+
if (sub !== "list") throw new Error("usage: tryharness board [list --all | show <id> | trace <id> | post --for <agent> --objective <text> [--file <brief.md>] [--run --workspace <dir>] | run <id> | release <id> | standing [<agent> <folder>... --cap <n> | <agent> off | <agent> on] | agents]");
|
|
383
408
|
const response = await daemonRequest({ command: "board.list", agent, all: has("--all") });
|
|
384
409
|
if (!response.ok) throw new Error(response.error || "board unavailable");
|
|
385
410
|
if (!response.tasks.length) { console.log("The board is empty."); return; }
|
package/lib/daemon.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import net from "node:net";
|
|
3
|
+
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { spawn } from "node:child_process";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
@@ -343,6 +344,37 @@ export async function startDaemon() {
|
|
|
343
344
|
// Tasks written before the context space existed get their brief and result as entries.
|
|
344
345
|
for (const id of store.backfillContributions()) log(`context: backfilled task ${id}`);
|
|
345
346
|
|
|
347
|
+
// Standing permission, decided when a task is posted: a run the person asked for (themselves or through Harness),
|
|
348
|
+
// for a coding agent installed here, in a folder the permission covers, under today's cap, starts without a Start.
|
|
349
|
+
// By default it covers the folders the person has already used that agent in; home and anything above it count only
|
|
350
|
+
// as themselves, never as a root. Anything another agent wrote never starts. Links are followed, so no path inside a
|
|
351
|
+
// covered folder leads out of it.
|
|
352
|
+
const RUNNERS = new Set(["claude-code", "codex", "pi", "opencode"]);
|
|
353
|
+
const runners = () => installedAgents.filter((agent) => RUNNERS.has(agent));
|
|
354
|
+
const startOfToday = () => { const day = new Date(); day.setHours(0, 0, 0, 0); return day.toISOString(); };
|
|
355
|
+
const realDir = (dir) => { try { return fs.realpathSync(dir); } catch { return path.resolve(dir); } };
|
|
356
|
+
const within = (dir, root) => { const rel = path.relative(realDir(root), realDir(dir)); return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel)); };
|
|
357
|
+
const aboveHome = (dir) => within(os.homedir(), dir);
|
|
358
|
+
const covers = (rule, dir) => rule.workspaces.some((root) => rule.mode === "default" && aboveHome(root) ? realDir(root) === realDir(dir) : within(dir, root));
|
|
359
|
+
const standingRules = () => store.standing(runners(), startOfToday());
|
|
360
|
+
// null when the run may start now; otherwise why it waits for the person.
|
|
361
|
+
const standingVerdict = ({ trust, target, workspace }) => {
|
|
362
|
+
if (trust !== "user") return "Another agent wrote it, so it never starts without the person.";
|
|
363
|
+
const rule = standingRules().find((entry) => entry.agent === target);
|
|
364
|
+
if (!rule) return `${target} is not a coding agent installed on this machine.`;
|
|
365
|
+
if (rule.mode === "off") return `${target} is set to wait for the person's Start.`;
|
|
366
|
+
if (!covers(rule, workspace)) return rule.mode === "default" ? `${target} has not worked in ${workspace} before, so its first run there waits for the person.` : `${workspace} is outside the folders ${target} may run in without asking (${rule.workspaces.join(", ")}).`;
|
|
367
|
+
if (rule.runs_today >= rule.daily_cap) return `${target} already started ${rule.runs_today} runs today without asking, its daily limit.`;
|
|
368
|
+
return null;
|
|
369
|
+
};
|
|
370
|
+
const applyStanding = (task) => {
|
|
371
|
+
if (!task?.run_workspace || task.run_authorized_at) return undefined;
|
|
372
|
+
const start = `It waits for the person's Start on the Harness Board, or: npx tryharness board run ${task.id}`;
|
|
373
|
+
const reason = standingVerdict({ trust: task.trust, target: task.target_agent, workspace: task.run_workspace });
|
|
374
|
+
if (reason) return { started: false, reason: `${reason} ${start}` };
|
|
375
|
+
return store.authorizeStanding(task.id) ? { started: true } : { started: false, reason: `It could not be authorized. ${start}` };
|
|
376
|
+
};
|
|
377
|
+
|
|
346
378
|
const boardCommand = (request) => {
|
|
347
379
|
if (!validAgent(request.agent)) return { ok: false, error: "board requests need an agent name" };
|
|
348
380
|
const agent = String(request.agent);
|
|
@@ -371,6 +403,11 @@ export async function startDaemon() {
|
|
|
371
403
|
if (request.id != null && !validRunId(request.id)) return { ok: false, error: "invalid task id" };
|
|
372
404
|
if (request.id != null && store.getTask(String(request.id))) return { ok: false, error: "a task with this id already exists", task: store.getTask(String(request.id)) };
|
|
373
405
|
const thread = request.thread && typeof request.thread === "object" ? request.thread : null;
|
|
406
|
+
// A run asked for now, covered or not posted at all: nothing is left waiting that the person did not see.
|
|
407
|
+
if (request.requireStanding === true) {
|
|
408
|
+
const reason = thread || !runWorkspace || !request.targetAgent ? "a run without a Start needs one target agent and a folder" : standingVerdict({ trust: provenance.trust, target: String(request.targetAgent), workspace: runWorkspace });
|
|
409
|
+
if (reason) return { ok: false, error: reason, standing: { started: false, reason } };
|
|
410
|
+
}
|
|
374
411
|
if (thread && (!validAgent(thread.with) || !["asker", "target"].includes(thread.role))) return { ok: false, error: "a thread needs the other side's agent and a role" };
|
|
375
412
|
// Several agents at once: one group task with a part per agent.
|
|
376
413
|
const targetAgents = Array.isArray(request.targetAgents) ? [...new Set(request.targetAgents.map(String))] : null;
|
|
@@ -381,7 +418,7 @@ export async function startDaemon() {
|
|
|
381
418
|
else {
|
|
382
419
|
const posted = store.postGroup({ ...(request.id != null ? { id: String(request.id) } : {}), objective: objective.slice(0, 500), brief: String(request.brief || "").slice(0, 200_000), originAgent: agent, provenance,
|
|
383
420
|
targetAgents, contextRefs: refs, runWorkspace, runAuthorizedBy: runWorkspace && agent === "user" ? "user" : null });
|
|
384
|
-
for (const part of posted.children) notifyBoard(store.getTask(part.id));
|
|
421
|
+
for (const part of posted.children) { applyStanding(store.getTask(part.id)); notifyBoard(store.getTask(part.id)); }
|
|
385
422
|
notifyBoard(posted.group);
|
|
386
423
|
return { ok: true, task: posted.group };
|
|
387
424
|
}
|
|
@@ -390,7 +427,8 @@ export async function startDaemon() {
|
|
|
390
427
|
originAgent: agent, provenance, targetAgent: request.targetAgent ? String(request.targetAgent) : null, parentId: request.parentId ? String(request.parentId) : null, contextRefs: refs,
|
|
391
428
|
runWorkspace: thread ? null : runWorkspace, runAuthorizedBy: !thread && runWorkspace && agent === "user" ? "user" : null });
|
|
392
429
|
if (thread) store.openThread(task.id, { withAgent: String(thread.with), role: thread.role });
|
|
393
|
-
|
|
430
|
+
const standing = thread ? undefined : applyStanding(task);
|
|
431
|
+
outcome = { ok: true, task: store.getTask(task.id), thread: thread ? store.getThread(task.id) : null, ...(standing ? { standing } : {}) };
|
|
394
432
|
} else if (request.command === "board.list") {
|
|
395
433
|
const tasks = store.listTasks({ agent, status: request.status ? String(request.status) : undefined, all: request.all === true, limit: request.limit });
|
|
396
434
|
return request.unread ? { ok: true, tasks, unread: store.unreadFor(agent) } : { ok: true, tasks };
|
|
@@ -438,6 +476,28 @@ export async function startDaemon() {
|
|
|
438
476
|
} else if (request.command === "board.children") return { ok: true, tasks: store.children(id).map((child) => store.getTask(child.id)).filter(Boolean) };
|
|
439
477
|
// A trace reads both homes, and only Harness can: the machine asks it.
|
|
440
478
|
else if (request.command === "board.trace") return state.connected ? forwardToHarness(request) : { ok: false, error: "a trace reads the whole chain on this machine and in Harness, and the daemon is not connected to Harness right now" };
|
|
479
|
+
else if (request.command === "board.standing") return { ok: true, standing: standingRules() };
|
|
480
|
+
else if (request.command === "board.standing.check") {
|
|
481
|
+
const reason = standingVerdict({ trust: "user", target: String(request.targetAgent || ""), workspace: String(request.workspace || "") });
|
|
482
|
+
return path.isAbsolute(String(request.workspace || "")) ? { ok: true, covered: !reason, ...(reason ? { reason } : {}) } : { ok: false, error: "a folder must be an absolute path" };
|
|
483
|
+
}
|
|
484
|
+
else if (["board.standing.set", "board.standing.off", "board.standing.on"].includes(request.command)) {
|
|
485
|
+
if (agent !== "user") return { ok: false, error: "only the person changes standing permission" };
|
|
486
|
+
const target = String(request.targetAgent || "");
|
|
487
|
+
if (!RUNNERS.has(target)) return { ok: false, error: `standing permission is for a coding agent: ${[...RUNNERS].join(", ")}` };
|
|
488
|
+
if (request.command === "board.standing.off") store.setStanding(target, "off");
|
|
489
|
+
else if (request.command === "board.standing.on") store.resetStanding(target);
|
|
490
|
+
else {
|
|
491
|
+
const workspaces = Array.isArray(request.workspaces) ? [...new Set(request.workspaces.map(String))] : [];
|
|
492
|
+
if (!workspaces.length || workspaces.length > 20) return { ok: false, error: "name one to twenty folders" };
|
|
493
|
+
const bad = workspaces.find((dir) => !path.isAbsolute(dir) || !fs.statSync(dir, { throwIfNoEntry: false })?.isDirectory());
|
|
494
|
+
if (bad) return { ok: false, error: `${bad} is not an absolute path to a folder` };
|
|
495
|
+
const dailyCap = Number(request.dailyCap);
|
|
496
|
+
if (!Number.isInteger(dailyCap) || dailyCap < 1 || dailyCap > 100) return { ok: false, error: "the daily limit is a whole number from 1 to 100" };
|
|
497
|
+
store.setStanding(target, "named", workspaces.map((dir) => path.resolve(dir)), dailyCap);
|
|
498
|
+
}
|
|
499
|
+
return { ok: true, standing: standingRules() };
|
|
500
|
+
}
|
|
441
501
|
else if (request.command === "board.agents") return { ok: true, agents: store.agentsSeen(), running: [...activeRuns.entries()].map(([runAgent, taskId]) => ({ agent: runAgent, id: taskId })) };
|
|
442
502
|
else if (["board.claim", "board.report", "board.answer", "board.release"].includes(request.command) && !store.getTask(id) && state.connected) return forwardToHarness(request);
|
|
443
503
|
else if (request.command === "board.claim") outcome = store.claimTask(id, agent, expected, Number.isInteger(request.leaseMinutes) ? request.leaseMinutes : 60);
|
|
@@ -456,6 +516,13 @@ export async function startDaemon() {
|
|
|
456
516
|
// agent at a time; the rest wait open. Nothing here polls: a board write calls runBoard.
|
|
457
517
|
const activeRuns = new Map();
|
|
458
518
|
for (const id of store.settleInterruptedRuns()) log(`board task ${id} was running when the daemon stopped; marked unknown`);
|
|
519
|
+
// Standing permission is on by default; the person hears so once, on the first start that has it.
|
|
520
|
+
const defaults = standingRules().filter((rule) => rule.mode === "default");
|
|
521
|
+
if (defaults.length && !store.meta("standing_announced")) {
|
|
522
|
+
const one = defaults.length === 1;
|
|
523
|
+
const text = `${defaults.map((rule) => rule.agent).join(", ")} now ${one ? "starts" : "start"} the tasks you ask for in folders ${one ? "it has" : "they have"} worked in, up to ${defaults[0].daily_cap} a day${one ? "" : " each"}, without a Start. Change it: npx tryharness board standing`;
|
|
524
|
+
log(text); notifyDesktop(text); store.meta("standing_announced", new Date().toISOString());
|
|
525
|
+
}
|
|
459
526
|
const runBoard = () => {
|
|
460
527
|
for (const task of store.runnableTasks(installedAgents.filter((agent) => !activeRuns.has(agent)))) {
|
|
461
528
|
const agent = task.target_agent;
|
package/lib/store.mjs
CHANGED
|
@@ -16,6 +16,8 @@ function bindings(values) {
|
|
|
16
16
|
)));
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
export const DEFAULT_DAILY_RUNS = 10;
|
|
20
|
+
|
|
19
21
|
export function openStore(file) {
|
|
20
22
|
const db = new Database(file);
|
|
21
23
|
// The daemon is the database's single writer. DELETE mode is portable across the
|
|
@@ -80,6 +82,17 @@ export function openStore(file) {
|
|
|
80
82
|
updated_at text not null
|
|
81
83
|
);
|
|
82
84
|
create index if not exists board_tasks_status_idx on board_tasks(status, created_at desc);
|
|
85
|
+
create table if not exists board_standing (
|
|
86
|
+
agent text primary key,
|
|
87
|
+
mode text not null,
|
|
88
|
+
workspaces text not null,
|
|
89
|
+
daily_cap integer not null,
|
|
90
|
+
set_at text not null
|
|
91
|
+
);
|
|
92
|
+
create table if not exists board_meta (
|
|
93
|
+
key text primary key,
|
|
94
|
+
value text not null
|
|
95
|
+
);
|
|
83
96
|
create table if not exists board_agents (
|
|
84
97
|
agent text primary key,
|
|
85
98
|
last_seen_at text not null
|
|
@@ -133,6 +146,7 @@ export function openStore(file) {
|
|
|
133
146
|
}
|
|
134
147
|
// Boards written before the lock existed: give every task a holder and a version.
|
|
135
148
|
const boardColumns = new Set(db.all("pragma table_info(board_tasks)").map((column) => column.name));
|
|
149
|
+
if (!db.all("pragma table_info(board_standing)").some((column) => column.name === "mode")) db.exec("alter table board_standing add column mode text not null default 'named'");
|
|
136
150
|
if (!boardColumns.has("holder")) {
|
|
137
151
|
db.exec(`alter table board_tasks add column holder text;
|
|
138
152
|
alter table board_tasks add column version integer not null default 1;
|
|
@@ -344,6 +358,38 @@ export function openStore(file) {
|
|
|
344
358
|
if (written.changes === 1) return { ok: true, task: this.getTask(id) };
|
|
345
359
|
return conflict(this.getTask(id), agent, null, (task) => task.run_authorized_at ? "the run is already authorized" : !task.run_workspace ? "this task did not ask for a run" : `task is ${task.status}`);
|
|
346
360
|
},
|
|
361
|
+
// Standing permission: an agent starts the work the person or Harness posts for it without a Start each time. By
|
|
362
|
+
// default it covers the folders the person has used that agent in (its indexed sessions); the person can name
|
|
363
|
+
// folders instead (mode 'named') or turn it off ('off'). Only the person changes it; the daemon decides each post.
|
|
364
|
+
setStanding(agent, mode, workspaces = [], dailyCap = DEFAULT_DAILY_RUNS) {
|
|
365
|
+
execute("run", `insert into board_standing(agent, mode, workspaces, daily_cap, set_at) values(?, ?, ?, ?, ?)
|
|
366
|
+
on conflict(agent) do update set mode = excluded.mode, workspaces = excluded.workspaces, daily_cap = excluded.daily_cap, set_at = excluded.set_at`,
|
|
367
|
+
agent, mode, JSON.stringify(workspaces), dailyCap, new Date().toISOString());
|
|
368
|
+
},
|
|
369
|
+
resetStanding(agent) {
|
|
370
|
+
return execute("run", "delete from board_standing where agent = ?", agent).changes === 1;
|
|
371
|
+
},
|
|
372
|
+
// One rule per agent installed here, with the runs it started since `since`.
|
|
373
|
+
standing(agents, since) {
|
|
374
|
+
const rows = new Map(execute("all", "select * from board_standing").map((row) => [row.agent, row]));
|
|
375
|
+
return agents.map((agent) => {
|
|
376
|
+
const row = rows.get(agent);
|
|
377
|
+
const mode = row?.mode || "default";
|
|
378
|
+
const workspaces = mode === "default" ? execute("all", "select distinct cwd from sessions where agent = ? and cwd is not null and cwd != '' order by cwd", agent).map((entry) => entry.cwd) : JSON.parse(row.workspaces);
|
|
379
|
+
return { agent, mode, workspaces, daily_cap: row ? Number(row.daily_cap) : DEFAULT_DAILY_RUNS, set_at: row?.set_at || null,
|
|
380
|
+
runs_today: Number(execute("get", "select count(*) as n from board_tasks where target_agent = ? and run_authorized_by = 'standing' and run_authorized_at >= ?", agent, since).n) };
|
|
381
|
+
});
|
|
382
|
+
},
|
|
383
|
+
meta(key, value) {
|
|
384
|
+
if (value === undefined) return execute("get", "select value from board_meta where key = ?", key)?.value ?? null;
|
|
385
|
+
execute("run", "insert into board_meta(key, value) values(?, ?) on conflict(key) do update set value = excluded.value", key, String(value));
|
|
386
|
+
},
|
|
387
|
+
authorizeStanding(id) {
|
|
388
|
+
const now = new Date().toISOString();
|
|
389
|
+
const written = execute("run", `update board_tasks set run_authorized_by = 'standing', run_authorized_at = @now, updated_at = @now, version = version + 1
|
|
390
|
+
where id = @id and status = 'open' and trust = 'user' and run_workspace is not null and run_authorized_at is null and id not in (select task_id from board_threads)`, { id, now });
|
|
391
|
+
return written.changes === 1;
|
|
392
|
+
},
|
|
347
393
|
// Tasks the daemon should run now: authorized, still open, for an agent installed here.
|
|
348
394
|
runnableTasks(agents) {
|
|
349
395
|
if (!agents.length) return [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tryharness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Harness Relay: the daemon that turns the agents on this machine into your personal swarm. Memory, history, and work handed between any of them.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"tryharness": "cli.mjs"
|