memgineering 0.22.0 → 0.23.0
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 +44 -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 +225 -27
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,50 @@ language the reader wants. The bilingual rule the monorepo applies to
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [0.23.0] — 2026-09-09
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **`revise <ref> --no-rule` on its own now takes a marking back and asks for
|
|
19
|
+
nothing else.** It used to require `--claim`, because `revise` is a verb
|
|
20
|
+
about conclusions — so unbinding a rule meant restating a conclusion that had
|
|
21
|
+
not changed, from text `open` hands back truncated, with `--summary`
|
|
22
|
+
defaulting to whatever you passed. Unbinding thirteen rules on a real hosted
|
|
23
|
+
brain was only possible by reading the note bodies out of the database. The
|
|
24
|
+
conclusion, the summary and the title now come back byte for byte, and
|
|
25
|
+
`undo` reverses it. Adding a claim, a summary, a title, a date, an explicit
|
|
26
|
+
`--action` or a boundary word still makes it an ordinary revise —
|
|
27
|
+
except `--action reinforce`, which means "the same conclusion" and is
|
|
28
|
+
therefore what unbinding already says.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- **`consolidate` no longer assumes which way an unclassified rule should
|
|
33
|
+
go.** A memory marked as binding with no kind was answered "raise this to a
|
|
34
|
+
decision". Measured on a real brain, 55 of 57 standing rules had no kind and
|
|
35
|
+
thirteen of them were not rules at all — every one was told to become a
|
|
36
|
+
decision, which would have made the mistake harder to see. Such a note is now
|
|
37
|
+
reported as `binding_unclassified` and names both moves: classify it, or stop
|
|
38
|
+
it binding. Only somebody who reads the note can say which.
|
|
39
|
+
- **`consolidate` shares its page between the signals it found.** One rule used
|
|
40
|
+
to take the whole list — 55 of 57 candidates — so a duplicate and a
|
|
41
|
+
contradiction underneath got one line each, and raising `--limit` returned
|
|
42
|
+
more of the same rule. Candidates now alternate across the rules that found
|
|
43
|
+
something, and the answer carries `by_rule` totals, which the page itself can
|
|
44
|
+
no longer tell you.
|
|
45
|
+
- **`rules` against a hosted brain now lists the rules scoped to other
|
|
46
|
+
projects**, under their own heading, as the local one already did. They are
|
|
47
|
+
still never shown to an agent working here. Without the list there was no
|
|
48
|
+
surface anywhere on which to notice a scope with a typo in it — such a rule
|
|
49
|
+
binds nothing and appears nowhere.
|
|
50
|
+
|
|
51
|
+
### Agent guidance
|
|
52
|
+
|
|
53
|
+
`memgineering-rules` and `memgineering-writing` both changed: how to take a
|
|
54
|
+
marking back and how it differs from `retire`, the two budgets no longer
|
|
55
|
+
crowding out project rules, and the new consolidation signals. Restart your
|
|
56
|
+
agent session after upgrading so it reads them.
|
|
57
|
+
|
|
14
58
|
## [0.22.0] — 2026-09-09
|
|
15
59
|
|
|
16
60
|
### 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.0
|
|
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
|
@@ -2235,8 +2235,11 @@ function isLifecycleAction(action) {
|
|
|
2235
2235
|
function isClassifyAction(action) {
|
|
2236
2236
|
return action === "reclassify";
|
|
2237
2237
|
}
|
|
2238
|
+
function isBindingAction(action) {
|
|
2239
|
+
return action === "unbind";
|
|
2240
|
+
}
|
|
2238
2241
|
function carriesNoClaim(action) {
|
|
2239
|
-
return isLifecycleAction(action) || isClassifyAction(action);
|
|
2242
|
+
return isLifecycleAction(action) || isClassifyAction(action) || isBindingAction(action);
|
|
2240
2243
|
}
|
|
2241
2244
|
function isAccepted(value) {
|
|
2242
2245
|
return typeof value === "object" && value !== null && value[ACCEPTED] === true;
|
|
@@ -2265,6 +2268,21 @@ var init_schema2 = __esm({
|
|
|
2265
2268
|
"retire",
|
|
2266
2269
|
"unretire",
|
|
2267
2270
|
"reclassify",
|
|
2271
|
+
/**
|
|
2272
|
+
* A memory stops binding. Nothing else about it changes.
|
|
2273
|
+
*
|
|
2274
|
+
* It exists because there was no way to do this without saying something
|
|
2275
|
+
* else. Taking a marking back meant `revise --claim`, which is a verb about
|
|
2276
|
+
* conclusions — so unbinding thirteen rules on a real hosted brain required
|
|
2277
|
+
* restating thirteen conclusions the author had not changed, from text the
|
|
2278
|
+
* CLI hands back TRUNCATED. The person doing it read the note bodies out of
|
|
2279
|
+
* the database to avoid corrupting them, which is not a thing a user can do.
|
|
2280
|
+
*
|
|
2281
|
+
* One direction on purpose. `unbind` that could also bind would be a name
|
|
2282
|
+
* that lies half the time; marking something as a rule is already reachable
|
|
2283
|
+
* (`remember --rule`, or a `revise` that is genuinely saying something new).
|
|
2284
|
+
*/
|
|
2285
|
+
"unbind",
|
|
2268
2286
|
"ignore"
|
|
2269
2287
|
]);
|
|
2270
2288
|
StorableActionSchema = ProposalActionSchema.exclude(["ignore"]);
|
|
@@ -2460,7 +2478,7 @@ var init_schema2 = __esm({
|
|
|
2460
2478
|
path: ["source_excerpt"]
|
|
2461
2479
|
}
|
|
2462
2480
|
).refine((p) => carriesNoClaim(p.action) || p.claim !== null, {
|
|
2463
|
-
message: "only retire, unretire and
|
|
2481
|
+
message: "only retire, unretire, reclassify and unbind may omit the claim",
|
|
2464
2482
|
path: ["claim"]
|
|
2465
2483
|
}).refine((p) => isClassifyAction(p.action) === (p.proposed_kind !== null), {
|
|
2466
2484
|
message: "proposed_kind belongs to reclassify, and reclassify requires one",
|
|
@@ -2496,12 +2514,42 @@ function detectConsolidation(entries, opts = {}) {
|
|
|
2496
2514
|
...titles
|
|
2497
2515
|
];
|
|
2498
2516
|
const out = rankCandidates(scored);
|
|
2517
|
+
const limit = opts.limit ?? DEFAULT_LIMIT;
|
|
2499
2518
|
return {
|
|
2500
|
-
candidates: out
|
|
2519
|
+
candidates: shareTheList(out, limit),
|
|
2520
|
+
by_rule: countByRule(out),
|
|
2501
2521
|
scanned: budget.scanned,
|
|
2502
2522
|
truncated: budget.truncated
|
|
2503
2523
|
};
|
|
2504
2524
|
}
|
|
2525
|
+
function shareTheList(ranked, limit) {
|
|
2526
|
+
const lanes = /* @__PURE__ */ new Map();
|
|
2527
|
+
for (const candidate of ranked) {
|
|
2528
|
+
const lane = lanes.get(candidate.rule);
|
|
2529
|
+
if (lane) lane.push(candidate);
|
|
2530
|
+
else lanes.set(candidate.rule, [candidate]);
|
|
2531
|
+
}
|
|
2532
|
+
const out = [];
|
|
2533
|
+
const queues = [...lanes.values()];
|
|
2534
|
+
let round = 0;
|
|
2535
|
+
for (; ; ) {
|
|
2536
|
+
let added = false;
|
|
2537
|
+
for (const queue of queues) {
|
|
2538
|
+
if (out.length >= limit) return out;
|
|
2539
|
+
const next = queue[round];
|
|
2540
|
+
if (next === void 0) continue;
|
|
2541
|
+
out.push(next);
|
|
2542
|
+
added = true;
|
|
2543
|
+
}
|
|
2544
|
+
if (!added) return out;
|
|
2545
|
+
round += 1;
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
function countByRule(ranked) {
|
|
2549
|
+
const out = {};
|
|
2550
|
+
for (const candidate of ranked) out[candidate.rule] = (out[candidate.rule] ?? 0) + 1;
|
|
2551
|
+
return out;
|
|
2552
|
+
}
|
|
2505
2553
|
function titleTerms(title) {
|
|
2506
2554
|
const out = /* @__PURE__ */ new Set();
|
|
2507
2555
|
for (const term of terms(title)) {
|
|
@@ -2854,16 +2902,16 @@ function kindPromotions(entries) {
|
|
|
2854
2902
|
if (kind === void 0) {
|
|
2855
2903
|
out.push({
|
|
2856
2904
|
candidate: {
|
|
2857
|
-
rule: "
|
|
2905
|
+
rule: "binding_unclassified",
|
|
2858
2906
|
memory_id: entry.memory.id,
|
|
2859
2907
|
path: entry.path,
|
|
2860
2908
|
title: entry.memory.title,
|
|
2861
2909
|
other_id: null,
|
|
2862
2910
|
other_path: null,
|
|
2863
2911
|
other_title: null,
|
|
2864
|
-
evidence: "marked as binding
|
|
2865
|
-
suggested_action: "
|
|
2866
|
-
suggested_kind:
|
|
2912
|
+
evidence: "marked as binding and left unclassified \u2014 either a decision nobody classified, or something that was never a rule",
|
|
2913
|
+
suggested_action: "decide",
|
|
2914
|
+
suggested_kind: null
|
|
2867
2915
|
},
|
|
2868
2916
|
confidence: 4
|
|
2869
2917
|
});
|
|
@@ -2999,7 +3047,8 @@ var init_detect = __esm({
|
|
|
2999
3047
|
"shared_context",
|
|
3000
3048
|
"similar_title",
|
|
3001
3049
|
"kind_promotion",
|
|
3002
|
-
"rule_demotion"
|
|
3050
|
+
"rule_demotion",
|
|
3051
|
+
"binding_unclassified"
|
|
3003
3052
|
];
|
|
3004
3053
|
DEFAULT_LIMIT = 50;
|
|
3005
3054
|
DEFAULT_PAIR_BUDGET = 2e5;
|
|
@@ -3252,12 +3301,12 @@ function commit(prepared, input, opts = {}) {
|
|
|
3252
3301
|
if (carriesNoClaim(parsed.action)) {
|
|
3253
3302
|
if (parsed.claim !== null) {
|
|
3254
3303
|
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.`
|
|
3304
|
+
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
3305
|
});
|
|
3257
3306
|
}
|
|
3258
3307
|
} else if (parsed.claim === null) {
|
|
3259
3308
|
throw Object.assign(new Error(`${parsed.action} needs a claim`), {
|
|
3260
|
-
hint: "Only retire, unretire and
|
|
3309
|
+
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
3310
|
});
|
|
3262
3311
|
} else if ((parsed.action === "supersede" || parsed.action === "create") && parsed.claim.summary === null) {
|
|
3263
3312
|
throw Object.assign(new Error(`${parsed.action} needs a summary as well as a claim`), {
|
|
@@ -3313,10 +3362,10 @@ var init_commit = __esm({
|
|
|
3313
3362
|
*/
|
|
3314
3363
|
memory_id: z5.string().min(1).nullable().default(null),
|
|
3315
3364
|
/**
|
|
3316
|
-
* Omitted by `retire` / `unretire` / `reclassify`, which change a
|
|
3317
|
-
* standing
|
|
3318
|
-
* the other action rules so the error can
|
|
3319
|
-
* at a field.
|
|
3365
|
+
* Omitted by `retire` / `unretire` / `reclassify` / `unbind`, which change a
|
|
3366
|
+
* memory's standing, its classification or whether it binds — rather than
|
|
3367
|
+
* what it says. Checked below with the other action rules so the error can
|
|
3368
|
+
* name the action instead of pointing at a field.
|
|
3320
3369
|
*/
|
|
3321
3370
|
claim: ProposedClaimSchema.nullable().default(null),
|
|
3322
3371
|
/** What `reclassify` is reclassifying this memory AS. Only that action takes one. */
|
|
@@ -3421,6 +3470,16 @@ function reclassifyInput(entry, kind, opts = {}) {
|
|
|
3421
3470
|
target_path: entry.path
|
|
3422
3471
|
};
|
|
3423
3472
|
}
|
|
3473
|
+
function unbindInput(entry, opts = {}) {
|
|
3474
|
+
return {
|
|
3475
|
+
action: "unbind",
|
|
3476
|
+
memory_id: entry.memory.id,
|
|
3477
|
+
claim: null,
|
|
3478
|
+
reason: opts.reason ?? "no longer binding",
|
|
3479
|
+
source_excerpt: opts.because ?? entry.memory.summary ?? entry.memory.title,
|
|
3480
|
+
target_path: entry.path
|
|
3481
|
+
};
|
|
3482
|
+
}
|
|
3424
3483
|
function saysNothingNew(entry, input, normalize) {
|
|
3425
3484
|
const norm = (v) => normalize(v ?? "").trim();
|
|
3426
3485
|
const claim = input.claim;
|
|
@@ -3510,6 +3569,10 @@ function desiredFields(proposal, existing) {
|
|
|
3510
3569
|
out["kind"] = proposal.proposed_kind;
|
|
3511
3570
|
return out;
|
|
3512
3571
|
}
|
|
3572
|
+
if (isBindingAction(proposal.action)) {
|
|
3573
|
+
delete out["binding"];
|
|
3574
|
+
return out;
|
|
3575
|
+
}
|
|
3513
3576
|
const claim = proposal.claim;
|
|
3514
3577
|
if (claim === null) {
|
|
3515
3578
|
throw new Error(`${proposal.action} reached the patcher with no claim`);
|
|
@@ -3807,6 +3870,46 @@ function verifyPatch(patched, proposal, before) {
|
|
|
3807
3870
|
}
|
|
3808
3871
|
return;
|
|
3809
3872
|
}
|
|
3873
|
+
if (isBindingAction(proposal.action)) {
|
|
3874
|
+
if (mapped.memory.binding === true) {
|
|
3875
|
+
throw new PatchError(
|
|
3876
|
+
"the patched note still reads as binding",
|
|
3877
|
+
"This is a defect in the patcher, not in your note. Nothing was written."
|
|
3878
|
+
);
|
|
3879
|
+
}
|
|
3880
|
+
if (before === void 0) {
|
|
3881
|
+
throw new PatchError(
|
|
3882
|
+
`${proposal.action} cannot be verified without the note it started from`,
|
|
3883
|
+
"This is a defect in the caller, not in your note. Nothing was written."
|
|
3884
|
+
);
|
|
3885
|
+
}
|
|
3886
|
+
const was = mapNoteToMemory({ path: proposal.target.path, content: before, revision: null });
|
|
3887
|
+
if (was.claims[0]?.text !== mapped.claims[0]?.text) {
|
|
3888
|
+
throw new PatchError(
|
|
3889
|
+
`${proposal.action} changed the conclusion, which it must never do`,
|
|
3890
|
+
"Taking a marking back leaves what the note says exactly as it was. Nothing was written."
|
|
3891
|
+
);
|
|
3892
|
+
}
|
|
3893
|
+
if (was.memory.summary !== mapped.memory.summary) {
|
|
3894
|
+
throw new PatchError(
|
|
3895
|
+
`${proposal.action} changed the summary, which it must never do`,
|
|
3896
|
+
"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."
|
|
3897
|
+
);
|
|
3898
|
+
}
|
|
3899
|
+
if (was.memory.title !== mapped.memory.title) {
|
|
3900
|
+
throw new PatchError(
|
|
3901
|
+
`${proposal.action} changed the title, which it must never do`,
|
|
3902
|
+
"Taking a marking back leaves what the note says exactly as it was. Nothing was written."
|
|
3903
|
+
);
|
|
3904
|
+
}
|
|
3905
|
+
if (was.memory.status !== mapped.memory.status) {
|
|
3906
|
+
throw new PatchError(
|
|
3907
|
+
`${proposal.action} changed the status, which it must never do`,
|
|
3908
|
+
"A memory that no longer binds is still current. To take it out of the answers as well, use retire. Nothing was written."
|
|
3909
|
+
);
|
|
3910
|
+
}
|
|
3911
|
+
return;
|
|
3912
|
+
}
|
|
3810
3913
|
const supersedes = new Set(
|
|
3811
3914
|
mapped.relations.filter((r) => r.type === "supersedes").map((r) => r.to)
|
|
3812
3915
|
);
|
|
@@ -4709,7 +4812,7 @@ async function createCloudBrain(token, baseUrl, name) {
|
|
|
4709
4812
|
});
|
|
4710
4813
|
return res.body;
|
|
4711
4814
|
}
|
|
4712
|
-
var call, askedFromField, cloudRecall, cloudOpen, cloudEvidence, cloudRemember, cloudUndo, cloudLog, cloudPutNote, cloudListNotes, cloudRevise, cloudBaseWrite, cloudLifecycle, cloudExclude, RANKED_BY_PROJECT, rankedOnProject, cloudResurface, cloudRules, cloudReindex;
|
|
4815
|
+
var call, askedFromField, cloudRecall, cloudOpen, cloudEvidence, cloudRemember, cloudUndo, cloudLog, cloudPutNote, cloudListNotes, cloudRevise, cloudBaseWrite, cloudLifecycle, cloudUnbind, cloudExclude, RANKED_BY_PROJECT, rankedOnProject, cloudResurface, cloudRules, cloudReindex;
|
|
4713
4816
|
var init_cloud_brain = __esm({
|
|
4714
4817
|
"src/lib/cloud-brain.ts"() {
|
|
4715
4818
|
"use strict";
|
|
@@ -4799,6 +4902,10 @@ var init_cloud_brain = __esm({
|
|
|
4799
4902
|
ref: args.ref,
|
|
4800
4903
|
...args.reason === void 0 ? {} : { reason: args.reason }
|
|
4801
4904
|
});
|
|
4905
|
+
cloudUnbind = (brain, args) => call(brain, "POST", "/unbind", {
|
|
4906
|
+
ref: args.ref,
|
|
4907
|
+
...args.reason === void 0 ? {} : { reason: args.reason }
|
|
4908
|
+
});
|
|
4802
4909
|
cloudExclude = (brain, mode, args) => call(brain, "POST", `/${mode}`, {
|
|
4803
4910
|
path: args.path,
|
|
4804
4911
|
...args.reason === void 0 ? {} : { reason: args.reason }
|
|
@@ -12946,10 +13053,13 @@ function yamlString(value) {
|
|
|
12946
13053
|
|
|
12947
13054
|
// src/commands/revise.ts
|
|
12948
13055
|
function reviseCommand() {
|
|
13056
|
+
const said = { action: false };
|
|
12949
13057
|
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
13058
|
"--valid-from <iso>",
|
|
12951
13059
|
"when this became true (defaults: supersede = now, otherwise unchanged)"
|
|
12952
|
-
).option("--action <kind>", "reinforce | supersede | conflict", "supersede").
|
|
13060
|
+
).option("--action <kind>", "reinforce | supersede | conflict", "supersede").on("option:action", () => {
|
|
13061
|
+
said.action = true;
|
|
13062
|
+
}).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
13063
|
"--never-store-add <words>",
|
|
12954
13064
|
"add words to this note\u2019s boundary, keeping the ones already there"
|
|
12955
13065
|
).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 +13079,24 @@ function reviseCommand() {
|
|
|
12969
13079
|
].join("\n")
|
|
12970
13080
|
).action(async (ref, opts) => {
|
|
12971
13081
|
const added = boundaryAddition(opts);
|
|
13082
|
+
const bareUnbind = opts.rule === false && // `reinforce` is the one action that AGREES with unbinding: it means
|
|
13083
|
+
// "the same conclusion", which is exactly what is being said. A fresh
|
|
13084
|
+
// agent reaches for it unprompted — measured in the Round, the first
|
|
13085
|
+
// session typed `--no-rule --action reinforce`, was refused for
|
|
13086
|
+
// having no claim, and then supplied one, writing a `claim:` key the
|
|
13087
|
+
// note never had. That is the failure this release exists to remove,
|
|
13088
|
+
// reproduced by the guard meant to be careful. `supersede` and
|
|
13089
|
+
// `conflict` still disqualify: both assert something new.
|
|
13090
|
+
(!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
13091
|
const where = await resolveTarget({
|
|
12973
13092
|
...opts.local === void 0 ? {} : { local: opts.local },
|
|
12974
13093
|
...opts.vault === void 0 ? {} : { vault: opts.vault }
|
|
12975
13094
|
});
|
|
12976
13095
|
if (where.kind === "cloud") {
|
|
13096
|
+
if (bareUnbind) {
|
|
13097
|
+
await unbindHosted(where, ref, opts);
|
|
13098
|
+
return;
|
|
13099
|
+
}
|
|
12977
13100
|
await reviseHosted(where, ref, opts, added);
|
|
12978
13101
|
return;
|
|
12979
13102
|
}
|
|
@@ -12985,7 +13108,10 @@ function reviseCommand() {
|
|
|
12985
13108
|
const built = await openIndex(adapter, brain.root);
|
|
12986
13109
|
const entry = resolveRef(built.entries, ref);
|
|
12987
13110
|
const widened = widenedBoundary(entry.memory.never_store, added, ref);
|
|
12988
|
-
const input =
|
|
13111
|
+
const input = bareUnbind ? unbindInput(entry, {
|
|
13112
|
+
...opts.reason === void 0 ? {} : { reason: opts.reason },
|
|
13113
|
+
...opts.because === void 0 ? {} : { because: opts.because }
|
|
13114
|
+
}) : await buildCommitInput(entry, opts, widened);
|
|
12989
13115
|
const rationale = sanitizeRationale(
|
|
12990
13116
|
input.reason === NO_REASON_GIVEN ? null : input.reason ?? null
|
|
12991
13117
|
);
|
|
@@ -13177,6 +13303,30 @@ Undo: \`${undoHint(applied.record.op_id, opts)}\``));
|
|
|
13177
13303
|
});
|
|
13178
13304
|
});
|
|
13179
13305
|
}
|
|
13306
|
+
async function unbindHosted(where, ref, opts) {
|
|
13307
|
+
if (opts.dryRun === true) {
|
|
13308
|
+
throw memgError(
|
|
13309
|
+
"invalid_input",
|
|
13310
|
+
"--dry-run is not supported for `--no-rule` on a hosted brain",
|
|
13311
|
+
"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."
|
|
13312
|
+
);
|
|
13313
|
+
}
|
|
13314
|
+
const brain = await openCloudBrain(where);
|
|
13315
|
+
const result = await cloudUnbind(brain, {
|
|
13316
|
+
ref,
|
|
13317
|
+
...opts.reason === void 0 ? {} : { reason: opts.reason }
|
|
13318
|
+
});
|
|
13319
|
+
printDual({
|
|
13320
|
+
json: result,
|
|
13321
|
+
human: () => {
|
|
13322
|
+
printHuman(`Unbound \`${oneLine(result.target, WIDTH.path)}\`.
|
|
13323
|
+
`);
|
|
13324
|
+
printHuman(c.gray(oneLine(result.note, WIDTH.text)));
|
|
13325
|
+
printHuman(c.gray(`
|
|
13326
|
+
\`memgineering undo ${oneLine(result.op_id, WIDTH.handle)}\``));
|
|
13327
|
+
}
|
|
13328
|
+
});
|
|
13329
|
+
}
|
|
13180
13330
|
async function reviseHosted(where, ref, opts, added) {
|
|
13181
13331
|
if (opts.input) {
|
|
13182
13332
|
throw memgError(
|
|
@@ -13875,6 +14025,10 @@ function consolidateCommand() {
|
|
|
13875
14025
|
" rule_demotion a note marked as binding but classified as a fact, a",
|
|
13876
14026
|
" procedure, an episode or a goal \u2014 the author said it is",
|
|
13877
14027
|
" not a decision, so the MARKING is the field to doubt",
|
|
14028
|
+
" binding_unclassified a note marked as binding with no kind at all. Two",
|
|
14029
|
+
" fields, agreeing on nothing: it is either a decision",
|
|
14030
|
+
" nobody classified or something that was never a rule,",
|
|
14031
|
+
" and only somebody who reads it can say which",
|
|
13878
14032
|
"",
|
|
13879
14033
|
"Nothing here becomes a proposal on its own. Read a candidate, decide what it",
|
|
13880
14034
|
"means, then say so:",
|
|
@@ -13888,11 +14042,13 @@ function consolidateCommand() {
|
|
|
13888
14042
|
limit === void 0 ? {} : { limit }
|
|
13889
14043
|
) : await detectLocally(where.brain.root, limit);
|
|
13890
14044
|
const { candidates, truncated } = report3;
|
|
14045
|
+
const byRule = Object.entries(report3.by_rule ?? {}).sort((a, b) => b[1] - a[1]);
|
|
13891
14046
|
printDual({
|
|
13892
14047
|
json: {
|
|
13893
14048
|
candidates,
|
|
13894
14049
|
count: candidates.length,
|
|
13895
14050
|
scanned: report3.scanned,
|
|
14051
|
+
by_rule: report3.by_rule ?? {},
|
|
13896
14052
|
// Reported rather than left to be inferred from a short list. A run
|
|
13897
14053
|
// that stopped early and said nothing would be indistinguishable from
|
|
13898
14054
|
// a brain with little to find, which is the one thing the reader
|
|
@@ -13916,6 +14072,19 @@ function consolidateCommand() {
|
|
|
13916
14072
|
printHuman(`## ${candidates.length} worth a look
|
|
13917
14073
|
`);
|
|
13918
14074
|
for (const candidate of candidates) printHuman(renderCandidate(candidate));
|
|
14075
|
+
const held = byRule.filter(
|
|
14076
|
+
([rule, total]) => total > candidates.filter((c2) => c2.rule === rule).length
|
|
14077
|
+
);
|
|
14078
|
+
if (held.length > 0) {
|
|
14079
|
+
printHuman(
|
|
14080
|
+
c.gray(
|
|
14081
|
+
`
|
|
14082
|
+
${held.map(([rule, total]) => `${rule}: ${total}`).join(" \xB7 ")}
|
|
14083
|
+
Found in total \u2014 the list above shows a few of each, so one noisy signal
|
|
14084
|
+
cannot bury the rest. \`--limit\` raises the page.`
|
|
14085
|
+
)
|
|
14086
|
+
);
|
|
14087
|
+
}
|
|
13919
14088
|
if (truncated) {
|
|
13920
14089
|
printHuman(
|
|
13921
14090
|
c.yellow(`
|
|
@@ -13962,7 +14131,16 @@ function renderCandidate(candidate) {
|
|
|
13962
14131
|
// a second door to the same field would give a brain two ways to record one
|
|
13963
14132
|
// change. So the suggestion names the command rather than a verb this CLI
|
|
13964
14133
|
// does not have.
|
|
13965
|
-
candidate.suggested_action === "unmark_rule" ? c.gray(` suggested: memgineering revise ${candidate.memory_id} --no-rule --reason "\u2026"`) :
|
|
14134
|
+
candidate.suggested_action === "unmark_rule" ? c.gray(` suggested: memgineering revise ${candidate.memory_id} --no-rule --reason "\u2026"`) : (
|
|
14135
|
+
// BOTH moves, because the tool genuinely cannot tell. It used to print
|
|
14136
|
+
// one — "promote this to a decision" — for every unclassified rule,
|
|
14137
|
+
// including the thirteen on a real brain that were not rules at all.
|
|
14138
|
+
candidate.suggested_action === "decide" ? c.gray(
|
|
14139
|
+
` suggested: read it, then one of
|
|
14140
|
+
memgineering propose reclassify --memory ${candidate.memory_id} --kind decision --reason "\u2026" --excerpt "\u2026"
|
|
14141
|
+
memgineering revise ${candidate.memory_id} --no-rule --reason "\u2026"`
|
|
14142
|
+
) : candidate.suggested_kind === null ? c.gray(` suggested: ${candidate.suggested_action}`) : c.gray(` suggested: ${candidate.suggested_action} --kind ${candidate.suggested_kind}`)
|
|
14143
|
+
)
|
|
13966
14144
|
);
|
|
13967
14145
|
return lines2.join("\n");
|
|
13968
14146
|
}
|
|
@@ -21268,16 +21446,27 @@ function rulesCommand() {
|
|
|
21268
21446
|
});
|
|
21269
21447
|
if (where.kind === "cloud") {
|
|
21270
21448
|
const project2 = await projectOf().then((p) => p.name).catch(() => null);
|
|
21271
|
-
const {
|
|
21272
|
-
|
|
21273
|
-
|
|
21449
|
+
const {
|
|
21450
|
+
rules: rules2,
|
|
21451
|
+
total: total2,
|
|
21452
|
+
eligible,
|
|
21453
|
+
elsewhere: elsewhere2 = []
|
|
21454
|
+
} = await cloudRules(await openCloudBrain(where), { project: project2 });
|
|
21274
21455
|
const shownOf = eligible ?? rules2.length;
|
|
21275
21456
|
printDual({
|
|
21276
|
-
json: {
|
|
21457
|
+
json: {
|
|
21458
|
+
brain: where.name,
|
|
21459
|
+
hosted: true,
|
|
21460
|
+
rules: rules2,
|
|
21461
|
+
elsewhere: elsewhere2,
|
|
21462
|
+
total: total2,
|
|
21463
|
+
project: project2,
|
|
21464
|
+
eligible: shownOf
|
|
21465
|
+
},
|
|
21277
21466
|
human: () => {
|
|
21278
21467
|
printHuman(`## rules \u2014 ${oneLine(where.name, WIDTH.title)} (${total2})
|
|
21279
21468
|
`);
|
|
21280
|
-
if (rules2.length === 0) {
|
|
21469
|
+
if (rules2.length === 0 && elsewhere2.length === 0) {
|
|
21281
21470
|
printHuman("_Nothing is marked as a rule in this brain._\n");
|
|
21282
21471
|
printHuman(
|
|
21283
21472
|
c.gray(
|
|
@@ -21299,12 +21488,21 @@ function rulesCommand() {
|
|
|
21299
21488
|
printHuman(c.gray(`
|
|
21300
21489
|
${shownOf - rules2.length} more not shown.`));
|
|
21301
21490
|
}
|
|
21302
|
-
if (
|
|
21491
|
+
if (elsewhere2.length > 0) {
|
|
21492
|
+
printHuman(`
|
|
21493
|
+
### scoped to other projects (${elsewhere2.length})
|
|
21494
|
+
`);
|
|
21303
21495
|
printHuman(
|
|
21304
|
-
c.gray(
|
|
21305
|
-
`${total2 - shownOf} more belong to other projects and are not shown here \u2014 they bind there.`
|
|
21306
|
-
)
|
|
21496
|
+
c.gray("Not shown to an agent working here. They bind where they say.\n")
|
|
21307
21497
|
);
|
|
21498
|
+
for (const rule of elsewhere2) {
|
|
21499
|
+
printHuman(`- ${oneLine(rule.summary ?? rule.title, WIDTH.text)}`);
|
|
21500
|
+
printHuman(
|
|
21501
|
+
c.gray(
|
|
21502
|
+
` \`open: ${oneLine(rule.handle, WIDTH.handle)}\` \xB7 ${oneLine(rule.scope ?? "", WIDTH.title)}`
|
|
21503
|
+
)
|
|
21504
|
+
);
|
|
21505
|
+
}
|
|
21308
21506
|
}
|
|
21309
21507
|
printHuman(
|
|
21310
21508
|
"\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