@ouro.bot/cli 0.1.0-alpha.1 → 0.1.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/heart/core.js +32 -2
- package/dist/heart/daemon/daemon-cli.js +68 -19
- package/dist/heart/daemon/daemon.js +3 -0
- package/dist/heart/daemon/hatch-flow.js +2 -1
- package/dist/heart/daemon/log-tailer.js +146 -0
- package/dist/heart/daemon/os-cron.js +260 -0
- package/dist/heart/daemon/process-manager.js +18 -1
- package/dist/heart/daemon/task-scheduler.js +4 -1
- package/dist/heart/identity.js +14 -3
- package/dist/mind/associative-recall.js +23 -2
- package/dist/mind/context.js +85 -1
- package/dist/mind/memory.js +62 -0
- package/dist/mind/pending.js +93 -0
- package/dist/mind/prompt-refresh.js +20 -0
- package/dist/mind/prompt.js +98 -0
- package/dist/nerves/coverage/file-completeness.js +14 -4
- package/dist/repertoire/tools-base.js +92 -0
- package/dist/senses/cli.js +89 -8
- package/dist/senses/inner-dialog.js +15 -0
- package/dist/senses/session-lock.js +119 -0
- package/dist/senses/teams.js +1 -0
- package/package.json +1 -1
- package/subagents/README.md +3 -1
- package/subagents/work-merger.md +33 -2
package/dist/senses/cli.js
CHANGED
|
@@ -48,6 +48,8 @@ const phrases_1 = require("../mind/phrases");
|
|
|
48
48
|
const format_1 = require("../mind/format");
|
|
49
49
|
const config_1 = require("../heart/config");
|
|
50
50
|
const context_1 = require("../mind/context");
|
|
51
|
+
const pending_1 = require("../mind/pending");
|
|
52
|
+
const prompt_refresh_1 = require("../mind/prompt-refresh");
|
|
51
53
|
const commands_1 = require("./commands");
|
|
52
54
|
const identity_1 = require("../heart/identity");
|
|
53
55
|
const nerves_1 = require("../nerves");
|
|
@@ -57,6 +59,7 @@ const tokens_1 = require("../mind/friends/tokens");
|
|
|
57
59
|
const cli_logging_1 = require("../nerves/cli-logging");
|
|
58
60
|
const runtime_1 = require("../nerves/runtime");
|
|
59
61
|
const trust_gate_1 = require("./trust-gate");
|
|
62
|
+
const session_lock_1 = require("./session-lock");
|
|
60
63
|
// spinner that only touches stderr, cleans up after itself
|
|
61
64
|
// exported for direct testability (stop-without-start branch)
|
|
62
65
|
class Spinner {
|
|
@@ -365,7 +368,10 @@ function createCliCallbacks() {
|
|
|
365
368
|
},
|
|
366
369
|
};
|
|
367
370
|
}
|
|
368
|
-
async function main() {
|
|
371
|
+
async function main(agentName, options) {
|
|
372
|
+
if (agentName)
|
|
373
|
+
(0, identity_1.setAgentName)(agentName);
|
|
374
|
+
const pasteDebounceMs = options?.pasteDebounceMs ?? 50;
|
|
369
375
|
// Fail fast if provider is misconfigured (triggers human-readable error + exit)
|
|
370
376
|
(0, core_1.getProvider)();
|
|
371
377
|
const registry = (0, commands_1.createCommandRegistry)();
|
|
@@ -388,15 +394,50 @@ async function main() {
|
|
|
388
394
|
signin: async () => undefined,
|
|
389
395
|
context: resolvedContext,
|
|
390
396
|
friendStore,
|
|
397
|
+
summarize: (0, core_1.createSummarize)(),
|
|
391
398
|
};
|
|
392
399
|
const friendId = resolvedContext.friend.id;
|
|
393
|
-
(0,
|
|
400
|
+
const agentConfig = (0, identity_1.loadAgentConfig)();
|
|
401
|
+
(0, cli_logging_1.configureCliRuntimeLogger)(friendId, {
|
|
402
|
+
level: agentConfig.logging?.level,
|
|
403
|
+
sinks: agentConfig.logging?.sinks,
|
|
404
|
+
});
|
|
394
405
|
const sessPath = (0, config_1.sessionPath)(friendId, "cli", "session");
|
|
406
|
+
let sessionLock = null;
|
|
407
|
+
try {
|
|
408
|
+
sessionLock = (0, session_lock_1.acquireSessionLock)(`${sessPath}.lock`, (0, identity_1.getAgentName)());
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
/* v8 ignore start -- integration: main() is interactive, lock tested in session-lock.test.ts @preserve */
|
|
412
|
+
if (error instanceof session_lock_1.SessionLockError) {
|
|
413
|
+
process.stderr.write(`${error.message}\n`);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
throw error;
|
|
417
|
+
/* v8 ignore stop */
|
|
418
|
+
}
|
|
395
419
|
// Load existing session or start fresh
|
|
396
420
|
const existing = (0, context_1.loadSession)(sessPath);
|
|
397
421
|
const messages = existing?.messages && existing.messages.length > 0
|
|
398
422
|
? existing.messages
|
|
399
423
|
: [{ role: "system", content: await (0, prompt_1.buildSystem)("cli", undefined, resolvedContext) }];
|
|
424
|
+
// Pending queue drain: inject pending messages as harness-context + assistant-content pairs
|
|
425
|
+
const pendingDir = (0, pending_1.getPendingDir)((0, identity_1.getAgentName)(), friendId, "cli", "session");
|
|
426
|
+
const drainToMessages = () => {
|
|
427
|
+
const pending = (0, pending_1.drainPending)(pendingDir);
|
|
428
|
+
if (pending.length === 0)
|
|
429
|
+
return 0;
|
|
430
|
+
for (const msg of pending) {
|
|
431
|
+
messages.push({ role: "user", name: "harness", content: `[proactive message from ${msg.from}]` });
|
|
432
|
+
messages.push({ role: "assistant", content: msg.content });
|
|
433
|
+
}
|
|
434
|
+
return pending.length;
|
|
435
|
+
};
|
|
436
|
+
// Startup drain: deliver offline messages
|
|
437
|
+
const startupCount = drainToMessages();
|
|
438
|
+
if (startupCount > 0) {
|
|
439
|
+
(0, context_1.saveSession)(sessPath, messages);
|
|
440
|
+
}
|
|
400
441
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
401
442
|
const ctrl = new InputController(rl);
|
|
402
443
|
let currentAbort = null;
|
|
@@ -429,8 +470,40 @@ async function main() {
|
|
|
429
470
|
rl.close();
|
|
430
471
|
}
|
|
431
472
|
});
|
|
473
|
+
// Debounced line iterator: collects rapid-fire lines (paste) into a single input
|
|
474
|
+
async function* debouncedLines(source) {
|
|
475
|
+
if (pasteDebounceMs <= 0) {
|
|
476
|
+
yield* source;
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
const iter = source[Symbol.asyncIterator]();
|
|
480
|
+
while (true) {
|
|
481
|
+
const first = await iter.next();
|
|
482
|
+
if (first.done)
|
|
483
|
+
break;
|
|
484
|
+
// Collect any lines that arrive within the debounce window (paste detection)
|
|
485
|
+
const lines = [first.value];
|
|
486
|
+
let more = true;
|
|
487
|
+
while (more) {
|
|
488
|
+
const raced = await Promise.race([
|
|
489
|
+
iter.next().then((r) => ({ kind: "line", result: r })),
|
|
490
|
+
new Promise((r) => setTimeout(() => r({ kind: "timeout" }), pasteDebounceMs)),
|
|
491
|
+
]);
|
|
492
|
+
if (raced.kind === "timeout") {
|
|
493
|
+
more = false;
|
|
494
|
+
}
|
|
495
|
+
else if (raced.result.done) {
|
|
496
|
+
more = false;
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
lines.push(raced.result.value);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
yield lines.join("\n");
|
|
503
|
+
}
|
|
504
|
+
}
|
|
432
505
|
try {
|
|
433
|
-
for await (const input of rl) {
|
|
506
|
+
for await (const input of debouncedLines(rl)) {
|
|
434
507
|
if (closed)
|
|
435
508
|
break;
|
|
436
509
|
if (!input.trim()) {
|
|
@@ -477,12 +550,15 @@ async function main() {
|
|
|
477
550
|
}
|
|
478
551
|
}
|
|
479
552
|
}
|
|
480
|
-
// Re-style the echoed input
|
|
481
|
-
//
|
|
553
|
+
// Re-style the echoed input lines (readline terminal:true echoes each line)
|
|
554
|
+
// For multiline paste, each line was echoed separately — erase them all
|
|
482
555
|
const cols = process.stdout.columns || 80;
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
556
|
+
const inputLines = input.split("\n");
|
|
557
|
+
let echoRows = 0;
|
|
558
|
+
for (const line of inputLines) {
|
|
559
|
+
echoRows += Math.ceil((2 + line.length) / cols); // "> " prefix + line content
|
|
560
|
+
}
|
|
561
|
+
process.stdout.write(`\x1b[${echoRows}A\x1b[K` + `\x1b[1m> ${inputLines[0]}${inputLines.length > 1 ? ` (+${inputLines.length - 1} lines)` : ""}\x1b[0m\n\n`);
|
|
486
562
|
messages.push({ role: "user", content: input });
|
|
487
563
|
addHistory(history, input);
|
|
488
564
|
currentAbort = new AbortController();
|
|
@@ -510,12 +586,17 @@ async function main() {
|
|
|
510
586
|
process.stdout.write("\n\n");
|
|
511
587
|
(0, context_1.postTurn)(messages, sessPath, result?.usage);
|
|
512
588
|
await (0, tokens_1.accumulateFriendTokens)(friendStore, resolvedContext.friend.id, result?.usage);
|
|
589
|
+
// Post-turn: drain any pending messages that arrived during runAgent
|
|
590
|
+
drainToMessages();
|
|
591
|
+
// Post-turn: refresh system prompt so active sessions metadata is current
|
|
592
|
+
await (0, prompt_refresh_1.refreshSystemPrompt)(messages, "cli", undefined, resolvedContext);
|
|
513
593
|
if (closed)
|
|
514
594
|
break;
|
|
515
595
|
process.stdout.write("\x1b[36m> \x1b[0m");
|
|
516
596
|
}
|
|
517
597
|
}
|
|
518
598
|
finally {
|
|
599
|
+
sessionLock?.release();
|
|
519
600
|
rl.close();
|
|
520
601
|
// eslint-disable-next-line no-console -- terminal UX: goodbye
|
|
521
602
|
console.log("bye");
|
|
@@ -194,6 +194,21 @@ async function runInnerDialogTurn(options) {
|
|
|
194
194
|
const instinctPrompt = buildInstinctUserMessage(instincts, reason, state);
|
|
195
195
|
messages.push({ role: "user", content: instinctPrompt });
|
|
196
196
|
}
|
|
197
|
+
const inboxMessages = options?.drainInbox?.() ?? [];
|
|
198
|
+
if (inboxMessages.length > 0) {
|
|
199
|
+
const lastUserIdx = messages.length - 1;
|
|
200
|
+
const lastUser = messages[lastUserIdx];
|
|
201
|
+
/* v8 ignore next -- defensive: all code paths push a user message before here @preserve */
|
|
202
|
+
if (lastUser?.role === "user" && typeof lastUser.content === "string") {
|
|
203
|
+
const section = inboxMessages
|
|
204
|
+
.map((msg) => `- **${msg.from}**: ${msg.content}`)
|
|
205
|
+
.join("\n");
|
|
206
|
+
messages[lastUserIdx] = {
|
|
207
|
+
...lastUser,
|
|
208
|
+
content: `${lastUser.content}\n\n## incoming messages\n${section}`,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
197
212
|
const callbacks = createInnerDialogCallbacks();
|
|
198
213
|
const traceId = (0, nerves_1.createTraceId)();
|
|
199
214
|
const result = await (0, core_1.runAgent)(messages, callbacks, "cli", options?.signal, {
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.SessionLockError = void 0;
|
|
37
|
+
exports.acquireSessionLock = acquireSessionLock;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const runtime_1 = require("../nerves/runtime");
|
|
40
|
+
class SessionLockError extends Error {
|
|
41
|
+
agentName;
|
|
42
|
+
constructor(agentName) {
|
|
43
|
+
super(`already chatting with ${agentName} in another terminal`);
|
|
44
|
+
this.name = "SessionLockError";
|
|
45
|
+
this.agentName = agentName;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
exports.SessionLockError = SessionLockError;
|
|
49
|
+
function defaultIsProcessAlive(pid) {
|
|
50
|
+
try {
|
|
51
|
+
process.kill(pid, 0);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const defaultDeps = {
|
|
59
|
+
writeFileSync: (p, d, o) => fs.writeFileSync(p, d, o),
|
|
60
|
+
readFileSync: (p, e) => fs.readFileSync(p, e),
|
|
61
|
+
unlinkSync: (p) => fs.unlinkSync(p),
|
|
62
|
+
pid: process.pid,
|
|
63
|
+
isProcessAlive: defaultIsProcessAlive,
|
|
64
|
+
};
|
|
65
|
+
function acquireSessionLock(lockPath, agentName, deps = defaultDeps) {
|
|
66
|
+
const tryWrite = () => {
|
|
67
|
+
try {
|
|
68
|
+
deps.writeFileSync(lockPath, String(deps.pid), { flag: "wx" });
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
if (!tryWrite()) {
|
|
76
|
+
// Lock file exists — check if stale
|
|
77
|
+
let existingPid;
|
|
78
|
+
try {
|
|
79
|
+
existingPid = parseInt(deps.readFileSync(lockPath, "utf-8").trim(), 10);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Can't read lock file — try removing and retrying
|
|
83
|
+
try {
|
|
84
|
+
deps.unlinkSync(lockPath);
|
|
85
|
+
}
|
|
86
|
+
catch { /* ignore */ }
|
|
87
|
+
if (!tryWrite())
|
|
88
|
+
throw new SessionLockError(agentName);
|
|
89
|
+
return makeRelease(lockPath, deps);
|
|
90
|
+
}
|
|
91
|
+
if (Number.isNaN(existingPid) || !deps.isProcessAlive(existingPid)) {
|
|
92
|
+
// Stale lock — remove and retry
|
|
93
|
+
try {
|
|
94
|
+
deps.unlinkSync(lockPath);
|
|
95
|
+
}
|
|
96
|
+
catch { /* ignore */ }
|
|
97
|
+
if (!tryWrite())
|
|
98
|
+
throw new SessionLockError(agentName);
|
|
99
|
+
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.session_lock_stolen", message: "stole stale session lock from dead process", meta: { agentName, existingPid } });
|
|
100
|
+
return makeRelease(lockPath, deps);
|
|
101
|
+
}
|
|
102
|
+
throw new SessionLockError(agentName);
|
|
103
|
+
}
|
|
104
|
+
return makeRelease(lockPath, deps);
|
|
105
|
+
}
|
|
106
|
+
function makeRelease(lockPath, deps) {
|
|
107
|
+
let released = false;
|
|
108
|
+
return {
|
|
109
|
+
release: () => {
|
|
110
|
+
if (released)
|
|
111
|
+
return;
|
|
112
|
+
released = true;
|
|
113
|
+
try {
|
|
114
|
+
deps.unlinkSync(lockPath);
|
|
115
|
+
}
|
|
116
|
+
catch { /* ignore */ }
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
package/dist/senses/teams.js
CHANGED
|
@@ -444,6 +444,7 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
|
|
|
444
444
|
githubToken: teamsContext.githubToken,
|
|
445
445
|
signin: teamsContext.signin,
|
|
446
446
|
friendStore: store,
|
|
447
|
+
summarize: (0, core_1.createSummarize)(),
|
|
447
448
|
} : undefined;
|
|
448
449
|
if (toolContext) {
|
|
449
450
|
const resolver = new resolver_1.FriendResolver(store, {
|
package/package.json
CHANGED
package/subagents/README.md
CHANGED
|
@@ -22,12 +22,14 @@ For tools that support skills but not Claude sub-agents, install these as skills
|
|
|
22
22
|
```bash
|
|
23
23
|
mkdir -p ~/.codex/skills/work-planner ~/.codex/skills/work-doer ~/.codex/skills/work-merger
|
|
24
24
|
|
|
25
|
-
#
|
|
25
|
+
# Hard-link to keep one source of truth
|
|
26
26
|
ln -f "$(pwd)/subagents/work-planner.md" ~/.codex/skills/work-planner/SKILL.md
|
|
27
27
|
ln -f "$(pwd)/subagents/work-doer.md" ~/.codex/skills/work-doer/SKILL.md
|
|
28
28
|
ln -f "$(pwd)/subagents/work-merger.md" ~/.codex/skills/work-merger/SKILL.md
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
**Important:** Hard-links break when editors save by replacing the file (new inode). After editing any `subagents/*.md` file, re-run the `ln -f` command for that file to restore the link. You can verify with `stat -f '%i'` — both files should share the same inode.
|
|
32
|
+
|
|
31
33
|
Optional UI metadata:
|
|
32
34
|
|
|
33
35
|
```bash
|
package/subagents/work-merger.md
CHANGED
|
@@ -327,12 +327,43 @@ done
|
|
|
327
327
|
### Step 4: Handle CI result
|
|
328
328
|
|
|
329
329
|
**CI passes:**
|
|
330
|
-
- Proceed to Step 5 (merge).
|
|
330
|
+
- Proceed to Step 5 (pre-merge sanity check).
|
|
331
331
|
|
|
332
332
|
**CI fails:**
|
|
333
333
|
- Proceed to **CI Failure Self-Repair**.
|
|
334
334
|
|
|
335
|
-
### Step 5:
|
|
335
|
+
### Step 5: Pre-merge sanity check
|
|
336
|
+
|
|
337
|
+
Before merging, verify the PR delivers what the planning/doing doc intended. This is a lightweight review, not a full audit.
|
|
338
|
+
|
|
339
|
+
1. Re-read the doing doc (already available from On Startup)
|
|
340
|
+
2. Review the PR diff: `gh pr diff "${BRANCH}"`
|
|
341
|
+
3. Check that:
|
|
342
|
+
- All completion criteria from the doing doc are addressed
|
|
343
|
+
- No unrelated changes slipped in
|
|
344
|
+
- The PR title and body accurately describe what shipped
|
|
345
|
+
4. Post findings as a PR comment:
|
|
346
|
+
|
|
347
|
+
```bash
|
|
348
|
+
gh pr comment "${BRANCH}" --body "$(cat <<'REVIEW'
|
|
349
|
+
## Pre-merge sanity check
|
|
350
|
+
|
|
351
|
+
Checked PR against doing doc: `<doing-doc-path>`
|
|
352
|
+
|
|
353
|
+
- [ ] All completion criteria addressed
|
|
354
|
+
- [ ] No unrelated changes
|
|
355
|
+
- [ ] PR description accurate
|
|
356
|
+
|
|
357
|
+
<any notes or concerns>
|
|
358
|
+
|
|
359
|
+
Proceeding to merge.
|
|
360
|
+
REVIEW
|
|
361
|
+
)"
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
If the check reveals a genuine gap (missing criteria, wrong files included), fix it before merging. If everything looks good, proceed to Step 6.
|
|
365
|
+
|
|
366
|
+
### Step 6: Merge the PR
|
|
336
367
|
|
|
337
368
|
```bash
|
|
338
369
|
gh pr merge "${BRANCH}" --merge --delete-branch
|