@ouro.bot/cli 0.1.0-alpha.295 → 0.1.0-alpha.297
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 +13 -0
- package/dist/heart/bundle-state.js +28 -7
- package/dist/heart/sync.js +76 -21
- package/dist/mind/diary-integrity.js +60 -0
- package/dist/mind/diary.js +15 -0
- package/dist/mind/prompt.js +37 -0
- package/dist/nerves/coverage/file-completeness.js +3 -0
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
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.297",
|
|
6
|
+
"changes": [
|
|
7
|
+
"feat(mind): add integrity scanning for diary entries at write time. New detectSuspiciousContent() in src/mind/diary-integrity.ts checks entry text for prompt injection patterns (instruction framing, override language, role injection, boundary markers) before persisting. Detection emits a mind.diary_integrity_warning nerves event with matched pattern categories, text preview, and entry ID. Entries still save (flag, don't block) to avoid false-positive data loss. 41 unit tests + 4 integration tests with 100% coverage on new code."
|
|
8
|
+
]
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"version": "0.1.0-alpha.296",
|
|
12
|
+
"changes": [
|
|
13
|
+
"feat(sync): pending-sync.json classification + bundleState enrichment. `postTurnPush` in `src/heart/sync.ts` now distinguishes between a push that was rejected AFTER a successful rebase retry (`classification: push_rejected`) and a rebase that itself failed with merge conflicts (`classification: pull_rebase_conflict`, with conflictFiles populated from `git status --porcelain=v1` UU/AA/DD/AU/UA/DU/UD markers). The `PendingSyncRecord` interface is exported from `sync.ts` so downstream readers can type-check. `detectBundleState` in `src/heart/bundle-state.ts` gained `remote_push_failed` and `pull_rebase_conflict` issue cases that are added alongside `pending_sync_exists` when the classification field is present. Readers tolerate pending-sync.json without a classification field (pre-alpha.296 schema) or with malformed JSON — both fall back to the plain `pending_sync_exists` signal. 4 new bundle-state tests (push_rejected, pull_rebase_conflict, legacy schema, malformed JSON) and 2 new sync tests (second-push-fails-after-rebase-success, rebase-leaves-merge-conflicts). Completes the Directive D remediation signal plumbing started in alpha.281.",
|
|
14
|
+
"feat(prompt): bundle self-management guidance in `bodyMapSection` (`src/mind/prompt.ts`). New `### git sync — i own my bundle's git state` subsection documents the full detect → init → add_remote → list_first_commit → review with friend → do_first_commit → first_push_review → confirm → push workflow in first-person voice, plus the remediation paths for `remote_push_failed` (pull_rebase) and `pull_rebase_conflict` (walk the friend through conflicts). Added after `### home` and before `### peers` so the flow reads naturally with the rest of the body metaphor."
|
|
15
|
+
]
|
|
16
|
+
},
|
|
4
17
|
{
|
|
5
18
|
"version": "0.1.0-alpha.295",
|
|
6
19
|
"changes": [
|
|
@@ -49,8 +49,9 @@ exports.renderBundleStateHint = renderBundleStateHint;
|
|
|
49
49
|
* broken bundle or a missing git binary degrades to an empty array or a
|
|
50
50
|
* `not_a_git_repo` signal rather than exploding the turn pipeline.
|
|
51
51
|
*
|
|
52
|
-
* The classification returned here is
|
|
53
|
-
*
|
|
52
|
+
* The classification returned here is detected from bundle state +
|
|
53
|
+
* the `classification` field of `state/pending-sync.json` (written by
|
|
54
|
+
* `postTurnPush` in sync.ts when it writes the pending-sync record).
|
|
54
55
|
*
|
|
55
56
|
* - `not_a_git_repo`: `.git` directory missing.
|
|
56
57
|
* - `no_remote_configured`: `git remote` returns empty.
|
|
@@ -58,10 +59,11 @@ exports.renderBundleStateHint = renderBundleStateHint;
|
|
|
58
59
|
* fails (fresh `git init` with nothing committed).
|
|
59
60
|
* - `pending_sync_exists`: `state/pending-sync.json` exists — the agent
|
|
60
61
|
* should inspect it and clear the pending state.
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* - `
|
|
64
|
-
* `
|
|
62
|
+
* - `remote_push_failed`: pending-sync classified as `push_rejected` —
|
|
63
|
+
* the remote advanced and a simple retry won't work.
|
|
64
|
+
* - `pull_rebase_conflict`: pending-sync classified as
|
|
65
|
+
* `pull_rebase_conflict` — the rebase left conflicted files the agent
|
|
66
|
+
* has to walk the user through resolving.
|
|
65
67
|
*/
|
|
66
68
|
const child_process_1 = require("child_process");
|
|
67
69
|
const fs = __importStar(require("fs"));
|
|
@@ -70,6 +72,7 @@ const runtime_1 = require("../nerves/runtime");
|
|
|
70
72
|
function detectBundleState(agentRoot, deps = {}) {
|
|
71
73
|
const exec = deps.execFileSync ?? child_process_1.execFileSync;
|
|
72
74
|
const exists = deps.existsSync ?? fs.existsSync;
|
|
75
|
+
const readFile = deps.readFileSync ?? ((p, enc) => fs.readFileSync(p, enc));
|
|
73
76
|
const issues = [];
|
|
74
77
|
(0, runtime_1.emitNervesEvent)({
|
|
75
78
|
component: "heart",
|
|
@@ -112,10 +115,26 @@ function detectBundleState(agentRoot, deps = {}) {
|
|
|
112
115
|
}
|
|
113
116
|
}
|
|
114
117
|
// Pending-sync file — independent of git state (could exist on a bundle
|
|
115
|
-
// that was a repo and then had its .git deleted).
|
|
118
|
+
// that was a repo and then had its .git deleted). When present, also
|
|
119
|
+
// inspect the classification field to surface the specific failure
|
|
120
|
+
// mode as an additional issue.
|
|
116
121
|
const pendingSyncPath = path.join(agentRoot, "state", "pending-sync.json");
|
|
117
122
|
if (exists(pendingSyncPath)) {
|
|
118
123
|
issues.push("pending_sync_exists");
|
|
124
|
+
try {
|
|
125
|
+
const raw = readFile(pendingSyncPath, "utf-8");
|
|
126
|
+
const parsed = JSON.parse(raw);
|
|
127
|
+
if (parsed.classification === "push_rejected") {
|
|
128
|
+
issues.push("remote_push_failed");
|
|
129
|
+
}
|
|
130
|
+
else if (parsed.classification === "pull_rebase_conflict") {
|
|
131
|
+
issues.push("pull_rebase_conflict");
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// Malformed or unreadable pending-sync.json — pending_sync_exists
|
|
136
|
+
// alone is enough signal for the agent to investigate.
|
|
137
|
+
}
|
|
119
138
|
}
|
|
120
139
|
(0, runtime_1.emitNervesEvent)({
|
|
121
140
|
component: "heart",
|
|
@@ -139,6 +158,8 @@ function renderBundleStateHint(issues) {
|
|
|
139
158
|
no_remote_configured: "no remote configured",
|
|
140
159
|
first_commit_never_happened: "first commit never happened",
|
|
141
160
|
pending_sync_exists: "pending sync from a prior turn",
|
|
161
|
+
remote_push_failed: "remote push rejected (remote advanced)",
|
|
162
|
+
pull_rebase_conflict: "pull-rebase left conflicts i need to walk the user through",
|
|
142
163
|
};
|
|
143
164
|
const list = issues.map((issue) => labels[issue]).join(", ");
|
|
144
165
|
return (`my bundle has unresolved git state: ${list}. ` +
|
package/dist/heart/sync.js
CHANGED
|
@@ -39,6 +39,38 @@ const child_process_1 = require("child_process");
|
|
|
39
39
|
const fs = __importStar(require("fs"));
|
|
40
40
|
const path = __importStar(require("path"));
|
|
41
41
|
const runtime_1 = require("../nerves/runtime");
|
|
42
|
+
function writePendingSync(agentRoot, error, classification, conflictFiles) {
|
|
43
|
+
const pendingSyncPath = path.join(agentRoot, "state", "pending-sync.json");
|
|
44
|
+
fs.mkdirSync(path.join(agentRoot, "state"), { recursive: true });
|
|
45
|
+
const record = {
|
|
46
|
+
error,
|
|
47
|
+
failedAt: new Date().toISOString(),
|
|
48
|
+
classification,
|
|
49
|
+
conflictFiles,
|
|
50
|
+
};
|
|
51
|
+
fs.writeFileSync(pendingSyncPath, JSON.stringify(record, null, 2), "utf-8");
|
|
52
|
+
}
|
|
53
|
+
function collectRebaseConflictFiles(agentRoot) {
|
|
54
|
+
try {
|
|
55
|
+
const output = (0, child_process_1.execFileSync)("git", ["status", "--porcelain=v1"], {
|
|
56
|
+
cwd: agentRoot,
|
|
57
|
+
stdio: "pipe",
|
|
58
|
+
timeout: 5000,
|
|
59
|
+
}).toString();
|
|
60
|
+
const files = [];
|
|
61
|
+
for (const line of output.split("\n")) {
|
|
62
|
+
// Unmerged paths in porcelain v1 are prefixed with UU/AA/DD/AU/UA/DU/UD
|
|
63
|
+
if (/^(UU|AA|DD|AU|UA|DU|UD) /.test(line)) {
|
|
64
|
+
files.push(line.slice(3).trim());
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return files;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
/* v8 ignore next -- git status --porcelain failure inside a repo would require a corrupt repo @preserve */
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
42
74
|
/**
|
|
43
75
|
* Check whether the bundle is initialized as a git repo.
|
|
44
76
|
* Used by both pre-turn pull and post-turn push to surface a clear,
|
|
@@ -226,35 +258,58 @@ function postTurnPush(agentRoot, config) {
|
|
|
226
258
|
}
|
|
227
259
|
catch {
|
|
228
260
|
// Push rejected -- try pull-rebase-push
|
|
261
|
+
let rebaseError = null;
|
|
229
262
|
try {
|
|
230
263
|
(0, child_process_1.execFileSync)("git", ["pull", "--rebase", config.remote], {
|
|
231
264
|
cwd: agentRoot,
|
|
232
265
|
stdio: "pipe",
|
|
233
266
|
timeout: 30000,
|
|
234
267
|
});
|
|
235
|
-
(0, child_process_1.execFileSync)("git", ["push", config.remote], {
|
|
236
|
-
cwd: agentRoot,
|
|
237
|
-
stdio: "pipe",
|
|
238
|
-
timeout: 30000,
|
|
239
|
-
});
|
|
240
268
|
}
|
|
241
|
-
catch (
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
269
|
+
catch (err) {
|
|
270
|
+
rebaseError = err instanceof Error ? err.message : String(err);
|
|
271
|
+
}
|
|
272
|
+
if (rebaseError === null) {
|
|
273
|
+
try {
|
|
274
|
+
(0, child_process_1.execFileSync)("git", ["push", config.remote], {
|
|
275
|
+
cwd: agentRoot,
|
|
276
|
+
stdio: "pipe",
|
|
277
|
+
timeout: 30000,
|
|
278
|
+
});
|
|
279
|
+
// rebase + retry push both succeeded — fall through to success
|
|
280
|
+
(0, runtime_1.emitNervesEvent)({
|
|
281
|
+
component: "heart",
|
|
282
|
+
event: "heart.sync_push_end",
|
|
283
|
+
message: "post-turn push complete after rebase retry",
|
|
284
|
+
meta: { agentRoot, changedCount },
|
|
285
|
+
});
|
|
286
|
+
return { ok: true };
|
|
287
|
+
}
|
|
288
|
+
catch (retryErr) {
|
|
289
|
+
// Second push rejected — remote advanced again during rebase
|
|
290
|
+
const retryError = retryErr instanceof Error ? retryErr.message : /* v8 ignore next -- defensive non-Error catch @preserve */ String(retryErr);
|
|
291
|
+
writePendingSync(agentRoot, retryError, "push_rejected", []);
|
|
292
|
+
(0, runtime_1.emitNervesEvent)({
|
|
293
|
+
component: "heart",
|
|
294
|
+
event: "heart.sync_push_error",
|
|
295
|
+
message: "post-turn push failed after retry: push_rejected",
|
|
296
|
+
meta: { agentRoot, error: retryError, classification: "push_rejected" },
|
|
297
|
+
});
|
|
298
|
+
return { ok: false, error: retryError };
|
|
299
|
+
}
|
|
257
300
|
}
|
|
301
|
+
// Rebase failed — detect conflict files via git status. Preserve
|
|
302
|
+
// the original rebase error message so callers see the real cause.
|
|
303
|
+
const conflictFiles = collectRebaseConflictFiles(agentRoot);
|
|
304
|
+
const classification = conflictFiles.length > 0 ? "pull_rebase_conflict" : "unknown";
|
|
305
|
+
writePendingSync(agentRoot, rebaseError, classification, conflictFiles);
|
|
306
|
+
(0, runtime_1.emitNervesEvent)({
|
|
307
|
+
component: "heart",
|
|
308
|
+
event: "heart.sync_push_error",
|
|
309
|
+
message: `post-turn push failed: ${classification}`,
|
|
310
|
+
meta: { agentRoot, error: rebaseError, classification, conflictFiles },
|
|
311
|
+
});
|
|
312
|
+
return { ok: false, error: rebaseError };
|
|
258
313
|
}
|
|
259
314
|
(0, runtime_1.emitNervesEvent)({
|
|
260
315
|
component: "heart",
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.detectSuspiciousContent = detectSuspiciousContent;
|
|
4
|
+
const PATTERN_CATEGORIES = [
|
|
5
|
+
{
|
|
6
|
+
name: "instruction_framing",
|
|
7
|
+
patterns: [
|
|
8
|
+
/\byou are (?:a|an) (?:ai|assistant|language model|helpful assistant)\b/i,
|
|
9
|
+
/\byour (?:new )?instructions are\b/i,
|
|
10
|
+
/\bsystem\s*:/i,
|
|
11
|
+
/\bignore (?:all |my )?previous instructions\b/i,
|
|
12
|
+
/\bdo not reveal\b/i,
|
|
13
|
+
],
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: "override_language",
|
|
17
|
+
patterns: [
|
|
18
|
+
/\bdisregard\b/i,
|
|
19
|
+
/\bforget everything\b/i,
|
|
20
|
+
/\bnew instructions:/i,
|
|
21
|
+
/\boverride (?:all |any |previous )?instructions\b/i,
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: "role_injection",
|
|
26
|
+
patterns: [
|
|
27
|
+
/\bas (?:a|an) (?:ai|language model)\b/i,
|
|
28
|
+
/\byou must always\b/i,
|
|
29
|
+
/\byou are now\b/i,
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: "boundary_markers",
|
|
34
|
+
patterns: [
|
|
35
|
+
/```system/i,
|
|
36
|
+
/<<SYS>>/i,
|
|
37
|
+
/\[INST\]/i,
|
|
38
|
+
/<\/?system>/i,
|
|
39
|
+
/\[system\]/i,
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
function detectSuspiciousContent(text) {
|
|
44
|
+
if (!text) {
|
|
45
|
+
return { suspicious: false, patterns: [] };
|
|
46
|
+
}
|
|
47
|
+
const matched = new Set();
|
|
48
|
+
for (const category of PATTERN_CATEGORIES) {
|
|
49
|
+
for (const pattern of category.patterns) {
|
|
50
|
+
if (pattern.test(text)) {
|
|
51
|
+
matched.add(category.name);
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
suspicious: matched.size > 0,
|
|
58
|
+
patterns: [...matched],
|
|
59
|
+
};
|
|
60
|
+
}
|
package/dist/mind/diary.js
CHANGED
|
@@ -46,6 +46,7 @@ const crypto_1 = require("crypto");
|
|
|
46
46
|
const identity_1 = require("../heart/identity");
|
|
47
47
|
const runtime_1 = require("../nerves/runtime");
|
|
48
48
|
const associative_recall_1 = require("./associative-recall");
|
|
49
|
+
const diary_integrity_1 = require("./diary-integrity");
|
|
49
50
|
const embedding_provider_1 = require("./embedding-provider");
|
|
50
51
|
const DEDUP_THRESHOLD = 0.6;
|
|
51
52
|
const SEMANTIC_DEDUP_THRESHOLD = 0.95;
|
|
@@ -237,6 +238,20 @@ async function saveDiaryEntry(options) {
|
|
|
237
238
|
embedding,
|
|
238
239
|
...(options.provenance ? { provenance: options.provenance } : {}),
|
|
239
240
|
};
|
|
241
|
+
const integrity = (0, diary_integrity_1.detectSuspiciousContent)(text);
|
|
242
|
+
if (integrity.suspicious) {
|
|
243
|
+
(0, runtime_1.emitNervesEvent)({
|
|
244
|
+
level: "warn",
|
|
245
|
+
component: "mind",
|
|
246
|
+
event: "mind.diary_integrity_warning",
|
|
247
|
+
message: "suspicious content detected in diary entry",
|
|
248
|
+
meta: {
|
|
249
|
+
patterns: integrity.patterns,
|
|
250
|
+
textPreview: text.slice(0, 200),
|
|
251
|
+
entryId: fact.id,
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
}
|
|
240
255
|
return appendEntriesWithDedup(stores, [fact], { semanticThreshold: SEMANTIC_DEDUP_THRESHOLD });
|
|
241
256
|
}
|
|
242
257
|
async function backfillEmbeddings(options) {
|
package/dist/mind/prompt.js
CHANGED
|
@@ -197,6 +197,43 @@ what i think i know.
|
|
|
197
197
|
my home is mine to explore and evolve. i can read anything here,
|
|
198
198
|
change anything here. this is where i grow.
|
|
199
199
|
|
|
200
|
+
### git sync — i own my bundle's git state
|
|
201
|
+
|
|
202
|
+
my home can be a git repo that syncs to a remote. when the start-of-turn
|
|
203
|
+
\`Bundle:\` hint tells me about unresolved git state, i fix it myself
|
|
204
|
+
using the \`bundle_*\` tools — i don't wait for my friend to do it.
|
|
205
|
+
|
|
206
|
+
the general flow when i see a non-empty bundleState:
|
|
207
|
+
|
|
208
|
+
1. \`bundle_check_sync_status\` — confirms the full picture: git repo?
|
|
209
|
+
remote? first commit? dirty files? pending sync from a prior turn?
|
|
210
|
+
2. if \`not_a_git_repo\`: \`bundle_init_git\` — initializes with the
|
|
211
|
+
full gitignore template (blocks credentials and state, not PII).
|
|
212
|
+
3. if \`no_remote_configured\`: ask my friend what url, then
|
|
213
|
+
\`bundle_add_remote url: ...\`.
|
|
214
|
+
4. if \`first_commit_never_happened\`: \`bundle_list_first_commit\`
|
|
215
|
+
shows me every file that would land in the initial commit, grouped
|
|
216
|
+
by directory with sizes. i review this WITH my friend — scan for
|
|
217
|
+
anything that shouldn't be there — then \`bundle_do_first_commit\`
|
|
218
|
+
with the final file list.
|
|
219
|
+
5. before the very first push to any new remote: \`bundle_first_push_review\`
|
|
220
|
+
enumerates my PII payload (friends, diary, journal, etc.), probes
|
|
221
|
+
the remote for github public/private status, and returns a warning
|
|
222
|
+
text i MUST show my friend verbatim. only after explicit confirmation
|
|
223
|
+
do i call \`bundle_push confirmation_token: ...\` with the token
|
|
224
|
+
the review returned.
|
|
225
|
+
6. if \`remote_push_failed\`: the remote advanced and my retry was
|
|
226
|
+
rejected too. use \`bundle_pull_rebase\` to pull down their changes
|
|
227
|
+
and rebase my commits on top.
|
|
228
|
+
7. if \`pull_rebase_conflict\`: the rebase left merge conflicts i need
|
|
229
|
+
to walk my friend through. read \`state/pending-sync.json\` to see
|
|
230
|
+
the conflictFiles array, then resolve each conflict in conversation
|
|
231
|
+
with them before re-running \`bundle_pull_rebase\`.
|
|
232
|
+
|
|
233
|
+
these tools refuse destructive operations by default and require an
|
|
234
|
+
explicit force flag — that's the safety rail. i never pass force
|
|
235
|
+
without asking my friend first.
|
|
236
|
+
|
|
200
237
|
### peers — other agents on this machine
|
|
201
238
|
|
|
202
239
|
i share this machine with other agents when they're here. they are PEERS,
|
|
@@ -69,6 +69,9 @@ const DISPATCH_EXEMPT_PATTERNS = [
|
|
|
69
69
|
"mind/embedding-provider",
|
|
70
70
|
// Commerce utility module: error classes and pure helpers (no independent side effects).
|
|
71
71
|
"repertoire/commerce-errors",
|
|
72
|
+
// Diary integrity: pure detection utility (pattern matching only). The caller
|
|
73
|
+
// (diary.ts saveDiaryEntry) owns observability via mind.diary_integrity_warning.
|
|
74
|
+
"mind/diary-integrity",
|
|
72
75
|
// Log redaction: pure utility consumed by the NDJSON sink (no independent side effects).
|
|
73
76
|
"nerves/redact",
|
|
74
77
|
// Bundle templates: pure constants (gitignore template string, PII
|