@ouro.bot/cli 0.1.0-alpha.280 → 0.1.0-alpha.282
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 +14 -1
- package/dist/heart/bundle-state.js +147 -0
- package/dist/heart/core.js +1 -0
- package/dist/heart/start-of-turn-packet.js +9 -0
- package/dist/senses/cli.js +6 -0
- package/dist/senses/pipeline.js +8 -0
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
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.282",
|
|
6
|
+
"changes": [
|
|
7
|
+
"feat(cli): crash-resilient sessions \u2014 saves after each tool result, repairs orphaned tool calls on resume."
|
|
8
|
+
]
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"version": "0.1.0-alpha.281",
|
|
12
|
+
"changes": [
|
|
13
|
+
"feat(bundle): new `src/heart/bundle-state.ts` module exports `detectBundleState(agentRoot)` which returns a structured `BundleStateIssue[]` describing git-level problems the agent can remediate. Enum cases: `not_a_git_repo`, `no_remote_configured`, `first_commit_never_happened`, `pending_sync_exists`. Detection never throws \u2014 every git call is wrapped in try/catch so a broken bundle degrades to a clear signal rather than crashing the turn pipeline. Also exports `renderBundleStateHint(issues)` which produces first-person remediation guidance (per the memory rule) that tells the agent to call `bundle_check_sync_status` and the `bundle_*` tools shipping in a follow-up PR.",
|
|
14
|
+
"feat(bundle): `StartOfTurnPacket` gains an optional `bundleState?: BundleStateIssue[]` field, populated by the senses pipeline in `handleInboundTurn` at packet assembly time. Renders via a new `case \"bundleState\":` branch in `formatSections` with the `**Bundle:**` prefix, at the same priority tier as the legacy `syncFailure` free-form string (priority 7, truncated last). The two signals coexist during the transition \u2014 `syncFailure` is still emitted by sync.ts for humans, while `bundleState` is the structured form the agent pattern-matches on. Deferred to follow-up: `pending-sync.json` schema extension with `classification` / `conflictFiles` so sync.ts can distinguish push-rejected from pull-rebase-conflict."
|
|
15
|
+
]
|
|
16
|
+
},
|
|
4
17
|
{
|
|
5
18
|
"version": "0.1.0-alpha.280",
|
|
6
19
|
"changes": [
|
|
7
|
-
"chore(tests): prod-path isolation ratchet + tmpBundle leak guard + no-rm-rf contract. Extends `src/__tests__/heart/daemon/test-isolation.contract.test.ts` with three new rules and adds a runtime leak guard. (1) Three new prod-path block rules mirror the existing ~/AgentBundles check: no test file may construct a write path under `~/.ouro-cli`, `~/.agentsecrets`, or `~/.claude` without mocking fs. Each rule has its own empty-seeded ratchet allowlist (except `.ouro-cli` and `.agentsecrets` which are seeded with the pre-existing offenders that this rule newly catches
|
|
20
|
+
"chore(tests): prod-path isolation ratchet + tmpBundle leak guard + no-rm-rf contract. Extends `src/__tests__/heart/daemon/test-isolation.contract.test.ts` with three new rules and adds a runtime leak guard. (1) Three new prod-path block rules mirror the existing ~/AgentBundles check: no test file may construct a write path under `~/.ouro-cli`, `~/.agentsecrets`, or `~/.claude` without mocking fs. Each rule has its own empty-seeded ratchet allowlist (except `.ouro-cli` and `.agentsecrets` which are seeded with the pre-existing offenders that this rule newly catches \u2014 follow-up PRs can convert those to mocked-fs and ratchet down). The path-scan loop is factored into a shared `runProdPathCheck` helper. (2) New Directive-A contract rule: agent-callable production code under `src/` (excluding `src/__tests__/`) must not call `fs.rmSync(..., { recursive: true })` or shell out to `rm -rf` / `rm -fr` / `rm --recursive --force`. The rule is about making deletion auditable and interruptible: an agent should enumerate the files it wants to delete instead of recursively blasting a directory. Four legitimate infrastructure callsites are on `RM_RECURSIVE_ALLOWLIST` with explicit justifications: specialist-tools.ts (adoption rollback), ouro-version-manager.ts (CLI version pruning), ouro-uti.ts (macOS icon pipeline), cli-defaults.ts (self-setup temp dir). Three files that are themselves the rm-rf enforcement layer (guardrails.ts, shell-sessions.ts, prompt.ts) are in `RM_RULE_ENFORCEMENT_FILES` and skipped by the scan since they contain the literal \"rm -rf\" only as regex patterns or prompt strings designed to BLOCK the call. (3) `createTmpBundle` in `src/__tests__/test-helpers/tmpdir-bundle.ts` now tracks live handles in a module-level `_liveHandles: Set<TmpBundleHandle>`. `cleanup()` removes the handle from the set; a new `__getLiveTmpBundleHandles()` export returns a readonly view. (4) `src/__tests__/nerves/global-capture.ts` adds a global vitest `afterEach` leak guard that iterates `__getLiveTmpBundleHandles` and calls `cleanup()` on any remaining handles, logging a `console.warn` naming the test that leaked them. Runs AFTER the pairing guard from alpha.276 so pairing failures surface first. Both guards co-exist cleanly."
|
|
8
21
|
]
|
|
9
22
|
},
|
|
10
23
|
{
|
|
@@ -0,0 +1,147 @@
|
|
|
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.detectBundleState = detectBundleState;
|
|
37
|
+
exports.renderBundleStateHint = renderBundleStateHint;
|
|
38
|
+
/**
|
|
39
|
+
* Bundle state detection: inspects an agent bundle's git + pending-sync
|
|
40
|
+
* state and returns a structured list of issues the agent needs to remediate
|
|
41
|
+
* via its `bundle_*` tools.
|
|
42
|
+
*
|
|
43
|
+
* Previously the sync pipeline surfaced a single free-form `syncFailure`
|
|
44
|
+
* string that was good for humans but hard for the agent to act on. The
|
|
45
|
+
* new `BundleStateIssue` enum lets the agent pattern-match on discrete
|
|
46
|
+
* cases and pick the correct remediation tool (shipping in PR 6).
|
|
47
|
+
*
|
|
48
|
+
* Detection never throws: every git call is wrapped in try/catch so a
|
|
49
|
+
* broken bundle or a missing git binary degrades to an empty array or a
|
|
50
|
+
* `not_a_git_repo` signal rather than exploding the turn pipeline.
|
|
51
|
+
*
|
|
52
|
+
* The classification returned here is the subset that's detectable purely
|
|
53
|
+
* from bundle state:
|
|
54
|
+
*
|
|
55
|
+
* - `not_a_git_repo`: `.git` directory missing.
|
|
56
|
+
* - `no_remote_configured`: `git remote` returns empty.
|
|
57
|
+
* - `first_commit_never_happened`: `.git` exists but `git rev-parse HEAD`
|
|
58
|
+
* fails (fresh `git init` with nothing committed).
|
|
59
|
+
* - `pending_sync_exists`: `state/pending-sync.json` exists — the agent
|
|
60
|
+
* should inspect it and clear the pending state.
|
|
61
|
+
*
|
|
62
|
+
* Future extensions (deferred to a follow-up PR):
|
|
63
|
+
* - `remote_push_failed` / `pull_rebase_conflict`: derived from the
|
|
64
|
+
* `classification` field of pending-sync.json once sync.ts writes it.
|
|
65
|
+
*/
|
|
66
|
+
const child_process_1 = require("child_process");
|
|
67
|
+
const fs = __importStar(require("fs"));
|
|
68
|
+
const path = __importStar(require("path"));
|
|
69
|
+
const runtime_1 = require("../nerves/runtime");
|
|
70
|
+
function detectBundleState(agentRoot, deps = {}) {
|
|
71
|
+
const exec = deps.execFileSync ?? child_process_1.execFileSync;
|
|
72
|
+
const exists = deps.existsSync ?? fs.existsSync;
|
|
73
|
+
const issues = [];
|
|
74
|
+
(0, runtime_1.emitNervesEvent)({
|
|
75
|
+
component: "heart",
|
|
76
|
+
event: "heart.bundle_state_detect_start",
|
|
77
|
+
message: "detecting bundle state",
|
|
78
|
+
meta: { agentRoot },
|
|
79
|
+
});
|
|
80
|
+
const gitDir = path.join(agentRoot, ".git");
|
|
81
|
+
const isGitRepo = exists(gitDir);
|
|
82
|
+
if (!isGitRepo) {
|
|
83
|
+
issues.push("not_a_git_repo");
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
// Check remote presence
|
|
87
|
+
try {
|
|
88
|
+
const remoteOutput = exec("git", ["remote"], {
|
|
89
|
+
cwd: agentRoot,
|
|
90
|
+
stdio: "pipe",
|
|
91
|
+
timeout: 5000,
|
|
92
|
+
}).toString().trim();
|
|
93
|
+
if (remoteOutput.length === 0) {
|
|
94
|
+
issues.push("no_remote_configured");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Git missing or broken — treat as no_remote_configured so the
|
|
99
|
+
// agent surfaces a "fix git" signal rather than a silent pass.
|
|
100
|
+
issues.push("no_remote_configured");
|
|
101
|
+
}
|
|
102
|
+
// Check for initial commit
|
|
103
|
+
try {
|
|
104
|
+
exec("git", ["rev-parse", "HEAD"], {
|
|
105
|
+
cwd: agentRoot,
|
|
106
|
+
stdio: "pipe",
|
|
107
|
+
timeout: 5000,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
issues.push("first_commit_never_happened");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Pending-sync file — independent of git state (could exist on a bundle
|
|
115
|
+
// that was a repo and then had its .git deleted).
|
|
116
|
+
const pendingSyncPath = path.join(agentRoot, "state", "pending-sync.json");
|
|
117
|
+
if (exists(pendingSyncPath)) {
|
|
118
|
+
issues.push("pending_sync_exists");
|
|
119
|
+
}
|
|
120
|
+
(0, runtime_1.emitNervesEvent)({
|
|
121
|
+
component: "heart",
|
|
122
|
+
event: "heart.bundle_state_detect_end",
|
|
123
|
+
message: `bundle state detected: ${issues.length} issue(s)`,
|
|
124
|
+
meta: { agentRoot, issues },
|
|
125
|
+
});
|
|
126
|
+
return issues;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* First-person remediation hint text for the start-of-turn packet. Reads
|
|
130
|
+
* as the agent's own voice per the "declarative first-person" memory rule.
|
|
131
|
+
* Returns empty string when there are no issues (so the packet renderer
|
|
132
|
+
* can skip the section entirely).
|
|
133
|
+
*/
|
|
134
|
+
function renderBundleStateHint(issues) {
|
|
135
|
+
if (issues.length === 0)
|
|
136
|
+
return "";
|
|
137
|
+
const labels = {
|
|
138
|
+
not_a_git_repo: "not a git repo",
|
|
139
|
+
no_remote_configured: "no remote configured",
|
|
140
|
+
first_commit_never_happened: "first commit never happened",
|
|
141
|
+
pending_sync_exists: "pending sync from a prior turn",
|
|
142
|
+
};
|
|
143
|
+
const list = issues.map((issue) => labels[issue]).join(", ");
|
|
144
|
+
return (`my bundle has unresolved git state: ${list}. ` +
|
|
145
|
+
`i have the bundle_* tools available to fix this — i should run ` +
|
|
146
|
+
`bundle_check_sync_status first, then use the appropriate remediation tool.`);
|
|
147
|
+
}
|
package/dist/heart/core.js
CHANGED
|
@@ -911,6 +911,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
911
911
|
callbacks.onToolEnd(tc.name, (0, tools_1.buildToolResultSummary)(tc.name, args, toolResult, success), success);
|
|
912
912
|
messages.push({ role: "tool", tool_call_id: tc.id, content: toolResult });
|
|
913
913
|
providerRuntime.appendToolOutput(tc.id, toolResult);
|
|
914
|
+
callbacks.onToolResult?.(messages);
|
|
914
915
|
}
|
|
915
916
|
}
|
|
916
917
|
}
|
|
@@ -40,6 +40,7 @@ exports.renderCompactStartOfTurnPacket = renderCompactStartOfTurnPacket;
|
|
|
40
40
|
const fs = __importStar(require("fs"));
|
|
41
41
|
const path = __importStar(require("path"));
|
|
42
42
|
const runtime_1 = require("../nerves/runtime");
|
|
43
|
+
const bundle_state_1 = require("./bundle-state");
|
|
43
44
|
const tempo_1 = require("./tempo");
|
|
44
45
|
function estimateTokens(text) {
|
|
45
46
|
return Math.ceil(text.length / 4);
|
|
@@ -222,6 +223,11 @@ function renderStartOfTurnPacket(packet) {
|
|
|
222
223
|
// Assemble sections in priority order (highest priority first for budget allocation)
|
|
223
224
|
// Each section is { label, content, priority } where lower priority number = truncated first
|
|
224
225
|
const sections = [
|
|
226
|
+
// bundleState and syncFailure share the top remediation tier — both
|
|
227
|
+
// are actionable "fix your git" signals. bundleState is preferred
|
|
228
|
+
// because it's structured (array of enum values) while syncFailure is
|
|
229
|
+
// a legacy free-form string; both render when populated.
|
|
230
|
+
{ label: "bundleState", content: (0, bundle_state_1.renderBundleStateHint)(packet.bundleState ?? []), priority: 7 },
|
|
225
231
|
{ label: "syncFailure", content: packet.syncFailure ?? "", priority: 7 },
|
|
226
232
|
{ label: "resume", content: packet.resumeHint, priority: 6 },
|
|
227
233
|
{ label: "obligations", content: packet.obligations, priority: 5 },
|
|
@@ -294,6 +300,9 @@ function formatSections(sections) {
|
|
|
294
300
|
case "syncFailure":
|
|
295
301
|
parts.push(`**Sync warning:** ${section.content}`);
|
|
296
302
|
break;
|
|
303
|
+
case "bundleState":
|
|
304
|
+
parts.push(`**Bundle:** ${section.content}`);
|
|
305
|
+
break;
|
|
297
306
|
}
|
|
298
307
|
}
|
|
299
308
|
return parts.join("\n\n");
|
package/dist/senses/cli.js
CHANGED
|
@@ -976,6 +976,8 @@ async function main(agentName, options) {
|
|
|
976
976
|
const sessionMessages = existing?.messages && existing.messages.length > 0
|
|
977
977
|
? existing.messages
|
|
978
978
|
: [{ role: "system", content: await (0, prompt_1.buildSystem)("cli", {}, resolvedContext) }];
|
|
979
|
+
// Repair any orphaned tool calls from a crash mid-turn
|
|
980
|
+
(0, core_1.repairOrphanedToolCalls)(sessionMessages);
|
|
979
981
|
// Per-turn pipeline input: CLI capabilities and pending dir
|
|
980
982
|
const cliCapabilities = (0, channel_1.getChannelCapabilities)("cli");
|
|
981
983
|
const currentAgentName = (0, identity_1.getAgentName)();
|
|
@@ -1002,6 +1004,10 @@ async function main(agentName, options) {
|
|
|
1002
1004
|
/* v8 ignore start -- failover-aware callback wrapper: tested via pipeline integration @preserve */
|
|
1003
1005
|
const failoverAwareCallbacks = {
|
|
1004
1006
|
...callbacks,
|
|
1007
|
+
// Save session after each tool result for crash recovery
|
|
1008
|
+
onToolResult: (turnMessages) => {
|
|
1009
|
+
(0, context_1.postTurn)(turnMessages, sessPath, undefined, undefined, sessionState);
|
|
1010
|
+
},
|
|
1005
1011
|
onError: (error, severity) => {
|
|
1006
1012
|
if (severity === "terminal" && failoverState) {
|
|
1007
1013
|
capturedTerminalError = error;
|
package/dist/senses/pipeline.js
CHANGED
|
@@ -57,6 +57,7 @@ const auth_flow_1 = require("../heart/auth/auth-flow");
|
|
|
57
57
|
const tempo_1 = require("../heart/tempo");
|
|
58
58
|
const temporal_view_1 = require("../heart/temporal-view");
|
|
59
59
|
const start_of_turn_packet_1 = require("../heart/start-of-turn-packet");
|
|
60
|
+
const bundle_state_1 = require("../heart/bundle-state");
|
|
60
61
|
const sync_1 = require("../heart/sync");
|
|
61
62
|
const config_1 = require("../heart/config");
|
|
62
63
|
const presence_1 = require("../arc/presence");
|
|
@@ -388,6 +389,13 @@ async function handleInboundTurn(input) {
|
|
|
388
389
|
if (syncFailure) {
|
|
389
390
|
startOfTurnPacket.syncFailure = syncFailure;
|
|
390
391
|
}
|
|
392
|
+
// Structured bundle state detection — surfaces discrete issues the
|
|
393
|
+
// agent can remediate via the bundle_* tools. Runs independently of
|
|
394
|
+
// syncFailure so the two signals coexist during the transition away
|
|
395
|
+
// from the legacy free-form syncFailure string. Always assigned; the
|
|
396
|
+
// packet renderer's empty-filter handles the empty-array case without
|
|
397
|
+
// a separate branch here.
|
|
398
|
+
startOfTurnPacket.bundleState = (0, bundle_state_1.detectBundleState)(agentRoot);
|
|
391
399
|
const capabilities = (0, start_of_turn_packet_1.buildCapabilitiesSection)(agentRoot);
|
|
392
400
|
if (capabilities) {
|
|
393
401
|
startOfTurnPacket.capabilities = capabilities;
|