memgineering 0.21.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 +89 -0
- package/assets/MEMGINEERING.md +1 -1
- package/assets/memgineering-rules/SKILL.md +33 -14
- package/assets/memgineering-writing/SKILL.md +47 -8
- package/dist/index.js +461 -76
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,95 @@ 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
|
+
|
|
58
|
+
## [0.22.0] — 2026-09-09
|
|
59
|
+
|
|
60
|
+
### Added
|
|
61
|
+
|
|
62
|
+
- **`consolidate` now asks whether a rule is a rule.** It already proposed
|
|
63
|
+
raising a binding memory to a decision; it never proposed the other
|
|
64
|
+
direction. A memory marked as binding but classified as a fact or a
|
|
65
|
+
procedure is now surfaced as a candidate to stop binding — audited against a
|
|
66
|
+
real brain, 13 of 54 standing rules were a fact, a status, or a
|
|
67
|
+
troubleshooting procedure rather than anything an agent should obey.
|
|
68
|
+
|
|
69
|
+
### Changed
|
|
70
|
+
|
|
71
|
+
- **`rules` and the before-edit hook now fill two budgets, not one list.**
|
|
72
|
+
Rules that hold everywhere and rules that hold only in this project are drawn
|
|
73
|
+
separately and labelled, so a project's own rules can no longer be pushed out
|
|
74
|
+
by more-used general ones. Measured before the change on a brain with 54
|
|
75
|
+
rules: not one project-scoped rule reached the hook in its own project.
|
|
76
|
+
- **A rule write has to say what it binds.** `remember --rule` now records a
|
|
77
|
+
scope. The CLI fills it from the project you are in, so a rule written at a
|
|
78
|
+
terminal is unaffected; a caller that sends neither a scope nor a project —
|
|
79
|
+
an MCP tool call, a direct API write — is refused and told to send the
|
|
80
|
+
project name, or `global` for a rule that must hold everywhere. `global` is
|
|
81
|
+
stored as no scope at all, which is what such a rule already looked like.
|
|
82
|
+
Ordinary `remember` is unchanged: an observation with no scope is a normal
|
|
83
|
+
observation.
|
|
84
|
+
|
|
85
|
+
### Fixed
|
|
86
|
+
|
|
87
|
+
- **The before-edit hook no longer pays for the network on every tool call.**
|
|
88
|
+
It claims the session's turn before asking the server rather than after, so
|
|
89
|
+
the second and later calls in a session return without a round-trip. It
|
|
90
|
+
spoke once per session already; it now costs once per session too.
|
|
91
|
+
- **`init` scaffolds the folder `remember` actually writes to.** New brains
|
|
92
|
+
were laid out with `20_TIMELINE/observations/` while writes went to
|
|
93
|
+
`_observations/`, leaving the scaffolded folder empty and a second one beside
|
|
94
|
+
it. Existing notes are not moved.
|
|
95
|
+
|
|
96
|
+
### Agent guidance
|
|
97
|
+
|
|
98
|
+
The bundled hub and sub-skills changed: the hub now hands the install job to
|
|
99
|
+
`memgineering registry pull` outright instead of describing it, and the rules
|
|
100
|
+
skill covers the two budgets and the scope a rule now carries. Restart your
|
|
101
|
+
agent session after upgrading so it re-reads them.
|
|
102
|
+
|
|
14
103
|
## [0.21.0] — 2026-09-08
|
|
15
104
|
|
|
16
105
|
### 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
|
|
@@ -85,12 +85,23 @@ alone earned — not that the top card is a rule, and not that it never could be
|
|
|
85
85
|
since the engine noticing a note is decision-shaped is weaker evidence than its
|
|
86
86
|
author saying it binds.
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
88
|
+
**Rules arrive in two budgets, not one list.** What a session is shown is the
|
|
89
|
+
strongest few that hold everywhere, plus the strongest few scoped to the project
|
|
90
|
+
it is standing in — ten in total, and each half gives its unused room to the
|
|
91
|
+
other, so a brain with nothing scoped still fills the list. Rules scoped to a
|
|
92
|
+
DIFFERENT project are not shown to an agent working here at all; they bind
|
|
93
|
+
there. `memgineering rules` lists them separately under "scoped to other
|
|
94
|
+
projects", which is the one place to notice a scope with a typo in it — such a
|
|
95
|
+
rule binds nowhere and would otherwise appear nowhere.
|
|
96
|
+
|
|
97
|
+
Before the split there was one list ranked by use, and it did not work: measured
|
|
98
|
+
in a real repository, every slot went to rules about other things and a rule
|
|
99
|
+
written that morning about that project's own layout ranked 49th of 54.
|
|
100
|
+
|
|
101
|
+
The before-edit hook and `memgineering rules` read the same brain — the hosted
|
|
102
|
+
one when this machine is pointed at it, the folder on disk otherwise — so what
|
|
103
|
+
the hook will deliver is what the command shows. Add `--local` to read the
|
|
104
|
+
folder on this disk specifically.
|
|
94
105
|
|
|
95
106
|
## What the user's agent actually sees
|
|
96
107
|
|
|
@@ -135,11 +146,19 @@ a long list is fewer rules, not a longer one.
|
|
|
135
146
|
|
|
136
147
|
Two consequences worth knowing before you mark things:
|
|
137
148
|
|
|
138
|
-
- **Global habits crowd out project decisions
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
-
|
|
144
|
-
|
|
145
|
-
|
|
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.
|
|
@@ -33,14 +33,34 @@ memgineering remember "we always squash before merging" --rule --no-scope \
|
|
|
33
33
|
--reason "they said from now on, about every repo — not this one"
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
- `--no-scope` for
|
|
37
|
-
rule
|
|
38
|
-
|
|
36
|
+
- `--no-scope` for a decision that genuinely holds in every project — a
|
|
37
|
+
security rule, a way of writing commits, something about the person rather
|
|
38
|
+
than the work. It is a real answer, not the safe one: use it when it is true.
|
|
39
39
|
- `--scope <name>` when the note is ABOUT a project you are not standing in.
|
|
40
40
|
|
|
41
|
+
**Prefer the project scope for a rule that is about one project.** `--no-scope`
|
|
42
|
+
used to be the advice for rules in general, on the reasoning that a scoped rule
|
|
43
|
+
drops out of other projects' answers — it does, and that is now the point. Rules
|
|
44
|
+
reach a session in two budgets, one for what holds everywhere and one for the
|
|
45
|
+
project in front of it, so a scoped rule is no longer competing with every other
|
|
46
|
+
rule in the brain for the same few slots. Measured before the split: in a real
|
|
47
|
+
repository all the slots went to rules about other things, and a rule written
|
|
48
|
+
that morning about that project's own layout ranked 49th of 54 — present,
|
|
49
|
+
enforced by nothing, invisible to the session it existed to stop.
|
|
50
|
+
|
|
41
51
|
`--scope` and `--no-scope` together are refused — they answer the same
|
|
42
52
|
question both ways.
|
|
43
53
|
|
|
54
|
+
**A rule has to say what it binds, and one of the two is required.** A `--rule`
|
|
55
|
+
write is refused when nothing names a scope and nothing can be derived — a
|
|
56
|
+
folder that is not a repository and has no bound project. Add `--scope "<the
|
|
57
|
+
project>"` or `--no-scope`, and retry; nothing was written. On the hosted API
|
|
58
|
+
(`brain_remember`, or `POST /remember` directly) the same refusal applies with
|
|
59
|
+
no derivation at all: send `scope`, or `"scope": "global"` for one that really
|
|
60
|
+
does hold everywhere. `global` is reserved and is stored as no scope, which is
|
|
61
|
+
what a machine-wide rule already looks like — so there is only ever one
|
|
62
|
+
spelling of it to search for.
|
|
63
|
+
|
|
44
64
|
**`--reason` on every write.** It goes in the ledger and is the only part of the
|
|
45
65
|
record that still means anything six months later. Every write verb takes it:
|
|
46
66
|
`remember`, `revise`, `retire`, `exclude`, `undo`. The one on `undo` matters
|
|
@@ -215,11 +235,30 @@ it is about without opening them. It does not carry their claims, so deciding
|
|
|
215
235
|
which of a pair is right still means reading them: two notes can share a title
|
|
216
236
|
and hold opposite conclusions, which is exactly the case a proposal exists for.
|
|
217
237
|
It returns them with the evidence that caught them and the action it would
|
|
218
|
-
suggest, under
|
|
219
|
-
`contradiction`, a disagreement nobody settled; `semantic_similarity`,
|
|
220
|
-
meaning agreeing reciprocally; `shared_context`, one scope naming the
|
|
221
|
-
thing; `similar_title`; and
|
|
222
|
-
|
|
238
|
+
suggest, under eight rules: `declared_supersession`, a replaced note still
|
|
239
|
+
active; `contradiction`, a disagreement nobody settled; `semantic_similarity`,
|
|
240
|
+
indexed meaning agreeing reciprocally; `shared_context`, one scope naming the
|
|
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
|
|
223
262
|
vector generation and never calls a model during consolidation. Authored
|
|
224
263
|
scope/entity context permits reciprocal neighbours; without it, evidence must
|
|
225
264
|
form a stricter three-note semantic triangle.
|
package/dist/index.js
CHANGED
|
@@ -277,13 +277,17 @@ var init_errors = __esm({
|
|
|
277
277
|
|
|
278
278
|
// ../../packages/memory-engine/src/schema.ts
|
|
279
279
|
import { z } from "zod";
|
|
280
|
-
|
|
280
|
+
function isGlobalScope(scope) {
|
|
281
|
+
return scope != null && scope.trim().toLowerCase() === GLOBAL_SCOPE;
|
|
282
|
+
}
|
|
283
|
+
var MappingModeSchema, ScopeSourceSchema, SCOPE_LIMIT, GLOBAL_SCOPE, TELEMETRY_ERROR_KINDS, TELEMETRY_MESSAGE_LIMIT, TelemetryErrorPayloadSchema, ProvenanceSchema, MemoryStatusSchema, ClaimConfidenceSchema, MemoryKindSchema, ClaimSchema, MemorySchema, RelationTypeSchema, RelationSourceSchema, RelationSchema;
|
|
281
284
|
var init_schema = __esm({
|
|
282
285
|
"../../packages/memory-engine/src/schema.ts"() {
|
|
283
286
|
"use strict";
|
|
284
287
|
MappingModeSchema = z.enum(["explicit", "heuristic"]);
|
|
285
288
|
ScopeSourceSchema = z.enum(["explicit", "path", "none"]);
|
|
286
289
|
SCOPE_LIMIT = 128;
|
|
290
|
+
GLOBAL_SCOPE = "global";
|
|
287
291
|
TELEMETRY_ERROR_KINDS = [
|
|
288
292
|
"network_unreachable",
|
|
289
293
|
"server_error",
|
|
@@ -444,6 +448,55 @@ var init_schema = __esm({
|
|
|
444
448
|
}
|
|
445
449
|
});
|
|
446
450
|
|
|
451
|
+
// ../../packages/memory-engine/src/layout.ts
|
|
452
|
+
var OBSERVATIONS_DIR;
|
|
453
|
+
var init_layout = __esm({
|
|
454
|
+
"../../packages/memory-engine/src/layout.ts"() {
|
|
455
|
+
"use strict";
|
|
456
|
+
OBSERVATIONS_DIR = "_observations";
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// ../../packages/memory-engine/src/rule-budget.ts
|
|
461
|
+
function ruleLane(scope, project) {
|
|
462
|
+
if (scope == null || scope.trim() === "" || isGlobalScope(scope)) return "global";
|
|
463
|
+
if (project === null) return "elsewhere";
|
|
464
|
+
return scope.trim().toLowerCase() === project.trim().toLowerCase() ? "project" : "elsewhere";
|
|
465
|
+
}
|
|
466
|
+
function declaredScopeOf(memory) {
|
|
467
|
+
return memory.provenance.scopeSource === "explicit" ? memory.scope : null;
|
|
468
|
+
}
|
|
469
|
+
function splitRuleBudget(ranked, scopeOf, project, budget = DEFAULT_RULE_BUDGET) {
|
|
470
|
+
const globalLane = [];
|
|
471
|
+
const projectLane = [];
|
|
472
|
+
const elsewhere = [];
|
|
473
|
+
for (const item of ranked) {
|
|
474
|
+
const lane = ruleLane(scopeOf(item), project);
|
|
475
|
+
if (lane === "global") globalLane.push(item);
|
|
476
|
+
else if (lane === "project") projectLane.push(item);
|
|
477
|
+
else elsewhere.push(item);
|
|
478
|
+
}
|
|
479
|
+
const total = budget.global + budget.project;
|
|
480
|
+
const globalRoom = Math.max(budget.global, total - Math.min(projectLane.length, budget.project));
|
|
481
|
+
const projectRoom = Math.max(budget.project, total - Math.min(globalLane.length, budget.global));
|
|
482
|
+
return {
|
|
483
|
+
global: globalLane.slice(0, globalRoom),
|
|
484
|
+
project: projectLane.slice(0, projectRoom),
|
|
485
|
+
elsewhere,
|
|
486
|
+
// Everything either budget COULD have shown, so "5 of 12" is honest about
|
|
487
|
+
// the cap without counting another project's rules as things withheld.
|
|
488
|
+
eligible: globalLane.length + projectLane.length
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
var DEFAULT_RULE_BUDGET;
|
|
492
|
+
var init_rule_budget = __esm({
|
|
493
|
+
"../../packages/memory-engine/src/rule-budget.ts"() {
|
|
494
|
+
"use strict";
|
|
495
|
+
init_schema();
|
|
496
|
+
DEFAULT_RULE_BUDGET = { global: 5, project: 5 };
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
|
|
447
500
|
// ../../packages/memory-engine/src/boundary.ts
|
|
448
501
|
function normalizeForMatch(value) {
|
|
449
502
|
return value.normalize("NFC").replace(ZERO_WIDTH, "").replace(WHITESPACE_RUN, " ").trim();
|
|
@@ -2182,8 +2235,11 @@ function isLifecycleAction(action) {
|
|
|
2182
2235
|
function isClassifyAction(action) {
|
|
2183
2236
|
return action === "reclassify";
|
|
2184
2237
|
}
|
|
2238
|
+
function isBindingAction(action) {
|
|
2239
|
+
return action === "unbind";
|
|
2240
|
+
}
|
|
2185
2241
|
function carriesNoClaim(action) {
|
|
2186
|
-
return isLifecycleAction(action) || isClassifyAction(action);
|
|
2242
|
+
return isLifecycleAction(action) || isClassifyAction(action) || isBindingAction(action);
|
|
2187
2243
|
}
|
|
2188
2244
|
function isAccepted(value) {
|
|
2189
2245
|
return typeof value === "object" && value !== null && value[ACCEPTED] === true;
|
|
@@ -2212,6 +2268,21 @@ var init_schema2 = __esm({
|
|
|
2212
2268
|
"retire",
|
|
2213
2269
|
"unretire",
|
|
2214
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",
|
|
2215
2286
|
"ignore"
|
|
2216
2287
|
]);
|
|
2217
2288
|
StorableActionSchema = ProposalActionSchema.exclude(["ignore"]);
|
|
@@ -2407,7 +2478,7 @@ var init_schema2 = __esm({
|
|
|
2407
2478
|
path: ["source_excerpt"]
|
|
2408
2479
|
}
|
|
2409
2480
|
).refine((p) => carriesNoClaim(p.action) || p.claim !== null, {
|
|
2410
|
-
message: "only retire, unretire and
|
|
2481
|
+
message: "only retire, unretire, reclassify and unbind may omit the claim",
|
|
2411
2482
|
path: ["claim"]
|
|
2412
2483
|
}).refine((p) => isClassifyAction(p.action) === (p.proposed_kind !== null), {
|
|
2413
2484
|
message: "proposed_kind belongs to reclassify, and reclassify requires one",
|
|
@@ -2443,12 +2514,42 @@ function detectConsolidation(entries, opts = {}) {
|
|
|
2443
2514
|
...titles
|
|
2444
2515
|
];
|
|
2445
2516
|
const out = rankCandidates(scored);
|
|
2517
|
+
const limit = opts.limit ?? DEFAULT_LIMIT;
|
|
2446
2518
|
return {
|
|
2447
|
-
candidates: out
|
|
2519
|
+
candidates: shareTheList(out, limit),
|
|
2520
|
+
by_rule: countByRule(out),
|
|
2448
2521
|
scanned: budget.scanned,
|
|
2449
2522
|
truncated: budget.truncated
|
|
2450
2523
|
};
|
|
2451
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
|
+
}
|
|
2452
2553
|
function titleTerms(title) {
|
|
2453
2554
|
const out = /* @__PURE__ */ new Set();
|
|
2454
2555
|
for (const term of terms(title)) {
|
|
@@ -2796,19 +2897,38 @@ function kindPromotions(entries) {
|
|
|
2796
2897
|
const out = [];
|
|
2797
2898
|
for (const entry of entries) {
|
|
2798
2899
|
if (entry.memory.binding !== true) continue;
|
|
2799
|
-
|
|
2900
|
+
const kind = entry.memory.kind;
|
|
2901
|
+
if (kind !== void 0 && RULE_SHAPED_KINDS.has(kind)) continue;
|
|
2902
|
+
if (kind === void 0) {
|
|
2903
|
+
out.push({
|
|
2904
|
+
candidate: {
|
|
2905
|
+
rule: "binding_unclassified",
|
|
2906
|
+
memory_id: entry.memory.id,
|
|
2907
|
+
path: entry.path,
|
|
2908
|
+
title: entry.memory.title,
|
|
2909
|
+
other_id: null,
|
|
2910
|
+
other_path: null,
|
|
2911
|
+
other_title: null,
|
|
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
|
|
2915
|
+
},
|
|
2916
|
+
confidence: 4
|
|
2917
|
+
});
|
|
2918
|
+
continue;
|
|
2919
|
+
}
|
|
2800
2920
|
out.push({
|
|
2801
2921
|
candidate: {
|
|
2802
|
-
rule: "
|
|
2922
|
+
rule: "rule_demotion",
|
|
2803
2923
|
memory_id: entry.memory.id,
|
|
2804
2924
|
path: entry.path,
|
|
2805
2925
|
title: entry.memory.title,
|
|
2806
2926
|
other_id: null,
|
|
2807
2927
|
other_path: null,
|
|
2808
2928
|
other_title: null,
|
|
2809
|
-
evidence:
|
|
2810
|
-
suggested_action: "
|
|
2811
|
-
suggested_kind:
|
|
2929
|
+
evidence: `marked as binding but classified as ${kind} \u2014 the author said it is not a decision, so it may not be a rule`,
|
|
2930
|
+
suggested_action: "unmark_rule",
|
|
2931
|
+
suggested_kind: null
|
|
2812
2932
|
},
|
|
2813
2933
|
confidence: 4
|
|
2814
2934
|
});
|
|
@@ -2915,7 +3035,7 @@ function* pairsFrom(buckets, budget) {
|
|
|
2915
3035
|
}
|
|
2916
3036
|
}
|
|
2917
3037
|
}
|
|
2918
|
-
var CONSOLIDATION_RULES, DEFAULT_LIMIT, DEFAULT_PAIR_BUDGET, SEMANTIC_PAIR_BUDGET, TITLE_OVERLAP, SAME_SCOPE_SEMANTIC_SIMILARITY, SHARED_ENTITY_SEMANTIC_SIMILARITY, GLOBAL_SEMANTIC_SIMILARITY, RECIPROCAL_NEIGHBOURS, CandidateComponents;
|
|
3038
|
+
var CONSOLIDATION_RULES, DEFAULT_LIMIT, DEFAULT_PAIR_BUDGET, SEMANTIC_PAIR_BUDGET, TITLE_OVERLAP, SAME_SCOPE_SEMANTIC_SIMILARITY, SHARED_ENTITY_SEMANTIC_SIMILARITY, GLOBAL_SEMANTIC_SIMILARITY, RECIPROCAL_NEIGHBOURS, RULE_SHAPED_KINDS, CandidateComponents;
|
|
2919
3039
|
var init_detect = __esm({
|
|
2920
3040
|
"../../packages/memory-engine/src/proposal/detect.ts"() {
|
|
2921
3041
|
"use strict";
|
|
@@ -2926,7 +3046,9 @@ var init_detect = __esm({
|
|
|
2926
3046
|
"semantic_similarity",
|
|
2927
3047
|
"shared_context",
|
|
2928
3048
|
"similar_title",
|
|
2929
|
-
"kind_promotion"
|
|
3049
|
+
"kind_promotion",
|
|
3050
|
+
"rule_demotion",
|
|
3051
|
+
"binding_unclassified"
|
|
2930
3052
|
];
|
|
2931
3053
|
DEFAULT_LIMIT = 50;
|
|
2932
3054
|
DEFAULT_PAIR_BUDGET = 2e5;
|
|
@@ -2936,6 +3058,7 @@ var init_detect = __esm({
|
|
|
2936
3058
|
SHARED_ENTITY_SEMANTIC_SIMILARITY = 0.75;
|
|
2937
3059
|
GLOBAL_SEMANTIC_SIMILARITY = 0.72;
|
|
2938
3060
|
RECIPROCAL_NEIGHBOURS = 2;
|
|
3061
|
+
RULE_SHAPED_KINDS = /* @__PURE__ */ new Set(["decision", "preference"]);
|
|
2939
3062
|
CandidateComponents = class {
|
|
2940
3063
|
parent = /* @__PURE__ */ new Map();
|
|
2941
3064
|
root(id) {
|
|
@@ -3178,12 +3301,12 @@ function commit(prepared, input, opts = {}) {
|
|
|
3178
3301
|
if (carriesNoClaim(parsed.action)) {
|
|
3179
3302
|
if (parsed.claim !== null) {
|
|
3180
3303
|
throw Object.assign(new Error(`${parsed.action} does not take a claim`), {
|
|
3181
|
-
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.`
|
|
3182
3305
|
});
|
|
3183
3306
|
}
|
|
3184
3307
|
} else if (parsed.claim === null) {
|
|
3185
3308
|
throw Object.assign(new Error(`${parsed.action} needs a claim`), {
|
|
3186
|
-
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."
|
|
3187
3310
|
});
|
|
3188
3311
|
} else if ((parsed.action === "supersede" || parsed.action === "create") && parsed.claim.summary === null) {
|
|
3189
3312
|
throw Object.assign(new Error(`${parsed.action} needs a summary as well as a claim`), {
|
|
@@ -3239,10 +3362,10 @@ var init_commit = __esm({
|
|
|
3239
3362
|
*/
|
|
3240
3363
|
memory_id: z5.string().min(1).nullable().default(null),
|
|
3241
3364
|
/**
|
|
3242
|
-
* Omitted by `retire` / `unretire` / `reclassify`, which change a
|
|
3243
|
-
* standing
|
|
3244
|
-
* the other action rules so the error can
|
|
3245
|
-
* 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.
|
|
3246
3369
|
*/
|
|
3247
3370
|
claim: ProposedClaimSchema.nullable().default(null),
|
|
3248
3371
|
/** What `reclassify` is reclassifying this memory AS. Only that action takes one. */
|
|
@@ -3347,6 +3470,16 @@ function reclassifyInput(entry, kind, opts = {}) {
|
|
|
3347
3470
|
target_path: entry.path
|
|
3348
3471
|
};
|
|
3349
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
|
+
}
|
|
3350
3483
|
function saysNothingNew(entry, input, normalize) {
|
|
3351
3484
|
const norm = (v) => normalize(v ?? "").trim();
|
|
3352
3485
|
const claim = input.claim;
|
|
@@ -3436,6 +3569,10 @@ function desiredFields(proposal, existing) {
|
|
|
3436
3569
|
out["kind"] = proposal.proposed_kind;
|
|
3437
3570
|
return out;
|
|
3438
3571
|
}
|
|
3572
|
+
if (isBindingAction(proposal.action)) {
|
|
3573
|
+
delete out["binding"];
|
|
3574
|
+
return out;
|
|
3575
|
+
}
|
|
3439
3576
|
const claim = proposal.claim;
|
|
3440
3577
|
if (claim === null) {
|
|
3441
3578
|
throw new Error(`${proposal.action} reached the patcher with no claim`);
|
|
@@ -3733,6 +3870,46 @@ function verifyPatch(patched, proposal, before) {
|
|
|
3733
3870
|
}
|
|
3734
3871
|
return;
|
|
3735
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
|
+
}
|
|
3736
3913
|
const supersedes = new Set(
|
|
3737
3914
|
mapped.relations.filter((r) => r.type === "supersedes").map((r) => r.to)
|
|
3738
3915
|
);
|
|
@@ -4051,6 +4228,8 @@ var init_src = __esm({
|
|
|
4051
4228
|
"../../packages/memory-engine/src/index.ts"() {
|
|
4052
4229
|
"use strict";
|
|
4053
4230
|
init_schema();
|
|
4231
|
+
init_layout();
|
|
4232
|
+
init_rule_budget();
|
|
4054
4233
|
init_boundary();
|
|
4055
4234
|
init_markdown_mapping();
|
|
4056
4235
|
init_build();
|
|
@@ -4482,6 +4661,7 @@ var BrainSchema, ConfigSchema, EMPTY_CONFIG;
|
|
|
4482
4661
|
var init_config = __esm({
|
|
4483
4662
|
"src/lib/config.ts"() {
|
|
4484
4663
|
"use strict";
|
|
4664
|
+
init_src();
|
|
4485
4665
|
init_atomic();
|
|
4486
4666
|
init_brand();
|
|
4487
4667
|
BrainSchema = z8.object({
|
|
@@ -4495,8 +4675,11 @@ var init_config = __esm({
|
|
|
4495
4675
|
* place for observations in its layout, while a folder someone has kept
|
|
4496
4676
|
* notes in for years does not — and picking one for them is how a tool
|
|
4497
4677
|
* starts rearranging a vault it promised only to read.
|
|
4678
|
+
*
|
|
4679
|
+
* The DEFAULT is shared with everything else that writes an observation, and
|
|
4680
|
+
* is not a second opinion about where they go — see `OBSERVATIONS_DIR`.
|
|
4498
4681
|
*/
|
|
4499
|
-
observationsDir: z8.string().min(1).default(
|
|
4682
|
+
observationsDir: z8.string().min(1).default(OBSERVATIONS_DIR)
|
|
4500
4683
|
});
|
|
4501
4684
|
ConfigSchema = z8.object({
|
|
4502
4685
|
brain: z8.object({
|
|
@@ -4629,7 +4812,7 @@ async function createCloudBrain(token, baseUrl, name) {
|
|
|
4629
4812
|
});
|
|
4630
4813
|
return res.body;
|
|
4631
4814
|
}
|
|
4632
|
-
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;
|
|
4633
4816
|
var init_cloud_brain = __esm({
|
|
4634
4817
|
"src/lib/cloud-brain.ts"() {
|
|
4635
4818
|
"use strict";
|
|
@@ -4719,6 +4902,10 @@ var init_cloud_brain = __esm({
|
|
|
4719
4902
|
ref: args.ref,
|
|
4720
4903
|
...args.reason === void 0 ? {} : { reason: args.reason }
|
|
4721
4904
|
});
|
|
4905
|
+
cloudUnbind = (brain, args) => call(brain, "POST", "/unbind", {
|
|
4906
|
+
ref: args.ref,
|
|
4907
|
+
...args.reason === void 0 ? {} : { reason: args.reason }
|
|
4908
|
+
});
|
|
4722
4909
|
cloudExclude = (brain, mode, args) => call(brain, "POST", `/${mode}`, {
|
|
4723
4910
|
path: args.path,
|
|
4724
4911
|
...args.reason === void 0 ? {} : { reason: args.reason }
|
|
@@ -4730,7 +4917,12 @@ var init_cloud_brain = __esm({
|
|
|
4730
4917
|
...askedFromField(args.askedFrom)
|
|
4731
4918
|
});
|
|
4732
4919
|
cloudRules = (brain, args = {}) => call(brain, "POST", "/rules", {
|
|
4733
|
-
...args.limit === void 0 ? {} : { limit: args.limit }
|
|
4920
|
+
...args.limit === void 0 ? {} : { limit: args.limit },
|
|
4921
|
+
// The server cannot see this machine's folders, so the project it is
|
|
4922
|
+
// standing in has to be told. Omitted rather than sent as null when there
|
|
4923
|
+
// is none: absent means "fill the global budget only", which is what a
|
|
4924
|
+
// folder that names no project should get.
|
|
4925
|
+
...args.project == null ? {} : { project: args.project }
|
|
4734
4926
|
});
|
|
4735
4927
|
cloudReindex = (brain, args = {}) => call(brain, "POST", "/reindex", {
|
|
4736
4928
|
...args.rebuild === true ? { rebuild: true } : {}
|
|
@@ -5542,14 +5734,14 @@ function initCommand() {
|
|
|
5542
5734
|
brain: canonicalRoot2,
|
|
5543
5735
|
folders: FOLDERS,
|
|
5544
5736
|
files: Object.keys(files(opts.name)),
|
|
5545
|
-
observations_dir:
|
|
5737
|
+
observations_dir: OBSERVATIONS_DIR2,
|
|
5546
5738
|
indexed
|
|
5547
5739
|
},
|
|
5548
5740
|
human: () => {
|
|
5549
5741
|
printHuman(`Created a brain at \`${canonicalRoot2}\`.
|
|
5550
5742
|
`);
|
|
5551
5743
|
printHuman(`- \`01_BASE/\` \u2014 five files your agent reads first`);
|
|
5552
|
-
printHuman(`- \`${
|
|
5744
|
+
printHuman(`- \`${OBSERVATIONS_DIR2}/\` \u2014 where \`remember\` writes`);
|
|
5553
5745
|
printHuman(`- nine more folders, ready when you need them
|
|
5554
5746
|
`);
|
|
5555
5747
|
printHuman(c.gray("The five base files are empty templates.\n"));
|
|
@@ -5589,7 +5781,7 @@ async function createBrain(path, name) {
|
|
|
5589
5781
|
brains.push({
|
|
5590
5782
|
root: canonicalRoot2,
|
|
5591
5783
|
linkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5592
|
-
observationsDir:
|
|
5784
|
+
observationsDir: OBSERVATIONS_DIR2
|
|
5593
5785
|
});
|
|
5594
5786
|
await saveConfig({ ...cfg, brain: withDefaultBrain({ ...cfg.brain, brains }) });
|
|
5595
5787
|
return { root: canonicalRoot2, indexed: built.entries.length };
|
|
@@ -5634,7 +5826,7 @@ function files(name) {
|
|
|
5634
5826
|
"| Folder | What lives here |",
|
|
5635
5827
|
"| --- | --- |",
|
|
5636
5828
|
"| `10_ENTITIES/` | People, projects, systems \u2014 one note each |",
|
|
5637
|
-
"| `20_TIMELINE/` | What happened, when
|
|
5829
|
+
"| `20_TIMELINE/` | What happened, when |",
|
|
5638
5830
|
"| `30_ACTIONS/` | Open work and decisions waiting on something |",
|
|
5639
5831
|
"| `40_KNOWLEDGE/` | Conclusions worth keeping: how things work, why they were decided |",
|
|
5640
5832
|
"| `50_SKILLS/` | Procedures \u2014 the steps for doing a thing again |",
|
|
@@ -5642,6 +5834,10 @@ function files(name) {
|
|
|
5642
5834
|
"| `70_SOURCES/` | Where something came from: links, papers, transcripts |",
|
|
5643
5835
|
"| `80_SHAREABLE/` | Notes you would be willing to publish |",
|
|
5644
5836
|
"| `90_ARCHIVE/` | Kept, but no longer current |",
|
|
5837
|
+
`| \`${OBSERVATIONS_DIR2}/\` | Written by the agent \u2014 what \`remember\` records |`,
|
|
5838
|
+
"",
|
|
5839
|
+
"An underscore in front means the folder is the agent's: it is written",
|
|
5840
|
+
"and rewritten without asking. A number in front means it is yours.",
|
|
5645
5841
|
""
|
|
5646
5842
|
].join("\n"),
|
|
5647
5843
|
"01_BASE/CRITICAL_FACTS.md": base(
|
|
@@ -5701,30 +5897,33 @@ function files(name) {
|
|
|
5701
5897
|
)
|
|
5702
5898
|
};
|
|
5703
5899
|
}
|
|
5704
|
-
var
|
|
5900
|
+
var OBSERVATIONS_DIR2, FOLDERS;
|
|
5705
5901
|
var init_init = __esm({
|
|
5706
5902
|
"src/commands/init.ts"() {
|
|
5707
5903
|
"use strict";
|
|
5904
|
+
init_src();
|
|
5708
5905
|
init_adapter();
|
|
5709
5906
|
init_errors();
|
|
5710
5907
|
init_config();
|
|
5711
5908
|
init_index_build();
|
|
5712
5909
|
init_vault();
|
|
5713
5910
|
init_ui();
|
|
5714
|
-
|
|
5911
|
+
OBSERVATIONS_DIR2 = OBSERVATIONS_DIR;
|
|
5715
5912
|
FOLDERS = [
|
|
5716
5913
|
"00_HUB",
|
|
5717
5914
|
"01_BASE",
|
|
5718
5915
|
"10_ENTITIES",
|
|
5719
5916
|
"20_TIMELINE",
|
|
5720
|
-
"20_TIMELINE/observations",
|
|
5721
5917
|
"30_ACTIONS",
|
|
5722
5918
|
"40_KNOWLEDGE",
|
|
5723
5919
|
"50_SKILLS",
|
|
5724
5920
|
"60_TEMPLATES",
|
|
5725
5921
|
"70_SOURCES",
|
|
5726
5922
|
"80_SHAREABLE",
|
|
5727
|
-
"90_ARCHIVE"
|
|
5923
|
+
"90_ARCHIVE",
|
|
5924
|
+
// Last rather than in numeric order, because it is not in that order: the
|
|
5925
|
+
// numbered folders are the person's and this one is the agent's.
|
|
5926
|
+
OBSERVATIONS_DIR2
|
|
5728
5927
|
];
|
|
5729
5928
|
}
|
|
5730
5929
|
});
|
|
@@ -6163,7 +6362,7 @@ var init_link = __esm({
|
|
|
6163
6362
|
init_index_build();
|
|
6164
6363
|
init_vault();
|
|
6165
6364
|
init_ui();
|
|
6166
|
-
DEFAULT_OBSERVATIONS_DIR =
|
|
6365
|
+
DEFAULT_OBSERVATIONS_DIR = OBSERVATIONS_DIR;
|
|
6167
6366
|
}
|
|
6168
6367
|
});
|
|
6169
6368
|
|
|
@@ -12539,6 +12738,14 @@ function rememberCommand() {
|
|
|
12539
12738
|
const neverStore = parseNeverStore(opts.neverStore);
|
|
12540
12739
|
const binding = opts.rule === true || neverStore.length > 0;
|
|
12541
12740
|
const scope = await scopeToWrite(opts.scope);
|
|
12741
|
+
if (binding && scope === void 0 && !said.noScope) {
|
|
12742
|
+
throw memgError(
|
|
12743
|
+
"invalid_input",
|
|
12744
|
+
"a rule needs a scope, and this folder does not suggest one",
|
|
12745
|
+
'Nothing was written. This folder is not a repository and has no bound project name, so there is nothing to stamp. Add `--scope "<the project>"` for a decision about one thing, or `--no-scope` for one that has to hold everywhere.'
|
|
12746
|
+
);
|
|
12747
|
+
}
|
|
12748
|
+
const wireScope = scope ?? (binding ? GLOBAL_SCOPE : void 0);
|
|
12542
12749
|
const target = await resolveTarget({
|
|
12543
12750
|
...opts.local === void 0 ? {} : { local: opts.local },
|
|
12544
12751
|
...opts.vault === void 0 ? {} : { vault: opts.vault }
|
|
@@ -12547,7 +12754,7 @@ function rememberCommand() {
|
|
|
12547
12754
|
const cloud = await openCloudBrain(target);
|
|
12548
12755
|
const written = await cloudRemember(cloud, {
|
|
12549
12756
|
text: body,
|
|
12550
|
-
...
|
|
12757
|
+
...wireScope ? { scope: wireScope } : {},
|
|
12551
12758
|
...rationale ? { reason: rationale } : {},
|
|
12552
12759
|
...binding ? { rule: true } : {},
|
|
12553
12760
|
...neverStore.length > 0 ? { neverStore } : {},
|
|
@@ -12846,10 +13053,13 @@ function yamlString(value) {
|
|
|
12846
13053
|
|
|
12847
13054
|
// src/commands/revise.ts
|
|
12848
13055
|
function reviseCommand() {
|
|
13056
|
+
const said = { action: false };
|
|
12849
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(
|
|
12850
13058
|
"--valid-from <iso>",
|
|
12851
13059
|
"when this became true (defaults: supersede = now, otherwise unchanged)"
|
|
12852
|
-
).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(
|
|
12853
13063
|
"--never-store-add <words>",
|
|
12854
13064
|
"add words to this note\u2019s boundary, keeping the ones already there"
|
|
12855
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(
|
|
@@ -12869,11 +13079,24 @@ function reviseCommand() {
|
|
|
12869
13079
|
].join("\n")
|
|
12870
13080
|
).action(async (ref, opts) => {
|
|
12871
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;
|
|
12872
13091
|
const where = await resolveTarget({
|
|
12873
13092
|
...opts.local === void 0 ? {} : { local: opts.local },
|
|
12874
13093
|
...opts.vault === void 0 ? {} : { vault: opts.vault }
|
|
12875
13094
|
});
|
|
12876
13095
|
if (where.kind === "cloud") {
|
|
13096
|
+
if (bareUnbind) {
|
|
13097
|
+
await unbindHosted(where, ref, opts);
|
|
13098
|
+
return;
|
|
13099
|
+
}
|
|
12877
13100
|
await reviseHosted(where, ref, opts, added);
|
|
12878
13101
|
return;
|
|
12879
13102
|
}
|
|
@@ -12885,7 +13108,10 @@ function reviseCommand() {
|
|
|
12885
13108
|
const built = await openIndex(adapter, brain.root);
|
|
12886
13109
|
const entry = resolveRef(built.entries, ref);
|
|
12887
13110
|
const widened = widenedBoundary(entry.memory.never_store, added, ref);
|
|
12888
|
-
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);
|
|
12889
13115
|
const rationale = sanitizeRationale(
|
|
12890
13116
|
input.reason === NO_REASON_GIVEN ? null : input.reason ?? null
|
|
12891
13117
|
);
|
|
@@ -13077,6 +13303,30 @@ Undo: \`${undoHint(applied.record.op_id, opts)}\``));
|
|
|
13077
13303
|
});
|
|
13078
13304
|
});
|
|
13079
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
|
+
}
|
|
13080
13330
|
async function reviseHosted(where, ref, opts, added) {
|
|
13081
13331
|
if (opts.input) {
|
|
13082
13332
|
throw memgError(
|
|
@@ -13770,8 +14020,15 @@ function consolidateCommand() {
|
|
|
13770
14020
|
" semantic_similarity calibrated indexed meaning agrees reciprocally",
|
|
13771
14021
|
" shared_context two notes in one scope naming the same thing",
|
|
13772
14022
|
" similar_title two notes titled with mostly the same words",
|
|
13773
|
-
" kind_promotion a note marked as binding
|
|
13774
|
-
"
|
|
14023
|
+
" kind_promotion a note marked as binding with no kind at all \u2014 the",
|
|
14024
|
+
" marking is deliberate, the classification never happened",
|
|
14025
|
+
" rule_demotion a note marked as binding but classified as a fact, a",
|
|
14026
|
+
" procedure, an episode or a goal \u2014 the author said it is",
|
|
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",
|
|
13775
14032
|
"",
|
|
13776
14033
|
"Nothing here becomes a proposal on its own. Read a candidate, decide what it",
|
|
13777
14034
|
"means, then say so:",
|
|
@@ -13785,11 +14042,13 @@ function consolidateCommand() {
|
|
|
13785
14042
|
limit === void 0 ? {} : { limit }
|
|
13786
14043
|
) : await detectLocally(where.brain.root, limit);
|
|
13787
14044
|
const { candidates, truncated } = report3;
|
|
14045
|
+
const byRule = Object.entries(report3.by_rule ?? {}).sort((a, b) => b[1] - a[1]);
|
|
13788
14046
|
printDual({
|
|
13789
14047
|
json: {
|
|
13790
14048
|
candidates,
|
|
13791
14049
|
count: candidates.length,
|
|
13792
14050
|
scanned: report3.scanned,
|
|
14051
|
+
by_rule: report3.by_rule ?? {},
|
|
13793
14052
|
// Reported rather than left to be inferred from a short list. A run
|
|
13794
14053
|
// that stopped early and said nothing would be indistinguishable from
|
|
13795
14054
|
// a brain with little to find, which is the one thing the reader
|
|
@@ -13813,6 +14072,19 @@ function consolidateCommand() {
|
|
|
13813
14072
|
printHuman(`## ${candidates.length} worth a look
|
|
13814
14073
|
`);
|
|
13815
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
|
+
}
|
|
13816
14088
|
if (truncated) {
|
|
13817
14089
|
printHuman(
|
|
13818
14090
|
c.yellow(`
|
|
@@ -13854,7 +14126,21 @@ function renderCandidate(candidate) {
|
|
|
13854
14126
|
);
|
|
13855
14127
|
}
|
|
13856
14128
|
lines2.push(
|
|
13857
|
-
|
|
14129
|
+
// `unmark_rule` is not a proposal action and deliberately is not one: taking
|
|
14130
|
+
// a marking BACK is `revise --no-rule`, which already exists, and inventing
|
|
14131
|
+
// a second door to the same field would give a brain two ways to record one
|
|
14132
|
+
// change. So the suggestion names the command rather than a verb this CLI
|
|
14133
|
+
// does not have.
|
|
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
|
+
)
|
|
13858
14144
|
);
|
|
13859
14145
|
return lines2.join("\n");
|
|
13860
14146
|
}
|
|
@@ -15475,7 +15761,7 @@ init_src();
|
|
|
15475
15761
|
init_index_build();
|
|
15476
15762
|
init_ui();
|
|
15477
15763
|
init_vault();
|
|
15478
|
-
var RULE_LIMIT =
|
|
15764
|
+
var RULE_LIMIT = DEFAULT_RULE_BUDGET.global + DEFAULT_RULE_BUDGET.project;
|
|
15479
15765
|
async function collectRules(vault, contextDir, now = /* @__PURE__ */ new Date()) {
|
|
15480
15766
|
const { brain, unreachableDefault } = await resolveBrain(vault, contextDir);
|
|
15481
15767
|
const adapter = await openBrainOrExplain(brain.root);
|
|
@@ -15493,11 +15779,21 @@ async function collectRules(vault, contextDir, now = /* @__PURE__ */ new Date())
|
|
|
15493
15779
|
here: (event) => isInside(event.context_dir, contextDir)
|
|
15494
15780
|
}).map((r) => [r.entry.memory.id, r.score])
|
|
15495
15781
|
);
|
|
15496
|
-
const
|
|
15782
|
+
const ranked = declared.map((entry) => ({ entry, score: order.get(entry.memory.id) ?? 0 })).sort((a, b) => b.score - a.score || a.entry.memory.id.localeCompare(b.entry.memory.id));
|
|
15783
|
+
const project = await projectOf(contextDir).then((p) => p.name).catch(() => null);
|
|
15784
|
+
const split = splitRuleBudget(ranked, (r) => declaredScopeOf(r.entry.memory), project);
|
|
15497
15785
|
return {
|
|
15498
15786
|
brainRoot: brain.root,
|
|
15499
|
-
|
|
15500
|
-
|
|
15787
|
+
// Global first: they hold here too, and they are the ones a reader is most
|
|
15788
|
+
// likely to already know, so the project's own decisions land last and
|
|
15789
|
+
// closest to the work.
|
|
15790
|
+
rules: [
|
|
15791
|
+
...split.global.map((rule) => ({ ...rule, lane: "global" })),
|
|
15792
|
+
...split.project.map((rule) => ({ ...rule, lane: "project" }))
|
|
15793
|
+
],
|
|
15794
|
+
elsewhere: split.elsewhere.map((rule) => ({ ...rule, lane: "elsewhere" })),
|
|
15795
|
+
total: ranked.length,
|
|
15796
|
+
project,
|
|
15501
15797
|
...unreachableDefault ? { unreachableDefault } : {}
|
|
15502
15798
|
};
|
|
15503
15799
|
}
|
|
@@ -15508,6 +15804,17 @@ function renderRuleLine(text, width = RULE_WIDTH) {
|
|
|
15508
15804
|
function renderRuleLines(rules) {
|
|
15509
15805
|
return rules.map(({ entry }) => renderRuleLine(entry.memory.summary ?? entry.memory.title));
|
|
15510
15806
|
}
|
|
15807
|
+
function renderRuleGroups(rules, project) {
|
|
15808
|
+
const globals = rules.filter((rule) => rule.lane === "global");
|
|
15809
|
+
const scoped = rules.filter((rule) => rule.lane === "project");
|
|
15810
|
+
const out = [];
|
|
15811
|
+
if (globals.length > 0 && scoped.length > 0) {
|
|
15812
|
+
out.push("Everywhere:", ...renderRuleLines(globals), "");
|
|
15813
|
+
out.push(`In ${oneLine(project ?? "this project", WIDTH.title)}:`, ...renderRuleLines(scoped));
|
|
15814
|
+
return out;
|
|
15815
|
+
}
|
|
15816
|
+
return renderRuleLines(rules);
|
|
15817
|
+
}
|
|
15511
15818
|
|
|
15512
15819
|
// src/commands/guard.ts
|
|
15513
15820
|
init_deadline();
|
|
@@ -15576,11 +15883,16 @@ async function runStopGuard(payload, contextDirFlag) {
|
|
|
15576
15883
|
});
|
|
15577
15884
|
}
|
|
15578
15885
|
async function claimRules(sessionId, contextDir) {
|
|
15579
|
-
const
|
|
15580
|
-
if (
|
|
15581
|
-
if (
|
|
15582
|
-
|
|
15583
|
-
|
|
15886
|
+
const where = await resolveTarget({}).catch(() => null);
|
|
15887
|
+
if (where?.kind === "cloud") {
|
|
15888
|
+
if (!await claimSession(sessionId, where.brainId)) return null;
|
|
15889
|
+
const hosted = await hostedRuleLines(where, contextDir).catch(() => null);
|
|
15890
|
+
if (hosted !== null) {
|
|
15891
|
+
if (hosted.lines.length > 0) return { lines: hosted.lines, total: hosted.total };
|
|
15892
|
+
await releaseSession(sessionId, where.brainId);
|
|
15893
|
+
return null;
|
|
15894
|
+
}
|
|
15895
|
+
await releaseSession(sessionId, where.brainId);
|
|
15584
15896
|
}
|
|
15585
15897
|
const collected = await collectRules(void 0, contextDir).catch(() => null);
|
|
15586
15898
|
if (collected === null) return null;
|
|
@@ -15599,7 +15911,7 @@ async function claimRules(sessionId, contextDir) {
|
|
|
15599
15911
|
limit: RULE_LIMIT,
|
|
15600
15912
|
returnedIds: rules.map((r) => r.entry.memory.id)
|
|
15601
15913
|
});
|
|
15602
|
-
return { lines:
|
|
15914
|
+
return { lines: renderRuleGroups(rules, collected.project), total };
|
|
15603
15915
|
}
|
|
15604
15916
|
function contextFor(lines2, total) {
|
|
15605
15917
|
return `${rulesQuoted(lines2, total)}
|
|
@@ -15662,11 +15974,14 @@ async function misdeliveryNotice(toolName, filePath, sessionId) {
|
|
|
15662
15974
|
if (!await claimSession(sessionId, "\0misdelivery")) return null;
|
|
15663
15975
|
return 'This file is this harness\u2019s own memory \u2014 no other tool the user works with will ever read it. Durable facts about the user belong in their brain, which every tool shares: memgineering remember "<the fact>" --reason "<why>" (add --rule if they stated a standing rule). Write it there instead, or in addition.';
|
|
15664
15976
|
}
|
|
15665
|
-
|
|
15666
|
-
|
|
15977
|
+
function markerFor(sessionId, brain) {
|
|
15978
|
+
return join18(
|
|
15667
15979
|
guardDir(),
|
|
15668
15980
|
`${createHash7("sha256").update(`${sessionId}\0${brain}`).digest("hex").slice(0, 32)}`
|
|
15669
15981
|
);
|
|
15982
|
+
}
|
|
15983
|
+
async function claimSession(sessionId, brain) {
|
|
15984
|
+
const marker = markerFor(sessionId, brain);
|
|
15670
15985
|
try {
|
|
15671
15986
|
await mkdir11(guardDir(), { recursive: true, mode: 448 });
|
|
15672
15987
|
await writeFile9(marker, "", { flag: "wx", mode: 384 });
|
|
@@ -15676,6 +15991,9 @@ async function claimSession(sessionId, brain) {
|
|
|
15676
15991
|
void pruneOldMarkers();
|
|
15677
15992
|
return true;
|
|
15678
15993
|
}
|
|
15994
|
+
async function releaseSession(sessionId, brain) {
|
|
15995
|
+
await unlink3(markerFor(sessionId, brain)).catch(() => void 0);
|
|
15996
|
+
}
|
|
15679
15997
|
var MARKER_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
15680
15998
|
async function pruneOldMarkers() {
|
|
15681
15999
|
try {
|
|
@@ -15690,15 +16008,24 @@ async function pruneOldMarkers() {
|
|
|
15690
16008
|
} catch {
|
|
15691
16009
|
}
|
|
15692
16010
|
}
|
|
15693
|
-
async function hostedRuleLines() {
|
|
15694
|
-
const
|
|
15695
|
-
|
|
15696
|
-
const
|
|
15697
|
-
return {
|
|
15698
|
-
|
|
15699
|
-
|
|
15700
|
-
|
|
15701
|
-
|
|
16011
|
+
async function hostedRuleLines(where, contextDir) {
|
|
16012
|
+
const project = await projectOf(contextDir).then((p) => p.name).catch(() => null);
|
|
16013
|
+
const { rules, total } = await cloudRules(await openCloudBrain(where), { project });
|
|
16014
|
+
const grouped = groupHostedLines(rules, project);
|
|
16015
|
+
return { lines: grouped, total };
|
|
16016
|
+
}
|
|
16017
|
+
function groupHostedLines(rules, project) {
|
|
16018
|
+
const line = (r) => renderRuleLine(r.summary ?? r.title);
|
|
16019
|
+
const globals = rules.filter((r) => r.lane === "global");
|
|
16020
|
+
const scoped = rules.filter((r) => r.lane === "project");
|
|
16021
|
+
if (globals.length === 0 || scoped.length === 0) return rules.map(line);
|
|
16022
|
+
return [
|
|
16023
|
+
"Everywhere:",
|
|
16024
|
+
...globals.map(line),
|
|
16025
|
+
"",
|
|
16026
|
+
`In ${project ?? "this project"}:`,
|
|
16027
|
+
...scoped.map(line)
|
|
16028
|
+
];
|
|
15702
16029
|
}
|
|
15703
16030
|
|
|
15704
16031
|
// src/program.ts
|
|
@@ -21118,13 +21445,28 @@ function rulesCommand() {
|
|
|
21118
21445
|
...opts.vault === void 0 ? {} : { vault: opts.vault }
|
|
21119
21446
|
});
|
|
21120
21447
|
if (where.kind === "cloud") {
|
|
21121
|
-
const
|
|
21448
|
+
const project2 = await projectOf().then((p) => p.name).catch(() => null);
|
|
21449
|
+
const {
|
|
21450
|
+
rules: rules2,
|
|
21451
|
+
total: total2,
|
|
21452
|
+
eligible,
|
|
21453
|
+
elsewhere: elsewhere2 = []
|
|
21454
|
+
} = await cloudRules(await openCloudBrain(where), { project: project2 });
|
|
21455
|
+
const shownOf = eligible ?? rules2.length;
|
|
21122
21456
|
printDual({
|
|
21123
|
-
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
|
+
},
|
|
21124
21466
|
human: () => {
|
|
21125
21467
|
printHuman(`## rules \u2014 ${oneLine(where.name, WIDTH.title)} (${total2})
|
|
21126
21468
|
`);
|
|
21127
|
-
if (rules2.length === 0) {
|
|
21469
|
+
if (rules2.length === 0 && elsewhere2.length === 0) {
|
|
21128
21470
|
printHuman("_Nothing is marked as a rule in this brain._\n");
|
|
21129
21471
|
printHuman(
|
|
21130
21472
|
c.gray(
|
|
@@ -21135,11 +21477,32 @@ function rulesCommand() {
|
|
|
21135
21477
|
}
|
|
21136
21478
|
for (const rule of rules2) {
|
|
21137
21479
|
printHuman(`- ${oneLine(rule.summary ?? rule.title, WIDTH.text)}`);
|
|
21138
|
-
|
|
21480
|
+
const lane = rule.lane === "project" ? oneLine(project2 ?? "this project", WIDTH.title) : "global";
|
|
21481
|
+
printHuman(
|
|
21482
|
+
c.gray(
|
|
21483
|
+
` \`open: ${oneLine(rule.handle, WIDTH.handle)}\`${rule.lane ? ` \xB7 ${lane}` : ""}`
|
|
21484
|
+
)
|
|
21485
|
+
);
|
|
21139
21486
|
}
|
|
21140
|
-
if (
|
|
21487
|
+
if (shownOf > rules2.length) {
|
|
21141
21488
|
printHuman(c.gray(`
|
|
21142
|
-
${
|
|
21489
|
+
${shownOf - rules2.length} more not shown.`));
|
|
21490
|
+
}
|
|
21491
|
+
if (elsewhere2.length > 0) {
|
|
21492
|
+
printHuman(`
|
|
21493
|
+
### scoped to other projects (${elsewhere2.length})
|
|
21494
|
+
`);
|
|
21495
|
+
printHuman(
|
|
21496
|
+
c.gray("Not shown to an agent working here. They bind where they say.\n")
|
|
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
|
+
}
|
|
21143
21506
|
}
|
|
21144
21507
|
printHuman(
|
|
21145
21508
|
"\nThese are decisions, not suggestions. Follow them without being asked, and say\nso if what you are about to do goes against one."
|
|
@@ -21148,12 +21511,9 @@ ${total2 - rules2.length} more not shown.`));
|
|
|
21148
21511
|
});
|
|
21149
21512
|
return;
|
|
21150
21513
|
}
|
|
21151
|
-
const { rules, total, unreachableDefault, brainRoot } = await collectRules(
|
|
21152
|
-
|
|
21153
|
-
|
|
21154
|
-
);
|
|
21155
|
-
const allIds = rules.map((r) => r.entry.memory.id);
|
|
21156
|
-
if (rules.length === 0) {
|
|
21514
|
+
const { rules, elsewhere, total, project, unreachableDefault, brainRoot } = await collectRules(opts.vault, process.cwd());
|
|
21515
|
+
const allIds = [...rules, ...elsewhere].map((r) => r.entry.memory.id);
|
|
21516
|
+
if (rules.length === 0 && elsewhere.length === 0) {
|
|
21157
21517
|
const hint = unreachableDefault ? `Your default brain is not on disk right now: ${unreachableDefault}
|
|
21158
21518
|
This answered from \`${brainRoot}\` instead, so rules you wrote in the missing one are not shown \u2014 they are not lost. If it moved, \`memgineering use --default <new path>\`. If it is gone for good, \`memgineering unlink ${unreachableDefault}\`.` : 'Nothing here binds you yet. When the user decides something that should hold \u2014 a colour they will not use, a step never to skip \u2014 record it with `memgineering remember "<the decision>" --rule --reason "<why>"`, and it will be shown before the next edit.';
|
|
21159
21519
|
printDual({
|
|
@@ -21178,27 +21538,52 @@ This answered from \`${brainRoot}\` instead, so rules you wrote in the missing o
|
|
|
21178
21538
|
scope: entry.memory.scope,
|
|
21179
21539
|
path: entry.path
|
|
21180
21540
|
})),
|
|
21541
|
+
elsewhere: elsewhere.map(({ entry }) => ({
|
|
21542
|
+
id: entry.memory.id,
|
|
21543
|
+
handle: displayHandle(entry.memory.id, allIds),
|
|
21544
|
+
title: entry.memory.title,
|
|
21545
|
+
summary: entry.memory.summary,
|
|
21546
|
+
scope: entry.memory.scope,
|
|
21547
|
+
path: entry.path
|
|
21548
|
+
})),
|
|
21181
21549
|
total,
|
|
21182
|
-
|
|
21183
|
-
|
|
21550
|
+
project,
|
|
21551
|
+
...total > rules.length ? {
|
|
21552
|
+
hint: `${total} rules are declared and ${rules.length} are shown${project === null ? "" : ` for \`${project}\``}. Up to ${RULE_LIMIT} fit at once, split between rules that hold everywhere and rules scoped to this project; anything scoped to a DIFFERENT project is not shown here and binds there instead. If the number is high because old decisions never got retired, \`memgineering retire <handle>\`.`
|
|
21184
21553
|
} : {}
|
|
21185
21554
|
},
|
|
21186
21555
|
human: () => {
|
|
21187
|
-
printHuman(
|
|
21188
|
-
`)
|
|
21189
|
-
|
|
21556
|
+
printHuman(
|
|
21557
|
+
`## rules (${rules.length}${total > rules.length ? ` of ${total}` : ""}${project === null ? "" : ` \xB7 ${oneLine(project, WIDTH.title)}`})
|
|
21558
|
+
`
|
|
21559
|
+
);
|
|
21560
|
+
for (const { entry, lane } of rules) {
|
|
21190
21561
|
printHuman(`- ${oneLine(entry.memory.summary ?? entry.memory.title, WIDTH.text)}`);
|
|
21191
21562
|
printHuman(
|
|
21192
21563
|
c.gray(
|
|
21193
|
-
` \`open: ${displayHandle(entry.memory.id, allIds)}
|
|
21564
|
+
` \`open: ${displayHandle(entry.memory.id, allIds)}\` \xB7 ${lane === "project" ? oneLine(project ?? "this project", WIDTH.title) : "global"}`
|
|
21194
21565
|
)
|
|
21195
21566
|
);
|
|
21196
21567
|
}
|
|
21568
|
+
if (elsewhere.length > 0) {
|
|
21569
|
+
printHuman(`
|
|
21570
|
+
### scoped to other projects (${elsewhere.length})
|
|
21571
|
+
`);
|
|
21572
|
+
printHuman(c.gray("Not shown to an agent working here. They bind where they say.\n"));
|
|
21573
|
+
for (const { entry } of elsewhere) {
|
|
21574
|
+
printHuman(`- ${oneLine(entry.memory.summary ?? entry.memory.title, WIDTH.text)}`);
|
|
21575
|
+
printHuman(
|
|
21576
|
+
c.gray(
|
|
21577
|
+
` \`open: ${displayHandle(entry.memory.id, allIds)}\` \xB7 ${oneLine(entry.memory.scope ?? "", WIDTH.title)}`
|
|
21578
|
+
)
|
|
21579
|
+
);
|
|
21580
|
+
}
|
|
21581
|
+
}
|
|
21197
21582
|
printHuman("");
|
|
21198
21583
|
printHuman(
|
|
21199
21584
|
c.gray(
|
|
21200
|
-
total >
|
|
21201
|
-
|
|
21585
|
+
total > rules.length ? `${total} declared, ${rules.length} shown here. Rules scoped to another project bind there, not here.
|
|
21586
|
+
If the rest are decisions that no longer decide anything: \`memgineering retire <handle>\`.` : "These are decisions, not suggestions. Follow them without being asked, and say\nso if what you are about to do goes against one."
|
|
21202
21587
|
)
|
|
21203
21588
|
);
|
|
21204
21589
|
}
|
package/package.json
CHANGED