@ouro.bot/cli 0.1.0-alpha.277 → 0.1.0-alpha.279
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/changelog.json +15 -3
- package/dist/heart/auth/auth-flow.js +10 -1
- package/dist/heart/identity.js +14 -13
- package/dist/senses/cli/ouro-tui.js +49 -15
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.279",
|
|
6
|
+
"changes": [
|
|
7
|
+
"fix(tui): paste cursor to end of pasted text, ANSI string rendering for wrapped-line cursor, Option+Backspace word-delete, fn+Delete forward-delete, strip ANSI and \\r from paste."
|
|
8
|
+
]
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"version": "0.1.0-alpha.278",
|
|
12
|
+
"changes": [
|
|
13
|
+
"fix(identity): spread-with-validation loader eliminates the field-drop bug class that caused #349 (silent `sync` drop). Two distinct structural bugs fixed, one in each agent.json loader: (1) `loadAgentConfig` in `src/heart/identity.ts` previously built the returned `AgentConfig` via a hand-rolled object literal that listed every field explicitly \u2014 any new field on `AgentConfig` that wasn't added to this literal got silently dropped. Root cause of #349. Refactored to a spread-then-override pattern: start with `{ ...parsed as AgentConfig }`, then explicitly override `version`, `enabled`, `humanFacing`, `agentFacing`, `senses`, `phrases` which need validation/normalization. The deprecated `provider` field is re-attached from the validated `rawProvider` check. (2) `readAgentConfigForAgent` in `src/heart/auth/auth-flow.ts` previously did `parsed as unknown as AgentConfig` which passed through ALL fields unconditionally (so no silent drops) but ALSO had zero per-field validation \u2014 any garbage in agent.json leaked into the returned config. Refactored to apply the same spread-with-validation pattern, reusing `normalizeSenses` (now exported from identity.ts). Both entry points now return equivalent configs for the same agent.json file. New `src/__tests__/heart/identity-fixture.ts` exports `FULL_AGENT_JSON satisfies DeepRequired<AgentConfig>` \u2014 a compile-time regression guard that fails to build if any new field is added to `AgentConfig` without updating the fixture. Two new test files: `identity-contract.test.ts` exercises `readAgentConfigForAgent` with `createTmpBundle`; `identity-load-contract.test.ts` exercises `loadAgentConfig` via `vi.mock(\"fs\")` (split to avoid the fs-mock-vs-real-fs conflict). 7 tests total."
|
|
14
|
+
]
|
|
15
|
+
},
|
|
4
16
|
{
|
|
5
17
|
"version": "0.1.0-alpha.277",
|
|
6
18
|
"changes": [
|
|
7
|
-
"feat(pulse): multi-agent situational awareness for peer agents on the same machine. The harness scales horizontally
|
|
19
|
+
"feat(pulse): multi-agent situational awareness for peer agents on the same machine. The harness scales horizontally \u2014 multiple peer agents share a machine, each with their own identity and bundle (the Bob model from We Are Legion / We Are Bob). Without explicit awareness, peer agents are isolated workers who don't even know each other exist. The pulse fixes that.",
|
|
8
20
|
"feat(pulse): new `src/heart/daemon/pulse.ts` module. Daemon writes `~/.ouro-cli/pulse.json` whenever any managed agent's snapshot changes (status, errorReason, fixHint, etc.). Each entry includes name, bundle path, status, last-seen-at, errorReason+fixHint when broken, alertId for at-most-once delivery tracking, and currentActivity (read from each agent's `state/sessions/self/inner/runtime.json` when running). Pure helpers `buildPulseState`, `findNovelBrokenAgents`, `findRecoveredAgents`, `pickWakeRecipient`, `flushPulse`, `readAgentActivity`, `buildAlertId`, `buildRecoveryAlertId`, `pruneDeliveredState` exported for unit coverage; I/O wrappers `writePulse`, `readPulse`, `writeDeliveredState`, `readDeliveredState` use injectable deps.",
|
|
9
21
|
"feat(pulse): both passive AND active surfacing. Passive: every agent's prompt assembly renders a `## the pulse` section in Group 7 (dynamic state) showing siblings grouped into broken / reachable / idle buckets. Self-excluded so each agent describes its peers, not itself. Renders nothing on single-agent machines (zero token cost). Active: when a sibling newly breaks (or recovers), the daemon fires `inner.wake` on the most-recently-active running agent so the user finds out within seconds rather than next-time-they-talk-to-someone. Persistent at-most-once delivery via `~/.ouro-cli/pulse-delivered.json` so daemon restarts don't re-page.",
|
|
10
|
-
"feat(pulse): horizontal-scaling norm in bodyMapSection. New `### peers` subsection teaches the agent the Bob model directly: 'i talk first. when i need a sibling's help, i `send_message` them
|
|
22
|
+
"feat(pulse): horizontal-scaling norm in bodyMapSection. New `### peers` subsection teaches the agent the Bob model directly: 'i talk first. when i need a sibling's help, i `send_message` them \u2014 that's how peers coordinate, the same way humans on a team do. i only open a sibling's bundle directly via read_file/glob/grep when conversation isn't possible (they're crashed, sleeping, or i need history they haven't surfaced).' Direct declarative voice \u2014 no hedging, no soft modal verbs.",
|
|
11
23
|
"feat(daemon): `DaemonAgentSnapshot` gains `errorReason` and `fixHint` fields, populated by `checkAgentConfig` results in `startAgent` (set on failure, cleared on recovery). Cleared error fields are how the recovery wake fires: when a previously-broken agent transitions to running with `errorReason: null`, `findRecoveredAgents` flags it.",
|
|
12
24
|
"feat(daemon): `DaemonProcessManager` gains `onSnapshotChange` callback option. Called after every snapshot mutation (start, exit, config-fail, recovery, restart-exhausted). Errors from the observer are swallowed so lifecycle code never breaks because the observer threw. The daemon-entry registers a callback that calls `flushPulse` to update the pulse state and fire wakes.",
|
|
13
25
|
"feat(daemon): wake recipient picker (`pickWakeRecipient`) chooses the most-recently-active running sibling. Excludes the broken agent itself, non-running siblings, and siblings that have never been seen alive. Returns null when no eligible recipient exists, in which case the alert is still marked delivered to avoid spam on subsequent flushes."
|
|
@@ -16,7 +28,7 @@
|
|
|
16
28
|
{
|
|
17
29
|
"version": "0.1.0-alpha.276",
|
|
18
30
|
"changes": [
|
|
19
|
-
"fix(nerves): root-cause the `start_end_pairing` audit flake on `daemon.server_start`, `daemon.update_checker_start`, and `daemon.apply_pending_updates_start`. Three fixes: (1) `applyPendingUpdates` in `update-hooks.ts` now wraps its body in try/finally so `_end` always fires, including on the early returns for `!fs.existsSync(bundlesRoot)` and `readdirSync` throws that previously orphaned the `_start`. (2) `daemon.start()` now wraps the ~380-line startup body in a try/catch that emits `daemon.server_error` with the error message and rethrows
|
|
31
|
+
"fix(nerves): root-cause the `start_end_pairing` audit flake on `daemon.server_start`, `daemon.update_checker_start`, and `daemon.apply_pending_updates_start`. Three fixes: (1) `applyPendingUpdates` in `update-hooks.ts` now wraps its body in try/finally so `_end` always fires, including on the early returns for `!fs.existsSync(bundlesRoot)` and `readdirSync` throws that previously orphaned the `_start`. (2) `daemon.start()` now wraps the ~380-line startup body in a try/catch that emits `daemon.server_error` with the error message and rethrows \u2014 the audit's pairing rule accepts `_end` OR `_error` as a valid closure for a `_start`, so startup throws no longer orphan `server_start`. The body was extracted to a private `startInner()` method to keep the try/catch small and readable. (3) `startUpdateChecker` callers in the update-checker test suite were already paired via an `afterEach(stopUpdateChecker)`; audited and confirmed no new unpaired callers. Also adds a vitest `afterEach` pairing guard in `src/__tests__/nerves/global-capture.ts` that fails loudly on any orphaned lifecycle `_start` in a test's per-test event stream \u2014 scoped to the three lifecycle events above so it catches regressions without false-positiving on legitimate narrow-slice operational events like `repertoire.task_scan_start`. New `src/__tests__/nerves/pairing-regression.test.ts` locks in the contract with 5 tests (nonexistent-dir, readdirSync-throws, happy-path, startUpdateChecker-pair, daemon-start-throw). Three consecutive `npm run test:coverage` runs confirm zero flakes on the target events."
|
|
20
32
|
]
|
|
21
33
|
},
|
|
22
34
|
{
|
|
@@ -159,9 +159,18 @@ function readAgentConfigForAgent(agentName, bundlesRoot = (0, identity_1.getAgen
|
|
|
159
159
|
if (!agentFacing || typeof agentFacing !== "object") {
|
|
160
160
|
throw new Error(`agent.json at ${configPath} has unsupported provider '${String(parsed.provider)}'`);
|
|
161
161
|
}
|
|
162
|
+
// Spread-with-validation: same pattern as loadAgentConfig to eliminate
|
|
163
|
+
// the unvalidated-pass-through bug class. The spread carries through
|
|
164
|
+
// every field present in parsed; senses goes through the same
|
|
165
|
+
// normalization as loadAgentConfig so the two entry points return
|
|
166
|
+
// equivalent configs for the same file.
|
|
167
|
+
const config = {
|
|
168
|
+
...parsed,
|
|
169
|
+
senses: (0, identity_1.normalizeSenses)(parsed.senses, configPath),
|
|
170
|
+
};
|
|
162
171
|
return {
|
|
163
172
|
configPath,
|
|
164
|
-
config
|
|
173
|
+
config,
|
|
165
174
|
};
|
|
166
175
|
}
|
|
167
176
|
function writeAgentProviderSelection(agentName, facing, provider, bundlesRoot = (0, identity_1.getAgentBundlesRoot)()) {
|
package/dist/heart/identity.js
CHANGED
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.HARNESS_CANONICAL_REPO_URL = exports.DEFAULT_AGENT_SENSES = exports.DEFAULT_VAULT_SERVER_URL = exports.DEFAULT_AGENT_PHRASES = exports.DEFAULT_AGENT_CONTEXT = exports.PROVIDER_CREDENTIALS = void 0;
|
|
37
37
|
exports.resolveVaultConfig = resolveVaultConfig;
|
|
38
|
+
exports.normalizeSenses = normalizeSenses;
|
|
38
39
|
exports.buildDefaultAgentTemplate = buildDefaultAgentTemplate;
|
|
39
40
|
exports.getAgentName = getAgentName;
|
|
40
41
|
exports.getRepoRoot = getRepoRoot;
|
|
@@ -404,27 +405,27 @@ function loadAgentConfig() {
|
|
|
404
405
|
// Tolerate deprecated provider field for backward compatibility
|
|
405
406
|
const rawProvider = parsed.provider;
|
|
406
407
|
const provider = isValidProvider(rawProvider) ? rawProvider : undefined;
|
|
408
|
+
// Spread parsed first so any field present in AgentConfig is carried
|
|
409
|
+
// through by default, then explicitly override the fields that need
|
|
410
|
+
// validation or normalization. This eliminates the field-drop bug class
|
|
411
|
+
// that caused the `sync` block (and previously `shell`) to be silently
|
|
412
|
+
// omitted from the returned config. Regression-guarded by the
|
|
413
|
+
// Required<AgentConfig> contract test in identity-contract.test.ts.
|
|
407
414
|
const config = {
|
|
415
|
+
...parsed,
|
|
408
416
|
version,
|
|
409
417
|
enabled,
|
|
410
|
-
...(provider !== undefined ? { provider } : {}),
|
|
411
418
|
humanFacing,
|
|
412
419
|
agentFacing,
|
|
413
|
-
context: parsed.context,
|
|
414
|
-
logging: parsed.logging,
|
|
415
420
|
senses: normalizeSenses(parsed.senses, configFile),
|
|
416
|
-
mcpServers: parsed.mcpServers,
|
|
417
421
|
phrases: parsed.phrases,
|
|
418
|
-
vault: parsed.vault,
|
|
419
|
-
// The sync block was missing from this hand-rolled object literal since
|
|
420
|
-
// it was added to AgentConfig. Result: `getSyncConfig()` always read
|
|
421
|
-
// `agentConfig.sync?.enabled ?? false` and always saw undefined, so the
|
|
422
|
-
// entire sync code path (preTurnPull / postTurnPush in pipeline.ts) was
|
|
423
|
-
// dead. The `ouro status` per-agent Git Sync display kept reporting
|
|
424
|
-
// "enabled" because it reads agent.json directly via `listBundleSyncRows`,
|
|
425
|
-
// not through loadAgentConfig — so the bug hid in plain sight for weeks.
|
|
426
|
-
sync: parsed.sync,
|
|
427
422
|
};
|
|
423
|
+
if (provider !== undefined) {
|
|
424
|
+
config.provider = provider;
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
delete config.provider;
|
|
428
|
+
}
|
|
428
429
|
(0, runtime_1.emitNervesEvent)({
|
|
429
430
|
event: "identity.resolve",
|
|
430
431
|
component: "config/identity",
|
|
@@ -282,23 +282,26 @@ function InputArea({ onSubmit, onCtrlC, history, queuedInputs, onPopQueue, agent
|
|
|
282
282
|
historyIdx.current = -1;
|
|
283
283
|
return;
|
|
284
284
|
}
|
|
285
|
-
// Backspace / Delete:
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
//
|
|
285
|
+
// Backspace / Forward Delete:
|
|
286
|
+
// Ink 3.2 maps \x7f (macOS backspace) to key.delete (meta=false).
|
|
287
|
+
// Ink maps \x1b[3~ (fn+Delete / forward delete) to key.delete (meta=true,
|
|
288
|
+
// because \x1b prefix triggers meta detection).
|
|
289
|
+
// Ink maps \x08 (Ctrl+H style) to key.backspace.
|
|
290
|
+
// Distinguishing: key.meta differentiates fn+Delete from plain backspace.
|
|
291
|
+
if ((key.backspace || key.delete) && key.meta) {
|
|
292
|
+
// fn+Delete: forward delete
|
|
293
|
+
const result = (0, input_keys_1.handleForwardDelete)(inputRef.current, cursorRef.current);
|
|
294
|
+
updateInput(result.text, result.cursorPos);
|
|
295
|
+
historyIdx.current = -1;
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
290
298
|
if (key.backspace || key.delete) {
|
|
299
|
+
// Plain backspace: delete backward
|
|
291
300
|
if (cursorRef.current > 0) {
|
|
292
|
-
// Token-aware: check for image ref chip before cursor
|
|
293
301
|
const chip = (0, image_ref_navigation_1.deleteTokenBefore)(inputRef.current, cursorRef.current);
|
|
294
302
|
if (chip) {
|
|
295
303
|
updateInput(chip.text, chip.pos);
|
|
296
304
|
}
|
|
297
|
-
else if (key.meta) {
|
|
298
|
-
// Option+Backspace: delete word (also pushes to kill ring)
|
|
299
|
-
const result = (0, input_keys_1.handleKillWordBack)(inputRef.current, cursorRef.current, killRing);
|
|
300
|
-
updateInput(result.text, result.cursorPos);
|
|
301
|
-
}
|
|
302
305
|
else {
|
|
303
306
|
const result = (0, input_keys_1.handleBackspace)(inputRef.current, cursorRef.current);
|
|
304
307
|
updateInput(result.text, result.cursorPos);
|
|
@@ -503,6 +506,23 @@ function InputArea({ onSubmit, onCtrlC, history, queuedInputs, onPopQueue, agent
|
|
|
503
506
|
updateInput(result.text, result.cursorPos);
|
|
504
507
|
return;
|
|
505
508
|
}
|
|
509
|
+
// Option+Backspace: Ink parses \x1b\x7f as meta=true, inputChar="" (empty because
|
|
510
|
+
// \x7f matches key.delete but the whole input was \x1b\x7f not \x7f).
|
|
511
|
+
// Signature: key.meta && !inputChar && !key.escape && !key.return
|
|
512
|
+
if (key.meta && !inputChar && !key.escape && !key.return && !key.tab) {
|
|
513
|
+
if (cursorRef.current > 0) {
|
|
514
|
+
const chip = (0, image_ref_navigation_1.deleteTokenBefore)(inputRef.current, cursorRef.current);
|
|
515
|
+
if (chip) {
|
|
516
|
+
updateInput(chip.text, chip.pos);
|
|
517
|
+
}
|
|
518
|
+
else {
|
|
519
|
+
const result = (0, input_keys_1.handleKillWordBack)(inputRef.current, cursorRef.current, killRing);
|
|
520
|
+
updateInput(result.text, result.cursorPos);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
historyIdx.current = -1;
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
506
526
|
// ─── Non-kill/non-yank keystroke resets ────────────────────────
|
|
507
527
|
killRing.resetAccumulation();
|
|
508
528
|
killRing.resetYankState();
|
|
@@ -538,17 +558,31 @@ function InputArea({ onSubmit, onCtrlC, history, queuedInputs, onPopQueue, agent
|
|
|
538
558
|
return;
|
|
539
559
|
// Regular character: insert at cursor position
|
|
540
560
|
if (!key.ctrl && !key.meta && inputChar) {
|
|
561
|
+
// Strip ANSI escape codes and normalize \r to \n (SSH/paste compat)
|
|
562
|
+
const cleaned = inputChar.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/\r\n?/g, "\n");
|
|
563
|
+
if (!cleaned)
|
|
564
|
+
return;
|
|
541
565
|
const before = inputRef.current.slice(0, cursorRef.current);
|
|
542
566
|
const after = inputRef.current.slice(cursorRef.current);
|
|
543
|
-
updateInput(before +
|
|
567
|
+
updateInput(before + cleaned + after, cursorRef.current + cleaned.length);
|
|
544
568
|
historyIdx.current = -1;
|
|
545
569
|
}
|
|
546
570
|
});
|
|
547
571
|
const isMultiline = input.includes("\n");
|
|
548
572
|
const inputLines = input.split("\n");
|
|
549
|
-
return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: "─".repeat(termWidth()) }), isMultiline ? ((0, jsx_runtime_1.jsx)(ink_1.Box, { flexDirection: "column", children: inputLines.map((line, i) => ((0, jsx_runtime_1.jsxs)(ink_1.Box, { children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.teal, bold: true, children: i === 0 ? ") " : "· " }), (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.bone, children: line }), i === inputLines.length - 1 ? (cursorVisible ? (0, jsx_runtime_1.jsx)(ink_1.Text, { backgroundColor: OURO.scale, color: "#000000", children: " " }) : (0, jsx_runtime_1.jsx)(ink_1.Text, { children: " " })) : null] }, i))) })) : ((0, jsx_runtime_1.
|
|
550
|
-
|
|
551
|
-
|
|
573
|
+
return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: "─".repeat(termWidth()) }), isMultiline ? ((0, jsx_runtime_1.jsx)(ink_1.Box, { flexDirection: "column", children: inputLines.map((line, i) => ((0, jsx_runtime_1.jsxs)(ink_1.Box, { children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.teal, bold: true, children: i === 0 ? ") " : "· " }), (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.bone, children: line }), i === inputLines.length - 1 ? (cursorVisible ? (0, jsx_runtime_1.jsx)(ink_1.Text, { backgroundColor: OURO.scale, color: "#000000", children: " " }) : (0, jsx_runtime_1.jsx)(ink_1.Text, { children: " " })) : null] }, i))) })) : ((0, jsx_runtime_1.jsx)(ink_1.Box, { children: (0, jsx_runtime_1.jsx)(ink_1.Text, { wrap: "wrap", children: (() => {
|
|
574
|
+
const prompt = `\x1b[1m\x1b[38;2;78;201;176m) \x1b[0m`;
|
|
575
|
+
if (!input && queuedInputs.length > 0) {
|
|
576
|
+
return `${prompt}\x1b[38;2;112;131;115mPress up to edit queued messages\x1b[0m`;
|
|
577
|
+
}
|
|
578
|
+
const beforeCursor = `\x1b[38;2;238;242;234m${input.slice(0, cursorPos)}\x1b[0m`;
|
|
579
|
+
const cursorChar = input[cursorPos] ?? " ";
|
|
580
|
+
const cursor = cursorVisible
|
|
581
|
+
? `\x1b[48;2;47;143;78m\x1b[38;2;0;0;0m${cursorChar}\x1b[0m`
|
|
582
|
+
: `\x1b[38;2;238;242;234m${cursorChar}\x1b[0m`;
|
|
583
|
+
const afterCursor = `\x1b[38;2;238;242;234m${input.slice(cursorPos + 1)}\x1b[0m`;
|
|
584
|
+
return `${prompt}${beforeCursor}${cursor}${afterCursor}`;
|
|
585
|
+
})() }) })), (0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: "─".repeat(termWidth()) }), (0, jsx_runtime_1.jsxs)(ink_1.Box, { children: [(0, jsx_runtime_1.jsxs)(ink_1.Text, { dimColor: true, children: [" ", agentName, model ? ` · ${model}` : "", " \u00B7 /help"] }), (0, jsx_runtime_1.jsx)(ink_1.Box, { flexGrow: 1 }), tooltip ? (0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: tooltip }) : (0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: "opt+enter for newline" })] })] }));
|
|
552
586
|
}
|
|
553
587
|
// ─── Main TUI Component ─────────────────────────────────────────────
|
|
554
588
|
function OuroTui({ agentName, model, completedMessages, inputHistory, queuedInputs, live, elapsedSeconds, contextPercent, onSubmit, onCtrlC, onPopQueue, cwd, resumeInfo, onImageMap, onHistoryAdd, }) {
|