memgineering 0.22.0 → 0.23.1
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.md +70 -0
- package/assets/MEMGINEERING.md +1 -1
- package/assets/memgineering-rules/SKILL.md +16 -8
- package/assets/memgineering-writing/SKILL.md +22 -11
- package/dist/index.js +286 -35
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,76 @@ language the reader wants. The bilingual rule the monorepo applies to
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [0.23.1] — 2026-09-11
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **The rules a hook shows now include the sentence that says what to do.**
|
|
19
|
+
`memgineering guard` quoted each rule's `summary`, which is the note's text
|
|
20
|
+
cut at 200 characters — so a rule long enough to describe a problem before
|
|
21
|
+
prescribing an answer arrived with the answer removed. Measured across 24
|
|
22
|
+
sessions: of six rules planted as global rules, five were cut immediately
|
|
23
|
+
before their prescription, and every session holding one of them walked into
|
|
24
|
+
the trap its own rule described. The hook now quotes the rule as written, and
|
|
25
|
+
when it still has to shorten one it elides the MIDDLE, keeping the opening and
|
|
26
|
+
the closing instruction.
|
|
27
|
+
- **A short list of rules is now read further than a long one.** The width is a
|
|
28
|
+
budget shared across the whole block rather than a fixed cap per line, so two
|
|
29
|
+
standing decisions arrive in full where ten are abbreviated. Ten rules cost
|
|
30
|
+
exactly what ten rules cost before — nothing here prints more than it used to.
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- **A hosted brain sends the rule, not just the card.** `POST /rules` gained a
|
|
35
|
+
`text` field carrying the rule as written; a client older than this ignores it
|
|
36
|
+
and keeps rendering `summary`, and a server older than this makes the new
|
|
37
|
+
client fall back to `summary` as well. Deploy the server first to get the
|
|
38
|
+
benefit; neither order breaks anything.
|
|
39
|
+
|
|
40
|
+
## [0.23.0] — 2026-09-09
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
|
|
44
|
+
- **`revise <ref> --no-rule` on its own now takes a marking back and asks for
|
|
45
|
+
nothing else.** It used to require `--claim`, because `revise` is a verb
|
|
46
|
+
about conclusions — so unbinding a rule meant restating a conclusion that had
|
|
47
|
+
not changed, from text `open` hands back truncated, with `--summary`
|
|
48
|
+
defaulting to whatever you passed. Unbinding thirteen rules on a real hosted
|
|
49
|
+
brain was only possible by reading the note bodies out of the database. The
|
|
50
|
+
conclusion, the summary and the title now come back byte for byte, and
|
|
51
|
+
`undo` reverses it. Adding a claim, a summary, a title, a date, an explicit
|
|
52
|
+
`--action` or a boundary word still makes it an ordinary revise —
|
|
53
|
+
except `--action reinforce`, which means "the same conclusion" and is
|
|
54
|
+
therefore what unbinding already says.
|
|
55
|
+
|
|
56
|
+
### Changed
|
|
57
|
+
|
|
58
|
+
- **`consolidate` no longer assumes which way an unclassified rule should
|
|
59
|
+
go.** A memory marked as binding with no kind was answered "raise this to a
|
|
60
|
+
decision". Measured on a real brain, 55 of 57 standing rules had no kind and
|
|
61
|
+
thirteen of them were not rules at all — every one was told to become a
|
|
62
|
+
decision, which would have made the mistake harder to see. Such a note is now
|
|
63
|
+
reported as `binding_unclassified` and names both moves: classify it, or stop
|
|
64
|
+
it binding. Only somebody who reads the note can say which.
|
|
65
|
+
- **`consolidate` shares its page between the signals it found.** One rule used
|
|
66
|
+
to take the whole list — 55 of 57 candidates — so a duplicate and a
|
|
67
|
+
contradiction underneath got one line each, and raising `--limit` returned
|
|
68
|
+
more of the same rule. Candidates now alternate across the rules that found
|
|
69
|
+
something, and the answer carries `by_rule` totals, which the page itself can
|
|
70
|
+
no longer tell you.
|
|
71
|
+
- **`rules` against a hosted brain now lists the rules scoped to other
|
|
72
|
+
projects**, under their own heading, as the local one already did. They are
|
|
73
|
+
still never shown to an agent working here. Without the list there was no
|
|
74
|
+
surface anywhere on which to notice a scope with a typo in it — such a rule
|
|
75
|
+
binds nothing and appears nowhere.
|
|
76
|
+
|
|
77
|
+
### Agent guidance
|
|
78
|
+
|
|
79
|
+
`memgineering-rules` and `memgineering-writing` both changed: how to take a
|
|
80
|
+
marking back and how it differs from `retire`, the two budgets no longer
|
|
81
|
+
crowding out project rules, and the new consolidation signals. Restart your
|
|
82
|
+
agent session after upgrading so it reads them.
|
|
83
|
+
|
|
14
84
|
## [0.22.0] — 2026-09-09
|
|
15
85
|
|
|
16
86
|
### Added
|
package/assets/MEMGINEERING.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: memgineering
|
|
3
3
|
description: Use whenever the user refers to something they told you before, asks what was decided, tells you something worth keeping, or settles something that should hold next time. The memory lives in their own folder and outlives this session; check it before answering from guesswork, and write to it when you learn something durable.
|
|
4
4
|
type: skill
|
|
5
|
-
version: 0.
|
|
5
|
+
version: 0.23.1
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# memgineering
|
|
@@ -146,11 +146,19 @@ a long list is fewer rules, not a longer one.
|
|
|
146
146
|
|
|
147
147
|
Two consequences worth knowing before you mark things:
|
|
148
148
|
|
|
149
|
-
- **Global habits crowd out project decisions
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
-
|
|
155
|
-
|
|
156
|
-
|
|
149
|
+
- **Global habits used to crowd out project decisions**, and the two budgets
|
|
150
|
+
are why they no longer can: a rule scoped to the project has its own room and
|
|
151
|
+
cannot lose every slot to whatever the user has been reaching for lately. It
|
|
152
|
+
still pays to keep the standing set small — ten is what a session reads.
|
|
153
|
+
- **Two ways out, and they are not the same.** `memgineering revise <ref>
|
|
154
|
+
--no-rule --reason "…"` says it was never a rule: it stops being shown before
|
|
155
|
+
an edit, and stays a perfectly ordinary current memory that recall returns.
|
|
156
|
+
`memgineering retire <ref>` says it no longer holds: it stops binding AND
|
|
157
|
+
drops to the bottom of recall, labelled. Reach for the first when the marking
|
|
158
|
+
was the mistake and the second when the decision has been overtaken.
|
|
159
|
+
- **`--no-rule` on its own asks for nothing else.** No claim, no summary — the
|
|
160
|
+
conclusion, the summary and the title come back byte for byte, which is the
|
|
161
|
+
whole reason it exists. It used to go through `revise`, which requires a
|
|
162
|
+
claim, so taking a marking back meant restating a conclusion nobody had
|
|
163
|
+
changed, from text this CLI hands back truncated. Do not restate anything;
|
|
164
|
+
`memgineering undo` reverses it.
|
|
@@ -235,19 +235,30 @@ it is about without opening them. It does not carry their claims, so deciding
|
|
|
235
235
|
which of a pair is right still means reading them: two notes can share a title
|
|
236
236
|
and hold opposite conclusions, which is exactly the case a proposal exists for.
|
|
237
237
|
It returns them with the evidence that caught them and the action it would
|
|
238
|
-
suggest, under
|
|
238
|
+
suggest, under eight rules: `declared_supersession`, a replaced note still
|
|
239
239
|
active; `contradiction`, a disagreement nobody settled; `semantic_similarity`,
|
|
240
240
|
indexed meaning agreeing reciprocally; `shared_context`, one scope naming the
|
|
241
|
-
same thing; `similar_title`; and
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
241
|
+
same thing; `similar_title`; and three that read a note against itself, where
|
|
242
|
+
its `binding` flag and its `kind` disagree. `rule_demotion` is a binding note
|
|
243
|
+
the author classified as a fact, a procedure, an episode or a goal — two
|
|
244
|
+
deliberate fields contradicting each other, and the marking is the one to
|
|
245
|
+
doubt. `binding_unclassified` is a binding note with no kind at all, and it
|
|
246
|
+
names BOTH moves rather than picking one, because the tool reads fields and not
|
|
247
|
+
prose: it is either a decision nobody classified or something that was never a
|
|
248
|
+
rule, and only somebody who reads it can say which. (`kind_promotion` remains
|
|
249
|
+
in the vocabulary and no longer fires.)
|
|
250
|
+
|
|
251
|
+
That neutrality was measured rather than chosen for balance. On a real brain,
|
|
252
|
+
55 of 57 binding notes had no kind and every one was answered "promote this to
|
|
253
|
+
a decision" — including the thirteen that turned out not to be rules, where
|
|
254
|
+
approving the suggestion would have hardened the mistake.
|
|
255
|
+
|
|
256
|
+
**The page is shared between the signals.** One rule used to take the whole
|
|
257
|
+
list — 55 of 57 candidates — so the duplicate and the contradiction underneath
|
|
258
|
+
got a line each. Candidates now round-robin across the rules that found
|
|
259
|
+
something, and `by_rule` reports how many each found in total, which the page
|
|
260
|
+
can no longer tell you. Raising `--limit` widens the page rather than returning
|
|
261
|
+
more of one rule. Semantic detection reuses a complete, calibrated hosted
|
|
251
262
|
vector generation and never calls a model during consolidation. Authored
|
|
252
263
|
scope/entity context permits reciprocal neighbours; without it, evidence must
|
|
253
264
|
form a stricter three-note semantic triangle.
|
package/dist/index.js
CHANGED
|
@@ -497,6 +497,52 @@ var init_rule_budget = __esm({
|
|
|
497
497
|
}
|
|
498
498
|
});
|
|
499
499
|
|
|
500
|
+
// ../../packages/memory-engine/src/rule-text.ts
|
|
501
|
+
function ruleTextOf(entry) {
|
|
502
|
+
const intro = (entry.chunks ?? []).find((chunk) => chunk.label === null)?.text.trim();
|
|
503
|
+
if (intro !== void 0 && intro !== "") return intro;
|
|
504
|
+
return entry.memory.summary ?? entry.memory.title;
|
|
505
|
+
}
|
|
506
|
+
function ruleWidthFor(count) {
|
|
507
|
+
if (count <= 0) return RULE_WIDTH_MIN;
|
|
508
|
+
const share = Math.floor(RULE_BLOCK_BUDGET / count);
|
|
509
|
+
return Math.min(RULE_WIDTH_MAX, Math.max(RULE_WIDTH_MIN, share));
|
|
510
|
+
}
|
|
511
|
+
function rulePassage(text, max) {
|
|
512
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
513
|
+
const chars = [...flat];
|
|
514
|
+
if (chars.length <= max) return flat;
|
|
515
|
+
if (max <= 1) return "\u2026";
|
|
516
|
+
const budget = max - 1;
|
|
517
|
+
if (budget < 2) return `${chars.slice(0, budget).join("")}\u2026`;
|
|
518
|
+
const tailBudget = Math.min(budget - 1, Math.max(1, Math.floor(budget * 0.6)));
|
|
519
|
+
const tail = [...trailingSentences(flat, tailBudget)];
|
|
520
|
+
const head = chars.slice(0, budget - tail.length).join("").trimEnd();
|
|
521
|
+
return `${head}\u2026${tail.join("")}`;
|
|
522
|
+
}
|
|
523
|
+
function trailingSentences(flat, budget) {
|
|
524
|
+
const starts = [...flat.matchAll(/[.!?。!?](?=\s)/g)].map(
|
|
525
|
+
(match) => match.index + match[0].length
|
|
526
|
+
);
|
|
527
|
+
let best = null;
|
|
528
|
+
for (let i = starts.length - 1; i >= 0; i -= 1) {
|
|
529
|
+
const candidate = flat.slice(starts[i]).trim();
|
|
530
|
+
if ([...candidate].length > budget) break;
|
|
531
|
+
if (candidate !== "") best = candidate;
|
|
532
|
+
}
|
|
533
|
+
if (best !== null) return best;
|
|
534
|
+
return [...flat].slice(-budget).join("");
|
|
535
|
+
}
|
|
536
|
+
var RULE_WIDTH_MIN, RULE_WIDTH_MAX, RULE_BLOCK_BUDGET;
|
|
537
|
+
var init_rule_text = __esm({
|
|
538
|
+
"../../packages/memory-engine/src/rule-text.ts"() {
|
|
539
|
+
"use strict";
|
|
540
|
+
RULE_WIDTH_MIN = 200;
|
|
541
|
+
RULE_WIDTH_MAX = 600;
|
|
542
|
+
RULE_BLOCK_BUDGET = RULE_WIDTH_MIN * 10;
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
|
|
500
546
|
// ../../packages/memory-engine/src/boundary.ts
|
|
501
547
|
function normalizeForMatch(value) {
|
|
502
548
|
return value.normalize("NFC").replace(ZERO_WIDTH, "").replace(WHITESPACE_RUN, " ").trim();
|
|
@@ -2235,8 +2281,11 @@ function isLifecycleAction(action) {
|
|
|
2235
2281
|
function isClassifyAction(action) {
|
|
2236
2282
|
return action === "reclassify";
|
|
2237
2283
|
}
|
|
2284
|
+
function isBindingAction(action) {
|
|
2285
|
+
return action === "unbind";
|
|
2286
|
+
}
|
|
2238
2287
|
function carriesNoClaim(action) {
|
|
2239
|
-
return isLifecycleAction(action) || isClassifyAction(action);
|
|
2288
|
+
return isLifecycleAction(action) || isClassifyAction(action) || isBindingAction(action);
|
|
2240
2289
|
}
|
|
2241
2290
|
function isAccepted(value) {
|
|
2242
2291
|
return typeof value === "object" && value !== null && value[ACCEPTED] === true;
|
|
@@ -2265,6 +2314,21 @@ var init_schema2 = __esm({
|
|
|
2265
2314
|
"retire",
|
|
2266
2315
|
"unretire",
|
|
2267
2316
|
"reclassify",
|
|
2317
|
+
/**
|
|
2318
|
+
* A memory stops binding. Nothing else about it changes.
|
|
2319
|
+
*
|
|
2320
|
+
* It exists because there was no way to do this without saying something
|
|
2321
|
+
* else. Taking a marking back meant `revise --claim`, which is a verb about
|
|
2322
|
+
* conclusions — so unbinding thirteen rules on a real hosted brain required
|
|
2323
|
+
* restating thirteen conclusions the author had not changed, from text the
|
|
2324
|
+
* CLI hands back TRUNCATED. The person doing it read the note bodies out of
|
|
2325
|
+
* the database to avoid corrupting them, which is not a thing a user can do.
|
|
2326
|
+
*
|
|
2327
|
+
* One direction on purpose. `unbind` that could also bind would be a name
|
|
2328
|
+
* that lies half the time; marking something as a rule is already reachable
|
|
2329
|
+
* (`remember --rule`, or a `revise` that is genuinely saying something new).
|
|
2330
|
+
*/
|
|
2331
|
+
"unbind",
|
|
2268
2332
|
"ignore"
|
|
2269
2333
|
]);
|
|
2270
2334
|
StorableActionSchema = ProposalActionSchema.exclude(["ignore"]);
|
|
@@ -2460,7 +2524,7 @@ var init_schema2 = __esm({
|
|
|
2460
2524
|
path: ["source_excerpt"]
|
|
2461
2525
|
}
|
|
2462
2526
|
).refine((p) => carriesNoClaim(p.action) || p.claim !== null, {
|
|
2463
|
-
message: "only retire, unretire and
|
|
2527
|
+
message: "only retire, unretire, reclassify and unbind may omit the claim",
|
|
2464
2528
|
path: ["claim"]
|
|
2465
2529
|
}).refine((p) => isClassifyAction(p.action) === (p.proposed_kind !== null), {
|
|
2466
2530
|
message: "proposed_kind belongs to reclassify, and reclassify requires one",
|
|
@@ -2496,12 +2560,42 @@ function detectConsolidation(entries, opts = {}) {
|
|
|
2496
2560
|
...titles
|
|
2497
2561
|
];
|
|
2498
2562
|
const out = rankCandidates(scored);
|
|
2563
|
+
const limit = opts.limit ?? DEFAULT_LIMIT;
|
|
2499
2564
|
return {
|
|
2500
|
-
candidates: out
|
|
2565
|
+
candidates: shareTheList(out, limit),
|
|
2566
|
+
by_rule: countByRule(out),
|
|
2501
2567
|
scanned: budget.scanned,
|
|
2502
2568
|
truncated: budget.truncated
|
|
2503
2569
|
};
|
|
2504
2570
|
}
|
|
2571
|
+
function shareTheList(ranked, limit) {
|
|
2572
|
+
const lanes = /* @__PURE__ */ new Map();
|
|
2573
|
+
for (const candidate of ranked) {
|
|
2574
|
+
const lane = lanes.get(candidate.rule);
|
|
2575
|
+
if (lane) lane.push(candidate);
|
|
2576
|
+
else lanes.set(candidate.rule, [candidate]);
|
|
2577
|
+
}
|
|
2578
|
+
const out = [];
|
|
2579
|
+
const queues = [...lanes.values()];
|
|
2580
|
+
let round = 0;
|
|
2581
|
+
for (; ; ) {
|
|
2582
|
+
let added = false;
|
|
2583
|
+
for (const queue of queues) {
|
|
2584
|
+
if (out.length >= limit) return out;
|
|
2585
|
+
const next = queue[round];
|
|
2586
|
+
if (next === void 0) continue;
|
|
2587
|
+
out.push(next);
|
|
2588
|
+
added = true;
|
|
2589
|
+
}
|
|
2590
|
+
if (!added) return out;
|
|
2591
|
+
round += 1;
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
function countByRule(ranked) {
|
|
2595
|
+
const out = {};
|
|
2596
|
+
for (const candidate of ranked) out[candidate.rule] = (out[candidate.rule] ?? 0) + 1;
|
|
2597
|
+
return out;
|
|
2598
|
+
}
|
|
2505
2599
|
function titleTerms(title) {
|
|
2506
2600
|
const out = /* @__PURE__ */ new Set();
|
|
2507
2601
|
for (const term of terms(title)) {
|
|
@@ -2854,16 +2948,16 @@ function kindPromotions(entries) {
|
|
|
2854
2948
|
if (kind === void 0) {
|
|
2855
2949
|
out.push({
|
|
2856
2950
|
candidate: {
|
|
2857
|
-
rule: "
|
|
2951
|
+
rule: "binding_unclassified",
|
|
2858
2952
|
memory_id: entry.memory.id,
|
|
2859
2953
|
path: entry.path,
|
|
2860
2954
|
title: entry.memory.title,
|
|
2861
2955
|
other_id: null,
|
|
2862
2956
|
other_path: null,
|
|
2863
2957
|
other_title: null,
|
|
2864
|
-
evidence: "marked as binding
|
|
2865
|
-
suggested_action: "
|
|
2866
|
-
suggested_kind:
|
|
2958
|
+
evidence: "marked as binding and left unclassified \u2014 either a decision nobody classified, or something that was never a rule",
|
|
2959
|
+
suggested_action: "decide",
|
|
2960
|
+
suggested_kind: null
|
|
2867
2961
|
},
|
|
2868
2962
|
confidence: 4
|
|
2869
2963
|
});
|
|
@@ -2999,7 +3093,8 @@ var init_detect = __esm({
|
|
|
2999
3093
|
"shared_context",
|
|
3000
3094
|
"similar_title",
|
|
3001
3095
|
"kind_promotion",
|
|
3002
|
-
"rule_demotion"
|
|
3096
|
+
"rule_demotion",
|
|
3097
|
+
"binding_unclassified"
|
|
3003
3098
|
];
|
|
3004
3099
|
DEFAULT_LIMIT = 50;
|
|
3005
3100
|
DEFAULT_PAIR_BUDGET = 2e5;
|
|
@@ -3252,12 +3347,12 @@ function commit(prepared, input, opts = {}) {
|
|
|
3252
3347
|
if (carriesNoClaim(parsed.action)) {
|
|
3253
3348
|
if (parsed.claim !== null) {
|
|
3254
3349
|
throw Object.assign(new Error(`${parsed.action} does not take a claim`), {
|
|
3255
|
-
hint: isClassifyAction(parsed.action) ? `${parsed.action} says what sort of memory this is, not what it says. To change the conclusion too, use supersede.` : `${parsed.action} changes whether a memory is current, not what it says. To change the conclusion too, use supersede.`
|
|
3350
|
+
hint: isClassifyAction(parsed.action) ? `${parsed.action} says what sort of memory this is, not what it says. To change the conclusion too, use supersede.` : isBindingAction(parsed.action) ? `${parsed.action} takes a marking back and leaves the conclusion exactly as it is \u2014 that is the whole reason it exists. To change what the note says as well, use supersede.` : `${parsed.action} changes whether a memory is current, not what it says. To change the conclusion too, use supersede.`
|
|
3256
3351
|
});
|
|
3257
3352
|
}
|
|
3258
3353
|
} else if (parsed.claim === null) {
|
|
3259
3354
|
throw Object.assign(new Error(`${parsed.action} needs a claim`), {
|
|
3260
|
-
hint: "Only retire, unretire and
|
|
3355
|
+
hint: "Only retire, unretire, reclassify and unbind may omit it. They change a memory's standing, its classification or whether it binds, rather than its content."
|
|
3261
3356
|
});
|
|
3262
3357
|
} else if ((parsed.action === "supersede" || parsed.action === "create") && parsed.claim.summary === null) {
|
|
3263
3358
|
throw Object.assign(new Error(`${parsed.action} needs a summary as well as a claim`), {
|
|
@@ -3313,10 +3408,10 @@ var init_commit = __esm({
|
|
|
3313
3408
|
*/
|
|
3314
3409
|
memory_id: z5.string().min(1).nullable().default(null),
|
|
3315
3410
|
/**
|
|
3316
|
-
* Omitted by `retire` / `unretire` / `reclassify`, which change a
|
|
3317
|
-
* standing
|
|
3318
|
-
* the other action rules so the error can
|
|
3319
|
-
* at a field.
|
|
3411
|
+
* Omitted by `retire` / `unretire` / `reclassify` / `unbind`, which change a
|
|
3412
|
+
* memory's standing, its classification or whether it binds — rather than
|
|
3413
|
+
* what it says. Checked below with the other action rules so the error can
|
|
3414
|
+
* name the action instead of pointing at a field.
|
|
3320
3415
|
*/
|
|
3321
3416
|
claim: ProposedClaimSchema.nullable().default(null),
|
|
3322
3417
|
/** What `reclassify` is reclassifying this memory AS. Only that action takes one. */
|
|
@@ -3421,6 +3516,16 @@ function reclassifyInput(entry, kind, opts = {}) {
|
|
|
3421
3516
|
target_path: entry.path
|
|
3422
3517
|
};
|
|
3423
3518
|
}
|
|
3519
|
+
function unbindInput(entry, opts = {}) {
|
|
3520
|
+
return {
|
|
3521
|
+
action: "unbind",
|
|
3522
|
+
memory_id: entry.memory.id,
|
|
3523
|
+
claim: null,
|
|
3524
|
+
reason: opts.reason ?? "no longer binding",
|
|
3525
|
+
source_excerpt: opts.because ?? entry.memory.summary ?? entry.memory.title,
|
|
3526
|
+
target_path: entry.path
|
|
3527
|
+
};
|
|
3528
|
+
}
|
|
3424
3529
|
function saysNothingNew(entry, input, normalize) {
|
|
3425
3530
|
const norm = (v) => normalize(v ?? "").trim();
|
|
3426
3531
|
const claim = input.claim;
|
|
@@ -3510,6 +3615,10 @@ function desiredFields(proposal, existing) {
|
|
|
3510
3615
|
out["kind"] = proposal.proposed_kind;
|
|
3511
3616
|
return out;
|
|
3512
3617
|
}
|
|
3618
|
+
if (isBindingAction(proposal.action)) {
|
|
3619
|
+
delete out["binding"];
|
|
3620
|
+
return out;
|
|
3621
|
+
}
|
|
3513
3622
|
const claim = proposal.claim;
|
|
3514
3623
|
if (claim === null) {
|
|
3515
3624
|
throw new Error(`${proposal.action} reached the patcher with no claim`);
|
|
@@ -3807,6 +3916,46 @@ function verifyPatch(patched, proposal, before) {
|
|
|
3807
3916
|
}
|
|
3808
3917
|
return;
|
|
3809
3918
|
}
|
|
3919
|
+
if (isBindingAction(proposal.action)) {
|
|
3920
|
+
if (mapped.memory.binding === true) {
|
|
3921
|
+
throw new PatchError(
|
|
3922
|
+
"the patched note still reads as binding",
|
|
3923
|
+
"This is a defect in the patcher, not in your note. Nothing was written."
|
|
3924
|
+
);
|
|
3925
|
+
}
|
|
3926
|
+
if (before === void 0) {
|
|
3927
|
+
throw new PatchError(
|
|
3928
|
+
`${proposal.action} cannot be verified without the note it started from`,
|
|
3929
|
+
"This is a defect in the caller, not in your note. Nothing was written."
|
|
3930
|
+
);
|
|
3931
|
+
}
|
|
3932
|
+
const was = mapNoteToMemory({ path: proposal.target.path, content: before, revision: null });
|
|
3933
|
+
if (was.claims[0]?.text !== mapped.claims[0]?.text) {
|
|
3934
|
+
throw new PatchError(
|
|
3935
|
+
`${proposal.action} changed the conclusion, which it must never do`,
|
|
3936
|
+
"Taking a marking back leaves what the note says exactly as it was. Nothing was written."
|
|
3937
|
+
);
|
|
3938
|
+
}
|
|
3939
|
+
if (was.memory.summary !== mapped.memory.summary) {
|
|
3940
|
+
throw new PatchError(
|
|
3941
|
+
`${proposal.action} changed the summary, which it must never do`,
|
|
3942
|
+
"Taking a marking back leaves what the note says exactly as it was \u2014 and the summary is what recall matches on. Nothing was written."
|
|
3943
|
+
);
|
|
3944
|
+
}
|
|
3945
|
+
if (was.memory.title !== mapped.memory.title) {
|
|
3946
|
+
throw new PatchError(
|
|
3947
|
+
`${proposal.action} changed the title, which it must never do`,
|
|
3948
|
+
"Taking a marking back leaves what the note says exactly as it was. Nothing was written."
|
|
3949
|
+
);
|
|
3950
|
+
}
|
|
3951
|
+
if (was.memory.status !== mapped.memory.status) {
|
|
3952
|
+
throw new PatchError(
|
|
3953
|
+
`${proposal.action} changed the status, which it must never do`,
|
|
3954
|
+
"A memory that no longer binds is still current. To take it out of the answers as well, use retire. Nothing was written."
|
|
3955
|
+
);
|
|
3956
|
+
}
|
|
3957
|
+
return;
|
|
3958
|
+
}
|
|
3810
3959
|
const supersedes = new Set(
|
|
3811
3960
|
mapped.relations.filter((r) => r.type === "supersedes").map((r) => r.to)
|
|
3812
3961
|
);
|
|
@@ -4127,6 +4276,7 @@ var init_src = __esm({
|
|
|
4127
4276
|
init_schema();
|
|
4128
4277
|
init_layout();
|
|
4129
4278
|
init_rule_budget();
|
|
4279
|
+
init_rule_text();
|
|
4130
4280
|
init_boundary();
|
|
4131
4281
|
init_markdown_mapping();
|
|
4132
4282
|
init_build();
|
|
@@ -4709,7 +4859,7 @@ async function createCloudBrain(token, baseUrl, name) {
|
|
|
4709
4859
|
});
|
|
4710
4860
|
return res.body;
|
|
4711
4861
|
}
|
|
4712
|
-
var call, askedFromField, cloudRecall, cloudOpen, cloudEvidence, cloudRemember, cloudUndo, cloudLog, cloudPutNote, cloudListNotes, cloudRevise, cloudBaseWrite, cloudLifecycle, cloudExclude, RANKED_BY_PROJECT, rankedOnProject, cloudResurface, cloudRules, cloudReindex;
|
|
4862
|
+
var call, askedFromField, cloudRecall, cloudOpen, cloudEvidence, cloudRemember, cloudUndo, cloudLog, cloudPutNote, cloudListNotes, cloudRevise, cloudBaseWrite, cloudLifecycle, cloudUnbind, cloudExclude, RANKED_BY_PROJECT, rankedOnProject, cloudResurface, cloudRules, cloudReindex;
|
|
4713
4863
|
var init_cloud_brain = __esm({
|
|
4714
4864
|
"src/lib/cloud-brain.ts"() {
|
|
4715
4865
|
"use strict";
|
|
@@ -4799,6 +4949,10 @@ var init_cloud_brain = __esm({
|
|
|
4799
4949
|
ref: args.ref,
|
|
4800
4950
|
...args.reason === void 0 ? {} : { reason: args.reason }
|
|
4801
4951
|
});
|
|
4952
|
+
cloudUnbind = (brain, args) => call(brain, "POST", "/unbind", {
|
|
4953
|
+
ref: args.ref,
|
|
4954
|
+
...args.reason === void 0 ? {} : { reason: args.reason }
|
|
4955
|
+
});
|
|
4802
4956
|
cloudExclude = (brain, mode, args) => call(brain, "POST", `/${mode}`, {
|
|
4803
4957
|
path: args.path,
|
|
4804
4958
|
...args.reason === void 0 ? {} : { reason: args.reason }
|
|
@@ -12946,10 +13100,13 @@ function yamlString(value) {
|
|
|
12946
13100
|
|
|
12947
13101
|
// src/commands/revise.ts
|
|
12948
13102
|
function reviseCommand() {
|
|
13103
|
+
const said = { action: false };
|
|
12949
13104
|
return new Command7("revise").description("change a memory's conclusion, and record how to undo it").argument("<ref>", "handle, id, path, or exact title").option("--claim <text>", "what is now true").option("--summary <text>", "the one line recall will show (defaults to the claim)").option("--title <text>", "the heading recall shows above it (unchanged when omitted)").option(
|
|
12950
13105
|
"--valid-from <iso>",
|
|
12951
13106
|
"when this became true (defaults: supersede = now, otherwise unchanged)"
|
|
12952
|
-
).option("--action <kind>", "reinforce | supersede | conflict", "supersede").
|
|
13107
|
+
).option("--action <kind>", "reinforce | supersede | conflict", "supersede").on("option:action", () => {
|
|
13108
|
+
said.action = true;
|
|
13109
|
+
}).option("--rule", "this memory is now a decision that binds, not a fact to look up").option("--no-rule", "it no longer binds \u2014 an ordinary memory again").option("--never-store <words>", "replace this note\u2019s boundary words (comma-separated)").option("--no-never-store", "this note no longer declares a boundary").option(
|
|
12953
13110
|
"--never-store-add <words>",
|
|
12954
13111
|
"add words to this note\u2019s boundary, keeping the ones already there"
|
|
12955
13112
|
).option("--contradicts <ids...>", "memories this disagrees with").option("--reason <text>", "why this change is right").option("--because <text>", "the passage from the note that justifies it").option("--input <json>", "the whole change as JSON (file path, `-`, or inline)").option("--dry-run", "show the diff and write nothing").option("--vault <path>", "which brain to write to").option("--local", "use the brain on this machine, even when signed in to a hosted one").addHelpText(
|
|
@@ -12969,11 +13126,24 @@ function reviseCommand() {
|
|
|
12969
13126
|
].join("\n")
|
|
12970
13127
|
).action(async (ref, opts) => {
|
|
12971
13128
|
const added = boundaryAddition(opts);
|
|
13129
|
+
const bareUnbind = opts.rule === false && // `reinforce` is the one action that AGREES with unbinding: it means
|
|
13130
|
+
// "the same conclusion", which is exactly what is being said. A fresh
|
|
13131
|
+
// agent reaches for it unprompted — measured in the Round, the first
|
|
13132
|
+
// session typed `--no-rule --action reinforce`, was refused for
|
|
13133
|
+
// having no claim, and then supplied one, writing a `claim:` key the
|
|
13134
|
+
// note never had. That is the failure this release exists to remove,
|
|
13135
|
+
// reproduced by the guard meant to be careful. `supersede` and
|
|
13136
|
+
// `conflict` still disqualify: both assert something new.
|
|
13137
|
+
(!said.action || opts.action === "reinforce") && opts.claim === void 0 && opts.summary === void 0 && opts.title === void 0 && opts.validFrom === void 0 && opts.contradicts === void 0 && opts.input === void 0 && opts.neverStore === void 0 && opts.neverStoreAdd === void 0;
|
|
12972
13138
|
const where = await resolveTarget({
|
|
12973
13139
|
...opts.local === void 0 ? {} : { local: opts.local },
|
|
12974
13140
|
...opts.vault === void 0 ? {} : { vault: opts.vault }
|
|
12975
13141
|
});
|
|
12976
13142
|
if (where.kind === "cloud") {
|
|
13143
|
+
if (bareUnbind) {
|
|
13144
|
+
await unbindHosted(where, ref, opts);
|
|
13145
|
+
return;
|
|
13146
|
+
}
|
|
12977
13147
|
await reviseHosted(where, ref, opts, added);
|
|
12978
13148
|
return;
|
|
12979
13149
|
}
|
|
@@ -12985,7 +13155,10 @@ function reviseCommand() {
|
|
|
12985
13155
|
const built = await openIndex(adapter, brain.root);
|
|
12986
13156
|
const entry = resolveRef(built.entries, ref);
|
|
12987
13157
|
const widened = widenedBoundary(entry.memory.never_store, added, ref);
|
|
12988
|
-
const input =
|
|
13158
|
+
const input = bareUnbind ? unbindInput(entry, {
|
|
13159
|
+
...opts.reason === void 0 ? {} : { reason: opts.reason },
|
|
13160
|
+
...opts.because === void 0 ? {} : { because: opts.because }
|
|
13161
|
+
}) : await buildCommitInput(entry, opts, widened);
|
|
12989
13162
|
const rationale = sanitizeRationale(
|
|
12990
13163
|
input.reason === NO_REASON_GIVEN ? null : input.reason ?? null
|
|
12991
13164
|
);
|
|
@@ -13177,6 +13350,30 @@ Undo: \`${undoHint(applied.record.op_id, opts)}\``));
|
|
|
13177
13350
|
});
|
|
13178
13351
|
});
|
|
13179
13352
|
}
|
|
13353
|
+
async function unbindHosted(where, ref, opts) {
|
|
13354
|
+
if (opts.dryRun === true) {
|
|
13355
|
+
throw memgError(
|
|
13356
|
+
"invalid_input",
|
|
13357
|
+
"--dry-run is not supported for `--no-rule` on a hosted brain",
|
|
13358
|
+
"Taking a marking back writes one key and leaves everything else byte for byte, so there is no diff worth previewing. Run it without `--dry-run`, and `memgineering undo` reverses it. Nothing was sent."
|
|
13359
|
+
);
|
|
13360
|
+
}
|
|
13361
|
+
const brain = await openCloudBrain(where);
|
|
13362
|
+
const result = await cloudUnbind(brain, {
|
|
13363
|
+
ref,
|
|
13364
|
+
...opts.reason === void 0 ? {} : { reason: opts.reason }
|
|
13365
|
+
});
|
|
13366
|
+
printDual({
|
|
13367
|
+
json: result,
|
|
13368
|
+
human: () => {
|
|
13369
|
+
printHuman(`Unbound \`${oneLine(result.target, WIDTH.path)}\`.
|
|
13370
|
+
`);
|
|
13371
|
+
printHuman(c.gray(oneLine(result.note, WIDTH.text)));
|
|
13372
|
+
printHuman(c.gray(`
|
|
13373
|
+
\`memgineering undo ${oneLine(result.op_id, WIDTH.handle)}\``));
|
|
13374
|
+
}
|
|
13375
|
+
});
|
|
13376
|
+
}
|
|
13180
13377
|
async function reviseHosted(where, ref, opts, added) {
|
|
13181
13378
|
if (opts.input) {
|
|
13182
13379
|
throw memgError(
|
|
@@ -13875,6 +14072,10 @@ function consolidateCommand() {
|
|
|
13875
14072
|
" rule_demotion a note marked as binding but classified as a fact, a",
|
|
13876
14073
|
" procedure, an episode or a goal \u2014 the author said it is",
|
|
13877
14074
|
" not a decision, so the MARKING is the field to doubt",
|
|
14075
|
+
" binding_unclassified a note marked as binding with no kind at all. Two",
|
|
14076
|
+
" fields, agreeing on nothing: it is either a decision",
|
|
14077
|
+
" nobody classified or something that was never a rule,",
|
|
14078
|
+
" and only somebody who reads it can say which",
|
|
13878
14079
|
"",
|
|
13879
14080
|
"Nothing here becomes a proposal on its own. Read a candidate, decide what it",
|
|
13880
14081
|
"means, then say so:",
|
|
@@ -13888,11 +14089,13 @@ function consolidateCommand() {
|
|
|
13888
14089
|
limit === void 0 ? {} : { limit }
|
|
13889
14090
|
) : await detectLocally(where.brain.root, limit);
|
|
13890
14091
|
const { candidates, truncated } = report3;
|
|
14092
|
+
const byRule = Object.entries(report3.by_rule ?? {}).sort((a, b) => b[1] - a[1]);
|
|
13891
14093
|
printDual({
|
|
13892
14094
|
json: {
|
|
13893
14095
|
candidates,
|
|
13894
14096
|
count: candidates.length,
|
|
13895
14097
|
scanned: report3.scanned,
|
|
14098
|
+
by_rule: report3.by_rule ?? {},
|
|
13896
14099
|
// Reported rather than left to be inferred from a short list. A run
|
|
13897
14100
|
// that stopped early and said nothing would be indistinguishable from
|
|
13898
14101
|
// a brain with little to find, which is the one thing the reader
|
|
@@ -13916,6 +14119,19 @@ function consolidateCommand() {
|
|
|
13916
14119
|
printHuman(`## ${candidates.length} worth a look
|
|
13917
14120
|
`);
|
|
13918
14121
|
for (const candidate of candidates) printHuman(renderCandidate(candidate));
|
|
14122
|
+
const held = byRule.filter(
|
|
14123
|
+
([rule, total]) => total > candidates.filter((c2) => c2.rule === rule).length
|
|
14124
|
+
);
|
|
14125
|
+
if (held.length > 0) {
|
|
14126
|
+
printHuman(
|
|
14127
|
+
c.gray(
|
|
14128
|
+
`
|
|
14129
|
+
${held.map(([rule, total]) => `${rule}: ${total}`).join(" \xB7 ")}
|
|
14130
|
+
Found in total \u2014 the list above shows a few of each, so one noisy signal
|
|
14131
|
+
cannot bury the rest. \`--limit\` raises the page.`
|
|
14132
|
+
)
|
|
14133
|
+
);
|
|
14134
|
+
}
|
|
13919
14135
|
if (truncated) {
|
|
13920
14136
|
printHuman(
|
|
13921
14137
|
c.yellow(`
|
|
@@ -13962,7 +14178,16 @@ function renderCandidate(candidate) {
|
|
|
13962
14178
|
// a second door to the same field would give a brain two ways to record one
|
|
13963
14179
|
// change. So the suggestion names the command rather than a verb this CLI
|
|
13964
14180
|
// does not have.
|
|
13965
|
-
candidate.suggested_action === "unmark_rule" ? c.gray(` suggested: memgineering revise ${candidate.memory_id} --no-rule --reason "\u2026"`) :
|
|
14181
|
+
candidate.suggested_action === "unmark_rule" ? c.gray(` suggested: memgineering revise ${candidate.memory_id} --no-rule --reason "\u2026"`) : (
|
|
14182
|
+
// BOTH moves, because the tool genuinely cannot tell. It used to print
|
|
14183
|
+
// one — "promote this to a decision" — for every unclassified rule,
|
|
14184
|
+
// including the thirteen on a real brain that were not rules at all.
|
|
14185
|
+
candidate.suggested_action === "decide" ? c.gray(
|
|
14186
|
+
` suggested: read it, then one of
|
|
14187
|
+
memgineering propose reclassify --memory ${candidate.memory_id} --kind decision --reason "\u2026" --excerpt "\u2026"
|
|
14188
|
+
memgineering revise ${candidate.memory_id} --no-rule --reason "\u2026"`
|
|
14189
|
+
) : candidate.suggested_kind === null ? c.gray(` suggested: ${candidate.suggested_action}`) : c.gray(` suggested: ${candidate.suggested_action} --kind ${candidate.suggested_kind}`)
|
|
14190
|
+
)
|
|
13966
14191
|
);
|
|
13967
14192
|
return lines2.join("\n");
|
|
13968
14193
|
}
|
|
@@ -15619,26 +15844,31 @@ async function collectRules(vault, contextDir, now = /* @__PURE__ */ new Date())
|
|
|
15619
15844
|
...unreachableDefault ? { unreachableDefault } : {}
|
|
15620
15845
|
};
|
|
15621
15846
|
}
|
|
15622
|
-
var RULE_WIDTH =
|
|
15847
|
+
var RULE_WIDTH = RULE_WIDTH_MIN;
|
|
15623
15848
|
function renderRuleLine(text, width = RULE_WIDTH) {
|
|
15624
|
-
return quoteBlock(
|
|
15849
|
+
return quoteBlock(rulePassage(oneLineWhole(text), width));
|
|
15625
15850
|
}
|
|
15626
|
-
function renderRuleLines(rules) {
|
|
15627
|
-
return rules.map(({ entry }) => renderRuleLine(entry
|
|
15851
|
+
function renderRuleLines(rules, width = ruleWidthFor(rules.length)) {
|
|
15852
|
+
return rules.map(({ entry }) => renderRuleLine(ruleTextOf(entry), width));
|
|
15628
15853
|
}
|
|
15629
15854
|
function renderRuleGroups(rules, project) {
|
|
15630
15855
|
const globals = rules.filter((rule) => rule.lane === "global");
|
|
15631
15856
|
const scoped = rules.filter((rule) => rule.lane === "project");
|
|
15857
|
+
const width = ruleWidthFor(rules.length);
|
|
15632
15858
|
const out = [];
|
|
15633
15859
|
if (globals.length > 0 && scoped.length > 0) {
|
|
15634
|
-
out.push("Everywhere:", ...renderRuleLines(globals), "");
|
|
15635
|
-
out.push(
|
|
15860
|
+
out.push("Everywhere:", ...renderRuleLines(globals, width), "");
|
|
15861
|
+
out.push(
|
|
15862
|
+
`In ${oneLine(project ?? "this project", WIDTH.title)}:`,
|
|
15863
|
+
...renderRuleLines(scoped, width)
|
|
15864
|
+
);
|
|
15636
15865
|
return out;
|
|
15637
15866
|
}
|
|
15638
|
-
return renderRuleLines(rules);
|
|
15867
|
+
return renderRuleLines(rules, width);
|
|
15639
15868
|
}
|
|
15640
15869
|
|
|
15641
15870
|
// src/commands/guard.ts
|
|
15871
|
+
init_src();
|
|
15642
15872
|
init_deadline();
|
|
15643
15873
|
init_cloud_brain();
|
|
15644
15874
|
function guardCommand() {
|
|
@@ -15837,7 +16067,8 @@ async function hostedRuleLines(where, contextDir) {
|
|
|
15837
16067
|
return { lines: grouped, total };
|
|
15838
16068
|
}
|
|
15839
16069
|
function groupHostedLines(rules, project) {
|
|
15840
|
-
const
|
|
16070
|
+
const width = ruleWidthFor(rules.length);
|
|
16071
|
+
const line = (r) => renderRuleLine(r.text ?? r.summary ?? r.title, width);
|
|
15841
16072
|
const globals = rules.filter((r) => r.lane === "global");
|
|
15842
16073
|
const scoped = rules.filter((r) => r.lane === "project");
|
|
15843
16074
|
if (globals.length === 0 || scoped.length === 0) return rules.map(line);
|
|
@@ -21268,16 +21499,27 @@ function rulesCommand() {
|
|
|
21268
21499
|
});
|
|
21269
21500
|
if (where.kind === "cloud") {
|
|
21270
21501
|
const project2 = await projectOf().then((p) => p.name).catch(() => null);
|
|
21271
|
-
const {
|
|
21272
|
-
|
|
21273
|
-
|
|
21502
|
+
const {
|
|
21503
|
+
rules: rules2,
|
|
21504
|
+
total: total2,
|
|
21505
|
+
eligible,
|
|
21506
|
+
elsewhere: elsewhere2 = []
|
|
21507
|
+
} = await cloudRules(await openCloudBrain(where), { project: project2 });
|
|
21274
21508
|
const shownOf = eligible ?? rules2.length;
|
|
21275
21509
|
printDual({
|
|
21276
|
-
json: {
|
|
21510
|
+
json: {
|
|
21511
|
+
brain: where.name,
|
|
21512
|
+
hosted: true,
|
|
21513
|
+
rules: rules2,
|
|
21514
|
+
elsewhere: elsewhere2,
|
|
21515
|
+
total: total2,
|
|
21516
|
+
project: project2,
|
|
21517
|
+
eligible: shownOf
|
|
21518
|
+
},
|
|
21277
21519
|
human: () => {
|
|
21278
21520
|
printHuman(`## rules \u2014 ${oneLine(where.name, WIDTH.title)} (${total2})
|
|
21279
21521
|
`);
|
|
21280
|
-
if (rules2.length === 0) {
|
|
21522
|
+
if (rules2.length === 0 && elsewhere2.length === 0) {
|
|
21281
21523
|
printHuman("_Nothing is marked as a rule in this brain._\n");
|
|
21282
21524
|
printHuman(
|
|
21283
21525
|
c.gray(
|
|
@@ -21299,12 +21541,21 @@ function rulesCommand() {
|
|
|
21299
21541
|
printHuman(c.gray(`
|
|
21300
21542
|
${shownOf - rules2.length} more not shown.`));
|
|
21301
21543
|
}
|
|
21302
|
-
if (
|
|
21544
|
+
if (elsewhere2.length > 0) {
|
|
21545
|
+
printHuman(`
|
|
21546
|
+
### scoped to other projects (${elsewhere2.length})
|
|
21547
|
+
`);
|
|
21303
21548
|
printHuman(
|
|
21304
|
-
c.gray(
|
|
21305
|
-
`${total2 - shownOf} more belong to other projects and are not shown here \u2014 they bind there.`
|
|
21306
|
-
)
|
|
21549
|
+
c.gray("Not shown to an agent working here. They bind where they say.\n")
|
|
21307
21550
|
);
|
|
21551
|
+
for (const rule of elsewhere2) {
|
|
21552
|
+
printHuman(`- ${oneLine(rule.summary ?? rule.title, WIDTH.text)}`);
|
|
21553
|
+
printHuman(
|
|
21554
|
+
c.gray(
|
|
21555
|
+
` \`open: ${oneLine(rule.handle, WIDTH.handle)}\` \xB7 ${oneLine(rule.scope ?? "", WIDTH.title)}`
|
|
21556
|
+
)
|
|
21557
|
+
);
|
|
21558
|
+
}
|
|
21308
21559
|
}
|
|
21309
21560
|
printHuman(
|
|
21310
21561
|
"\nThese are decisions, not suggestions. Follow them without being asked, and say\nso if what you are about to do goes against one."
|
package/package.json
CHANGED