@massu/core 2.5.0 → 2.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -0
- package/commands/massu-bearings.md +1 -1
- package/commands/massu-recap.md +16 -5
- package/dist/cli.js +138 -46
- package/dist/hooks/auto-learning-pipeline.js +53 -9
- package/dist/hooks/classify-failure.js +55 -11
- package/dist/hooks/cost-tracker.js +23 -9
- package/dist/hooks/fix-detector.js +54 -9
- package/dist/hooks/incident-pipeline.js +53 -9
- package/dist/hooks/intent-suggester.js +53 -9
- package/dist/hooks/memory-recall.js +53 -9
- package/dist/hooks/post-edit-context.js +53 -9
- package/dist/hooks/post-tool-use.js +19 -6
- package/dist/hooks/pre-compact.js +19 -6
- package/dist/hooks/pre-delete-check.js +53 -9
- package/dist/hooks/pre-tool-use-gate.js +57 -11
- package/dist/hooks/quality-event.js +23 -9
- package/dist/hooks/rule-enforcement-pipeline.js +53 -9
- package/dist/hooks/security-gate.js +53 -9
- package/dist/hooks/session-end.js +19 -6
- package/dist/hooks/session-start.js +120 -37
- package/dist/hooks/user-prompt.js +19 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,67 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
10
10
|
> redistributed under the **Apache License 2.0**. Full license + NOTICE:
|
|
11
11
|
> `packages/core/assets/embedder/MODEL-LICENSE`.
|
|
12
12
|
|
|
13
|
+
## [2.5.1] - 2026-08-09
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **Hooks emitted an output field that does not exist, so no hook message ever reached
|
|
18
|
+
Claude.** Eleven hooks wrote `{"message": …}` to stdout. `message` is not in Claude Code's
|
|
19
|
+
hook output schema at all — it appears only in `Notification`/`FileChanged` *input* — so
|
|
20
|
+
every advisory string those hooks produced was discarded. The failure was self-inflicted in
|
|
21
|
+
an exact way: because `{"message": …}` is valid JSON, `UserPromptSubmit` parsed it as JSON
|
|
22
|
+
instead of treating it as plain text, so the one event that would have delivered the string
|
|
23
|
+
as stdout is precisely the event that dropped it. A slightly malformed string would have
|
|
24
|
+
arrived.
|
|
25
|
+
|
|
26
|
+
Replaced by a single chokepoint emitting the documented shape,
|
|
27
|
+
`{"hookSpecificOutput": {"hookEventName", "additionalContext"}}`. `hookEventName` is
|
|
28
|
+
REQUIRED, so the helper takes the event as a typed, required argument and the old
|
|
29
|
+
one-argument form is DELETED rather than deprecated — a regression to the silent shape is
|
|
30
|
+
now a compile error, not a silent no-op. An unrecognised event emits nothing and says why
|
|
31
|
+
on stderr. Output is truncated at the spec's 10,000-char cap by us rather than by the
|
|
32
|
+
runtime.
|
|
33
|
+
|
|
34
|
+
**Blocking behaviour is unchanged.** The `PreToolUse` gates deny via `process.exit(2)` plus
|
|
35
|
+
stderr, which never depended on this path; what was lost was their *advisory* (warn-level)
|
|
36
|
+
messages, and everything `rule-enforcement-pipeline` emits, since that hook only ever
|
|
37
|
+
advises.
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
- **`scripts/ops/retag-release-to-npm-githead.sh`** — audits every release tag against the
|
|
42
|
+
commit npm actually published. A release tag is supposed to answer "which commit shipped as
|
|
43
|
+
X.Y.Z", and two things break that: the tag is created before the post-publish bookkeeping
|
|
44
|
+
commit and then moved forward, or it is never created at all. npm records `gitHead` with
|
|
45
|
+
every publish, so the answer is recoverable — this reads THAT and refuses anything else.
|
|
46
|
+
|
|
47
|
+
Dry-run by default; reports its denominator; FAILS CLOSED when the registry returns no
|
|
48
|
+
usable `gitHead` or names a commit absent from the repo, rather than guessing; never
|
|
49
|
+
force-pushes — it moves the local tag and prints the push command, so publishing a
|
|
50
|
+
rewritten ref stays deliberate.
|
|
51
|
+
|
|
52
|
+
First full audit: `50 tag(s) examined; 6 already correct, 34 drifted, 10 error(s)`. The 2.x
|
|
53
|
+
series is now 7/7 correct; 1.x is left as-is deliberately.
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
|
|
57
|
+
- **The documented Stage-D release ceremony had no legal ending.** It ordered
|
|
58
|
+
`bump -> tag -> publish -> .mcp.json pin refresh`, but the pin refresh is a COMMIT, so
|
|
59
|
+
following it always leaves commits on top of a published-and-tagged release — exactly what
|
|
60
|
+
CR-64 refuses. Measured during the 2.5.0 release: gate `[11/22]` returned
|
|
61
|
+
`versionIsPublished:true, tagSha 90c82344, headSha a74648ae`.
|
|
62
|
+
|
|
63
|
+
Corrected to **pin -> publish -> tag**, so the published tree, npm's `gitHead`, the tag and
|
|
64
|
+
HEAD are one commit by construction. Fixed at both sites that state the order. The repo's
|
|
65
|
+
own history already worked this way — `v2.3.1`, `v2.3.0` and `v2.1.0` each tag their pin
|
|
66
|
+
commit — so the prose was the outlier, not the practice.
|
|
67
|
+
|
|
68
|
+
- **The same plan predicted a release does not touch `website/`.** The CHANGELOG is mirrored
|
|
69
|
+
into `website/`, so every release does; gate `[9/22]` failed on a 29h deploy lag. Replaced
|
|
70
|
+
the prediction with the measured fact and made `/massu-deploy` an explicit ceremony step.
|
|
71
|
+
|
|
72
|
+
Incident: `docs/incidents/2026-08-01-release-ceremony-had-no-legal-ending.md`
|
|
73
|
+
|
|
13
74
|
## [2.5.0] - 2026-08-01
|
|
14
75
|
|
|
15
76
|
### Fixed
|
|
@@ -41,7 +41,7 @@ Read these in parallel where possible:
|
|
|
41
41
|
| 10 | `.claude/metrics/command-invocations.jsonl` | Command usage frequency — top/least used in last 7 days |
|
|
42
42
|
| 11 | `.claude/metrics/bearings-history.jsonl` | Previous bearings runs — show trends |
|
|
43
43
|
| 12 | `.claude/commands/` (ls -d */`) | Detect folder-based skills (directories vs flat files) |
|
|
44
|
-
| 13 |
|
|
44
|
+
| 13 | `~/.claude/bin/memory-contradiction-check . --baseline .claude/memory-contradiction-baseline.json` | Unresolved pairs + denominator; exit 2 = could-not-look, a FINDING |
|
|
45
45
|
| 14 | `massu_cost_session` + `massu_cost_trend` MCP tools | Token spend this session, cost trend over time |
|
|
46
46
|
| 15 | `session-state/deferred-verifications.md` | Pending production verifications from previous deploys |
|
|
47
47
|
|
package/commands/massu-recap.md
CHANGED
|
@@ -104,12 +104,15 @@ If no significant work: skip this section.
|
|
|
104
104
|
Run the memory contradiction scanner:
|
|
105
105
|
|
|
106
106
|
```bash
|
|
107
|
-
|
|
107
|
+
~/.claude/bin/memory-contradiction-check . --baseline .claude/memory-contradiction-baseline.json
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
- **
|
|
110
|
+
Read the EXIT CODE, and read the denominator it prints — a
|
|
111
|
+
verdict with no `memory files read` line means it aborted before reporting:
|
|
112
|
+
- **exit 0** — no unresolved contradictions. Quote `unresolved pairs : 0` as the evidence.
|
|
113
|
+
- **exit 1** — unresolved pairs, or a STALE baseline entry. Display each to the user:
|
|
114
|
+
- **exit 2** — it COULD NOT LOOK (missing corpus, unreadable file, bad baseline). This is
|
|
115
|
+
a FINDING, never a clean run. Report it as such and do not proceed as though clean.
|
|
113
116
|
|
|
114
117
|
```
|
|
115
118
|
MEMORY HEALTH
|
|
@@ -122,7 +125,15 @@ MEMORY HEALTH
|
|
|
122
125
|
|
|
123
126
|
**Do NOT auto-resolve contradictions.** Surface them so the user can decide which memory is stale.
|
|
124
127
|
|
|
125
|
-
If the
|
|
128
|
+
**If the checker is missing, report that as a FAILED check — never as a benign note.**
|
|
129
|
+
This step was unrunnable in this repo from the day it was written until 2026-08-02: it
|
|
130
|
+
named `scripts/memory-contradiction-check.sh`, which never existed here (0 tracked
|
|
131
|
+
matches, 0 commits ever touching the path — it was copied from another repo's protocol).
|
|
132
|
+
The instruction to "note it as unavailable" is what let a mandatory check report nothing
|
|
133
|
+
for months, because "could not look" and "looked and found nothing" read identically.
|
|
134
|
+
The checker is now version-controlled at `~/.claude/bin/memory-contradiction-check`, has a
|
|
135
|
+
caller in `npm test` (`memory-contradiction-check-caller.test.ts`), a fixture suite, and a
|
|
136
|
+
live-fire attack — so its absence is itself a red test.
|
|
126
137
|
|
|
127
138
|
### Step 3: Update Session State
|
|
128
139
|
|
package/dist/cli.js
CHANGED
|
@@ -1548,6 +1548,16 @@ var init_memory_vector = __esm({
|
|
|
1548
1548
|
}
|
|
1549
1549
|
});
|
|
1550
1550
|
|
|
1551
|
+
// src/lib/timestamps.ts
|
|
1552
|
+
function nowIso() {
|
|
1553
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1554
|
+
}
|
|
1555
|
+
var init_timestamps = __esm({
|
|
1556
|
+
"src/lib/timestamps.ts"() {
|
|
1557
|
+
"use strict";
|
|
1558
|
+
}
|
|
1559
|
+
});
|
|
1560
|
+
|
|
1551
1561
|
// src/db-backup.ts
|
|
1552
1562
|
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readdirSync as readdirSync2, statSync as statSync2, unlinkSync, copyFileSync, rmSync as rmSync4 } from "fs";
|
|
1553
1563
|
import { resolve as resolve3, join as join3, basename, dirname as dirname4 } from "path";
|
|
@@ -3972,8 +3982,8 @@ function linkSessionToTask(db, sessionId, taskId) {
|
|
|
3972
3982
|
}
|
|
3973
3983
|
function addConversationTurn(db, sessionId, turnNumber, userPrompt, assistantResponse, toolCallsJson, toolCallCount, promptTokens, responseTokens) {
|
|
3974
3984
|
const result = db.prepare(`
|
|
3975
|
-
INSERT INTO conversation_turns (session_id, turn_number, user_prompt, assistant_response, tool_calls_json, tool_call_count, prompt_tokens, response_tokens)
|
|
3976
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
3985
|
+
INSERT INTO conversation_turns (session_id, turn_number, user_prompt, assistant_response, tool_calls_json, tool_call_count, prompt_tokens, response_tokens, created_at)
|
|
3986
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3977
3987
|
`).run(
|
|
3978
3988
|
sessionId,
|
|
3979
3989
|
turnNumber,
|
|
@@ -3982,14 +3992,15 @@ function addConversationTurn(db, sessionId, turnNumber, userPrompt, assistantRes
|
|
|
3982
3992
|
toolCallsJson,
|
|
3983
3993
|
toolCallCount,
|
|
3984
3994
|
promptTokens,
|
|
3985
|
-
responseTokens
|
|
3995
|
+
responseTokens,
|
|
3996
|
+
nowIso()
|
|
3986
3997
|
);
|
|
3987
3998
|
return Number(result.lastInsertRowid);
|
|
3988
3999
|
}
|
|
3989
4000
|
function addToolCallDetail(db, sessionId, turnNumber, toolName, inputSummary, inputSize, outputSize, success, filesInvolved) {
|
|
3990
4001
|
db.prepare(`
|
|
3991
|
-
INSERT INTO tool_call_details (session_id, turn_number, tool_name, tool_input_summary, tool_input_size, tool_output_size, tool_success, files_involved)
|
|
3992
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
4002
|
+
INSERT INTO tool_call_details (session_id, turn_number, tool_name, tool_input_summary, tool_input_size, tool_output_size, tool_success, files_involved, created_at)
|
|
4003
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3993
4004
|
`).run(
|
|
3994
4005
|
sessionId,
|
|
3995
4006
|
turnNumber,
|
|
@@ -3998,7 +4009,8 @@ function addToolCallDetail(db, sessionId, turnNumber, toolName, inputSummary, in
|
|
|
3998
4009
|
inputSize,
|
|
3999
4010
|
outputSize,
|
|
4000
4011
|
success ? 1 : 0,
|
|
4001
|
-
filesInvolved ? JSON.stringify(filesInvolved) : null
|
|
4012
|
+
filesInvolved ? JSON.stringify(filesInvolved) : null,
|
|
4013
|
+
nowIso()
|
|
4002
4014
|
);
|
|
4003
4015
|
}
|
|
4004
4016
|
function getLastProcessedLine(db, sessionId) {
|
|
@@ -4257,6 +4269,7 @@ var init_memory_db = __esm({
|
|
|
4257
4269
|
init_db_driver();
|
|
4258
4270
|
init_config();
|
|
4259
4271
|
init_memory_vector();
|
|
4272
|
+
init_timestamps();
|
|
4260
4273
|
init_db_backup();
|
|
4261
4274
|
init_rule_delivery();
|
|
4262
4275
|
init_rule_candidate_store();
|
|
@@ -20573,7 +20586,7 @@ import { createHash as createHash8 } from "crypto";
|
|
|
20573
20586
|
function renderMemoryFiles(db, candidates, opts) {
|
|
20574
20587
|
const config = opts.config ?? resolveMemoryFilesConfig();
|
|
20575
20588
|
if (!config.renderEnabled) {
|
|
20576
|
-
return EMPTY("render_disabled");
|
|
20589
|
+
return EMPTY("render_disabled", candidates.length);
|
|
20577
20590
|
}
|
|
20578
20591
|
const dryRun = opts.dryRun === true;
|
|
20579
20592
|
const now = opts.now ?? Date.now();
|
|
@@ -20582,7 +20595,7 @@ function renderMemoryFiles(db, candidates, opts) {
|
|
|
20582
20595
|
});
|
|
20583
20596
|
const { memoryDir } = opts;
|
|
20584
20597
|
if (!existsSync33(memoryDir)) {
|
|
20585
|
-
return EMPTY("no_memory_dir");
|
|
20598
|
+
return EMPTY("no_memory_dir", candidates.length);
|
|
20586
20599
|
}
|
|
20587
20600
|
try {
|
|
20588
20601
|
return withMemoryIndexLock(
|
|
@@ -20591,10 +20604,10 @@ function renderMemoryFiles(db, candidates, opts) {
|
|
|
20591
20604
|
);
|
|
20592
20605
|
} catch (err) {
|
|
20593
20606
|
if (err instanceof MemoryIndexLockBusy) {
|
|
20594
|
-
return EMPTY("lock_busy");
|
|
20607
|
+
return EMPTY("lock_busy", candidates.length);
|
|
20595
20608
|
}
|
|
20596
20609
|
audit("memory_file_render_refused", { reason: "unexpected_error", error: String(err) });
|
|
20597
|
-
return EMPTY("error");
|
|
20610
|
+
return EMPTY("error", candidates.length);
|
|
20598
20611
|
}
|
|
20599
20612
|
}
|
|
20600
20613
|
function renderLocked(db, candidates, o) {
|
|
@@ -20604,11 +20617,14 @@ function renderLocked(db, candidates, o) {
|
|
|
20604
20617
|
try {
|
|
20605
20618
|
tombstones = readTombstones(memoryDir);
|
|
20606
20619
|
} catch {
|
|
20607
|
-
return EMPTY("tombstone_ledger_unreadable");
|
|
20620
|
+
return EMPTY("tombstone_ledger_unreadable", candidates.length);
|
|
20608
20621
|
}
|
|
20609
20622
|
const ownerOf = relPathOwnerLookup(db);
|
|
20610
20623
|
const planned = [];
|
|
20611
|
-
|
|
20624
|
+
let unchanged = 0;
|
|
20625
|
+
let capped = 0;
|
|
20626
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
20627
|
+
const c2 = candidates[i];
|
|
20612
20628
|
if (!isLocalOrigin(c2.origin)) {
|
|
20613
20629
|
refusals.push({ name: c2.name, reason: "non_local_origin", detail: c2.origin || "unknown" });
|
|
20614
20630
|
audit("memory_file_render_refused", { name: c2.name, reason: "non_local_origin" });
|
|
@@ -20664,7 +20680,10 @@ function renderLocked(db, candidates, o) {
|
|
|
20664
20680
|
continue;
|
|
20665
20681
|
}
|
|
20666
20682
|
const next = composeFile(c2, home);
|
|
20667
|
-
if (next !== null && next === existing)
|
|
20683
|
+
if (next !== null && next === existing) {
|
|
20684
|
+
unchanged++;
|
|
20685
|
+
continue;
|
|
20686
|
+
}
|
|
20668
20687
|
}
|
|
20669
20688
|
const content = composeFile(c2, home);
|
|
20670
20689
|
if (content === null) {
|
|
@@ -20672,9 +20691,24 @@ function renderLocked(db, candidates, o) {
|
|
|
20672
20691
|
continue;
|
|
20673
20692
|
}
|
|
20674
20693
|
planned.push({ c: c2, relPath, absPath, content });
|
|
20675
|
-
if (planned.length >= config.renderMaxFilesPerSession)
|
|
20694
|
+
if (planned.length >= config.renderMaxFilesPerSession) {
|
|
20695
|
+
capped = candidates.length - (i + 1);
|
|
20696
|
+
break;
|
|
20697
|
+
}
|
|
20698
|
+
}
|
|
20699
|
+
const accounted = planned.length + refusals.length + unchanged + capped;
|
|
20700
|
+
if (accounted !== candidates.length) {
|
|
20701
|
+
audit("memory_file_render_refused", {
|
|
20702
|
+
reason: "accounting_leak",
|
|
20703
|
+
considered: candidates.length,
|
|
20704
|
+
accounted
|
|
20705
|
+
});
|
|
20706
|
+
throw new RenderAccountingLeak(
|
|
20707
|
+
`render did not account for every candidate: considered=${candidates.length} but accounted=${accounted} (planned=${planned.length} refused=${refusals.length} unchanged=${unchanged} capped=${capped}). A candidate was dropped without a bucket.`
|
|
20708
|
+
);
|
|
20676
20709
|
}
|
|
20677
20710
|
const indexLines = buildIndexLines(db, memoryDir, planned, config);
|
|
20711
|
+
const books = { considered: candidates.length, unchanged, capped };
|
|
20678
20712
|
if (dryRun) {
|
|
20679
20713
|
return {
|
|
20680
20714
|
enabled: true,
|
|
@@ -20682,11 +20716,20 @@ function renderLocked(db, candidates, o) {
|
|
|
20682
20716
|
written: planned.map((p19) => p19.relPath),
|
|
20683
20717
|
refusals,
|
|
20684
20718
|
indexLines,
|
|
20685
|
-
bytesWritten: 0
|
|
20719
|
+
bytesWritten: 0,
|
|
20720
|
+
...books
|
|
20686
20721
|
};
|
|
20687
20722
|
}
|
|
20688
20723
|
if (planned.length === 0 && indexLines.length === 0) {
|
|
20689
|
-
return {
|
|
20724
|
+
return {
|
|
20725
|
+
enabled: true,
|
|
20726
|
+
dryRun: false,
|
|
20727
|
+
written: [],
|
|
20728
|
+
refusals,
|
|
20729
|
+
indexLines: [],
|
|
20730
|
+
bytesWritten: 0,
|
|
20731
|
+
...books
|
|
20732
|
+
};
|
|
20690
20733
|
}
|
|
20691
20734
|
if (!hasFreshBackup(memoryDir, home)) {
|
|
20692
20735
|
try {
|
|
@@ -20696,7 +20739,7 @@ function renderLocked(db, candidates, o) {
|
|
|
20696
20739
|
reason: "backup_failed",
|
|
20697
20740
|
error: err instanceof BackupError ? err.message : String(err)
|
|
20698
20741
|
});
|
|
20699
|
-
return EMPTY("backup_failed");
|
|
20742
|
+
return EMPTY("backup_failed", candidates.length);
|
|
20700
20743
|
}
|
|
20701
20744
|
}
|
|
20702
20745
|
const indexPath = o.indexPath ?? join19(memoryDir, "MEMORY.md");
|
|
@@ -20764,7 +20807,10 @@ function renderLocked(db, candidates, o) {
|
|
|
20764
20807
|
errors: restored.errors
|
|
20765
20808
|
});
|
|
20766
20809
|
return {
|
|
20767
|
-
...EMPTY("rollback_incomplete"),
|
|
20810
|
+
...EMPTY("rollback_incomplete", candidates.length),
|
|
20811
|
+
// The books were balanced before the write began; keep them rather than zeroing.
|
|
20812
|
+
unchanged,
|
|
20813
|
+
capped,
|
|
20768
20814
|
enabled: true,
|
|
20769
20815
|
refusals: [
|
|
20770
20816
|
...refusals,
|
|
@@ -20773,9 +20819,9 @@ function renderLocked(db, candidates, o) {
|
|
|
20773
20819
|
};
|
|
20774
20820
|
}
|
|
20775
20821
|
audit("memory_file_render_refused", { reason: "write_failed", error: String(err) });
|
|
20776
|
-
return { ...EMPTY("write_failed"), enabled: true, refusals };
|
|
20822
|
+
return { ...EMPTY("write_failed", candidates.length), enabled: true, refusals, unchanged, capped };
|
|
20777
20823
|
}
|
|
20778
|
-
return { enabled: true, dryRun: false, written, refusals, indexLines, bytesWritten };
|
|
20824
|
+
return { enabled: true, dryRun: false, written, refusals, indexLines, bytesWritten, ...books };
|
|
20779
20825
|
}
|
|
20780
20826
|
function sha2562(s) {
|
|
20781
20827
|
return createHash8("sha256").update(s, "utf8").digest("hex");
|
|
@@ -20844,7 +20890,7 @@ function buildIndexLines(db, memoryDir, planned, config) {
|
|
|
20844
20890
|
function oneLine(s) {
|
|
20845
20891
|
return s.replace(/[\r\n]+/g, " ").slice(0, 120);
|
|
20846
20892
|
}
|
|
20847
|
-
var EMPTY;
|
|
20893
|
+
var RenderAccountingLeak, EMPTY;
|
|
20848
20894
|
var init_memory_renderer = __esm({
|
|
20849
20895
|
"src/memory-renderer.ts"() {
|
|
20850
20896
|
"use strict";
|
|
@@ -20859,14 +20905,22 @@ var init_memory_renderer = __esm({
|
|
|
20859
20905
|
init_memory_index_region();
|
|
20860
20906
|
init_memory_files_config();
|
|
20861
20907
|
init_memory_file_ingest();
|
|
20862
|
-
|
|
20908
|
+
RenderAccountingLeak = class extends Error {
|
|
20909
|
+
};
|
|
20910
|
+
EMPTY = (reason, considered = 0) => ({
|
|
20863
20911
|
enabled: false,
|
|
20864
20912
|
dryRun: false,
|
|
20865
20913
|
written: [],
|
|
20866
20914
|
refusals: [],
|
|
20867
20915
|
indexLines: [],
|
|
20868
20916
|
bytesWritten: 0,
|
|
20869
|
-
skippedReason: reason
|
|
20917
|
+
skippedReason: reason,
|
|
20918
|
+
// An early gate refused, so every candidate was dropped — by that ONE named reason.
|
|
20919
|
+
// Reporting `considered` here is what distinguishes "nothing to do" from "walked away
|
|
20920
|
+
// from 59 rows because the lock was busy".
|
|
20921
|
+
considered,
|
|
20922
|
+
unchanged: 0,
|
|
20923
|
+
capped: 0
|
|
20870
20924
|
});
|
|
20871
20925
|
}
|
|
20872
20926
|
});
|
|
@@ -21057,25 +21111,46 @@ var init_memory_render_cli = __esm({
|
|
|
21057
21111
|
function loadRenderCandidates(db) {
|
|
21058
21112
|
const cfg = resolveMemoryFilesConfig();
|
|
21059
21113
|
const typePlaceholders = RENDERABLE_MEMORY_TYPES.map(() => "?").join(", ");
|
|
21114
|
+
const WHERE = `WHERE importance >= ?
|
|
21115
|
+
AND COALESCE(expired_at_epoch, 0) = 0
|
|
21116
|
+
AND type IN (${typePlaceholders})`;
|
|
21117
|
+
const { population } = db.prepare(`SELECT COUNT(*) AS population FROM observations ${WHERE}`).get(cfg.renderMinImportance, ...RENDERABLE_MEMORY_TYPES);
|
|
21060
21118
|
const rows = db.prepare(
|
|
21061
21119
|
`SELECT id, title, detail, importance, COALESCE(origin, 'local') AS origin
|
|
21062
21120
|
FROM observations
|
|
21063
|
-
|
|
21064
|
-
AND COALESCE(expired_at_epoch, 0) = 0
|
|
21065
|
-
AND type IN (${typePlaceholders})
|
|
21121
|
+
${WHERE}
|
|
21066
21122
|
ORDER BY importance DESC, created_at_epoch DESC
|
|
21067
|
-
LIMIT
|
|
21123
|
+
LIMIT ${CANDIDATE_WINDOW}`
|
|
21068
21124
|
).all(cfg.renderMinImportance, ...RENDERABLE_MEMORY_TYPES);
|
|
21069
|
-
|
|
21070
|
-
|
|
21071
|
-
|
|
21072
|
-
|
|
21073
|
-
|
|
21074
|
-
|
|
21075
|
-
|
|
21076
|
-
|
|
21125
|
+
const reingest = rows.filter((r2) => r2.title.startsWith("[memory-file]"));
|
|
21126
|
+
const kept = rows.filter((r2) => !r2.title.startsWith("[memory-file]"));
|
|
21127
|
+
const excluded = reingest.length ? [{ reason: EXCLUSION_MEMORY_FILE_REINGEST, count: reingest.length }] : [];
|
|
21128
|
+
const ledger = {
|
|
21129
|
+
population,
|
|
21130
|
+
windowed: rows.length,
|
|
21131
|
+
truncatedByWindow: Math.max(0, population - rows.length),
|
|
21132
|
+
excluded,
|
|
21133
|
+
returned: kept.length
|
|
21134
|
+
};
|
|
21135
|
+
const accounted = ledger.returned + ledger.truncatedByWindow + excluded.reduce((n, e2) => n + e2.count, 0);
|
|
21136
|
+
if (accounted !== population) {
|
|
21137
|
+
throw new CandidateAccountingLeak(
|
|
21138
|
+
`candidate ledger does not balance: population=${population} but accounted=${accounted} (returned=${ledger.returned} truncated=${ledger.truncatedByWindow} excluded=${JSON.stringify(excluded)}). A row was dropped without a bucket.`
|
|
21139
|
+
);
|
|
21140
|
+
}
|
|
21141
|
+
return {
|
|
21142
|
+
ledger,
|
|
21143
|
+
candidates: kept.map((r2) => ({
|
|
21144
|
+
observationId: r2.id,
|
|
21145
|
+
name: r2.title,
|
|
21146
|
+
title: r2.title,
|
|
21147
|
+
body: r2.detail ?? "",
|
|
21148
|
+
importance: r2.importance,
|
|
21149
|
+
origin: r2.origin
|
|
21150
|
+
}))
|
|
21151
|
+
};
|
|
21077
21152
|
}
|
|
21078
|
-
var RENDERABLE_MEMORY_TYPES;
|
|
21153
|
+
var RENDERABLE_MEMORY_TYPES, CANDIDATE_WINDOW, EXCLUSION_MEMORY_FILE_REINGEST, CandidateAccountingLeak;
|
|
21079
21154
|
var init_memory_render_candidates = __esm({
|
|
21080
21155
|
"src/memory-render-candidates.ts"() {
|
|
21081
21156
|
"use strict";
|
|
@@ -21086,6 +21161,10 @@ var init_memory_render_candidates = __esm({
|
|
|
21086
21161
|
"incident_near_miss",
|
|
21087
21162
|
"cr_violation"
|
|
21088
21163
|
];
|
|
21164
|
+
CANDIDATE_WINDOW = 50;
|
|
21165
|
+
EXCLUSION_MEMORY_FILE_REINGEST = "memory_file_reingest";
|
|
21166
|
+
CandidateAccountingLeak = class extends Error {
|
|
21167
|
+
};
|
|
21089
21168
|
}
|
|
21090
21169
|
});
|
|
21091
21170
|
|
|
@@ -21165,13 +21244,13 @@ function pruneNoiseObservations(db, opts = {}) {
|
|
|
21165
21244
|
const counts = countNoise(db);
|
|
21166
21245
|
if (dryRun || counts.total === 0) return { counts, expired: 0, dryRun };
|
|
21167
21246
|
const nowSec = opts.now ?? Math.floor(Date.now() / 1e3);
|
|
21168
|
-
const
|
|
21247
|
+
const nowIso2 = new Date(nowSec * 1e3).toISOString();
|
|
21169
21248
|
const res = db.prepare(
|
|
21170
21249
|
`UPDATE observations
|
|
21171
21250
|
SET expired_at = ?, expired_at_epoch = ?, valid_to = ?, valid_to_epoch = ?
|
|
21172
21251
|
WHERE COALESCE(expired_at_epoch, 0) = 0
|
|
21173
21252
|
AND ((${DA_PREDICATE}) OR (${FT_DECISION_PREDICATE}) OR (${DB_PREDICATE}))`
|
|
21174
|
-
).run(
|
|
21253
|
+
).run(nowIso2, nowSec, nowIso2, nowSec);
|
|
21175
21254
|
return { counts, expired: res.changes, dryRun: false };
|
|
21176
21255
|
}
|
|
21177
21256
|
var DA_PREDICATE, FT_DECISION_PREDICATE, DB_PREDICATE;
|
|
@@ -21216,7 +21295,7 @@ async function runMemoryRenderCli(sub, args2) {
|
|
|
21216
21295
|
};
|
|
21217
21296
|
}
|
|
21218
21297
|
const cfg = resolveMemoryFilesConfig();
|
|
21219
|
-
const candidates = loadRenderCandidates(db);
|
|
21298
|
+
const { candidates, ledger } = loadRenderCandidates(db);
|
|
21220
21299
|
const result = renderMemoryFiles(db, candidates, {
|
|
21221
21300
|
memoryDir,
|
|
21222
21301
|
dryRun: true,
|
|
@@ -21231,6 +21310,19 @@ async function runMemoryRenderCli(sub, args2) {
|
|
|
21231
21310
|
}
|
|
21232
21311
|
lines.push(`Massu would write ${result.written.length} file(s):`);
|
|
21233
21312
|
for (const f2 of result.written) lines.push(` ${f2}`);
|
|
21313
|
+
lines.push("", "Where every row went:");
|
|
21314
|
+
lines.push(` candidate population (type+importance+expiry) : ${ledger.population}`);
|
|
21315
|
+
if (ledger.truncatedByWindow > 0) {
|
|
21316
|
+
lines.push(` truncated by the row window : ${ledger.truncatedByWindow}`);
|
|
21317
|
+
}
|
|
21318
|
+
for (const e2 of ledger.excluded) {
|
|
21319
|
+
lines.push(` excluded: ${e2.reason.padEnd(30)} : ${e2.count}`);
|
|
21320
|
+
}
|
|
21321
|
+
lines.push(` -> reached the renderer : ${ledger.returned}`);
|
|
21322
|
+
lines.push(
|
|
21323
|
+
` renderer: considered ${result.considered} = written ${result.written.length} + refused ${result.refusals.length} + unchanged ${result.unchanged} + capped ${result.capped}`
|
|
21324
|
+
);
|
|
21325
|
+
lines.push(` skipped: ${result.skippedReason ?? "(none)"}`);
|
|
21234
21326
|
if (result.indexLines.length > 0) {
|
|
21235
21327
|
lines.push("", "MEMORY.md \u2014 inside the massu:learned region only:");
|
|
21236
21328
|
for (const l2 of result.indexLines) lines.push(` ${l2}`);
|
|
@@ -23389,7 +23481,7 @@ async function pullInstalledPackRules(db, opts = {}) {
|
|
|
23389
23481
|
}
|
|
23390
23482
|
const packs = parsePacks(envelope.packs_json);
|
|
23391
23483
|
const result = { ...ZERO };
|
|
23392
|
-
const
|
|
23484
|
+
const nowIso2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
23393
23485
|
for (const pack of packs) {
|
|
23394
23486
|
if (!isValidInstalledPack(pack)) continue;
|
|
23395
23487
|
for (const rawRule of pack.rules) {
|
|
@@ -23406,7 +23498,7 @@ async function pullInstalledPackRules(db, opts = {}) {
|
|
|
23406
23498
|
result.skipped += 1;
|
|
23407
23499
|
continue;
|
|
23408
23500
|
}
|
|
23409
|
-
materializeCandidate(projectRoot, candidatePath, promptHash, rule, pack, signedOrgId,
|
|
23501
|
+
materializeCandidate(projectRoot, candidatePath, promptHash, rule, pack, signedOrgId, nowIso2, opts.home ?? homedir20());
|
|
23410
23502
|
result.materialized += 1;
|
|
23411
23503
|
}
|
|
23412
23504
|
}
|
|
@@ -23453,7 +23545,7 @@ function alreadyApplied(db, promptHash) {
|
|
|
23453
23545
|
return false;
|
|
23454
23546
|
}
|
|
23455
23547
|
}
|
|
23456
|
-
function materializeCandidate(projectRoot, candidatePath, promptHash, rule, pack, orgId,
|
|
23548
|
+
function materializeCandidate(projectRoot, candidatePath, promptHash, rule, pack, orgId, nowIso2, home) {
|
|
23457
23549
|
const promptText = `${rule.title}: ${rule.description}`.replace(/\n+/g, " ").slice(0, 200) || `pack rule ${promptHash}`;
|
|
23458
23550
|
const draftText = rule.pattern ?? rule.check ?? rule.description;
|
|
23459
23551
|
const hardened = isExecutableDestination(rule.destination);
|
|
@@ -23469,7 +23561,7 @@ function materializeCandidate(projectRoot, candidatePath, promptHash, rule, pack
|
|
|
23469
23561
|
score: 0,
|
|
23470
23562
|
signals: [],
|
|
23471
23563
|
prior_turn_files: [],
|
|
23472
|
-
timestamp:
|
|
23564
|
+
timestamp: nowIso2,
|
|
23473
23565
|
session_id: `pack:${pack.slug}`,
|
|
23474
23566
|
// Provenance (P2-002/P2-003): the applier's team-origin / hardened apply-gate
|
|
23475
23567
|
// keys on this. `origin:'pack'` rides the SAME gate as a team origin
|
|
@@ -23481,7 +23573,7 @@ function materializeCandidate(projectRoot, candidatePath, promptHash, rule, pack
|
|
|
23481
23573
|
origin: "pack",
|
|
23482
23574
|
org_id: orgId,
|
|
23483
23575
|
promoted_by: `pack:${pack.slug}`,
|
|
23484
|
-
promoted_at:
|
|
23576
|
+
promoted_at: nowIso2,
|
|
23485
23577
|
signature_verified: true,
|
|
23486
23578
|
...applyMac ? { apply_mac: applyMac } : {},
|
|
23487
23579
|
pack_slug: pack.slug,
|
|
@@ -34681,7 +34773,7 @@ function indexAllKnowledge(db) {
|
|
|
34681
34773
|
files.push(...docsFiles);
|
|
34682
34774
|
}
|
|
34683
34775
|
const now = /* @__PURE__ */ new Date();
|
|
34684
|
-
const
|
|
34776
|
+
const nowIso2 = now.toISOString();
|
|
34685
34777
|
const nowEpoch = now.getTime();
|
|
34686
34778
|
const transaction = db.transaction(() => {
|
|
34687
34779
|
db.exec("DROP TRIGGER IF EXISTS kc_fts_delete");
|
|
@@ -34725,7 +34817,7 @@ function indexAllKnowledge(db) {
|
|
|
34725
34817
|
const category = categorizeFile(filePath);
|
|
34726
34818
|
const title = extractTitle(content, filePath);
|
|
34727
34819
|
const description = extractDescription2(content);
|
|
34728
|
-
const result = insertDoc.run(relPath, category, title, description, hash,
|
|
34820
|
+
const result = insertDoc.run(relPath, category, title, description, hash, nowIso2, nowEpoch);
|
|
34729
34821
|
const docId = result.lastInsertRowid;
|
|
34730
34822
|
stats.filesIndexed++;
|
|
34731
34823
|
const sections = parseSections(content, filePath);
|
|
@@ -1339,6 +1339,16 @@ var init_memory_vector = __esm({
|
|
|
1339
1339
|
}
|
|
1340
1340
|
});
|
|
1341
1341
|
|
|
1342
|
+
// src/lib/timestamps.ts
|
|
1343
|
+
function nowIso() {
|
|
1344
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1345
|
+
}
|
|
1346
|
+
var init_timestamps = __esm({
|
|
1347
|
+
"src/lib/timestamps.ts"() {
|
|
1348
|
+
"use strict";
|
|
1349
|
+
}
|
|
1350
|
+
});
|
|
1351
|
+
|
|
1342
1352
|
// src/db-backup.ts
|
|
1343
1353
|
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readdirSync, statSync, unlinkSync, copyFileSync, rmSync as rmSync4 } from "fs";
|
|
1344
1354
|
import { resolve as resolve3, join as join3, basename, dirname as dirname4 } from "path";
|
|
@@ -3599,8 +3609,8 @@ function linkSessionToTask(db, sessionId, taskId) {
|
|
|
3599
3609
|
}
|
|
3600
3610
|
function addConversationTurn(db, sessionId, turnNumber, userPrompt, assistantResponse, toolCallsJson, toolCallCount, promptTokens, responseTokens) {
|
|
3601
3611
|
const result = db.prepare(`
|
|
3602
|
-
INSERT INTO conversation_turns (session_id, turn_number, user_prompt, assistant_response, tool_calls_json, tool_call_count, prompt_tokens, response_tokens)
|
|
3603
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
3612
|
+
INSERT INTO conversation_turns (session_id, turn_number, user_prompt, assistant_response, tool_calls_json, tool_call_count, prompt_tokens, response_tokens, created_at)
|
|
3613
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3604
3614
|
`).run(
|
|
3605
3615
|
sessionId,
|
|
3606
3616
|
turnNumber,
|
|
@@ -3609,14 +3619,15 @@ function addConversationTurn(db, sessionId, turnNumber, userPrompt, assistantRes
|
|
|
3609
3619
|
toolCallsJson,
|
|
3610
3620
|
toolCallCount,
|
|
3611
3621
|
promptTokens,
|
|
3612
|
-
responseTokens
|
|
3622
|
+
responseTokens,
|
|
3623
|
+
nowIso()
|
|
3613
3624
|
);
|
|
3614
3625
|
return Number(result.lastInsertRowid);
|
|
3615
3626
|
}
|
|
3616
3627
|
function addToolCallDetail(db, sessionId, turnNumber, toolName, inputSummary, inputSize, outputSize, success, filesInvolved) {
|
|
3617
3628
|
db.prepare(`
|
|
3618
|
-
INSERT INTO tool_call_details (session_id, turn_number, tool_name, tool_input_summary, tool_input_size, tool_output_size, tool_success, files_involved)
|
|
3619
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
3629
|
+
INSERT INTO tool_call_details (session_id, turn_number, tool_name, tool_input_summary, tool_input_size, tool_output_size, tool_success, files_involved, created_at)
|
|
3630
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3620
3631
|
`).run(
|
|
3621
3632
|
sessionId,
|
|
3622
3633
|
turnNumber,
|
|
@@ -3625,7 +3636,8 @@ function addToolCallDetail(db, sessionId, turnNumber, toolName, inputSummary, in
|
|
|
3625
3636
|
inputSize,
|
|
3626
3637
|
outputSize,
|
|
3627
3638
|
success ? 1 : 0,
|
|
3628
|
-
filesInvolved ? JSON.stringify(filesInvolved) : null
|
|
3639
|
+
filesInvolved ? JSON.stringify(filesInvolved) : null,
|
|
3640
|
+
nowIso()
|
|
3629
3641
|
);
|
|
3630
3642
|
}
|
|
3631
3643
|
function getLastProcessedLine(db, sessionId) {
|
|
@@ -3884,6 +3896,7 @@ var init_memory_db = __esm({
|
|
|
3884
3896
|
init_db_driver();
|
|
3885
3897
|
init_config();
|
|
3886
3898
|
init_memory_vector();
|
|
3899
|
+
init_timestamps();
|
|
3887
3900
|
init_db_backup();
|
|
3888
3901
|
init_rule_delivery();
|
|
3889
3902
|
init_rule_candidate_store();
|
|
@@ -3980,12 +3993,43 @@ import { tmpdir as tmpdir2 } from "os";
|
|
|
3980
3993
|
import { join as join6 } from "path";
|
|
3981
3994
|
|
|
3982
3995
|
// src/hooks/lib/write-hook-message.ts
|
|
3983
|
-
|
|
3984
|
-
|
|
3996
|
+
var HOOK_EVENTS = [
|
|
3997
|
+
"UserPromptSubmit",
|
|
3998
|
+
"UserPromptExpansion",
|
|
3999
|
+
"SessionStart",
|
|
4000
|
+
"PreToolUse",
|
|
4001
|
+
"PostToolUse",
|
|
4002
|
+
"Stop",
|
|
4003
|
+
"SubagentStop",
|
|
4004
|
+
"PreCompact"
|
|
4005
|
+
];
|
|
4006
|
+
var ADDITIONAL_CONTEXT_MAX_CHARS = 1e4;
|
|
4007
|
+
function isHookEvent(value) {
|
|
4008
|
+
return typeof value === "string" && HOOK_EVENTS.includes(value);
|
|
4009
|
+
}
|
|
4010
|
+
function writeHookContext(hookEventName, message) {
|
|
4011
|
+
if (!isHookEvent(hookEventName)) {
|
|
4012
|
+
process.stderr.write(
|
|
4013
|
+
`[massu] writeHookContext: refusing to emit \u2014 unrecognised hook event ${JSON.stringify(hookEventName)}. Known: ${HOOK_EVENTS.join(", ")}.
|
|
4014
|
+
`
|
|
4015
|
+
);
|
|
4016
|
+
return;
|
|
4017
|
+
}
|
|
4018
|
+
if (typeof message !== "string" || message.length === 0) return;
|
|
4019
|
+
let additionalContext = message;
|
|
4020
|
+
if (additionalContext.length > ADDITIONAL_CONTEXT_MAX_CHARS) {
|
|
4021
|
+
const marker = `
|
|
4022
|
+
[massu] \u2026truncated at ${ADDITIONAL_CONTEXT_MAX_CHARS} chars (spec cap).`;
|
|
4023
|
+
additionalContext = additionalContext.slice(0, ADDITIONAL_CONTEXT_MAX_CHARS - marker.length) + marker;
|
|
4024
|
+
}
|
|
4025
|
+
process.stdout.write(
|
|
4026
|
+
JSON.stringify({ hookSpecificOutput: { hookEventName, additionalContext } }) + "\n"
|
|
4027
|
+
);
|
|
3985
4028
|
}
|
|
3986
4029
|
|
|
3987
4030
|
// src/hooks/auto-learning-pipeline.ts
|
|
3988
4031
|
init_hook_failure_signal();
|
|
4032
|
+
var HOOK_EVENT = "Stop";
|
|
3989
4033
|
var MAX_FULL_DIFF_BYTES = 2 * 1024 * 1024;
|
|
3990
4034
|
function getSessionFlagPath(sessionId) {
|
|
3991
4035
|
return join6(tmpdir2(), "massu-auto-learning", `fixes-${sessionId.slice(0, 12)}.jsonl`);
|
|
@@ -4104,7 +4148,7 @@ async function main() {
|
|
|
4104
4148
|
lines.push("");
|
|
4105
4149
|
lines.push("============================================================================");
|
|
4106
4150
|
lines.push("");
|
|
4107
|
-
|
|
4151
|
+
writeHookContext(HOOK_EVENT, lines.join("\n"));
|
|
4108
4152
|
cleanup(flagPath);
|
|
4109
4153
|
} catch (err) {
|
|
4110
4154
|
recordHookFailure("auto-learning-pipeline", err);
|