@monoes/monomindcli 2.14.1 → 2.15.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/.claude/helpers/control-start.cjs +10 -13
- package/.claude/skills/monodesign/scripts/context.mjs +4 -4
- package/.claude/skills/monodesign/scripts/critique-storage.mjs +1 -1
- package/.claude/skills/monodesign/scripts/detect-csp.mjs +1 -6
- package/.claude/skills/monodesign/scripts/detector/browser/injected/index.mjs +16 -18
- package/.claude/skills/monodesign/scripts/detector/cli/main.mjs +3 -3
- package/.claude/skills/monodesign/scripts/detector/detect-antipatterns-browser.js +67 -47
- package/.claude/skills/monodesign/scripts/detector/engines/regex/detect-text.mjs +6 -6
- package/.claude/skills/monodesign/scripts/detector/engines/static-html/css-cascade.mjs +6 -5
- package/.claude/skills/monodesign/scripts/detector/engines/static-html/detect-html.mjs +6 -6
- package/.claude/skills/monodesign/scripts/detector/fix/index.mjs +3 -3
- package/.claude/skills/monodesign/scripts/detector/node/file-system.mjs +1 -1
- package/.claude/skills/monodesign/scripts/detector/registry/antipatterns.mjs +1 -1
- package/.claude/skills/monodesign/scripts/detector/rules/checks.mjs +24 -25
- package/.claude/skills/monodesign/scripts/detector/shared/color.mjs +2 -2
- package/.claude/skills/monodesign/scripts/detector/shared/constants.mjs +1 -1
- package/.claude/skills/monodesign/scripts/detector/shared/inline-ignores.mjs +2 -2
- package/.claude/skills/monodesign/scripts/hook-admin.mjs +6 -6
- package/.claude/skills/monodesign/scripts/hook-before-edit.mjs +4 -4
- package/.claude/skills/monodesign/scripts/hook-lib.mjs +20 -11
- package/.claude/skills/monodesign/scripts/hook.mjs +1 -1
- package/.claude/skills/monodesign/scripts/lib/design-parser.mjs +9 -42
- package/.claude/skills/monodesign/scripts/lib/is-generated.mjs +1 -1
- package/.claude/skills/monodesign/scripts/lib/monodesign-config.mjs +5 -5
- package/.claude/skills/monodesign/scripts/lib/monodesign-paths.mjs +1 -1
- package/.claude/skills/monodesign/scripts/live/event-validation.mjs +13 -13
- package/.claude/skills/monodesign/scripts/live/manual-apply.mjs +4 -4
- package/.claude/skills/monodesign/scripts/live/manual-edits-buffer.mjs +1 -1
- package/.claude/skills/monodesign/scripts/live/session-store.mjs +5 -5
- package/.claude/skills/monodesign/scripts/live/svelte-component.mjs +13 -13
- package/.claude/skills/monodesign/scripts/live/sveltekit-adapter.mjs +4 -4
- package/.claude/skills/monodesign/scripts/live/ui-core.mjs +1 -1
- package/.claude/skills/monodesign/scripts/live-accept.mjs +24 -24
- package/.claude/skills/monodesign/scripts/live-browser-dom.js +8 -7
- package/.claude/skills/monodesign/scripts/live-browser-session.js +8 -7
- package/.claude/skills/monodesign/scripts/live-browser.js +405 -432
- package/.claude/skills/monodesign/scripts/live-commit-manual-edits.mjs +6 -6
- package/.claude/skills/monodesign/scripts/live-copy-edit-agent.mjs +2 -2
- package/.claude/skills/monodesign/scripts/live-discard-manual-edits.mjs +1 -1
- package/.claude/skills/monodesign/scripts/live-inject.mjs +7 -7
- package/.claude/skills/monodesign/scripts/live-insert.mjs +11 -11
- package/.claude/skills/monodesign/scripts/live-manual-edit-evidence.mjs +4 -4
- package/.claude/skills/monodesign/scripts/live-poll.mjs +5 -5
- package/.claude/skills/monodesign/scripts/live-server.mjs +10 -10
- package/.claude/skills/monodesign/scripts/live-wrap.mjs +31 -41
- package/.claude/skills/monodesign/scripts/live.mjs +5 -5
- package/.claude/skills/monodesign/scripts/palette.mjs +3 -1
- package/.claude/skills/monodesign/scripts/pin.mjs +1 -1
- package/dist/src/commands/doc-filters.d.ts.map +1 -1
- package/dist/src/commands/doc-filters.js +6 -0
- package/dist/src/commands/doc-filters.js.map +1 -1
- package/dist/src/commands/doc-library.d.ts +2 -1
- package/dist/src/commands/doc-library.d.ts.map +1 -1
- package/dist/src/commands/doc-library.js +66 -2
- package/dist/src/commands/doc-library.js.map +1 -1
- package/dist/src/commands/doc.d.ts.map +1 -1
- package/dist/src/commands/doc.js +2 -0
- package/dist/src/commands/doc.js.map +1 -1
- package/dist/src/commands/ui.d.ts.map +1 -1
- package/dist/src/commands/ui.js +60 -0
- package/dist/src/commands/ui.js.map +1 -1
- package/dist/src/knowledge/capture-envelope.d.ts +5 -0
- package/dist/src/knowledge/capture-envelope.d.ts.map +1 -1
- package/dist/src/knowledge/capture-envelope.js +2 -0
- package/dist/src/knowledge/capture-envelope.js.map +1 -1
- package/dist/src/knowledge/document-chunking.d.ts +42 -0
- package/dist/src/knowledge/document-chunking.d.ts.map +1 -0
- package/dist/src/knowledge/document-chunking.js +288 -0
- package/dist/src/knowledge/document-chunking.js.map +1 -0
- package/dist/src/knowledge/document-index.d.ts +124 -0
- package/dist/src/knowledge/document-index.d.ts.map +1 -0
- package/dist/src/knowledge/document-index.js +314 -0
- package/dist/src/knowledge/document-index.js.map +1 -0
- package/dist/src/knowledge/document-ingest.d.ts +23 -0
- package/dist/src/knowledge/document-ingest.d.ts.map +1 -0
- package/dist/src/knowledge/document-ingest.js +368 -0
- package/dist/src/knowledge/document-ingest.js.map +1 -0
- package/dist/src/knowledge/document-pipeline.d.ts +26 -208
- package/dist/src/knowledge/document-pipeline.d.ts.map +1 -1
- package/dist/src/knowledge/document-pipeline.js +25 -1104
- package/dist/src/knowledge/document-pipeline.js.map +1 -1
- package/dist/src/knowledge/document-search.d.ts +27 -0
- package/dist/src/knowledge/document-search.d.ts.map +1 -0
- package/dist/src/knowledge/document-search.js +138 -0
- package/dist/src/knowledge/document-search.js.map +1 -0
- package/dist/src/knowledge/document-store.d.ts +28 -0
- package/dist/src/knowledge/document-store.d.ts.map +1 -0
- package/dist/src/knowledge/document-store.js +61 -0
- package/dist/src/knowledge/document-store.js.map +1 -0
- package/dist/src/knowledge/document-types.d.ts +103 -0
- package/dist/src/knowledge/document-types.d.ts.map +1 -0
- package/dist/src/knowledge/document-types.js +12 -0
- package/dist/src/knowledge/document-types.js.map +1 -0
- package/dist/src/knowledge/highlights.d.ts +168 -0
- package/dist/src/knowledge/highlights.d.ts.map +1 -0
- package/dist/src/knowledge/highlights.js +311 -0
- package/dist/src/knowledge/highlights.js.map +1 -0
- package/dist/src/knowledge/lookup.d.ts +80 -0
- package/dist/src/knowledge/lookup.d.ts.map +1 -0
- package/dist/src/knowledge/lookup.js +156 -0
- package/dist/src/knowledge/lookup.js.map +1 -0
- package/dist/src/knowledge/okf-bundle.d.ts +19 -0
- package/dist/src/knowledge/okf-bundle.d.ts.map +1 -0
- package/dist/src/knowledge/okf-bundle.js +106 -0
- package/dist/src/knowledge/okf-bundle.js.map +1 -0
- package/dist/src/knowledge/profile-store.d.ts +134 -0
- package/dist/src/knowledge/profile-store.d.ts.map +1 -0
- package/dist/src/knowledge/profile-store.js +237 -0
- package/dist/src/knowledge/profile-store.js.map +1 -0
- package/dist/src/orgrt/agent-runner.d.ts +4 -0
- package/dist/src/orgrt/agent-runner.d.ts.map +1 -1
- package/dist/src/orgrt/agent-runner.js +22 -0
- package/dist/src/orgrt/agent-runner.js.map +1 -1
- package/dist/src/orgrt/antigravity-runner.d.ts.map +1 -1
- package/dist/src/orgrt/antigravity-runner.js +2 -1
- package/dist/src/orgrt/antigravity-runner.js.map +1 -1
- package/dist/src/orgrt/authority-mask.d.ts +32 -0
- package/dist/src/orgrt/authority-mask.d.ts.map +1 -0
- package/dist/src/orgrt/authority-mask.js +135 -0
- package/dist/src/orgrt/authority-mask.js.map +1 -0
- package/dist/src/orgrt/codex-runner.d.ts.map +1 -1
- package/dist/src/orgrt/codex-runner.js +2 -1
- package/dist/src/orgrt/codex-runner.js.map +1 -1
- package/dist/src/orgrt/copilot-runner.d.ts.map +1 -1
- package/dist/src/orgrt/copilot-runner.js +2 -1
- package/dist/src/orgrt/copilot-runner.js.map +1 -1
- package/dist/src/orgrt/crush-runner.d.ts.map +1 -1
- package/dist/src/orgrt/crush-runner.js +6 -1
- package/dist/src/orgrt/crush-runner.js.map +1 -1
- package/dist/src/orgrt/daemon.d.ts +4 -0
- package/dist/src/orgrt/daemon.d.ts.map +1 -1
- package/dist/src/orgrt/daemon.js +13 -2
- package/dist/src/orgrt/daemon.js.map +1 -1
- package/dist/src/orgrt/decisions.d.ts +9 -0
- package/dist/src/orgrt/decisions.d.ts.map +1 -1
- package/dist/src/orgrt/decisions.js +32 -5
- package/dist/src/orgrt/decisions.js.map +1 -1
- package/dist/src/orgrt/file-roots.d.ts +2 -1
- package/dist/src/orgrt/file-roots.d.ts.map +1 -1
- package/dist/src/orgrt/file-roots.js +7 -3
- package/dist/src/orgrt/file-roots.js.map +1 -1
- package/dist/src/orgrt/grok-runner.d.ts.map +1 -1
- package/dist/src/orgrt/grok-runner.js +2 -1
- package/dist/src/orgrt/grok-runner.js.map +1 -1
- package/dist/src/orgrt/hermes-runner.d.ts.map +1 -1
- package/dist/src/orgrt/hermes-runner.js +2 -1
- package/dist/src/orgrt/hermes-runner.js.map +1 -1
- package/dist/src/orgrt/inbox.d.ts.map +1 -1
- package/dist/src/orgrt/inbox.js +70 -4
- package/dist/src/orgrt/inbox.js.map +1 -1
- package/dist/src/orgrt/kimicode-runner.d.ts.map +1 -1
- package/dist/src/orgrt/kimicode-runner.js +2 -1
- package/dist/src/orgrt/kimicode-runner.js.map +1 -1
- package/dist/src/orgrt/opencode-runner.d.ts.map +1 -1
- package/dist/src/orgrt/opencode-runner.js +3 -2
- package/dist/src/orgrt/opencode-runner.js.map +1 -1
- package/dist/src/orgrt/pi-rpc-runner.d.ts.map +1 -1
- package/dist/src/orgrt/pi-rpc-runner.js +3 -2
- package/dist/src/orgrt/pi-rpc-runner.js.map +1 -1
- package/dist/src/orgrt/pi-runner.d.ts.map +1 -1
- package/dist/src/orgrt/pi-runner.js +2 -1
- package/dist/src/orgrt/pi-runner.js.map +1 -1
- package/dist/src/orgrt/policy.d.ts.map +1 -1
- package/dist/src/orgrt/policy.js +3 -0
- package/dist/src/orgrt/policy.js.map +1 -1
- package/dist/src/orgrt/qwen-rpc-runner.d.ts.map +1 -1
- package/dist/src/orgrt/qwen-rpc-runner.js +3 -2
- package/dist/src/orgrt/qwen-rpc-runner.js.map +1 -1
- package/dist/src/orgrt/qwen-runner.d.ts.map +1 -1
- package/dist/src/orgrt/qwen-runner.js +2 -1
- package/dist/src/orgrt/qwen-runner.js.map +1 -1
- package/dist/src/orgrt/role-sandbox.d.ts +21 -0
- package/dist/src/orgrt/role-sandbox.d.ts.map +1 -1
- package/dist/src/orgrt/role-sandbox.js +31 -6
- package/dist/src/orgrt/role-sandbox.js.map +1 -1
- package/dist/src/orgrt/session.d.ts.map +1 -1
- package/dist/src/orgrt/session.js +21 -2
- package/dist/src/orgrt/session.js.map +1 -1
- package/dist/src/orgrt/task-dag.d.ts +3 -0
- package/dist/src/orgrt/task-dag.d.ts.map +1 -1
- package/dist/src/orgrt/task-dag.js +1 -0
- package/dist/src/orgrt/task-dag.js.map +1 -1
- package/dist/src/orgrt/types.d.ts +5 -0
- package/dist/src/orgrt/types.d.ts.map +1 -1
- package/dist/src/orgrt/types.js +23 -1
- package/dist/src/orgrt/types.js.map +1 -1
- package/dist/src/ui/dashboard.html +28 -848
- package/dist/src/ui/human-auth.mjs +103 -0
- package/dist/src/ui/org-hil.mjs +86 -37
- package/dist/src/ui/org-runtime.mjs +21 -13
- package/dist/src/ui/routes-org.mjs +49 -445
- package/dist/src/ui/server.mjs +90 -2
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Proof that a human is at the browser, for the dashboard.
|
|
2
|
+
//
|
|
3
|
+
// The dashboard token (<project>/.monomind/dashboard-token) is a machine
|
|
4
|
+
// credential: hooks, the CLI and the org forwarder send it. It must not be
|
|
5
|
+
// enough to act as the human who supervises an org — approve a tool call,
|
|
6
|
+
// resolve a gate, answer a question, message a role, change an org's config —
|
|
7
|
+
// because anything running as this user, org roles included, can reach the
|
|
8
|
+
// dashboard's port, and the page used to hand the token to any GET /.
|
|
9
|
+
//
|
|
10
|
+
// Human authority is instead a browser session cookie derived from a secret
|
|
11
|
+
// in ~/.monomind/dashboard-auth/, a directory every org role is masked from
|
|
12
|
+
// (orgrt/authority-mask.ts). A browser gets the cookie by opening a one-time
|
|
13
|
+
// login link — `monomind dashboard open`, or the tab the dashboard opens when
|
|
14
|
+
// it starts — whose nonce is a file in that same directory, used once and
|
|
15
|
+
// short-lived, so a copy left in browser history is worthless.
|
|
16
|
+
import crypto from 'node:crypto';
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import os from 'node:os';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
|
|
21
|
+
export const HUMAN_COOKIE = 'mm_human';
|
|
22
|
+
const NONCE_TTL_MS = 10 * 60_000;
|
|
23
|
+
const SESSION_MAX_AGE_S = 30 * 24 * 60 * 60;
|
|
24
|
+
|
|
25
|
+
export function humanAuthDir(home = os.homedir()) {
|
|
26
|
+
return path.join(home, '.monomind', 'dashboard-auth');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The per-user secret, created owner-only on first use. */
|
|
30
|
+
function secret(home) {
|
|
31
|
+
const dir = humanAuthDir(home);
|
|
32
|
+
const file = path.join(dir, 'secret');
|
|
33
|
+
try {
|
|
34
|
+
return fs.readFileSync(file);
|
|
35
|
+
} catch {
|
|
36
|
+
/* create below */
|
|
37
|
+
}
|
|
38
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
39
|
+
try {
|
|
40
|
+
fs.writeFileSync(file, crypto.randomBytes(32), { mode: 0o600, flag: 'wx' });
|
|
41
|
+
} catch (err) {
|
|
42
|
+
if (err.code !== 'EEXIST') throw err; // another process created it first
|
|
43
|
+
}
|
|
44
|
+
return fs.readFileSync(file);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function sessionValue(home) {
|
|
48
|
+
return crypto.createHmac('sha256', secret(home)).update('dashboard-session-v1').digest('hex');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A new one-time login nonce (the `?login=` of a login link). */
|
|
52
|
+
export function issueLoginNonce(home = os.homedir()) {
|
|
53
|
+
secret(home); // the dir exists, owner-only
|
|
54
|
+
const pending = path.join(humanAuthDir(home), 'login');
|
|
55
|
+
fs.mkdirSync(pending, { recursive: true, mode: 0o700 });
|
|
56
|
+
const nonce = crypto.randomBytes(32).toString('hex');
|
|
57
|
+
fs.writeFileSync(path.join(pending, nonce), '', { mode: 0o600, flag: 'wx' });
|
|
58
|
+
return nonce;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Spend a login nonce: true once, for a nonce issued in the last 10 minutes. */
|
|
62
|
+
export function consumeLoginNonce(nonce, home = os.homedir()) {
|
|
63
|
+
if (typeof nonce !== 'string' || !/^[0-9a-f]{64}$/.test(nonce)) return false;
|
|
64
|
+
const file = path.join(humanAuthDir(home), 'login', nonce);
|
|
65
|
+
let st;
|
|
66
|
+
try {
|
|
67
|
+
st = fs.statSync(file);
|
|
68
|
+
fs.unlinkSync(file); // used once, whatever the outcome below
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
return Date.now() - st.mtimeMs <= NONCE_TTL_MS;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Set-Cookie value that marks this browser as the human's. */
|
|
76
|
+
export function humanSessionCookie(home = os.homedir()) {
|
|
77
|
+
return `${HUMAN_COOKIE}=${sessionValue(home)}; HttpOnly; SameSite=Strict; Path=/; Max-Age=${SESSION_MAX_AGE_S}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Whether `req` carries the human session cookie. */
|
|
81
|
+
export function isHumanRequest(req, home = os.homedir()) {
|
|
82
|
+
const header = req.headers?.cookie;
|
|
83
|
+
if (!header) return false;
|
|
84
|
+
const want = Buffer.from(sessionValue(home));
|
|
85
|
+
for (const part of String(header).split(';')) {
|
|
86
|
+
const [k, ...v] = part.trim().split('=');
|
|
87
|
+
if (k !== HUMAN_COOKIE) continue;
|
|
88
|
+
const got = Buffer.from(v.join('='));
|
|
89
|
+
if (got.length === want.length && crypto.timingSafeEqual(got, want)) return true;
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Page for a browser without the session: how to get a login link. */
|
|
95
|
+
export function loginRequiredPage() {
|
|
96
|
+
return `<!doctype html><meta charset="utf-8"><title>monomind dashboard</title>
|
|
97
|
+
<style>body{font:15px/1.5 system-ui,sans-serif;background:#0b0b0c;color:#ddd;display:grid;place-items:center;min-height:100vh;margin:0}
|
|
98
|
+
main{max-width:34rem;padding:2rem}code{background:#222;padding:.15em .4em;border-radius:4px}</style>
|
|
99
|
+
<main><h1 style="font-size:1.2rem">Open the dashboard from your terminal</h1>
|
|
100
|
+
<p>This browser has no dashboard session. Run</p><p><code>monomind dashboard open</code></p>
|
|
101
|
+
<p>It opens a one-time login link. The session then lasts 30 days in this browser.</p>
|
|
102
|
+
<p style="color:#888;font-size:13px">The dashboard can approve and answer for you, so it only trusts a browser you logged in yourself — not every program that can reach this port.</p></main>`;
|
|
103
|
+
}
|
package/dist/src/ui/org-hil.mjs
CHANGED
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
// decision is recorded in the org's own files, where the next run finds it.
|
|
9
9
|
// Unlike the CLI, a live daemon that REJECTS the call is reported as an error
|
|
10
10
|
// instead of falling back to a file write the daemon would overwrite.
|
|
11
|
+
import { randomBytes } from 'node:crypto';
|
|
11
12
|
import fs from 'node:fs';
|
|
12
13
|
import path from 'node:path';
|
|
13
14
|
// Compiled from orgrt/*.ts, like forwarder.js in routes-org.mjs.
|
|
14
|
-
import { lookupOrg, readOperatorCredential } from '../orgrt/broker.js';
|
|
15
|
+
import { lookupOrg, normalizeRoot, readOperatorCredential } from '../orgrt/broker.js';
|
|
15
16
|
import { queueMessage } from '../orgrt/inbox.js';
|
|
16
17
|
|
|
17
18
|
/** Recorded as resolvedBy on everything decided here (normalizeResolver-valid). */
|
|
@@ -49,20 +50,48 @@ function readList(root, org, file, key) {
|
|
|
49
50
|
return list;
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
/** Write `text` to `dest` via a fresh temp file and a rename. Roles can write
|
|
54
|
+
* into the org directory, so the temp name is unpredictable and created
|
|
55
|
+
* exclusively (O_EXCL): a link planted there fails the open instead of
|
|
56
|
+
* redirecting the dashboard's write to a file of the link's choosing. */
|
|
57
|
+
export function writeFileAtomic(dest, text) {
|
|
58
|
+
const tmp = `${dest}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`;
|
|
59
|
+
const fd = fs.openSync(tmp, 'wx', 0o600);
|
|
60
|
+
try {
|
|
61
|
+
try {
|
|
62
|
+
fs.writeFileSync(fd, text);
|
|
63
|
+
} finally {
|
|
64
|
+
fs.closeSync(fd);
|
|
65
|
+
}
|
|
66
|
+
fs.renameSync(tmp, dest);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
fs.rmSync(tmp, { force: true });
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
52
73
|
function writeList(root, org, file, key, list) {
|
|
53
74
|
const dir = orgDir(root, org);
|
|
54
75
|
fs.mkdirSync(dir, { recursive: true });
|
|
55
|
-
|
|
56
|
-
const tmp = `${dest}.${process.pid}.tmp`;
|
|
57
|
-
fs.writeFileSync(tmp, JSON.stringify({ [key]: list }, null, 2));
|
|
58
|
-
fs.renameSync(tmp, dest);
|
|
76
|
+
writeFileAtomic(path.join(dir, file), JSON.stringify({ [key]: list }, null, 2));
|
|
59
77
|
}
|
|
60
78
|
|
|
61
|
-
/**
|
|
62
|
-
*
|
|
63
|
-
|
|
79
|
+
/** The broker entry of the daemon hosting `org` for THIS project, or null.
|
|
80
|
+
* The registry is machine-wide and keyed by name, so a same-named org in
|
|
81
|
+
* another project must not receive this project's decisions. An entry that
|
|
82
|
+
* predates the root field (M4) is taken as this project's. */
|
|
83
|
+
export function hostingDaemon(root, org) {
|
|
64
84
|
const remote = lookupOrg(org);
|
|
65
85
|
if (!remote) return null;
|
|
86
|
+
if (remote.root && remote.root !== normalizeRoot(root)) return null;
|
|
87
|
+
return remote;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** POST to the hosting daemon's operator-only `route`. Returns null when no
|
|
91
|
+
* live daemon hosts `org` for `root`; otherwise the daemon's verdict. */
|
|
92
|
+
async function callDaemon(root, org, route, body) {
|
|
93
|
+
const remote = hostingDaemon(root, org);
|
|
94
|
+
if (!remote) return null;
|
|
66
95
|
const cred = readOperatorCredential(org);
|
|
67
96
|
if (!cred)
|
|
68
97
|
throw new HilError(
|
|
@@ -84,7 +113,7 @@ async function callDaemon(org, route, body) {
|
|
|
84
113
|
if (!res.ok || !data.ok)
|
|
85
114
|
throw new HilError(
|
|
86
115
|
res.status >= 400 ? res.status : 502,
|
|
87
|
-
data.error || `daemon returned ${res.status}`,
|
|
116
|
+
data.error || data.receipt || `daemon returned ${res.status}`,
|
|
88
117
|
);
|
|
89
118
|
return data;
|
|
90
119
|
}
|
|
@@ -95,15 +124,19 @@ function approvalStatus(a) {
|
|
|
95
124
|
return 'pending';
|
|
96
125
|
}
|
|
97
126
|
|
|
98
|
-
/** The org's tool/action approvals (`<org>/approvals.json`), newest first.
|
|
127
|
+
/** The org's tool/action approvals (`<org>/approvals.json`), newest first.
|
|
128
|
+
* An approval request lives in the daemon's memory and ends with its run —
|
|
129
|
+
* no daemon reads approvals.json back — so while no daemon hosts the org a
|
|
130
|
+
* pending one is 'expired': nothing is waiting for the answer. */
|
|
99
131
|
export function listApprovals(root, org) {
|
|
132
|
+
const live = !!hostingDaemon(root, org);
|
|
100
133
|
return readList(root, org, 'approvals.json', 'approvals')
|
|
101
134
|
.map((a) => ({
|
|
102
135
|
id: a.requestId || null,
|
|
103
136
|
roleId: a.roleId,
|
|
104
137
|
action: a.action,
|
|
105
138
|
input: a.input || null,
|
|
106
|
-
status: approvalStatus(a),
|
|
139
|
+
status: approvalStatus(a) === 'pending' && !live ? 'expired' : approvalStatus(a),
|
|
107
140
|
ts: a.ts || null,
|
|
108
141
|
resolvedBy: a.resolvedBy || null,
|
|
109
142
|
resolvedAt: a.resolvedAt || null,
|
|
@@ -150,12 +183,17 @@ export function pendingForProject(root) {
|
|
|
150
183
|
for (const e of entries) {
|
|
151
184
|
if (!e.isDirectory() || !/^[a-z0-9][a-z0-9_-]{0,63}$/i.test(e.name)) continue;
|
|
152
185
|
const org = e.name;
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
186
|
+
// One unreadable file must not hide the org's other two.
|
|
187
|
+
for (const [list, key] of [
|
|
188
|
+
[listQuestions, 'questions'],
|
|
189
|
+
[listApprovals, 'approvals'],
|
|
190
|
+
[listGates, 'gates'],
|
|
191
|
+
]) {
|
|
192
|
+
try {
|
|
193
|
+
for (const item of list(root, org)) out[key].push({ org, ...item });
|
|
194
|
+
} catch (err) {
|
|
195
|
+
out.errors.push({ org, error: err.message });
|
|
196
|
+
}
|
|
159
197
|
}
|
|
160
198
|
}
|
|
161
199
|
return out;
|
|
@@ -168,24 +206,35 @@ export async function resolveApproval(root, org, requestId, approved) {
|
|
|
168
206
|
if (!entry) throw new HilError(404, `approval "${requestId}" not found for org "${org}"`);
|
|
169
207
|
if (entry.approved !== null)
|
|
170
208
|
throw new HilError(409, `approval "${requestId}" already ${approvalStatus(entry)}`);
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
209
|
+
const ended = `approval "${requestId}" ended with the run that asked for it; the role asks again when it needs to`;
|
|
210
|
+
let live;
|
|
211
|
+
try {
|
|
212
|
+
live = await callDaemon(root, org, '/api/set-approval', {
|
|
213
|
+
org,
|
|
214
|
+
role: entry.roleId,
|
|
215
|
+
action: entry.action,
|
|
216
|
+
approved,
|
|
217
|
+
resolvedBy: DASHBOARD_RESOLVER,
|
|
218
|
+
requestId,
|
|
219
|
+
});
|
|
220
|
+
} catch (err) {
|
|
221
|
+
// A daemon started since (e.g. `--resume` in a new process) holds only
|
|
222
|
+
// the approvals its own run asked for.
|
|
223
|
+
if (err instanceof HilError && err.status === 404) {
|
|
224
|
+
// …or someone resolved it first: the daemon writes the file before it answers.
|
|
225
|
+
const now = readList(root, org, 'approvals.json', 'approvals').find(
|
|
226
|
+
(a) => a.requestId === requestId,
|
|
227
|
+
);
|
|
228
|
+
if (now && now.approved !== null)
|
|
229
|
+
throw new HilError(409, `approval "${requestId}" already ${approvalStatus(now)}`);
|
|
230
|
+
throw new HilError(409, ended);
|
|
231
|
+
}
|
|
232
|
+
throw err;
|
|
233
|
+
}
|
|
179
234
|
if (live) return { delivery: 'live' };
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if (!item || item.approved !== null)
|
|
184
|
-
throw new HilError(409, `approval "${requestId}" was resolved meanwhile`);
|
|
185
|
-
const now = Date.now();
|
|
186
|
-
Object.assign(item, { approved, ts: now, resolvedBy: DASHBOARD_RESOLVER, resolvedAt: now });
|
|
187
|
-
writeList(root, org, 'approvals.json', 'approvals', fresh);
|
|
188
|
-
return { delivery: 'recorded' };
|
|
235
|
+
// Unlike gates and answers, nothing reads a recorded approval back: the
|
|
236
|
+
// request lived in the daemon's memory, and a new run asks again.
|
|
237
|
+
throw new HilError(409, `no running daemon hosts org "${org}" for this project — ${ended}`);
|
|
189
238
|
}
|
|
190
239
|
|
|
191
240
|
export async function resolveGate(root, org, gateId, approved, resolution) {
|
|
@@ -193,7 +242,7 @@ export async function resolveGate(root, org, gateId, approved, resolution) {
|
|
|
193
242
|
if (!gate) throw new HilError(404, `gate "${gateId}" not found for org "${org}"`);
|
|
194
243
|
if (gate.status !== 'pending')
|
|
195
244
|
throw new HilError(409, `gate "${gateId}" already resolved (${gate.status})`);
|
|
196
|
-
const live = await callDaemon(org, '/api/resolve-gate', {
|
|
245
|
+
const live = await callDaemon(root, org, '/api/resolve-gate', {
|
|
197
246
|
org,
|
|
198
247
|
gateId,
|
|
199
248
|
approved,
|
|
@@ -223,7 +272,7 @@ export async function answerQuestion(root, org, questionId, answer) {
|
|
|
223
272
|
if (!q) throw new HilError(404, `question "${questionId}" not found for org "${org}"`);
|
|
224
273
|
if (q.answer !== null && q.answer !== undefined)
|
|
225
274
|
throw new HilError(409, `question "${questionId}" was already answered`);
|
|
226
|
-
const live = await callDaemon(org, '/api/answer-question', {
|
|
275
|
+
const live = await callDaemon(root, org, '/api/answer-question', {
|
|
227
276
|
org,
|
|
228
277
|
role: q.role,
|
|
229
278
|
questionId,
|
|
@@ -257,7 +306,7 @@ export async function answerQuestion(root, org, questionId, answer) {
|
|
|
257
306
|
/** Deliver `text` to `role`: live into its mailbox, else into the org's
|
|
258
307
|
* inbox, which the daemon drains when the org next starts. */
|
|
259
308
|
export async function sendHumanMessage(root, org, role, text) {
|
|
260
|
-
const live = await callDaemon(org, '/api/human-message', { org, role, text });
|
|
309
|
+
const live = await callDaemon(root, org, '/api/human-message', { org, role, text });
|
|
261
310
|
if (live) return { delivery: 'live' };
|
|
262
311
|
const queued = queueMessage(root, org, {
|
|
263
312
|
fromQualified: 'human',
|
|
@@ -11,16 +11,17 @@
|
|
|
11
11
|
// - the org definition, resolved through cost_tiers like session.ts does.
|
|
12
12
|
import fs from 'node:fs';
|
|
13
13
|
import path from 'node:path';
|
|
14
|
-
// Compiled from orgrt/*.ts, like forwarder.js in routes-org.mjs.
|
|
15
|
-
import { lookupOrg } from '../orgrt/broker.js';
|
|
16
14
|
import { resolveRoleCostTier, validateCostTiers } from '../orgrt/cost-tier.js';
|
|
17
15
|
import { readIdleStatus } from '../orgrt/idle-deadline.js';
|
|
18
16
|
import { OrgDefSchema } from '../orgrt/types.js';
|
|
17
|
+
import { hostingDaemon, writeFileAtomic } from './org-hil.mjs';
|
|
19
18
|
|
|
20
19
|
const orgDir = (root, org) => path.join(root, '.monomind', 'orgs', org);
|
|
21
20
|
|
|
22
|
-
/**
|
|
23
|
-
*
|
|
21
|
+
/** Reasons the Runtime pane lists: the completion-evidence gate, the idle
|
|
22
|
+
* watchdog, org_complete refusals and role failures. The runtime emits some
|
|
23
|
+
* as 'audit' events and some (budget exhaustion, agent-fatal, loadout
|
|
24
|
+
* mismatch) as 'status' events, so both types are read. */
|
|
24
25
|
export const RUNTIME_AUDIT_REASONS = new Set([
|
|
25
26
|
'task-evidence-refused',
|
|
26
27
|
'task-evidence-escalated',
|
|
@@ -37,6 +38,11 @@ export const RUNTIME_AUDIT_REASONS = new Set([
|
|
|
37
38
|
'session-result-error',
|
|
38
39
|
'loadout-unresolvable',
|
|
39
40
|
'loadout-mismatch',
|
|
41
|
+
'agent-session-crash',
|
|
42
|
+
'agent-context-limit',
|
|
43
|
+
'boss-context-limit',
|
|
44
|
+
'boss-restart',
|
|
45
|
+
'boss-restart-exhausted',
|
|
40
46
|
]);
|
|
41
47
|
const MAX_AUDIT = 60;
|
|
42
48
|
|
|
@@ -106,7 +112,7 @@ export function readRunDigest(root, org, run) {
|
|
|
106
112
|
u.cache_creation += Number(d.cache_creation) || 0;
|
|
107
113
|
u.cost_usd += Number(d.cost_usd) || 0;
|
|
108
114
|
u.turns += 1;
|
|
109
|
-
} else if (e.type === 'audit' && RUNTIME_AUDIT_REASONS.has(e.reason)) {
|
|
115
|
+
} else if ((e.type === 'audit' || e.type === 'status') && RUNTIME_AUDIT_REASONS.has(e.reason)) {
|
|
110
116
|
audit.push({
|
|
111
117
|
ts: e.ts,
|
|
112
118
|
reason: e.reason,
|
|
@@ -122,9 +128,10 @@ export function readRunDigest(root, org, run) {
|
|
|
122
128
|
return digest;
|
|
123
129
|
}
|
|
124
130
|
|
|
125
|
-
/** The live daemon's status for `org`, or null when no daemon hosts it
|
|
126
|
-
|
|
127
|
-
|
|
131
|
+
/** The live daemon's status for `org`, or null when no daemon hosts it for
|
|
132
|
+
* `root` (a same-named org in another project is not this one). */
|
|
133
|
+
async function liveStatus(root, org) {
|
|
134
|
+
const entry = hostingDaemon(root, org);
|
|
128
135
|
if (!entry) return null;
|
|
129
136
|
try {
|
|
130
137
|
const res = await fetch(`${entry.url}/api/status`, {
|
|
@@ -208,7 +215,7 @@ export async function runtimeView(root, org) {
|
|
|
208
215
|
const def = parsed.success ? parsed.data : { ...raw, run_config: raw.run_config ?? {} };
|
|
209
216
|
const rc = def.run_config ?? {};
|
|
210
217
|
const runtime = readJson(path.join(orgDir(root, org), 'runtime.json'));
|
|
211
|
-
const live = await liveStatus(org);
|
|
218
|
+
const live = await liveStatus(root, org);
|
|
212
219
|
const isLive = !!(live && !live.unreachable);
|
|
213
220
|
const run = (isLive ? live.run : null) ?? runtime?.run ?? null;
|
|
214
221
|
const digest = run ? readRunDigest(root, org, run) : null;
|
|
@@ -314,7 +321,10 @@ export function patchOrgConfig(root, org, patch) {
|
|
|
314
321
|
}
|
|
315
322
|
const problems = [];
|
|
316
323
|
const next = { ...raw };
|
|
317
|
-
if (patch.goal !== undefined)
|
|
324
|
+
if (patch.goal !== undefined) {
|
|
325
|
+
if (typeof patch.goal === 'string') next.goal = patch.goal;
|
|
326
|
+
else problems.push('goal must be a string');
|
|
327
|
+
}
|
|
318
328
|
if (patch.schedule !== undefined) next.schedule = patch.schedule === '' ? null : patch.schedule;
|
|
319
329
|
if (patch.run_config !== undefined) {
|
|
320
330
|
const rc = { ...(raw.run_config ?? {}) };
|
|
@@ -343,8 +353,6 @@ export function patchOrgConfig(root, org, patch) {
|
|
|
343
353
|
);
|
|
344
354
|
const tierProblems = validateCostTiers(parsed.data);
|
|
345
355
|
if (tierProblems.length) throw new ConfigRejected(tierProblems);
|
|
346
|
-
|
|
347
|
-
fs.writeFileSync(tmp, `${JSON.stringify(next, null, 2)}\n`);
|
|
348
|
-
fs.renameSync(tmp, file);
|
|
356
|
+
writeFileAtomic(file, `${JSON.stringify(next, null, 2)}\n`);
|
|
349
357
|
return next;
|
|
350
358
|
}
|