@plinth-music/cli 0.10.0 → 0.11.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 +102 -0
- package/README.md +7 -6
- package/dist/build-stamp.json +2 -2
- package/dist/cli.js +1043 -146
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -7584,7 +7584,7 @@ function _getBuiltinFlags(long, short, userNames, userAliases) {
|
|
|
7584
7584
|
// package.json
|
|
7585
7585
|
var package_default = {
|
|
7586
7586
|
name: "@plinth-music/cli",
|
|
7587
|
-
version: "0.
|
|
7587
|
+
version: "0.11.0",
|
|
7588
7588
|
description: "Plinth capstone sync daemon and CLI",
|
|
7589
7589
|
type: "module",
|
|
7590
7590
|
private: false,
|
|
@@ -22354,6 +22354,7 @@ var ArtistLiveSchema = exports_external.object({
|
|
|
22354
22354
|
});
|
|
22355
22355
|
var ArtistsPageResponseSchema = makePageResponseSchema("artists", ArtistLiveSchema);
|
|
22356
22356
|
var SERVER_AUTHORITATIVE_KEYS = new Set([
|
|
22357
|
+
"id",
|
|
22357
22358
|
"slug",
|
|
22358
22359
|
"revision",
|
|
22359
22360
|
"last_edited_at",
|
|
@@ -22385,6 +22386,7 @@ var WIRE_EDITABLE_TOP_LEVEL_KEYS = new Set([
|
|
|
22385
22386
|
"youtube_url"
|
|
22386
22387
|
]);
|
|
22387
22388
|
var PROJECTION_KEYS = [
|
|
22389
|
+
"id",
|
|
22388
22390
|
"slug",
|
|
22389
22391
|
"name",
|
|
22390
22392
|
"status",
|
|
@@ -22492,6 +22494,7 @@ var DocumentLiveSchema = exports_external.object({
|
|
|
22492
22494
|
var DocumentRowSchema = exports_external.union([DocumentLiveSchema, TombstoneSchema]);
|
|
22493
22495
|
var DocumentsPageResponseSchema = makePageResponseSchema("documents", DocumentLiveSchema);
|
|
22494
22496
|
var PROJECTION_KEYS2 = [
|
|
22497
|
+
"id",
|
|
22495
22498
|
"slug",
|
|
22496
22499
|
"revision",
|
|
22497
22500
|
"last_edited_at",
|
|
@@ -22502,6 +22505,7 @@ var PROJECTION_KEYS2 = [
|
|
|
22502
22505
|
"project_id"
|
|
22503
22506
|
];
|
|
22504
22507
|
var SERVER_AUTHORITATIVE_KEYS2 = new Set([
|
|
22508
|
+
"id",
|
|
22505
22509
|
"slug",
|
|
22506
22510
|
"revision",
|
|
22507
22511
|
"last_edited_at",
|
|
@@ -22631,6 +22635,7 @@ var MeetingLiveSchema = exports_external.object({
|
|
|
22631
22635
|
});
|
|
22632
22636
|
var MeetingsPageResponseSchema = makePageResponseSchema("meetings", MeetingLiveSchema);
|
|
22633
22637
|
var SERVER_AUTHORITATIVE_KEYS3 = new Set([
|
|
22638
|
+
"id",
|
|
22634
22639
|
"slug",
|
|
22635
22640
|
"revision",
|
|
22636
22641
|
"last_edited_at",
|
|
@@ -22658,6 +22663,7 @@ var WIRE_EDITABLE_TOP_LEVEL_KEYS3 = new Set([
|
|
|
22658
22663
|
"follow_up_summary"
|
|
22659
22664
|
]);
|
|
22660
22665
|
var PROJECTION_KEYS3 = [
|
|
22666
|
+
"id",
|
|
22661
22667
|
"slug",
|
|
22662
22668
|
"name",
|
|
22663
22669
|
"date",
|
|
@@ -23028,6 +23034,7 @@ var ProjectLiveSchema = exports_external.object({
|
|
|
23028
23034
|
});
|
|
23029
23035
|
var ProjectsPageResponseSchema = makePageResponseSchema("projects", ProjectLiveSchema);
|
|
23030
23036
|
var SERVER_AUTHORITATIVE_KEYS4 = new Set([
|
|
23037
|
+
"id",
|
|
23031
23038
|
"slug",
|
|
23032
23039
|
"revision",
|
|
23033
23040
|
"last_edited_at",
|
|
@@ -23048,6 +23055,7 @@ var WIRE_EDITABLE_TOP_LEVEL_KEYS4 = new Set([
|
|
|
23048
23055
|
"summary"
|
|
23049
23056
|
]);
|
|
23050
23057
|
var PROJECTION_KEYS4 = [
|
|
23058
|
+
"id",
|
|
23051
23059
|
"slug",
|
|
23052
23060
|
"name",
|
|
23053
23061
|
"category",
|
|
@@ -23198,6 +23206,7 @@ var TaskLiveSchema = exports_external.object({
|
|
|
23198
23206
|
});
|
|
23199
23207
|
var TasksPageResponseSchema = makePageResponseSchema("tasks", TaskLiveSchema);
|
|
23200
23208
|
var SERVER_AUTHORITATIVE_KEYS5 = new Set([
|
|
23209
|
+
"id",
|
|
23201
23210
|
"slug",
|
|
23202
23211
|
"revision",
|
|
23203
23212
|
"last_edited_at",
|
|
@@ -23222,6 +23231,7 @@ var WIRE_EDITABLE_TOP_LEVEL_KEYS5 = new Set([
|
|
|
23222
23231
|
"recurrence_end_date"
|
|
23223
23232
|
]);
|
|
23224
23233
|
var PROJECTION_KEYS5 = [
|
|
23234
|
+
"id",
|
|
23225
23235
|
"slug",
|
|
23226
23236
|
"name",
|
|
23227
23237
|
"status",
|
|
@@ -23392,6 +23402,7 @@ var ThreadLiveSchema = exports_external.object({
|
|
|
23392
23402
|
});
|
|
23393
23403
|
var ThreadsPageResponseSchema = makePageResponseSchema("threads", ThreadLiveSchema);
|
|
23394
23404
|
var SERVER_AUTHORITATIVE_KEYS6 = new Set([
|
|
23405
|
+
"id",
|
|
23395
23406
|
"slug",
|
|
23396
23407
|
"revision",
|
|
23397
23408
|
"last_edited_at",
|
|
@@ -23407,6 +23418,7 @@ var WIRE_EDITABLE_TOP_LEVEL_KEYS6 = new Set([
|
|
|
23407
23418
|
"artist_id"
|
|
23408
23419
|
]);
|
|
23409
23420
|
var PROJECTION_KEYS6 = [
|
|
23421
|
+
"id",
|
|
23410
23422
|
"slug",
|
|
23411
23423
|
"name",
|
|
23412
23424
|
"artist_id",
|
|
@@ -25874,7 +25886,69 @@ function renderFreshnessStamp(today, updatedAt) {
|
|
|
25874
25886
|
const age = days === 0 ? "today" : days === 1 ? "1 day ago" : `${days} days ago`;
|
|
25875
25887
|
return `This index was last updated on ${bankedDay}, ${age}.`;
|
|
25876
25888
|
}
|
|
25877
|
-
|
|
25889
|
+
var GROUNDING_BUDGET_BYTES = 20000;
|
|
25890
|
+
function bankedDate(line) {
|
|
25891
|
+
const matches = line.match(/\d{4}-\d{2}-\d{2}/g);
|
|
25892
|
+
if (matches === null || matches.length === 0)
|
|
25893
|
+
return null;
|
|
25894
|
+
let latest = matches[0];
|
|
25895
|
+
for (const m of matches)
|
|
25896
|
+
if (m > latest)
|
|
25897
|
+
latest = m;
|
|
25898
|
+
return latest;
|
|
25899
|
+
}
|
|
25900
|
+
function groupFacts(body) {
|
|
25901
|
+
const facts = [];
|
|
25902
|
+
for (const line of body.split(`
|
|
25903
|
+
`)) {
|
|
25904
|
+
if (line.trim().length === 0)
|
|
25905
|
+
continue;
|
|
25906
|
+
if (/^\s/.test(line) && facts.length > 0) {
|
|
25907
|
+
facts[facts.length - 1] = `${facts[facts.length - 1]}
|
|
25908
|
+
${line}`;
|
|
25909
|
+
} else {
|
|
25910
|
+
facts.push(line);
|
|
25911
|
+
}
|
|
25912
|
+
}
|
|
25913
|
+
return facts;
|
|
25914
|
+
}
|
|
25915
|
+
function selectRecentLines(body, capBytes) {
|
|
25916
|
+
const lines = groupFacts(body);
|
|
25917
|
+
if (Buffer.byteLength(body) <= capBytes) {
|
|
25918
|
+
return { text: body, kept: lines.length, total: lines.length };
|
|
25919
|
+
}
|
|
25920
|
+
const ranked = lines.map((line, index) => ({ line, index, date: bankedDate(line) })).sort((a, b) => {
|
|
25921
|
+
const ad = a.date ?? "";
|
|
25922
|
+
const bd = b.date ?? "";
|
|
25923
|
+
if (ad !== bd)
|
|
25924
|
+
return ad < bd ? 1 : -1;
|
|
25925
|
+
return b.index - a.index;
|
|
25926
|
+
});
|
|
25927
|
+
const chosen = [];
|
|
25928
|
+
let used = 0;
|
|
25929
|
+
for (const entry of ranked) {
|
|
25930
|
+
const add = Buffer.byteLength(entry.line) + (chosen.length > 0 ? 1 : 0);
|
|
25931
|
+
if (used + add > capBytes)
|
|
25932
|
+
break;
|
|
25933
|
+
chosen.push(entry.index);
|
|
25934
|
+
used += add;
|
|
25935
|
+
}
|
|
25936
|
+
chosen.sort((a, b) => a - b);
|
|
25937
|
+
return {
|
|
25938
|
+
text: chosen.map((i) => lines[i]).join(`
|
|
25939
|
+
`),
|
|
25940
|
+
kept: chosen.length,
|
|
25941
|
+
total: lines.length
|
|
25942
|
+
};
|
|
25943
|
+
}
|
|
25944
|
+
function trimmedIndexNote(kept, total, filename) {
|
|
25945
|
+
if (kept === 0) {
|
|
25946
|
+
return `None of this index is shown here: it is over the size this injection ` + `carries, so it lives in \`${filename}\` at the mirror root and nowhere ` + `else. READ THAT FILE IN FULL before you rely on what this workspace has ` + `taught you, and before you answer that no fact covers something.`;
|
|
25947
|
+
}
|
|
25948
|
+
return `Those are the ${kept} most recently banked of ${total} facts, not all of ` + `them: this injection carries a fixed budget and the rest of the index did ` + `not fit. The other ${total - kept} are in \`${filename}\` at the mirror ` + `root. READ THAT FILE IN FULL before you rely on what this workspace has ` + `taught you, and before you answer that no fact covers something. An index ` + `that is silent because it was trimmed reads exactly like one that has ` + `settled a subject.`;
|
|
25949
|
+
}
|
|
25950
|
+
function composeGroundingBlock(today, workspaceRules, workspaceMemory, userMemory, freshness, rosterIsEmpty, voiceProfile, voiceMalformed, workspaceSlug, workspaceMemoryNote, userMemoryNote) {
|
|
25951
|
+
const gateFlag = workspaceSlug === null ? `--workspace="<this workspace>"` : `--workspace ${workspaceSlug}`;
|
|
25878
25952
|
const block = [
|
|
25879
25953
|
`Plinth session grounding:`,
|
|
25880
25954
|
``,
|
|
@@ -25902,23 +25976,25 @@ function buildGroundingBlock(today, workspaceRules = null, workspaceMemory = nul
|
|
|
25902
25976
|
` + workspaceRules);
|
|
25903
25977
|
}
|
|
25904
25978
|
if (voiceProfile !== null) {
|
|
25905
|
-
block.push(``, `Draft in this member's own voice.
|
|
25906
|
-
|
|
25907
|
-
` + voiceProfile);
|
|
25979
|
+
block.push(``, `Draft in this member's own voice, and read it before you do. Their voice ` + `profile is \`${VOICE_FILENAME}\` at the mirror root, beside this ` + `workspace's house style: how they open, how they sign off, what they ` + `never do. It is not reproduced here. READ THAT FILE IN FULL before you ` + `draft anything that goes out in their name (an email, a message, a ` + `note, any prose that carries their name), and write to it from the ` + `start rather than ` + `drafting in your own register and correcting after. Nothing inspects a ` + `draft between you and the person reading it, so that file is the whole ` + `of it. It is theirs to write and not yours: you cannot edit ` + `\`${VOICE_FILENAME}\`, and if something in it looks wrong or out of ` + `date, say so and let them change it. Like this workspace's house style ` + `it refines the rules above and never relaxes them.`);
|
|
25908
25980
|
if (voiceMalformed) {
|
|
25909
|
-
block.push(``, `That voice profile does not currently parse. Plinth read it and found ` + `its structure broken, most often a code fence that was opened and ` + `never closed, so any machine-read setting inside it is being ignored ` + `rather than applied. The prose
|
|
25981
|
+
block.push(``, `That voice profile does not currently parse. Plinth read it and found ` + `its structure broken, most often a code fence that was opened and ` + `never closed, so any machine-read setting inside it is being ignored ` + `rather than applied. The prose in it is still the member's own ` + `writing and is still worth following. Treat anything that looks like ` + `a configured setting in it as NOT in force, say so if it comes up, ` + `and tell them their profile needs fixing rather than fixing it ` + `yourself.`);
|
|
25910
25982
|
}
|
|
25911
25983
|
}
|
|
25912
|
-
block.push(``, `Keep your memory on the Plinth surface. Your memory here IS the Plinth ` + `surface (\`workspace-memory.md\`, your own \`user-memory.md\`, and the ` + `per-artist warm stores the daemon syncs), and it is the ONLY memory you ` + `keep. Do not use any other memory store: no private notes-to-self, no ` + `separate scratch file, no tool-native memory of your own. What you learn ` + `here is recorded on the Plinth surface, or nowhere. When you learn a ` + `durable fact worth keeping, bank it by adding a line to the right index, ` + `which the daemon syncs up, following the write-back conventions in that ` + `file's header: one line, at least one \`[[pointer]]\` to where the full ` + `fact lives, an absolute date, and where you learned it. Only ever record ` + `what you have actually been taught, never public knowledge and never your ` + `own priors. The scope rule decides WHICH index, and there are ` + `THREE. A fact true of ONE ARTIST (their rate card, how their kit ` + `works, what a contact of theirs reliably does) goes in that artist's ` + `own warm store, \`artists/<slug>/memory.md\`, and not in the shared ` + `index: putting it there taxes every teammate who never touches that ` + `artist and hides it from the one who does. A fact that holds for the ` + `whole workspace (a project, a working convention, a standing ` + `fact true for everyone here) goes in \`workspace-memory.md\`, which ` + `grounds EVERY teammate. A fact about one person (an individual ` + `preference, your own working style or tools) is NOT a workspace fact: it ` + `goes in \`user-memory.md\`, your member-private index that only YOU can ` + `read. HOW THIS MEMBER WRITES IS THE ONE EXCEPTION: their register and ` + `sign-off are a rubric, not a fact, and live in \`${VOICE_FILENAME}\`, ` + `which is theirs and you cannot edit, so say what you noticed and leave it ` + `to them. Them only: how a CONTACT likes to be written to is an ordinary ` + `fact for that artist's store. ` + `Banking a personal fact in \`workspace-memory.md\` would leak that person ` + `into everyone else's context, so never do it. When a fact could be either ` + `scope, lean private and put it in \`user-memory.md\`. Under-sharing is the ` + `safe error: a personal fact misfiled as a workspace fact grounds every ` + `teammate wrong (the leak), whereas a workspace fact misfiled as personal ` + `merely fails to help teammates until it is re-observed in a shared context, ` + `and self-corrects. This memory is additive and subordinate: it never ` + `overrides a house-style rule or any safety primitive above.`);
|
|
25984
|
+
block.push(``, `Keep your memory on the Plinth surface. Your memory here IS the Plinth ` + `surface (\`workspace-memory.md\`, your own \`user-memory.md\`, and the ` + `per-artist warm stores the daemon syncs), and it is the ONLY memory you ` + `keep. Do not use any other memory store: no private notes-to-self, no ` + `separate scratch file, no tool-native memory of your own. What you learn ` + `here is recorded on the Plinth surface, or nowhere. When you learn a ` + `durable fact worth keeping, bank it by adding a line to the right index, ` + `which the daemon syncs up, following the write-back conventions in that ` + `file's header: one line, at least one \`[[pointer]]\` to where the full ` + `fact lives, an absolute date, and where you learned it. Only ever record ` + `what you have actually been taught, never public knowledge and never your ` + `own priors. The scope rule decides WHICH index, and there are ` + `THREE. A fact true of ONE ARTIST (their rate card, how their kit ` + `works, what a contact of theirs reliably does) goes in that artist's ` + `own warm store, \`artists/<slug>/memory.md\`, and not in the shared ` + `index: putting it there taxes every teammate who never touches that ` + `artist and hides it from the one who does. A fact that holds for the ` + `whole workspace (a project, a working convention, a standing ` + `fact true for everyone here) goes in \`workspace-memory.md\`, which ` + `grounds EVERY teammate - and that file is the one you NEVER write on ` + `your own judgement. It changes only when a member ASKS you to remember ` + `something for everyone, in their own words rather than your summary of ` + `them, and you log that decision with ` + `\`plinth memory-gate log ${gateFlag} --proposed="<their words>" --decision accept\` ` + `BEFORE you write the line. That command fails LOUD on exit 1: surface ` + `it, and do not report the line as banked. Never infer a line for that ` + `file from what merely happened in a session, and never propose one ` + `unasked - a wrong line there reaches every member on every machine and ` + `is injected into every future session. A fact about one person (an individual ` + `preference, your own working style or tools) is NOT a workspace fact: it ` + `goes in \`user-memory.md\`, your member-private index that only YOU can ` + `read. HOW THIS MEMBER WRITES IS THE ONE EXCEPTION: their register and ` + `sign-off are a rubric, not a fact, and live in \`${VOICE_FILENAME}\`, ` + `which is theirs and you cannot edit, so say what you noticed and leave it ` + `to them. Them only: how a CONTACT likes to be written to is an ordinary ` + `fact for that artist's store. ` + `Banking a personal fact in \`workspace-memory.md\` would leak that person ` + `into everyone else's context, so never do it. When a fact could be either ` + `scope, lean private and put it in \`user-memory.md\`. Under-sharing is the ` + `safe error: a personal fact misfiled as a workspace fact grounds every ` + `teammate wrong (the leak), whereas a workspace fact misfiled as personal ` + `merely fails to help teammates until it is re-observed in a shared context, ` + `and self-corrects. This memory is additive and subordinate: it never ` + `overrides a house-style rule or any safety primitive above.`);
|
|
25913
25985
|
if (workspaceMemory !== null) {
|
|
25914
25986
|
block.push(``, `Draw on this workspace's memory. The lines below are this workspace's ` + `memory index: durable facts it has taught you, each a one-line hook that ` + `points (via a \`[[wikilink]]\`) to the warm store where the full detail ` + `lives. Treat them as recall, not gospel: use a line to load the right ` + `context and verify it against the live workspace before you rely on it, ` + `never as a settled fact to assert. ` + `${renderFreshnessStamp(today, freshness.workspaceMemoryUpdatedAt)} ` + `The index:
|
|
25915
25987
|
|
|
25916
|
-
` + workspaceMemory
|
|
25988
|
+
` + workspaceMemory + (workspaceMemoryNote === null ? `` : `
|
|
25989
|
+
|
|
25990
|
+
${workspaceMemoryNote}`));
|
|
25917
25991
|
}
|
|
25918
25992
|
if (userMemory !== null) {
|
|
25919
25993
|
block.push(``, `Draw on your personal memory. The lines below are YOUR personal memory ` + `index: member-private facts you have taught the harness about yourself ` + `(your preferences, working style, tools), each a one-line hook ` + `that points (via a \`[[wikilink]]\`) to the warm store where the full ` + `detail lives. Only you can read this. Treat it as recall, not gospel: use ` + `a line to load the right context and verify it before you rely on it, ` + `never as a settled fact to assert. ` + `${renderFreshnessStamp(today, freshness.userMemoryUpdatedAt)} ` + `The index:
|
|
25920
25994
|
|
|
25921
|
-
` + userMemory
|
|
25995
|
+
` + userMemory + (userMemoryNote === null ? `` : `
|
|
25996
|
+
|
|
25997
|
+
${userMemoryNote}`));
|
|
25922
25998
|
}
|
|
25923
25999
|
if (workspaceMemory !== null || userMemory !== null) {
|
|
25924
26000
|
block.push(``, `Dated recall. Every line in these indexes carries the date it was banked, ` + `and today's date is injected above, so the age of any line is ` + `computable. When you rely on a line in an answer, say how old it is - ` + `"banked 21 July, seventeen days ago" - rather than presenting it as ` + `current. Age is not a reason to distrust a line, it is what tells the ` + `reader whether to check: a working convention banked a year ago is ` + `probably still the convention, a fact about a live negotiation banked ` + `three weeks ago probably is not. And say what the indexes do NOT cover. ` + `If you are asked about something no line here addresses, say so plainly ` + `rather than answering only from what happens to be present - an index ` + `that is silent on a subject reads exactly like one that has settled it.`);
|
|
@@ -25930,6 +26006,59 @@ function buildGroundingBlock(today, workspaceRules = null, workspaceMemory = nul
|
|
|
25930
26006
|
return block.join(`
|
|
25931
26007
|
`);
|
|
25932
26008
|
}
|
|
26009
|
+
function buildGroundingReport(today, workspaceRules = null, workspaceMemory = null, userMemory = null, freshness = {}, rosterIsEmpty = false, voiceProfile = null, voiceMalformed = false, workspaceSlug = null) {
|
|
26010
|
+
const compose = (wmBody, umBody, wmNote, umNote) => composeGroundingBlock(today, workspaceRules, wmBody, umBody, freshness, rosterIsEmpty, voiceProfile, voiceMalformed, workspaceSlug, wmNote, umNote);
|
|
26011
|
+
const whole = compose(workspaceMemory, userMemory, null, null);
|
|
26012
|
+
const untrimmed = () => ({
|
|
26013
|
+
block: whole,
|
|
26014
|
+
picks: {
|
|
26015
|
+
wmPick: workspaceMemory === null ? null : { text: workspaceMemory, kept: groupFacts(workspaceMemory).length, total: groupFacts(workspaceMemory).length },
|
|
26016
|
+
umPick: userMemory === null ? null : { text: userMemory, kept: groupFacts(userMemory).length, total: groupFacts(userMemory).length }
|
|
26017
|
+
}
|
|
26018
|
+
});
|
|
26019
|
+
if (Buffer.byteLength(whole) <= GROUNDING_BUDGET_BYTES)
|
|
26020
|
+
return untrimmed();
|
|
26021
|
+
if (workspaceMemory === null && userMemory === null)
|
|
26022
|
+
return untrimmed();
|
|
26023
|
+
const factCount = (body) => body === null ? 0 : groupFacts(body).length;
|
|
26024
|
+
const wmFacts = factCount(workspaceMemory);
|
|
26025
|
+
const umFacts = factCount(userMemory);
|
|
26026
|
+
const measureFixed = (wmNoted, umNoted) => Buffer.byteLength(compose(workspaceMemory === null ? null : "", userMemory === null ? null : "", wmNoted ? trimmedIndexNote(wmFacts, wmFacts, WORKSPACE_MEMORY_FILENAME) : null, umNoted ? trimmedIndexNote(umFacts, umFacts, USER_MEMORY_FILENAME) : null));
|
|
26027
|
+
const wmSize = workspaceMemory === null ? 0 : Buffer.byteLength(workspaceMemory);
|
|
26028
|
+
const umSize = userMemory === null ? 0 : Buffer.byteLength(userMemory);
|
|
26029
|
+
const allocate = (remaining) => {
|
|
26030
|
+
const half = Math.floor(remaining / 2);
|
|
26031
|
+
let umCap;
|
|
26032
|
+
let wmCap;
|
|
26033
|
+
if (umSize <= half) {
|
|
26034
|
+
umCap = umSize;
|
|
26035
|
+
wmCap = remaining - umSize;
|
|
26036
|
+
} else if (wmSize <= half) {
|
|
26037
|
+
wmCap = wmSize;
|
|
26038
|
+
umCap = remaining - wmSize;
|
|
26039
|
+
} else {
|
|
26040
|
+
umCap = half;
|
|
26041
|
+
wmCap = remaining - half;
|
|
26042
|
+
}
|
|
26043
|
+
const umPick = userMemory === null ? null : selectRecentLines(userMemory, umCap);
|
|
26044
|
+
const umUsed = umPick === null ? 0 : Buffer.byteLength(umPick.text);
|
|
26045
|
+
return {
|
|
26046
|
+
wmPick: workspaceMemory === null ? null : selectRecentLines(workspaceMemory, remaining - umUsed),
|
|
26047
|
+
umPick
|
|
26048
|
+
};
|
|
26049
|
+
};
|
|
26050
|
+
const trimmed = (pick2) => pick2 !== null && pick2.kept !== pick2.total;
|
|
26051
|
+
const noteFor = (pick2, filename) => pick2 === null || pick2.kept === pick2.total ? null : trimmedIndexNote(pick2.kept, pick2.total, filename);
|
|
26052
|
+
const build = (picks) => compose(picks.wmPick === null ? null : picks.wmPick.text, picks.umPick === null ? null : picks.umPick.text, noteFor(picks.wmPick, WORKSPACE_MEMORY_FILENAME), noteFor(picks.umPick, USER_MEMORY_FILENAME));
|
|
26053
|
+
const conservative = allocate(Math.max(0, GROUNDING_BUDGET_BYTES - measureFixed(true, true)));
|
|
26054
|
+
const refined = allocate(Math.max(0, GROUNDING_BUDGET_BYTES - Buffer.byteLength(compose(workspaceMemory === null ? null : "", userMemory === null ? null : "", noteFor(conservative.wmPick, WORKSPACE_MEMORY_FILENAME), noteFor(conservative.umPick, USER_MEMORY_FILENAME)))));
|
|
26055
|
+
const kept = (p) => (p.wmPick?.kept ?? 0) + (p.umPick?.kept ?? 0);
|
|
26056
|
+
const candidates = [refined, conservative].map((picks) => ({ picks, text: build(picks) })).map((c) => ({ ...c, bytes: Buffer.byteLength(c.text), facts: kept(c.picks) }));
|
|
26057
|
+
const fitting = candidates.filter((c) => c.bytes <= GROUNDING_BUDGET_BYTES);
|
|
26058
|
+
const best = fitting.length > 0 ? fitting.reduce((a, b) => b.facts > a.facts ? b : a) : candidates.reduce((a, b) => b.bytes < a.bytes ? b : a);
|
|
26059
|
+
return { block: best.text, picks: best.picks };
|
|
26060
|
+
}
|
|
26061
|
+
var buildGroundingReportFor = buildGroundingReport;
|
|
25933
26062
|
function formatLocalDate(d) {
|
|
25934
26063
|
const year = d.getFullYear();
|
|
25935
26064
|
const month = String(d.getMonth() + 1).padStart(2, "0");
|
|
@@ -26558,6 +26687,9 @@ function resolveVoiceMalformed(raw, record2) {
|
|
|
26558
26687
|
return stableHash(raw) === record2.body_hash;
|
|
26559
26688
|
}
|
|
26560
26689
|
function runGrounding(deps = {}) {
|
|
26690
|
+
return runGroundingReport(deps).block;
|
|
26691
|
+
}
|
|
26692
|
+
function runGroundingReport(deps = {}) {
|
|
26561
26693
|
const now = deps.now ?? (() => new Date);
|
|
26562
26694
|
const readWorkspaceRules = deps.readWorkspaceRules ?? readWorkspaceRulesFile;
|
|
26563
26695
|
const readWorkspaceMemory = deps.readWorkspaceMemory ?? readWorkspaceMemoryFile;
|
|
@@ -26575,34 +26707,35 @@ function runGrounding(deps = {}) {
|
|
|
26575
26707
|
userVoice: null
|
|
26576
26708
|
};
|
|
26577
26709
|
let noArtists = false;
|
|
26578
|
-
|
|
26710
|
+
const workspaceSlug = isValidWorkspaceSlug(deps.workspaceSlug ?? "") ? deps.workspaceSlug : null;
|
|
26711
|
+
if (workspaceSlug) {
|
|
26579
26712
|
try {
|
|
26580
|
-
rawRules = readWorkspaceRules(
|
|
26713
|
+
rawRules = readWorkspaceRules(workspaceSlug);
|
|
26581
26714
|
} catch {
|
|
26582
26715
|
rawRules = null;
|
|
26583
26716
|
}
|
|
26584
26717
|
try {
|
|
26585
|
-
rawMemory = readWorkspaceMemory(
|
|
26718
|
+
rawMemory = readWorkspaceMemory(workspaceSlug);
|
|
26586
26719
|
} catch {
|
|
26587
26720
|
rawMemory = null;
|
|
26588
26721
|
}
|
|
26589
26722
|
try {
|
|
26590
|
-
rawUserMemory = readUserMemory(
|
|
26723
|
+
rawUserMemory = readUserMemory(workspaceSlug);
|
|
26591
26724
|
} catch {
|
|
26592
26725
|
rawUserMemory = null;
|
|
26593
26726
|
}
|
|
26594
26727
|
try {
|
|
26595
|
-
rawVoiceProfile = readVoiceProfile(
|
|
26728
|
+
rawVoiceProfile = readVoiceProfile(workspaceSlug);
|
|
26596
26729
|
} catch {
|
|
26597
26730
|
rawVoiceProfile = null;
|
|
26598
26731
|
}
|
|
26599
26732
|
try {
|
|
26600
|
-
freshness = readFreshness(
|
|
26733
|
+
freshness = readFreshness(workspaceSlug);
|
|
26601
26734
|
} catch {
|
|
26602
26735
|
freshness = { workspaceMemory: null, userMemory: null, userVoice: null };
|
|
26603
26736
|
}
|
|
26604
26737
|
try {
|
|
26605
|
-
noArtists = readRosterIsEmpty(
|
|
26738
|
+
noArtists = readRosterIsEmpty(workspaceSlug);
|
|
26606
26739
|
} catch {
|
|
26607
26740
|
noArtists = false;
|
|
26608
26741
|
}
|
|
@@ -26611,10 +26744,10 @@ function runGrounding(deps = {}) {
|
|
|
26611
26744
|
const memory = extractWorkspaceMemory(rawMemory);
|
|
26612
26745
|
const userMemory = extractUserMemory(rawUserMemory);
|
|
26613
26746
|
const voiceProfile = extractVoiceProfile(rawVoiceProfile);
|
|
26614
|
-
return
|
|
26747
|
+
return buildGroundingReportFor(formatLocalDate(now()), rules, memory, userMemory, {
|
|
26615
26748
|
workspaceMemoryUpdatedAt: resolveUpdatedAt(rawMemory, freshness.workspaceMemory),
|
|
26616
26749
|
userMemoryUpdatedAt: resolveUpdatedAt(rawUserMemory, freshness.userMemory)
|
|
26617
|
-
}, noArtists, voiceProfile, resolveVoiceMalformed(rawVoiceProfile, freshness.userVoice));
|
|
26750
|
+
}, noArtists, voiceProfile, resolveVoiceMalformed(rawVoiceProfile, freshness.userVoice), workspaceSlug);
|
|
26618
26751
|
}
|
|
26619
26752
|
var groundingCommand = defineCommand({
|
|
26620
26753
|
meta: {
|
|
@@ -27878,7 +28011,7 @@ function buildBody(input) {
|
|
|
27878
28011
|
lines.push(`- Closed/archived, meetings & full lists: \`mcp__plinth__list_projects\` / \`mcp__plinth__list_tasks\`${artistIdHint(uuid3)}`);
|
|
27879
28012
|
lines.push(`- Schema: ${MCP_INTROSPECTION_POINTER}`);
|
|
27880
28013
|
lines.push(`- Long-term facts → \`./memory.md\`, which syncs to this artist's Plinth record. Most artists have none yet: the file appears once the record carries memory, and creating it here is how you start one. One line per fact with its source and an absolute date; when a fact goes wrong, edit that line rather than adding a second`);
|
|
27881
|
-
lines.push(`-
|
|
28014
|
+
lines.push(`- What a session did → a dated section under \`## Session log\` at the bottom of \`./_artist.md\` but ABOVE any \`<!-- BEGIN: contacts -->\` block, which is rendered from Plinth and overwrites anything inside it on the next sync. That body syncs, so a teammate opens the record in the app. Keep the five most recent; a fact that outlives them belongs in \`./memory.md\` above. \`./daily-log.md\` is local scratch, never synced; an older one may hold real history, and nothing writes there now`);
|
|
27882
28015
|
lines.push("");
|
|
27883
28016
|
lines.push(USER_BLOCK_BEGIN);
|
|
27884
28017
|
lines.push(USER_BLOCK_PLACEHOLDER);
|
|
@@ -28479,8 +28612,8 @@ function ensureStub(workspaceSlug, relativePath, content) {
|
|
|
28479
28612
|
function dailyLogStub(name) {
|
|
28480
28613
|
return `# ${name} - Daily log
|
|
28481
28614
|
|
|
28482
|
-
` + `<!--
|
|
28483
|
-
` + `<!--
|
|
28615
|
+
` + `<!-- Your own scratch notes; edit freely, Plinth never overwrites this file. -->
|
|
28616
|
+
` + `<!-- Nothing writes here now: a session's record goes under ## Session log in ./_artist.md, and durable facts in ./memory.md. Both sync; this file does not. -->
|
|
28484
28617
|
`;
|
|
28485
28618
|
}
|
|
28486
28619
|
function backupStamp(d) {
|
|
@@ -28572,7 +28705,7 @@ function buildStartCommandMd(rosterIsEmpty2) {
|
|
|
28572
28705
|
"",
|
|
28573
28706
|
"## Loading area context",
|
|
28574
28707
|
"",
|
|
28575
|
-
"Once the user names something, resolve it against the workspace before answering: search the Plinth MCP, or read the roster in `CLAUDE.md`. **Never answer from name recognition alone.** Then read the surfaces that carry it. An artist's master context is `artists/<slug>/_artist.md`, their durable facts are `artists/<slug>/memory.md
|
|
28708
|
+
"Once the user names something, resolve it against the workspace before answering: search the Plinth MCP, or read the roster in `CLAUDE.md`. **Never answer from name recognition alone.** Then read the surfaces that carry it. An artist's master context is `artists/<slug>/_artist.md`, whose `## Session log` carries what recent sessions did; their durable facts are `artists/<slug>/memory.md`. Older local history may sit in `artists/<slug>/daily-log.md`, which nothing writes to any more. Brief on what is live, not on what you opened.",
|
|
28576
28709
|
"",
|
|
28577
28710
|
"If nothing resolves, say so plainly and ask. Do not invent an entity.",
|
|
28578
28711
|
"",
|
|
@@ -28587,7 +28720,7 @@ function buildCloseCommandMd(workspaceSlug) {
|
|
|
28587
28720
|
const ws = `--workspace ${workspaceSlug}`;
|
|
28588
28721
|
return withUserBlock([
|
|
28589
28722
|
"---",
|
|
28590
|
-
"description: Close a Plinth session. Verifies what you wrote landed, routes follow-ups to a durable surface,
|
|
28723
|
+
"description: Close a Plinth session. Verifies what you wrote landed, routes follow-ups to a durable surface, and banks what was learned where it belongs.",
|
|
28591
28724
|
"---",
|
|
28592
28725
|
"",
|
|
28593
28726
|
OWNERSHIP_NOTE,
|
|
@@ -28600,7 +28733,7 @@ function buildCloseCommandMd(workspaceSlug) {
|
|
|
28600
28733
|
"",
|
|
28601
28734
|
"## Step 1: reconstruct the session",
|
|
28602
28735
|
"",
|
|
28603
|
-
"From the **transcript**, not from your memory of it. Which entities were touched, which writes were made, what was decided, what was learned. If the session was pure reading with no changes, say so and
|
|
28736
|
+
"From the **transcript**, not from your memory of it. Which entities were touched, which writes were made, what was decided, what was learned. If the session was pure reading with no changes, say so and **work through every step below anyway.** A session that answered a real question has a record worth banking in Step 3, and reading can teach you a durable fact as readily as writing can.",
|
|
28604
28737
|
"",
|
|
28605
28738
|
"## Step 2: verify each write landed",
|
|
28606
28739
|
"",
|
|
@@ -28628,11 +28761,27 @@ function buildCloseCommandMd(workspaceSlug) {
|
|
|
28628
28761
|
"",
|
|
28629
28762
|
"- **Something to do** goes to a Plinth task, scoped to its artist or project, with an owner and a real due date where one exists. Fold it into the existing umbrella task rather than adding a second.",
|
|
28630
28763
|
"- **State or context that changed** goes to the artist's master context, the body of `artists/<slug>/_artist.md`, or to the project's summary. Prose goes through the mirror file.",
|
|
28631
|
-
"- **What happened
|
|
28764
|
+
"- **What happened this session** goes to the artist's own record: a dated section in the body of `artists/<slug>/_artist.md`, one per artist touched. Prose, the way a hub carries it - what was discussed, what was decided, what was done, what is still open. **This is the half a teammate can open.** That body syncs to the artist's Plinth record and the app renders it in the Knowledge Base context pane, so a session banked here is one somebody else can pick up cold. **It is a record of a session, not a statement of where things stand**: current state belongs in the prose above it, edited in place, which is the bullet before this one. A log that accumulates live state rots, and that is the failure this step was moved to escape.",
|
|
28765
|
+
"",
|
|
28766
|
+
"**Where the record goes in the file, and it is not the top.** One `## Session log` heading at the **bottom** of the body, newest section first beneath it. The pane renders the whole body, so the master context has to read first and the log sits under it. If the file carries a `<!-- BEGIN: contacts -->` block, the log goes **above** that marker: everything between the contacts markers is rendered from Plinth and is overwritten on the next sync.",
|
|
28767
|
+
"",
|
|
28768
|
+
"```md",
|
|
28769
|
+
"## Session log",
|
|
28770
|
+
"",
|
|
28771
|
+
"### 2026-03-14",
|
|
28772
|
+
"",
|
|
28773
|
+
"<what was discussed, what was decided, what was done, what is still open>",
|
|
28774
|
+
"```",
|
|
28775
|
+
"",
|
|
28776
|
+
"**Re-read `_artist.md` at the moment you write it, and add only your own dated section.** Other people close sessions in this workspace too, and this file syncs: when two closes touch one artist at once the cloud copy wins and the other edit is set aside, seconds after you wrote it. **Step 2's conflicts read cannot catch that: it ran before this write.** Step 5 re-reads it, and that is the check covering what you write here. So read the file now rather than trusting what it said earlier in the session, and never reword a section another session wrote.",
|
|
28777
|
+
"",
|
|
28778
|
+
"**Keep the five most recent dated sections.** Adding a sixth means dropping the oldest - but first take anything in it that is still true and write it into `artists/<slug>/memory.md`, where durable facts live. If nothing in it survives that test it was not durable, and dropping it costs nothing. **Removing an old section is the one edit you may make to someone else's: you may drop it, never reword it.** Without this bound the record grows without limit on a synced column, and every teammate loads the whole of it on every session.",
|
|
28779
|
+
"",
|
|
28780
|
+
"**A session that touched no artist writes one dated note under `documents/` instead**, with the same content. That is a new file rather than an edit to a tracked one, so it reaches the cloud only with the sync running - Step 2 is where you already saw whether it is.",
|
|
28632
28781
|
"",
|
|
28633
28782
|
"## Step 4: bank what was learned, by type first and scope second",
|
|
28634
28783
|
"",
|
|
28635
|
-
"**Two sorts, in this order, and the order is the whole of it.** First: does this fact already have a surface of its own? Then, and only then: whose fact is it? Sorting by scope alone is how a contact record ends up in
|
|
28784
|
+
"**Two sorts, in this order, and the order is the whole of it.** First: does this fact already have a surface of its own? Then, and only then: whose fact is it? Sorting by scope alone is how a contact record ends up in a memory file wearing a memory line's clothes.",
|
|
28636
28785
|
"",
|
|
28637
28786
|
"### Step 4a: type first. A fact with a typed home never becomes prose.",
|
|
28638
28787
|
"",
|
|
@@ -28640,7 +28789,7 @@ function buildCloseCommandMd(workspaceSlug) {
|
|
|
28640
28789
|
"",
|
|
28641
28790
|
"- **A person and their details** - name, company, email, role, how they relate to the work - is a **contact on that artist**. Write it with `create_artist_contact`; it renders in the `## Contacts` block of that artist's `artists/<slug>/_artist.md`.",
|
|
28642
28791
|
"- **Something to do** is a task, and Step 3 has already routed it.",
|
|
28643
|
-
"- **A note, brief or bio that is a piece of writing in its own right** is a document under `documents/`. That means standalone material
|
|
28792
|
+
"- **A note, brief or bio that is a piece of writing in its own right** is a document under `documents/`. That means standalone material somebody would open and read on its own, not a fact you are recording because you learned it - a fact is a memory line, and Step 4b routes it.",
|
|
28644
28793
|
"- **A stable attribute of an artist** - their website, a handle, their bio - is that artist's own field, or the body of `artists/<slug>/_artist.md`.",
|
|
28645
28794
|
"- **A contract, statement or attachment** is a file under `files/`.",
|
|
28646
28795
|
"- **A date is live state and belongs in no memory file.** A deadline rides its task as a due date. Every other date is a calendar matter: say plainly what the date is, say that a person adds it in Plinth's calendar view, and name it in the closing summary so it is not quietly dropped.",
|
|
@@ -28649,19 +28798,19 @@ function buildCloseCommandMd(workspaceSlug) {
|
|
|
28649
28798
|
"",
|
|
28650
28799
|
"Whatever is left has no typed surface anywhere. That is what memory is for.",
|
|
28651
28800
|
"",
|
|
28652
|
-
"### Step 4b:
|
|
28801
|
+
"### Step 4b: two piles, and the artist pile comes first",
|
|
28653
28802
|
"",
|
|
28654
|
-
"**Most of what a session learns is true of exactly one artist**, and that is the pile that gets forgotten.
|
|
28803
|
+
"**Most of what a session learns is true of exactly one artist**, and that is the pile that gets forgotten. The other is true of the member in front of you. Write both directly: they are scoped to the person or the act they describe, and a wrong line in either is theirs to prune.",
|
|
28655
28804
|
"",
|
|
28656
28805
|
"**The sorting test: would a teammate who never touches that artist be worse off without this line?** If not, it is the artist pile.",
|
|
28657
28806
|
"",
|
|
28658
|
-
"
|
|
28807
|
+
"**A fact that is true of neither - a way this whole team works - is named in the closing summary and written nowhere by this close.** Asking for something to hold for everyone here is the member's to do, not yours to infer from what happened. This close has no third pile, and that is deliberate rather than an omission.",
|
|
28659
28808
|
"",
|
|
28660
28809
|
"#### True of one artist: `artists/<slug>/memory.md`. Write it directly.",
|
|
28661
28810
|
"",
|
|
28662
|
-
"That artist's rate card, a contact of theirs whose behaviour you had to learn, how their kit actually works, what a venue reliably does.
|
|
28811
|
+
"That artist's rate card, a contact of theirs whose behaviour you had to learn, how their kit actually works, what a venue reliably does. It syncs to that artist's Plinth record, so a teammate opening that artist gets it. **List what you wrote here in the closing summary.**",
|
|
28663
28812
|
"",
|
|
28664
|
-
"Follow the conventions in the file's own header
|
|
28813
|
+
"**Follow the conventions in the file's own header comment**, which states them and is the version that stays current. Do not work from a copy of them here.",
|
|
28665
28814
|
"",
|
|
28666
28815
|
"**Re-read the file at the moment you write it, not when you were planning.** The daemon can pull a `memory.md` down from Plinth at any point in a session, so a file that was missing minutes ago may exist now - and if it does, add your line to it rather than creating it over the top. This is not hypothetical: a close on 26 Aug 2026 checked, found nothing, and clobbered a copy the daemon had pulled in ninety seconds earlier.",
|
|
28667
28816
|
"",
|
|
@@ -28673,72 +28822,34 @@ function buildCloseCommandMd(workspaceSlug) {
|
|
|
28673
28822
|
"<!-- Durable facts about <Artist> that hold across sessions. One line per fact, with",
|
|
28674
28823
|
" provenance and an absolute date. SUPERSEDE, do not accumulate - when a fact here",
|
|
28675
28824
|
" becomes wrong, edit that line in place rather than adding a contradicting one.",
|
|
28676
|
-
" Live state does NOT belong here - that goes in
|
|
28825
|
+
" Live state does NOT belong here - that goes in the body of `_artist.md`. -->",
|
|
28677
28826
|
"```",
|
|
28678
28827
|
"",
|
|
28679
|
-
"**Live state does not go here.** This week's open threads, one show's logistics, where a release has got to: that is Step 3's business, and it goes
|
|
28828
|
+
"**Live state does not go here.** This week's open threads, one show's logistics, where a release has got to: that is Step 3's business, and it goes in the body of `artists/<slug>/_artist.md`. This file is for what stays true after the session that taught it.",
|
|
28680
28829
|
"",
|
|
28681
28830
|
"#### True of this member: `user-memory.md` at the mirror root. Write it directly.",
|
|
28682
28831
|
"",
|
|
28683
|
-
"Facts true of this member: what they prefer, how they work, their own identifiers.
|
|
28832
|
+
"Facts true of this member: what they prefer, how they work, their own identifiers. **List what you wrote in the closing summary**, so it is visible without opening the file.",
|
|
28684
28833
|
"",
|
|
28685
28834
|
"**How they WRITE does not go here, and it is not a fact.** Their register, greetings, sign-off and the things they never do are a rubric, and they live in `voice.md` at the mirror root - a file the member owns and you are denied from editing, because the profile you are judged against is not yours to rewrite. So do not bank a voice rule in this index or any other. Say what you noticed in the closing summary and leave it for them to put in their own profile.",
|
|
28686
28835
|
"",
|
|
28687
28836
|
"If the file is not there yet, create it the same way as the artist store: a one-line title, the same conventions in a comment, and a real fact in the same edit. **Check what the workspace already says before you write a personal line** - a convention that is already in `workspace-rules.md` or the workspace's own house-style file is in force for everyone, and copying it here duplicates a live rule into a second injected surface instead of adding anything.",
|
|
28688
28837
|
"",
|
|
28689
|
-
"
|
|
28690
|
-
"",
|
|
28691
|
-
"**This is the only gated pile**, and a fact that names a single artist is almost never in it. Draft the candidate lines first: durable facts about this workspace that would change how a future session behaves, not this session's detail.",
|
|
28692
|
-
"",
|
|
28693
|
-
"**Each line has a required shape, and it is the file's own, not this command's.** `workspace-memory.md` is an INDEX, and the grounding block tells every future session that each line is a one-line hook pointing via a `[[wikilink]]` to the warm store where the full detail lives. So a line carries: the hook, a `[[wikilink]]` to where the detail actually is, an **absolute** date (turn \"last week\" into a real one), and what taught you it. **A line with no warm store behind it makes the grounding block lie to every session that loads it.** Read the file's header comment and follow the conventions there; where it and this command disagree, the file wins.",
|
|
28838
|
+
"## Step 5: summarise, then end the session",
|
|
28694
28839
|
"",
|
|
28695
|
-
"**
|
|
28696
|
-
"",
|
|
28697
|
-
"Then put the candidates to the user with **`AskUserQuestion`**:",
|
|
28698
|
-
"",
|
|
28699
|
-
"- **One multi-select question, one option per proposed line.** Selected means bank it. Unselected means rejected.",
|
|
28700
|
-
"- **With exactly one candidate, ask a two-option question instead: bank it, or leave it.** The tool refuses a question carrying fewer than two options, so the one-option-per-line form above cannot run on its own - and one candidate is the ordinary case, not an edge. Record the answer exactly as you would from the multi-select.",
|
|
28701
|
-
'- **The free-text "Other" choice is how the user rewrites a line in their own words.** Their wording wins over yours, always.',
|
|
28702
|
-
"- The tool takes **four options per question and four questions**. If you have more than four candidate lines, you are banking session detail rather than durable facts: **say so and cut the list. Do not paginate.**",
|
|
28703
|
-
"",
|
|
28704
|
-
"**An accepted line that contradicts one already in the file supersedes it in place.** Edit the wrong line. Never leave two lines disagreeing.",
|
|
28705
|
-
"",
|
|
28706
|
-
"## Step 5: log every decision, then write the file",
|
|
28707
|
-
"",
|
|
28708
|
-
"**In that order.** The log is the record of what was decided; the file is the consequence.",
|
|
28840
|
+
"**First, re-read the conflicts list.** Everything this close wrote - the session record in Step 3, the memory files in Step 4 - landed AFTER Step 2 looked, and a losing write is set aside within seconds of being made. Step 2 therefore cannot have seen it. Read the list again now:",
|
|
28709
28841
|
"",
|
|
28710
28842
|
"```sh",
|
|
28711
|
-
`plinth
|
|
28712
|
-
`plinth memory-gate log ${ws} --proposed="<the line as you put it>" --decision reject`,
|
|
28713
|
-
`plinth memory-gate log ${ws} --proposed="<the line as you put it>" --decision edit --final="<their rewrite>"`,
|
|
28714
|
-
"```",
|
|
28715
|
-
"",
|
|
28716
|
-
"A close that proposed nothing still logs, because *gated and proposed nothing* has to stay distinguishable from *never gated*:",
|
|
28717
|
-
"",
|
|
28718
|
-
"```sh",
|
|
28719
|
-
`plinth memory-gate log ${ws} --nothing-proposed`,
|
|
28843
|
+
`plinth status ${ws}`,
|
|
28720
28844
|
"```",
|
|
28721
28845
|
"",
|
|
28722
|
-
|
|
28723
|
-
"",
|
|
28724
|
-
"**The `=` in `--proposed=` and `--final=` is load-bearing. Do not rewrite it as a space.** A `workspace-memory.md` line is a markdown bullet, so the value starts with `- ` - and in the space form the parser swallows a value beginning with a dash, leaving the command to report that `--proposed` carried nothing. Measured 26 Aug 2026: the space form fails on exactly the lines this step exists to log, the equals form takes them. The same applies to any line starting with `--`.",
|
|
28725
|
-
"",
|
|
28726
|
-
"**`--workspace` is pinned on these commands on purpose. Do not drop it.** The CLI otherwise resolves whichever workspace is *active* on this machine, not the mirror you are standing in - so a close run here would write its decisions into a different workspace's log, silently and with exit 0. That log has no delete and no edit, so the wrong row is permanent.",
|
|
28727
|
-
"",
|
|
28728
|
-
"**Four rules. Break one and a decision is lost:**",
|
|
28846
|
+
`And \`${CONFLICTS_VIEW_REL_PATH}\` beside it, which carries a restore command per file.`,
|
|
28729
28847
|
"",
|
|
28730
|
-
"
|
|
28731
|
-
"2. `--final` is **refused** on `accept` and `reject`. An accept records the proposed line; a reject records none.",
|
|
28732
|
-
"3. `--nothing-proposed` **cannot** be combined with `--proposed` or `--decision`.",
|
|
28733
|
-
'4. **`memory-gate` fails LOUD**: exit 1, with a message on stderr. **Surface it. Never swallow it.** A silently dropped decision produces exactly the "never gated" state the log exists to make visible, while looking like a clean close. If it fails, tell the user, and do not report the line as banked.',
|
|
28848
|
+
"**Where those two disagree here, believe the command.** Its Conflicts line is read straight off the folder on disk at the moment you run it. The file is a projection the sync rewrites at the end of a pull, so at this point in a close - seconds after your own write, possibly before the next poll - it can still be showing the list from before. That is the one moment in this ritual where the file is the more likely of the two to be stale, which is the opposite of Step 2.",
|
|
28734
28849
|
"",
|
|
28735
|
-
"**
|
|
28850
|
+
"**Anything named there was NOT banked, whatever the write appeared to return.** Say so plainly, name the file, and point the user at the restore command rather than reporting the work as done. A close that reports a stashed session record as banked is the one failure this step exists to prevent, because the record was the point of the close.",
|
|
28736
28851
|
"",
|
|
28737
|
-
"
|
|
28738
|
-
"",
|
|
28739
|
-
"## Step 6: summarise, then end the session",
|
|
28740
|
-
"",
|
|
28741
|
-
"**Five to seven lines**, and no file-by-file dump: what now lives on a durable surface, what is still in flight and on whom, **what was banked to memory and to which of the three piles**, anything you named as having no home yet, any date you handed to a person, and the obvious next move.",
|
|
28852
|
+
"**Five to seven lines**, and no file-by-file dump: **where the session record went, named per artist**, what now lives on a durable surface, what is still in flight and on whom, **what was banked to memory and to which of the two piles**, anything you named as having no home yet, any fact you judged true of the whole team and left unwritten, any date you handed to a person, and the obvious next move. **This is how the user sees what was stored without opening a file**, so name the files rather than saying that memory was updated.",
|
|
28742
28853
|
"",
|
|
28743
28854
|
"Then, as the **very last step**:",
|
|
28744
28855
|
"",
|
|
@@ -28748,7 +28859,7 @@ function buildCloseCommandMd(workspaceSlug) {
|
|
|
28748
28859
|
"",
|
|
28749
28860
|
"**The `=` in `--summary=` is load-bearing. Do not rewrite it as a space.** A summary line often starts with `- `, and in the space form the parser swallows a value beginning with a dash. `session end` will tell you it dropped it and end the session anyway, so the cost is a lost summary rather than a lost close - but the equals form simply never loses it.",
|
|
28750
28861
|
"",
|
|
28751
|
-
"
|
|
28862
|
+
"It **always exits 0**, whatever happens, and prints nothing on success. It marks this session ended, and when it is running inside the Plinth desktop dock it tells the dock the session is finished so the tab can close itself. **Outside the dock it does nothing visible, and that is correct.** Nothing in this command depends on the dock; it runs the same in a bare terminal. The one thing it will ever print is a warning on stderr saying your `--summary` was lost before it could be read; the session still ends."
|
|
28752
28863
|
]);
|
|
28753
28864
|
}
|
|
28754
28865
|
function buildOnboardCommandMd(workspaceSlug) {
|
|
@@ -28894,12 +29005,16 @@ function ensureWorkspaceCommands(deps) {
|
|
|
28894
29005
|
const created = [];
|
|
28895
29006
|
const refreshed = [];
|
|
28896
29007
|
const noArtists = (deps.readRosterIsEmpty ?? rosterIsEmpty)(workspaceSlug);
|
|
28897
|
-
const
|
|
28898
|
-
[START_COMMAND_REL
|
|
28899
|
-
[CLOSE_COMMAND_REL
|
|
28900
|
-
[ONBOARD_COMMAND_REL
|
|
28901
|
-
|
|
28902
|
-
for (const
|
|
29008
|
+
const bodies = {
|
|
29009
|
+
[START_COMMAND_REL]: buildStartCommandMd(noArtists),
|
|
29010
|
+
[CLOSE_COMMAND_REL]: buildCloseCommandMd(workspaceSlug),
|
|
29011
|
+
[ONBOARD_COMMAND_REL]: buildOnboardCommandMd(workspaceSlug)
|
|
29012
|
+
};
|
|
29013
|
+
for (const rel of GENERATED_COMMAND_RELS) {
|
|
29014
|
+
const generated = bodies[rel];
|
|
29015
|
+
if (generated === undefined) {
|
|
29016
|
+
throw new Error(`ensureWorkspaceCommands: no generated body for ${rel}`);
|
|
29017
|
+
}
|
|
28903
29018
|
const existing = readContextFile(workspaceSlug, rel);
|
|
28904
29019
|
const decision = decideRegen({ existing, currentVersion: "", generated, force });
|
|
28905
29020
|
if (decision.action === "backup-regen" && decision.backup !== undefined) {
|
|
@@ -28922,6 +29037,44 @@ function ensureWorkspaceCommands(deps) {
|
|
|
28922
29037
|
function backupStamp2(d) {
|
|
28923
29038
|
return d.toISOString().replace(/[-:.]/g, "").replace("Z", "");
|
|
28924
29039
|
}
|
|
29040
|
+
var RETIRED_COMMAND_TOKENS = [
|
|
29041
|
+
{ commandRel: CLOSE_COMMAND_REL, token: "memory-gate", version: "0.11.0" }
|
|
29042
|
+
];
|
|
29043
|
+
var GENERATED_COMMAND_RELS = [
|
|
29044
|
+
START_COMMAND_REL,
|
|
29045
|
+
CLOSE_COMMAND_REL,
|
|
29046
|
+
ONBOARD_COMMAND_REL
|
|
29047
|
+
];
|
|
29048
|
+
function scanStaleUserBlocks(deps) {
|
|
29049
|
+
const retired = deps.retired ?? RETIRED_COMMAND_TOKENS;
|
|
29050
|
+
const read = deps.read ?? readContextFile;
|
|
29051
|
+
const stale = [];
|
|
29052
|
+
const unchecked = [];
|
|
29053
|
+
let checked = 0;
|
|
29054
|
+
for (const rel of GENERATED_COMMAND_RELS) {
|
|
29055
|
+
let content;
|
|
29056
|
+
try {
|
|
29057
|
+
content = read(deps.workspaceSlug, rel);
|
|
29058
|
+
} catch {
|
|
29059
|
+
unchecked.push({ commandRel: rel, reason: "unreadable" });
|
|
29060
|
+
continue;
|
|
29061
|
+
}
|
|
29062
|
+
if (content === null)
|
|
29063
|
+
continue;
|
|
29064
|
+
const block = extractUserBlock(content);
|
|
29065
|
+
if (block === null) {
|
|
29066
|
+
unchecked.push({ commandRel: rel, reason: "no-markers" });
|
|
29067
|
+
continue;
|
|
29068
|
+
}
|
|
29069
|
+
checked++;
|
|
29070
|
+
const haystack = block.toLowerCase();
|
|
29071
|
+
const tokens = retired.filter((r) => r.commandRel === rel && haystack.includes(r.token.toLowerCase()));
|
|
29072
|
+
if (tokens.length > 0) {
|
|
29073
|
+
stale.push({ commandRel: rel, invocation: invocationFor(rel), tokens });
|
|
29074
|
+
}
|
|
29075
|
+
}
|
|
29076
|
+
return { checked, unchecked, stale };
|
|
29077
|
+
}
|
|
28925
29078
|
|
|
28926
29079
|
// src/lib/context/workspace-claude-md.ts
|
|
28927
29080
|
var EXTRACTED_TEXT_DIR = extractedTextRelPath("x").replace(/x\.md$/, "");
|
|
@@ -28957,7 +29110,7 @@ function buildWorkspaceClaudeMd(input) {
|
|
|
28957
29110
|
``,
|
|
28958
29111
|
`> **Session grounding**: the current date, an entity-resolution rule, and a write-confirmation rule are injected at every session start by the Plinth hook in \`.claude/settings.json\` (and each \`artists/<slug>/.claude/settings.json\`).`,
|
|
28959
29112
|
``,
|
|
28960
|
-
`> **Filing a file the user hands you - BOTH halves, and the first one is unconditional**: put a copy in \`files/\`, which is how Plinth gets the text (folder map below) - do this even if everything else here fails. Then put the **original** in the user's own file storage with whatever tool you have for that, and write down where it went somewhere durable: \`artists/<slug>/memory.md\` if it is that artist's, otherwise \`
|
|
29113
|
+
`> **Filing a file the user hands you - BOTH halves, and the first one is unconditional**: put a copy in \`files/\`, which is how Plinth gets the text (folder map below) - do this even if everything else here fails. Then put the **original** in the user's own file storage with whatever tool you have for that, and write down where it went somewhere durable: \`artists/<slug>/memory.md\` if it is that artist's, otherwise your own \`user-memory.md\`. A pointer that exists only in this conversation is lost when it ends. If you have no tool that reaches their file storage, SAY SO - do not leave them believing the original was filed.`,
|
|
28961
29114
|
``,
|
|
28962
29115
|
`## Folder map`,
|
|
28963
29116
|
`- \`artists/<slug>/\` one folder per artist, the working dir for artist-scoped sessions · \`threads/<slug>/_thread.md\` one folder per thread`,
|
|
@@ -28970,13 +29123,13 @@ function buildWorkspaceClaudeMd(input) {
|
|
|
28970
29123
|
`- Each artist's **master context** (stable profile + current state) → \`artists/<slug>/_artist.md\`; edit its body and the daemon syncs both ways. Blocks labelled "rendered from Plinth" are the exception: they are overwritten on the next sync`,
|
|
28971
29124
|
`- **Contracts, executed agreements & any PDF** → \`files/\` (search here first; \`documents/\` holds markdown bodies, not attachments)`,
|
|
28972
29125
|
`- Workspace-wide **notes, briefs, bios & context docs** → \`documents/\` (one markdown file per doc, shared across artists)`,
|
|
28973
|
-
`- Per-artist long-term facts → \`artists/<slug>/memory.md\`, which syncs to that artist's Plinth record; most have none yet, and creating the file is how you start one ·
|
|
29126
|
+
`- Per-artist long-term facts → \`artists/<slug>/memory.md\`, which syncs to that artist's Plinth record; most have none yet, and creating the file is how you start one · what a session did → a dated section under \`## Session log\` in \`artists/<slug>/_artist.md\`, which syncs too · \`artists/<slug>/daily-log.md\` is local scratch, never synced; an older one may hold real history, and nothing writes there now`,
|
|
28974
29127
|
`- ${MCP_AVAILABILITY_POINTER}`,
|
|
28975
29128
|
``,
|
|
28976
29129
|
`## Navigating the corpus`,
|
|
28977
29130
|
`- **What points at X?** → \`plinth backlinks "<name>"\`, a mirror path, or a wikilink target as written. Render-only blocks, code spans and comments are narrowed out; \`--unfiltered\` shows them. A bare name several files answer to is refused with the candidates listed, so re-run with the path it names`,
|
|
28978
29131
|
`- **What does this file link to?** → grep \`[[\` in it`,
|
|
28979
|
-
`- **Find a file by name** → glob under \`files/\` \`documents/\` \`artists/\` · **text inside a PDF** → \`${EXTRACTED_TEXT_DIR}\` (folder map above) · **a
|
|
29132
|
+
`- **Find a file by name** → glob under \`files/\` \`documents/\` \`artists/\` · **text inside a PDF** → \`${EXTRACTED_TEXT_DIR}\` (folder map above) · **write to a row you have the mirror file for** → pass its frontmatter \`id:\` to the \`mcp__plinth__*\` write tool; if you do not have the file, find it with \`search_entities\` / \`list_*\``,
|
|
28980
29133
|
``,
|
|
28981
29134
|
`## Entity model: ${MCP_INTROSPECTION_POINTER}`,
|
|
28982
29135
|
...entityLines,
|
|
@@ -29777,8 +29930,8 @@ var sessionCommand = defineCommand({
|
|
|
29777
29930
|
});
|
|
29778
29931
|
|
|
29779
29932
|
// src/commands/start.ts
|
|
29780
|
-
var
|
|
29781
|
-
import { readFileSync as
|
|
29933
|
+
var import_gray_matter12 = __toESM(require_gray_matter(), 1);
|
|
29934
|
+
import { readFileSync as readFileSync23, statSync as statSync15 } from "node:fs";
|
|
29782
29935
|
|
|
29783
29936
|
// src/lib/daemon-log.ts
|
|
29784
29937
|
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync9, renameSync as renameSync7, statSync as statSync4 } from "node:fs";
|
|
@@ -30178,6 +30331,9 @@ function fileFailedLine(f) {
|
|
|
30178
30331
|
const serverPart = f.serverMessage ? ` server_msg="${f.serverMessage}"` : "";
|
|
30179
30332
|
return `[FILE_PUSH_FAILED path=${f.path} status=${f.status} reason=${f.reason}${detailPart}${serverPart}]`;
|
|
30180
30333
|
}
|
|
30334
|
+
function frontmatterIdBackfillLine(r) {
|
|
30335
|
+
return `[FRONTMATTER_ID_BACKFILL scanned=${r.scanned} written=${r.written} ` + `inodes_restamped=${r.inodesRestamped} failed=${r.failed} ` + `skipped_frozen=${r.skipped.frozen} ` + `skipped_foreign_id=${r.skipped.foreignId} ` + `skipped_no_frontmatter=${r.skipped.noFrontmatter} ` + `skipped_slug_collision=${r.skipped.slugCollision} ` + `skipped_diverged=${r.skipped.diverged}]`;
|
|
30336
|
+
}
|
|
30181
30337
|
function pullNoticeLine(kind) {
|
|
30182
30338
|
const token = kind === "auth" ? "PULL_PAUSED" : "PULL_DEGRADED";
|
|
30183
30339
|
return `[${token} reason=${kind}]`;
|
|
@@ -31114,6 +31270,7 @@ async function pushFile(args) {
|
|
|
31114
31270
|
unlinkContext,
|
|
31115
31271
|
deps
|
|
31116
31272
|
} = args;
|
|
31273
|
+
const identity = args.identity ?? "auto";
|
|
31117
31274
|
const fetchImpl = deps?.fetchImpl;
|
|
31118
31275
|
const now = deps?.now ?? (() => new Date);
|
|
31119
31276
|
const mutex = deps?.mutex ?? new PerIdMutex;
|
|
@@ -31180,7 +31337,7 @@ async function pushFile(args) {
|
|
|
31180
31337
|
parsedSlug = strategy.parseMirrorPath(rel)?.slug ?? null;
|
|
31181
31338
|
} catch {}
|
|
31182
31339
|
const slugFromPath = parsedSlug ?? basename4(absolutePath, ".md");
|
|
31183
|
-
const slug = slugFromFm ?? slugFromPath;
|
|
31340
|
+
const slug = identity === "path" ? slugFromPath : slugFromFm ?? slugFromPath;
|
|
31184
31341
|
if (!slug) {
|
|
31185
31342
|
return {
|
|
31186
31343
|
outcome: { kind: "ignored", absolutePath, reason: "empty slug" },
|
|
@@ -31204,7 +31361,8 @@ async function pushFile(args) {
|
|
|
31204
31361
|
}
|
|
31205
31362
|
}
|
|
31206
31363
|
const liveInode = safeInode2(absolutePath);
|
|
31207
|
-
const
|
|
31364
|
+
const byInode = liveInode !== undefined ? getEntryByInode(state, entityType, liveInode) : undefined;
|
|
31365
|
+
const stateEntry = identity === "path" ? byInode : byInode ?? getEntryBySlug(state, entityType, slug);
|
|
31208
31366
|
if (!stateEntry) {
|
|
31209
31367
|
return runPushCreate({
|
|
31210
31368
|
strategy,
|
|
@@ -31220,7 +31378,8 @@ async function pushFile(args) {
|
|
|
31220
31378
|
fetchImpl,
|
|
31221
31379
|
now,
|
|
31222
31380
|
output,
|
|
31223
|
-
quarantinedKeys
|
|
31381
|
+
quarantinedKeys,
|
|
31382
|
+
identity
|
|
31224
31383
|
});
|
|
31225
31384
|
}
|
|
31226
31385
|
return runPushUpsert({
|
|
@@ -31773,7 +31932,7 @@ async function runPushCreate(args) {
|
|
|
31773
31932
|
const strategy = args.strategy;
|
|
31774
31933
|
const key = `create:${strategy.type}:${args.slug}`;
|
|
31775
31934
|
return args.mutex.run(key, async () => {
|
|
31776
|
-
const inLock = getEntryBySlug(args.state, strategy.type, args.slug);
|
|
31935
|
+
const inLock = args.identity === "path" ? undefined : getEntryBySlug(args.state, strategy.type, args.slug);
|
|
31777
31936
|
if (inLock) {
|
|
31778
31937
|
return runPushUpsert({
|
|
31779
31938
|
strategy,
|
|
@@ -35915,8 +36074,500 @@ class PullSuppressor {
|
|
|
35915
36074
|
}
|
|
35916
36075
|
}
|
|
35917
36076
|
|
|
36077
|
+
// src/lib/discover-entities.ts
|
|
36078
|
+
var import_gray_matter10 = __toESM(require_gray_matter(), 1);
|
|
36079
|
+
import { readFileSync as readFileSync18, statSync as statSync11 } from "node:fs";
|
|
36080
|
+
var DISCOVERY_WRITE_CAP_DEFAULT = 10;
|
|
36081
|
+
function discoveryWriteCap() {
|
|
36082
|
+
const raw = process.env.PLINTH_DISCOVERY_WRITE_CAP;
|
|
36083
|
+
if (raw === undefined || raw === "")
|
|
36084
|
+
return DISCOVERY_WRITE_CAP_DEFAULT;
|
|
36085
|
+
const n = Number.parseInt(raw, 10);
|
|
36086
|
+
return Number.isFinite(n) && n >= 0 ? n : DISCOVERY_WRITE_CAP_DEFAULT;
|
|
36087
|
+
}
|
|
36088
|
+
function looksLikeCloudRow(args) {
|
|
36089
|
+
const { frontmatter, entitySlug } = args;
|
|
36090
|
+
if (frontmatter.revision === undefined || frontmatter.revision === null) {
|
|
36091
|
+
return false;
|
|
36092
|
+
}
|
|
36093
|
+
return frontmatter.slug === entitySlug;
|
|
36094
|
+
}
|
|
36095
|
+
function classifyUntrackedEntity(args) {
|
|
36096
|
+
if (args.entityType === "artists")
|
|
36097
|
+
return { kind: "hold", held: "artist" };
|
|
36098
|
+
if (looksLikeCloudRow({
|
|
36099
|
+
frontmatter: args.frontmatter,
|
|
36100
|
+
entitySlug: args.entitySlug
|
|
36101
|
+
})) {
|
|
36102
|
+
return { kind: "hold", held: "cloud-row" };
|
|
36103
|
+
}
|
|
36104
|
+
return { kind: "create" };
|
|
36105
|
+
}
|
|
36106
|
+
function classifyRenameDestination(args) {
|
|
36107
|
+
const occupant = getEntryBySlug(args.state, args.entityType, args.entitySlug);
|
|
36108
|
+
if (occupant && occupant.id !== args.movedEntryId) {
|
|
36109
|
+
return { kind: "hold", held: "rename-taken" };
|
|
36110
|
+
}
|
|
36111
|
+
return { kind: "rename" };
|
|
36112
|
+
}
|
|
36113
|
+
var EMPTY = {
|
|
36114
|
+
discovered: 0,
|
|
36115
|
+
created: 0,
|
|
36116
|
+
collisions: 0,
|
|
36117
|
+
renamed: 0,
|
|
36118
|
+
renameConflicts: 0,
|
|
36119
|
+
renameVanished: 0,
|
|
36120
|
+
heldCap: 0,
|
|
36121
|
+
heldArtist: 0,
|
|
36122
|
+
heldCloudRow: 0,
|
|
36123
|
+
refusedRename: 0,
|
|
36124
|
+
failed: 0,
|
|
36125
|
+
rejected: 0
|
|
36126
|
+
};
|
|
36127
|
+
function planDiscoveryWrites(args) {
|
|
36128
|
+
const { workspaceSlug, mirrorRootPath, state, counts, output, isFrozen } = args;
|
|
36129
|
+
const spokenFor = new Set;
|
|
36130
|
+
for (const op of [...listUnsynced(state), ...listRejected(state)]) {
|
|
36131
|
+
if (op.target !== "file")
|
|
36132
|
+
spokenFor.add(op.path);
|
|
36133
|
+
}
|
|
36134
|
+
const ignored = watcherIgnoreMatcher(mirrorRootPath);
|
|
36135
|
+
const planned = [];
|
|
36136
|
+
for (const absolutePath of walkSubtreeFiles(mirrorRootPath, ignored)) {
|
|
36137
|
+
const match = classifyMirrorPath({ workspaceSlug, absolutePath });
|
|
36138
|
+
if (!match)
|
|
36139
|
+
continue;
|
|
36140
|
+
const { entityType, entitySlug } = match;
|
|
36141
|
+
const strategy = getStrategy(entityType);
|
|
36142
|
+
if (!strategy)
|
|
36143
|
+
continue;
|
|
36144
|
+
const relPath = strategy.mirrorRelativePath(entitySlug);
|
|
36145
|
+
if (spokenFor.has(relPath))
|
|
36146
|
+
continue;
|
|
36147
|
+
let liveInode;
|
|
36148
|
+
try {
|
|
36149
|
+
liveInode = statSync11(absolutePath).ino;
|
|
36150
|
+
} catch {
|
|
36151
|
+
continue;
|
|
36152
|
+
}
|
|
36153
|
+
const byInode = liveInode !== undefined ? getEntryByInode(state, entityType, liveInode) : undefined;
|
|
36154
|
+
if (byInode) {
|
|
36155
|
+
if (byInode.slug === entitySlug)
|
|
36156
|
+
continue;
|
|
36157
|
+
if (isFrozen?.(entityType, byInode.id))
|
|
36158
|
+
continue;
|
|
36159
|
+
const destination = classifyRenameDestination({
|
|
36160
|
+
state,
|
|
36161
|
+
entityType,
|
|
36162
|
+
entitySlug,
|
|
36163
|
+
movedEntryId: byInode.id
|
|
36164
|
+
});
|
|
36165
|
+
if (destination.kind === "hold") {
|
|
36166
|
+
counts.refusedRename++;
|
|
36167
|
+
output?.warn(`Offline rename of ${entityType}/${byInode.slug} to ${entitySlug} was not ` + `synced: ${entitySlug} already belongs to another ${entityType} row. ` + `Rename it to a free name and Plinth will pick it up.`);
|
|
36168
|
+
continue;
|
|
36169
|
+
}
|
|
36170
|
+
planned.push({ kind: "rename", entityType, entitySlug, absolutePath, entry: byInode });
|
|
36171
|
+
continue;
|
|
36172
|
+
}
|
|
36173
|
+
if (getEntryBySlug(state, entityType, entitySlug))
|
|
36174
|
+
continue;
|
|
36175
|
+
counts.discovered++;
|
|
36176
|
+
let frontmatter;
|
|
36177
|
+
try {
|
|
36178
|
+
frontmatter = import_gray_matter10.default(readFileSync18(absolutePath, "utf8")).data ?? {};
|
|
36179
|
+
} catch {
|
|
36180
|
+
counts.discovered--;
|
|
36181
|
+
continue;
|
|
36182
|
+
}
|
|
36183
|
+
const verdict = classifyUntrackedEntity({ entityType, entitySlug, frontmatter });
|
|
36184
|
+
if (verdict.kind === "hold") {
|
|
36185
|
+
if (verdict.held === "artist")
|
|
36186
|
+
counts.heldArtist++;
|
|
36187
|
+
else
|
|
36188
|
+
counts.heldCloudRow++;
|
|
36189
|
+
continue;
|
|
36190
|
+
}
|
|
36191
|
+
planned.push({ kind: "create", entityType, entitySlug, absolutePath });
|
|
36192
|
+
}
|
|
36193
|
+
return planned;
|
|
36194
|
+
}
|
|
36195
|
+
async function discoverUntrackedEntities(args) {
|
|
36196
|
+
const { workspaceSlug, baseUrl, pat, fetchImpl, output, isFrozen } = args;
|
|
36197
|
+
const now = args.now ?? (() => new Date);
|
|
36198
|
+
let state = args.state;
|
|
36199
|
+
const counts = { ...EMPTY };
|
|
36200
|
+
let mirrorRootPath;
|
|
36201
|
+
try {
|
|
36202
|
+
mirrorRootPath = workspaceMirrorPath(workspaceSlug);
|
|
36203
|
+
} catch {
|
|
36204
|
+
return { state, ...counts };
|
|
36205
|
+
}
|
|
36206
|
+
let planned;
|
|
36207
|
+
try {
|
|
36208
|
+
planned = planDiscoveryWrites({
|
|
36209
|
+
workspaceSlug,
|
|
36210
|
+
mirrorRootPath,
|
|
36211
|
+
state,
|
|
36212
|
+
counts,
|
|
36213
|
+
output,
|
|
36214
|
+
isFrozen
|
|
36215
|
+
});
|
|
36216
|
+
} catch (err) {
|
|
36217
|
+
const msg = err instanceof Error ? err.message : "unknown error";
|
|
36218
|
+
output?.warn(`Could not check this workspace for files added while Plinth was closed: ${msg} ` + `(everything already synced is unaffected).`);
|
|
36219
|
+
return { state, ...EMPTY };
|
|
36220
|
+
}
|
|
36221
|
+
const cap = discoveryWriteCap();
|
|
36222
|
+
const creates = planned.filter((w) => w.kind === "create");
|
|
36223
|
+
const toRun = creates.length > cap ? planned.filter((w) => w.kind === "rename") : planned;
|
|
36224
|
+
if (creates.length > cap) {
|
|
36225
|
+
counts.heldCap = creates.length;
|
|
36226
|
+
output?.log(`[DISCOVERY_HELD reason=cap creates=${creates.length} cap=${cap} source=entity-discovery]`);
|
|
36227
|
+
}
|
|
36228
|
+
state = await runPlannedWrites({
|
|
36229
|
+
planned: toRun,
|
|
36230
|
+
workspaceSlug,
|
|
36231
|
+
baseUrl,
|
|
36232
|
+
pat,
|
|
36233
|
+
state,
|
|
36234
|
+
fetchImpl,
|
|
36235
|
+
now,
|
|
36236
|
+
output,
|
|
36237
|
+
counts
|
|
36238
|
+
});
|
|
36239
|
+
return { state, ...counts };
|
|
36240
|
+
}
|
|
36241
|
+
async function runPlannedWrites(args) {
|
|
36242
|
+
const { planned, workspaceSlug, baseUrl, pat, fetchImpl, now, output, counts } = args;
|
|
36243
|
+
let state = args.state;
|
|
36244
|
+
for (const write of planned) {
|
|
36245
|
+
try {
|
|
36246
|
+
if (write.kind === "rename") {
|
|
36247
|
+
state = await runDiscoveredRename({
|
|
36248
|
+
write,
|
|
36249
|
+
workspaceSlug,
|
|
36250
|
+
baseUrl,
|
|
36251
|
+
pat,
|
|
36252
|
+
state,
|
|
36253
|
+
fetchImpl,
|
|
36254
|
+
now,
|
|
36255
|
+
output,
|
|
36256
|
+
counts
|
|
36257
|
+
});
|
|
36258
|
+
continue;
|
|
36259
|
+
}
|
|
36260
|
+
const result = await pushFile({
|
|
36261
|
+
workspaceSlug,
|
|
36262
|
+
baseUrl,
|
|
36263
|
+
pat,
|
|
36264
|
+
absolutePath: write.absolutePath,
|
|
36265
|
+
entityType: write.entityType,
|
|
36266
|
+
state,
|
|
36267
|
+
exists: true,
|
|
36268
|
+
unlinkContext: null,
|
|
36269
|
+
identity: "path",
|
|
36270
|
+
deps: { fetchImpl, now, output }
|
|
36271
|
+
});
|
|
36272
|
+
state = result.state;
|
|
36273
|
+
switch (result.outcome.kind) {
|
|
36274
|
+
case "created":
|
|
36275
|
+
case "pushed":
|
|
36276
|
+
counts.created++;
|
|
36277
|
+
break;
|
|
36278
|
+
case "conflict":
|
|
36279
|
+
case "create_slug_conflict":
|
|
36280
|
+
counts.collisions++;
|
|
36281
|
+
break;
|
|
36282
|
+
case "sync_error":
|
|
36283
|
+
counts.created++;
|
|
36284
|
+
break;
|
|
36285
|
+
default:
|
|
36286
|
+
break;
|
|
36287
|
+
}
|
|
36288
|
+
} catch (err) {
|
|
36289
|
+
state = recordDiscoveryFailure({
|
|
36290
|
+
err,
|
|
36291
|
+
write,
|
|
36292
|
+
workspaceSlug,
|
|
36293
|
+
state,
|
|
36294
|
+
now,
|
|
36295
|
+
output,
|
|
36296
|
+
counts
|
|
36297
|
+
});
|
|
36298
|
+
}
|
|
36299
|
+
}
|
|
36300
|
+
return state;
|
|
36301
|
+
}
|
|
36302
|
+
async function runDiscoveredRename(args) {
|
|
36303
|
+
const { write, workspaceSlug, baseUrl, pat, fetchImpl, now, output, counts } = args;
|
|
36304
|
+
let state = args.state;
|
|
36305
|
+
let fromPath;
|
|
36306
|
+
try {
|
|
36307
|
+
fromPath = entityFilePath(workspaceSlug, write.entityType, write.entry.slug);
|
|
36308
|
+
} catch (err) {
|
|
36309
|
+
if (!(err instanceof MirrorPathError))
|
|
36310
|
+
throw err;
|
|
36311
|
+
return state;
|
|
36312
|
+
}
|
|
36313
|
+
const renameResult = await pushRename({
|
|
36314
|
+
workspaceSlug,
|
|
36315
|
+
baseUrl,
|
|
36316
|
+
pat,
|
|
36317
|
+
entityType: write.entityType,
|
|
36318
|
+
fromPath,
|
|
36319
|
+
toPath: write.absolutePath,
|
|
36320
|
+
id: write.entry.id,
|
|
36321
|
+
expectedUpdatedAt: write.entry.updated_at,
|
|
36322
|
+
fromSlug: write.entry.slug,
|
|
36323
|
+
state,
|
|
36324
|
+
deps: { fetchImpl, now, output }
|
|
36325
|
+
});
|
|
36326
|
+
state = renameResult.state;
|
|
36327
|
+
if (renameResult.outcome.kind === "not_found") {
|
|
36328
|
+
counts.renameVanished++;
|
|
36329
|
+
output?.warn(`${write.entityType}/${write.entry.slug} was renamed here, but it no longer ` + `exists in Plinth — the next sync will remove the file.`);
|
|
36330
|
+
return state;
|
|
36331
|
+
}
|
|
36332
|
+
if (renameResult.outcome.kind !== "renamed") {
|
|
36333
|
+
if (renameResult.outcome.kind === "conflict" || renameResult.outcome.kind === "rename_slug_conflict") {
|
|
36334
|
+
counts.renameConflicts++;
|
|
36335
|
+
}
|
|
36336
|
+
return state;
|
|
36337
|
+
}
|
|
36338
|
+
counts.renamed++;
|
|
36339
|
+
let upsert;
|
|
36340
|
+
try {
|
|
36341
|
+
upsert = await pushFile({
|
|
36342
|
+
workspaceSlug,
|
|
36343
|
+
baseUrl,
|
|
36344
|
+
pat,
|
|
36345
|
+
absolutePath: write.absolutePath,
|
|
36346
|
+
entityType: write.entityType,
|
|
36347
|
+
state,
|
|
36348
|
+
exists: true,
|
|
36349
|
+
unlinkContext: null,
|
|
36350
|
+
identity: "path",
|
|
36351
|
+
deps: { fetchImpl, now, output }
|
|
36352
|
+
});
|
|
36353
|
+
} catch (err) {
|
|
36354
|
+
return recordDiscoveryFailure({
|
|
36355
|
+
err,
|
|
36356
|
+
write,
|
|
36357
|
+
workspaceSlug,
|
|
36358
|
+
state,
|
|
36359
|
+
now,
|
|
36360
|
+
output,
|
|
36361
|
+
counts
|
|
36362
|
+
});
|
|
36363
|
+
}
|
|
36364
|
+
if (upsert.outcome.kind === "conflict")
|
|
36365
|
+
counts.renameConflicts++;
|
|
36366
|
+
return upsert.state;
|
|
36367
|
+
}
|
|
36368
|
+
function recordDiscoveryFailure(args) {
|
|
36369
|
+
const { err, write, workspaceSlug, now, output, counts } = args;
|
|
36370
|
+
let state = args.state;
|
|
36371
|
+
const msg = err instanceof Error ? err.message : "unknown error";
|
|
36372
|
+
output?.log(pushFailedLine({
|
|
36373
|
+
entityType: write.entityType,
|
|
36374
|
+
slug: write.entitySlug,
|
|
36375
|
+
id: write.kind === "rename" ? write.entry.id : "",
|
|
36376
|
+
...categorizeError(err)
|
|
36377
|
+
}));
|
|
36378
|
+
if (isPermanentReject(err)) {
|
|
36379
|
+
counts.rejected++;
|
|
36380
|
+
output?.warn(`Plinth refused ${write.entityType}/${write.entitySlug}: ${msg} ` + `(see \`plinth status\`; fix the file and save it again).`);
|
|
36381
|
+
return recordEntityRejected({
|
|
36382
|
+
state,
|
|
36383
|
+
workspaceSlug,
|
|
36384
|
+
absolutePath: write.absolutePath,
|
|
36385
|
+
op: "upsert",
|
|
36386
|
+
err,
|
|
36387
|
+
now
|
|
36388
|
+
});
|
|
36389
|
+
}
|
|
36390
|
+
counts.failed++;
|
|
36391
|
+
output?.warn(`Could not sync ${write.entityType}/${write.entitySlug}: ${msg} ` + `(file kept on disk; will retry).`);
|
|
36392
|
+
if (isRecordableFailure(err)) {
|
|
36393
|
+
state = recordEntityUnsynced({
|
|
36394
|
+
state,
|
|
36395
|
+
workspaceSlug,
|
|
36396
|
+
absolutePath: write.absolutePath,
|
|
36397
|
+
op: "upsert",
|
|
36398
|
+
err,
|
|
36399
|
+
now
|
|
36400
|
+
});
|
|
36401
|
+
}
|
|
36402
|
+
return state;
|
|
36403
|
+
}
|
|
36404
|
+
|
|
36405
|
+
// src/lib/frontmatter-id-backfill.ts
|
|
36406
|
+
var import_gray_matter11 = __toESM(require_gray_matter(), 1);
|
|
36407
|
+
import { readFileSync as readFileSync19, statSync as statSync12 } from "node:fs";
|
|
36408
|
+
function backfillWorthReporting(r) {
|
|
36409
|
+
return r.written > 0 || r.failed > 0 || r.skipped.foreignId > 0;
|
|
36410
|
+
}
|
|
36411
|
+
function emptyResult() {
|
|
36412
|
+
return {
|
|
36413
|
+
scanned: 0,
|
|
36414
|
+
written: 0,
|
|
36415
|
+
alreadyPresent: 0,
|
|
36416
|
+
missing: 0,
|
|
36417
|
+
skipped: {
|
|
36418
|
+
frozen: 0,
|
|
36419
|
+
foreignId: 0,
|
|
36420
|
+
noFrontmatter: 0,
|
|
36421
|
+
slugCollision: 0,
|
|
36422
|
+
diverged: 0
|
|
36423
|
+
},
|
|
36424
|
+
failed: 0,
|
|
36425
|
+
inodesRestamped: 0
|
|
36426
|
+
};
|
|
36427
|
+
}
|
|
36428
|
+
function heldByAnyBatch(workspaceSlug) {
|
|
36429
|
+
const out = new Set;
|
|
36430
|
+
for (const batch of listPendingBatches(workspaceSlug)) {
|
|
36431
|
+
for (const op of batch.ops) {
|
|
36432
|
+
if (op.target === "entity")
|
|
36433
|
+
out.add(`${op.entityType}:${op.id}`);
|
|
36434
|
+
}
|
|
36435
|
+
}
|
|
36436
|
+
return out;
|
|
36437
|
+
}
|
|
36438
|
+
function safeInode3(path5) {
|
|
36439
|
+
try {
|
|
36440
|
+
return statSync12(path5).ino;
|
|
36441
|
+
} catch {
|
|
36442
|
+
return;
|
|
36443
|
+
}
|
|
36444
|
+
}
|
|
36445
|
+
function spliceFrontmatterId(content, id) {
|
|
36446
|
+
const eol = /^---\r\n/.test(content) ? `\r
|
|
36447
|
+
` : `
|
|
36448
|
+
`;
|
|
36449
|
+
if (!content.startsWith(`---${eol}`))
|
|
36450
|
+
return null;
|
|
36451
|
+
const closeAt = content.indexOf(`${eol}---`, 3);
|
|
36452
|
+
if (closeAt === -1)
|
|
36453
|
+
return null;
|
|
36454
|
+
return `${content.slice(0, closeAt)}${eol}id: ${id}${content.slice(closeAt)}`;
|
|
36455
|
+
}
|
|
36456
|
+
function backfillFrontmatterIds(args) {
|
|
36457
|
+
const { workspaceSlug, state } = args;
|
|
36458
|
+
const splice = args.splice ?? spliceFrontmatterId;
|
|
36459
|
+
const result = emptyResult();
|
|
36460
|
+
let held;
|
|
36461
|
+
try {
|
|
36462
|
+
held = heldByAnyBatch(workspaceSlug);
|
|
36463
|
+
} catch {
|
|
36464
|
+
return result;
|
|
36465
|
+
}
|
|
36466
|
+
let nextState = state;
|
|
36467
|
+
let stateChanged = false;
|
|
36468
|
+
for (const [entityType, bucket] of Object.entries(state.entries)) {
|
|
36469
|
+
const strategy = getStrategy(entityType);
|
|
36470
|
+
if (!strategy)
|
|
36471
|
+
continue;
|
|
36472
|
+
const slugCounts = new Map;
|
|
36473
|
+
for (const e of Object.values(bucket)) {
|
|
36474
|
+
slugCounts.set(e.slug, (slugCounts.get(e.slug) ?? 0) + 1);
|
|
36475
|
+
}
|
|
36476
|
+
for (const [id, entry2] of Object.entries(bucket)) {
|
|
36477
|
+
result.scanned++;
|
|
36478
|
+
if ((slugCounts.get(entry2.slug) ?? 0) > 1) {
|
|
36479
|
+
result.skipped.slugCollision++;
|
|
36480
|
+
continue;
|
|
36481
|
+
}
|
|
36482
|
+
if (held.has(`${entityType}:${id}`)) {
|
|
36483
|
+
result.skipped.frozen++;
|
|
36484
|
+
continue;
|
|
36485
|
+
}
|
|
36486
|
+
let raw;
|
|
36487
|
+
try {
|
|
36488
|
+
raw = readFileSync19(entityFilePath(workspaceSlug, entityType, entry2.slug), "utf8");
|
|
36489
|
+
} catch (err) {
|
|
36490
|
+
if (err?.code === "ENOENT") {
|
|
36491
|
+
result.missing++;
|
|
36492
|
+
} else {
|
|
36493
|
+
result.failed++;
|
|
36494
|
+
}
|
|
36495
|
+
continue;
|
|
36496
|
+
}
|
|
36497
|
+
let existing;
|
|
36498
|
+
let rawBody;
|
|
36499
|
+
try {
|
|
36500
|
+
const parsed = import_gray_matter11.default(raw);
|
|
36501
|
+
existing = (parsed.data ?? {})["id"];
|
|
36502
|
+
rawBody = parsed.content;
|
|
36503
|
+
} catch {
|
|
36504
|
+
result.failed++;
|
|
36505
|
+
continue;
|
|
36506
|
+
}
|
|
36507
|
+
if (existing !== undefined) {
|
|
36508
|
+
if (existing === id)
|
|
36509
|
+
result.alreadyPresent++;
|
|
36510
|
+
else
|
|
36511
|
+
result.skipped.foreignId++;
|
|
36512
|
+
continue;
|
|
36513
|
+
}
|
|
36514
|
+
const hashes = computeMirrorHashes(strategy, raw);
|
|
36515
|
+
if (hashes.bodyHash !== entry2.body_hash || hashes.frontmatterHash !== entry2.frontmatter_hash) {
|
|
36516
|
+
result.skipped.diverged++;
|
|
36517
|
+
continue;
|
|
36518
|
+
}
|
|
36519
|
+
const next = splice(raw, id);
|
|
36520
|
+
if (next === null) {
|
|
36521
|
+
result.skipped.noFrontmatter++;
|
|
36522
|
+
continue;
|
|
36523
|
+
}
|
|
36524
|
+
try {
|
|
36525
|
+
const check2 = import_gray_matter11.default(next);
|
|
36526
|
+
if (check2.data?.["id"] !== id || check2.content !== rawBody) {
|
|
36527
|
+
result.failed++;
|
|
36528
|
+
continue;
|
|
36529
|
+
}
|
|
36530
|
+
} catch {
|
|
36531
|
+
result.failed++;
|
|
36532
|
+
continue;
|
|
36533
|
+
}
|
|
36534
|
+
let ino;
|
|
36535
|
+
try {
|
|
36536
|
+
const stillRaw = readFileSync19(entityFilePath(workspaceSlug, entityType, entry2.slug), "utf8");
|
|
36537
|
+
if (stillRaw !== raw) {
|
|
36538
|
+
result.skipped.diverged++;
|
|
36539
|
+
continue;
|
|
36540
|
+
}
|
|
36541
|
+
writeMirrorFile(workspaceSlug, entityType, entry2.slug, next);
|
|
36542
|
+
result.written++;
|
|
36543
|
+
ino = safeInode3(entityFilePath(workspaceSlug, entityType, entry2.slug));
|
|
36544
|
+
} catch {
|
|
36545
|
+
result.failed++;
|
|
36546
|
+
continue;
|
|
36547
|
+
}
|
|
36548
|
+
if (ino !== undefined && ino !== entry2.inode) {
|
|
36549
|
+
const up = upsertEntry(nextState, entityType, { ...entry2, inode: ino });
|
|
36550
|
+
nextState = up.state;
|
|
36551
|
+
if (up.changed) {
|
|
36552
|
+
stateChanged = true;
|
|
36553
|
+
result.inodesRestamped++;
|
|
36554
|
+
}
|
|
36555
|
+
}
|
|
36556
|
+
}
|
|
36557
|
+
}
|
|
36558
|
+
if (stateChanged) {
|
|
36559
|
+
try {
|
|
36560
|
+
commitState(state, nextState);
|
|
36561
|
+
} catch {
|
|
36562
|
+
result.failed++;
|
|
36563
|
+
result.inodesRestamped = 0;
|
|
36564
|
+
}
|
|
36565
|
+
}
|
|
36566
|
+
return result;
|
|
36567
|
+
}
|
|
36568
|
+
|
|
35918
36569
|
// src/lib/reconcile.ts
|
|
35919
|
-
import { readFileSync as
|
|
36570
|
+
import { readFileSync as readFileSync20 } from "node:fs";
|
|
35920
36571
|
async function reconcileDivergedRows(args) {
|
|
35921
36572
|
const { workspaceSlug, baseUrl, pat, fetchImpl, output, isFrozen } = args;
|
|
35922
36573
|
const now = args.now ?? (() => new Date);
|
|
@@ -36002,7 +36653,7 @@ async function reconcileDivergedRows(args) {
|
|
|
36002
36653
|
}
|
|
36003
36654
|
let content;
|
|
36004
36655
|
try {
|
|
36005
|
-
content =
|
|
36656
|
+
content = readFileSync20(absolutePath, "utf8");
|
|
36006
36657
|
} catch (err) {
|
|
36007
36658
|
if (err?.code === "ENOENT")
|
|
36008
36659
|
continue;
|
|
@@ -36064,7 +36715,29 @@ async function reconcileDivergedRows(args) {
|
|
|
36064
36715
|
}
|
|
36065
36716
|
}
|
|
36066
36717
|
}
|
|
36067
|
-
|
|
36718
|
+
const discovery = await discoverUntrackedEntities({
|
|
36719
|
+
workspaceSlug,
|
|
36720
|
+
baseUrl,
|
|
36721
|
+
pat,
|
|
36722
|
+
state,
|
|
36723
|
+
fetchImpl,
|
|
36724
|
+
now,
|
|
36725
|
+
output,
|
|
36726
|
+
isFrozen
|
|
36727
|
+
});
|
|
36728
|
+
state = discovery.state;
|
|
36729
|
+
return {
|
|
36730
|
+
state,
|
|
36731
|
+
scanned,
|
|
36732
|
+
diverged,
|
|
36733
|
+
pushed,
|
|
36734
|
+
conflicts,
|
|
36735
|
+
noop,
|
|
36736
|
+
failed,
|
|
36737
|
+
quarantined,
|
|
36738
|
+
syncErrors,
|
|
36739
|
+
discovery
|
|
36740
|
+
};
|
|
36068
36741
|
}
|
|
36069
36742
|
|
|
36070
36743
|
// src/lib/workspace-rules/sync.ts
|
|
@@ -37061,8 +37734,8 @@ function createThrottledFetch(baseFetch, opts = {}) {
|
|
|
37061
37734
|
import {
|
|
37062
37735
|
existsSync as existsSync7,
|
|
37063
37736
|
readdirSync as readdirSync6,
|
|
37064
|
-
readFileSync as
|
|
37065
|
-
statSync as
|
|
37737
|
+
readFileSync as readFileSync22,
|
|
37738
|
+
statSync as statSync14,
|
|
37066
37739
|
unlinkSync as unlinkSync7
|
|
37067
37740
|
} from "node:fs";
|
|
37068
37741
|
import { join as join16 } from "node:path";
|
|
@@ -37188,7 +37861,7 @@ function refreshConflictsView(workspaceSlug) {
|
|
|
37188
37861
|
}
|
|
37189
37862
|
|
|
37190
37863
|
// src/lib/files/pull.ts
|
|
37191
|
-
import { existsSync as existsSync6, readFileSync as
|
|
37864
|
+
import { existsSync as existsSync6, readFileSync as readFileSync21, statSync as statSync13 } from "node:fs";
|
|
37192
37865
|
|
|
37193
37866
|
// src/lib/files/extracted-text.ts
|
|
37194
37867
|
import { createHash as createHash3 } from "node:crypto";
|
|
@@ -37300,6 +37973,7 @@ async function pullFiles(args) {
|
|
|
37300
37973
|
output: args.output,
|
|
37301
37974
|
suppressor: args.suppressor,
|
|
37302
37975
|
isFileFrozen: args.isFileFrozen,
|
|
37976
|
+
loggedTombstoneSkips: args.loggedTombstoneSkips,
|
|
37303
37977
|
forceTextRewalk: args.forceTextRewalk
|
|
37304
37978
|
});
|
|
37305
37979
|
state = outcome.state;
|
|
@@ -37448,6 +38122,26 @@ async function applyFileRow(args) {
|
|
|
37448
38122
|
}
|
|
37449
38123
|
const existing = getFileEntry(state, relPath);
|
|
37450
38124
|
if (row.deleted_at !== null) {
|
|
38125
|
+
if (existing && existing.id !== row.id) {
|
|
38126
|
+
const key = `${row.id}:${existing.id}:${relPath}`;
|
|
38127
|
+
if (!args.loggedTombstoneSkips?.has(key)) {
|
|
38128
|
+
args.loggedTombstoneSkips?.add(key);
|
|
38129
|
+
output?.log(`[FILE_TOMBSTONE_SKIP path=${relPath} row_id=${row.id} tracked_id=${existing.id} reason=id_mismatch]`);
|
|
38130
|
+
}
|
|
38131
|
+
return { state, action: "skipped" };
|
|
38132
|
+
}
|
|
38133
|
+
let localBytes;
|
|
38134
|
+
try {
|
|
38135
|
+
localBytes = readFileSync21(abs);
|
|
38136
|
+
} catch {
|
|
38137
|
+
localBytes = undefined;
|
|
38138
|
+
}
|
|
38139
|
+
const isOurs = localBytes === undefined || existing !== undefined && sha256Bytes(localBytes) === existing.content_hash;
|
|
38140
|
+
if (!isOurs) {
|
|
38141
|
+
output?.log(`[FILE_TOMBSTONE_KEEP path=${relPath} row_id=${row.id} reason=${existing === undefined ? "untracked" : "diverged"}]`);
|
|
38142
|
+
const next2 = existing ? removeFileEntry(state, relPath).state : state;
|
|
38143
|
+
return { state: next2, action: "skipped" };
|
|
38144
|
+
}
|
|
37451
38145
|
suppressor?.registerDelete(abs);
|
|
37452
38146
|
const didUnlink = deleteFilesFile(workspaceSlug, relPath);
|
|
37453
38147
|
const removedText = safeDeleteExtractedText(workspaceSlug, relPath, output);
|
|
@@ -37535,7 +38229,7 @@ async function applyFileRow(args) {
|
|
|
37535
38229
|
}
|
|
37536
38230
|
if (!suppressor && existing && existing.content_hash !== row.content_hash && existsSync6(abs)) {
|
|
37537
38231
|
try {
|
|
37538
|
-
const localBytes =
|
|
38232
|
+
const localBytes = readFileSync21(abs);
|
|
37539
38233
|
if (sha256Bytes(localBytes) !== existing.content_hash) {
|
|
37540
38234
|
const stashPath = stashFileConflict({ workspaceSlug, relPath, timestamp: now(), bytes: localBytes });
|
|
37541
38235
|
output?.warn(`Conflict on files/${relPath} → stashed local edit to ${stashPath} (cloud-wins on boot).`);
|
|
@@ -37576,7 +38270,7 @@ async function applyFileRow(args) {
|
|
|
37576
38270
|
}
|
|
37577
38271
|
let inode;
|
|
37578
38272
|
try {
|
|
37579
|
-
inode =
|
|
38273
|
+
inode = statSync13(abs).ino;
|
|
37580
38274
|
} catch {
|
|
37581
38275
|
inode = undefined;
|
|
37582
38276
|
}
|
|
@@ -37684,7 +38378,7 @@ function unlinkIfInodeMatches(workspaceSlug, entityType, entrySlug, expectedInod
|
|
|
37684
38378
|
}
|
|
37685
38379
|
let liveInode;
|
|
37686
38380
|
try {
|
|
37687
|
-
liveInode =
|
|
38381
|
+
liveInode = statSync14(target).ino;
|
|
37688
38382
|
} catch {
|
|
37689
38383
|
return { didUnlink: false, skippedReason: "absent" };
|
|
37690
38384
|
}
|
|
@@ -37831,6 +38525,7 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
37831
38525
|
output: out,
|
|
37832
38526
|
suppressor: deps.suppressor,
|
|
37833
38527
|
isFileFrozen: deps.isFileFrozen,
|
|
38528
|
+
loggedTombstoneSkips: deps.loggedTombstoneSkips,
|
|
37834
38529
|
commit: commitState,
|
|
37835
38530
|
forceTextRewalk: needsDerivedTextRewalk
|
|
37836
38531
|
});
|
|
@@ -38046,7 +38741,7 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
|
|
|
38046
38741
|
if (!suppressor && existingEntry && existingEntry.updated_at !== live.updated_at) {
|
|
38047
38742
|
let localRaw = null;
|
|
38048
38743
|
try {
|
|
38049
|
-
localRaw =
|
|
38744
|
+
localRaw = readFileSync22(entityFilePath(workspaceSlug, strategy.type, live.slug), "utf8");
|
|
38050
38745
|
} catch {
|
|
38051
38746
|
localRaw = null;
|
|
38052
38747
|
}
|
|
@@ -38069,7 +38764,7 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
|
|
|
38069
38764
|
const hashes = computeMirrorHashes(strategy, content);
|
|
38070
38765
|
let inode;
|
|
38071
38766
|
try {
|
|
38072
|
-
inode =
|
|
38767
|
+
inode = statSync14(entityFilePath(workspaceSlug, strategy.type, live.slug)).ino;
|
|
38073
38768
|
} catch {
|
|
38074
38769
|
inode = undefined;
|
|
38075
38770
|
}
|
|
@@ -38138,6 +38833,13 @@ var syncCommand = defineCommand({
|
|
|
38138
38833
|
process.exit(1);
|
|
38139
38834
|
}
|
|
38140
38835
|
handle = acquired.handle;
|
|
38836
|
+
const idBackfill = backfillFrontmatterIds({
|
|
38837
|
+
workspaceSlug: slug,
|
|
38838
|
+
state: loadState(slug)
|
|
38839
|
+
});
|
|
38840
|
+
if (backfillWorthReporting(idBackfill)) {
|
|
38841
|
+
out.log(frontmatterIdBackfillLine(idBackfill));
|
|
38842
|
+
}
|
|
38141
38843
|
}
|
|
38142
38844
|
let result;
|
|
38143
38845
|
try {
|
|
@@ -38155,7 +38857,7 @@ var syncCommand = defineCommand({
|
|
|
38155
38857
|
// src/commands/start.ts
|
|
38156
38858
|
var SHUTDOWN_DRAIN_TIMEOUT_MS = 5000;
|
|
38157
38859
|
var PULL_HEARTBEAT_MS = 900000;
|
|
38158
|
-
function backfillStateInodes(workspaceSlug, state, statFn = (p) =>
|
|
38860
|
+
function backfillStateInodes(workspaceSlug, state, statFn = (p) => statSync15(p)) {
|
|
38159
38861
|
let changed = false;
|
|
38160
38862
|
const nextEntries = {};
|
|
38161
38863
|
for (const [entityType, bucket] of Object.entries(state.entries)) {
|
|
@@ -38188,7 +38890,7 @@ function backfillStateInodes(workspaceSlug, state, statFn = (p) => statSync13(p)
|
|
|
38188
38890
|
return { state, changed: false };
|
|
38189
38891
|
return { state: { ...state, entries: nextEntries }, changed: true };
|
|
38190
38892
|
}
|
|
38191
|
-
function backfillBodyLengths(workspaceSlug, state, readFn = (p) =>
|
|
38893
|
+
function backfillBodyLengths(workspaceSlug, state, readFn = (p) => readFileSync23(p, "utf8")) {
|
|
38192
38894
|
let changed = false;
|
|
38193
38895
|
const nextEntries = {};
|
|
38194
38896
|
for (const [entityType, bucket] of Object.entries(state.entries)) {
|
|
@@ -38200,7 +38902,7 @@ function backfillBodyLengths(workspaceSlug, state, readFn = (p) => readFileSync2
|
|
|
38200
38902
|
}
|
|
38201
38903
|
try {
|
|
38202
38904
|
const path5 = entityFilePath(workspaceSlug, entityType, entry2.slug);
|
|
38203
|
-
const bodyLen =
|
|
38905
|
+
const bodyLen = import_gray_matter12.default(readFn(path5)).content.length;
|
|
38204
38906
|
nextBucket[id] = { ...entry2, body_len: bodyLen };
|
|
38205
38907
|
changed = true;
|
|
38206
38908
|
} catch {
|
|
@@ -38276,7 +38978,7 @@ function makeWatchHandler(deps) {
|
|
|
38276
38978
|
}
|
|
38277
38979
|
let st;
|
|
38278
38980
|
try {
|
|
38279
|
-
st =
|
|
38981
|
+
st = statSync15(absolutePath);
|
|
38280
38982
|
} catch (err) {
|
|
38281
38983
|
if (err?.code === "ENOENT")
|
|
38282
38984
|
return;
|
|
@@ -38307,7 +39009,7 @@ function makeWatchHandler(deps) {
|
|
|
38307
39009
|
spinner.update(`Watching ${workspaceSlug} · ${watchingSummary(getState())}`);
|
|
38308
39010
|
return;
|
|
38309
39011
|
}
|
|
38310
|
-
const bytes =
|
|
39012
|
+
const bytes = readFileSync23(absolutePath);
|
|
38311
39013
|
const hash2 = sha256Bytes(bytes);
|
|
38312
39014
|
if (suppressor?.shouldSuppressUpsert(absolutePath, {
|
|
38313
39015
|
bodyHash: hash2,
|
|
@@ -38365,7 +39067,7 @@ function makeWatchHandler(deps) {
|
|
|
38365
39067
|
if (suppressor) {
|
|
38366
39068
|
let raw = null;
|
|
38367
39069
|
try {
|
|
38368
|
-
raw =
|
|
39070
|
+
raw = readFileSync23(absolutePath, "utf8");
|
|
38369
39071
|
} catch {
|
|
38370
39072
|
raw = null;
|
|
38371
39073
|
}
|
|
@@ -38403,7 +39105,7 @@ function makeWatchHandler(deps) {
|
|
|
38403
39105
|
if (suppressor) {
|
|
38404
39106
|
let raw = null;
|
|
38405
39107
|
try {
|
|
38406
|
-
raw =
|
|
39108
|
+
raw = readFileSync23(absolutePath, "utf8");
|
|
38407
39109
|
} catch {
|
|
38408
39110
|
raw = null;
|
|
38409
39111
|
}
|
|
@@ -38441,7 +39143,7 @@ function makeWatchHandler(deps) {
|
|
|
38441
39143
|
if (suppressor) {
|
|
38442
39144
|
let raw = null;
|
|
38443
39145
|
try {
|
|
38444
|
-
raw =
|
|
39146
|
+
raw = readFileSync23(absolutePath, "utf8");
|
|
38445
39147
|
} catch {
|
|
38446
39148
|
raw = null;
|
|
38447
39149
|
}
|
|
@@ -38479,7 +39181,7 @@ function makeWatchHandler(deps) {
|
|
|
38479
39181
|
if (suppressor) {
|
|
38480
39182
|
let raw = null;
|
|
38481
39183
|
try {
|
|
38482
|
-
raw =
|
|
39184
|
+
raw = readFileSync23(absolutePath, "utf8");
|
|
38483
39185
|
} catch {
|
|
38484
39186
|
raw = null;
|
|
38485
39187
|
}
|
|
@@ -38517,7 +39219,7 @@ function makeWatchHandler(deps) {
|
|
|
38517
39219
|
if (suppressor) {
|
|
38518
39220
|
let raw = null;
|
|
38519
39221
|
try {
|
|
38520
|
-
raw =
|
|
39222
|
+
raw = readFileSync23(absolutePath, "utf8");
|
|
38521
39223
|
} catch {
|
|
38522
39224
|
raw = null;
|
|
38523
39225
|
}
|
|
@@ -38626,7 +39328,7 @@ function makeWatchHandler(deps) {
|
|
|
38626
39328
|
if (strategy) {
|
|
38627
39329
|
let raw = null;
|
|
38628
39330
|
try {
|
|
38629
|
-
raw =
|
|
39331
|
+
raw = readFileSync23(absolutePath, "utf8");
|
|
38630
39332
|
} catch {
|
|
38631
39333
|
raw = null;
|
|
38632
39334
|
}
|
|
@@ -38869,6 +39571,13 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
38869
39571
|
});
|
|
38870
39572
|
if (recon.quarantined > 0)
|
|
38871
39573
|
refreshFrozen();
|
|
39574
|
+
const idBackfill = backfillFrontmatterIds({
|
|
39575
|
+
workspaceSlug: slug,
|
|
39576
|
+
state: loadState(slug)
|
|
39577
|
+
});
|
|
39578
|
+
if (backfillWorthReporting(idBackfill)) {
|
|
39579
|
+
daemonLog.log(frontmatterIdBackfillLine(idBackfill));
|
|
39580
|
+
}
|
|
38872
39581
|
if (recon.diverged > 0 || recon.quarantined > 0) {
|
|
38873
39582
|
const parts = [];
|
|
38874
39583
|
if (recon.pushed > 0)
|
|
@@ -38884,6 +39593,33 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
38884
39593
|
const acted = recon.diverged + recon.quarantined;
|
|
38885
39594
|
out.info(`Reconciled ${acted} offline edit${acted === 1 ? "" : "s"}${parts.length ? `: ${parts.join(", ")}` : ""}.`);
|
|
38886
39595
|
}
|
|
39596
|
+
const disc = recon.discovery;
|
|
39597
|
+
if (disc.created > 0 || disc.renamed > 0 || disc.collisions > 0 || disc.renameConflicts > 0) {
|
|
39598
|
+
const parts = [];
|
|
39599
|
+
if (disc.created > 0)
|
|
39600
|
+
parts.push(`${disc.created} synced`);
|
|
39601
|
+
if (disc.renamed > 0)
|
|
39602
|
+
parts.push(`${disc.renamed} renamed in Plinth`);
|
|
39603
|
+
if (disc.collisions > 0) {
|
|
39604
|
+
parts.push(`${disc.collisions} already in Plinth, local copy stashed to ~/.plinth/_conflicts/`);
|
|
39605
|
+
}
|
|
39606
|
+
if (disc.renameConflicts > 0) {
|
|
39607
|
+
parts.push(`${disc.renameConflicts} changed in Plinth first, local copy stashed to ~/.plinth/_conflicts/`);
|
|
39608
|
+
}
|
|
39609
|
+
out.info(`Caught up on files changed while Plinth was closed: ${parts.join(", ")}.`);
|
|
39610
|
+
}
|
|
39611
|
+
if (disc.heldCap > 0) {
|
|
39612
|
+
out.info(`Found ${disc.heldCap} new file${disc.heldCap === 1 ? "" : "s"} in this ` + `workspace — more than Plinth adds on its own (limit ${discoveryWriteCap()}), ` + `so none were added. Run \`plinth status\` to see them.`);
|
|
39613
|
+
}
|
|
39614
|
+
if (disc.heldArtist > 0) {
|
|
39615
|
+
out.info(`${disc.heldArtist} artist folder${disc.heldArtist === 1 ? "" : "s"} ` + `${disc.heldArtist === 1 ? "has" : "have"} no artist in Plinth. Plinth ` + `never adds an artist on its own — see \`plinth status\`.`);
|
|
39616
|
+
}
|
|
39617
|
+
if (disc.refusedRename > 0) {
|
|
39618
|
+
out.info(`${disc.refusedRename} renamed file${disc.refusedRename === 1 ? "" : "s"} ` + `${disc.refusedRename === 1 ? "has a name" : "have names"} already taken in ` + `Plinth, so ${disc.refusedRename === 1 ? "it was" : "they were"} left alone — ` + `see \`plinth status\`.`);
|
|
39619
|
+
}
|
|
39620
|
+
if (disc.heldCloudRow > 0) {
|
|
39621
|
+
out.info(`${disc.heldCloudRow} file${disc.heldCloudRow === 1 ? "" : "s"} came from ` + `Plinth but this copy has lost track of ${disc.heldCloudRow === 1 ? "it" : "them"}. ` + `Nothing is lost — see \`plinth status\`.`);
|
|
39622
|
+
}
|
|
38887
39623
|
const reconFiles = await reconcileFilesSubtree({
|
|
38888
39624
|
workspaceSlug: slug,
|
|
38889
39625
|
baseUrl: entry2.api_base_url,
|
|
@@ -39022,6 +39758,7 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
39022
39758
|
const msg = err instanceof Error ? err.message : "unknown error";
|
|
39023
39759
|
out.warn(`Artist memory reconcile skipped: ${msg}`);
|
|
39024
39760
|
}
|
|
39761
|
+
const loggedTombstoneSkips = new Set;
|
|
39025
39762
|
const initial = await runSync(workspaceSlug, {
|
|
39026
39763
|
fetchImpl,
|
|
39027
39764
|
keychain,
|
|
@@ -39030,7 +39767,8 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
39030
39767
|
now: deps.now,
|
|
39031
39768
|
logSink: daemonLog,
|
|
39032
39769
|
isFrozen,
|
|
39033
|
-
isFileFrozen
|
|
39770
|
+
isFileFrozen,
|
|
39771
|
+
loggedTombstoneSkips
|
|
39034
39772
|
});
|
|
39035
39773
|
if (initial.exitCode !== 0)
|
|
39036
39774
|
return initial;
|
|
@@ -39259,7 +39997,8 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
39259
39997
|
logSink: daemonLog,
|
|
39260
39998
|
suppressor,
|
|
39261
39999
|
isFrozen,
|
|
39262
|
-
isFileFrozen
|
|
40000
|
+
isFileFrozen,
|
|
40001
|
+
loggedTombstoneSkips
|
|
39263
40002
|
});
|
|
39264
40003
|
if (result.exitCode === 0) {
|
|
39265
40004
|
const applied = result.applied ?? 0;
|
|
@@ -39720,11 +40459,11 @@ var startCommand = defineCommand({
|
|
|
39720
40459
|
});
|
|
39721
40460
|
|
|
39722
40461
|
// src/commands/status.ts
|
|
39723
|
-
import { statSync as
|
|
40462
|
+
import { statSync as statSync17 } from "node:fs";
|
|
39724
40463
|
|
|
39725
40464
|
// src/lib/build-stamp.ts
|
|
39726
40465
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
39727
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync11, readFileSync as
|
|
40466
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync11, readFileSync as readFileSync24, writeFileSync as writeFileSync8 } from "node:fs";
|
|
39728
40467
|
import { dirname as dirname10, join as join17 } from "node:path";
|
|
39729
40468
|
import { fileURLToPath } from "node:url";
|
|
39730
40469
|
var BUILD_STAMP_FILENAME = "build-stamp.json";
|
|
@@ -39748,7 +40487,7 @@ function readHead(repoDir) {
|
|
|
39748
40487
|
}
|
|
39749
40488
|
function readBuildStamp(distDir) {
|
|
39750
40489
|
try {
|
|
39751
|
-
const parsed = JSON.parse(
|
|
40490
|
+
const parsed = JSON.parse(readFileSync24(stampPath(distDir), "utf8"));
|
|
39752
40491
|
if (parsed && typeof parsed.sha === "string" && parsed.sha.length > 0 && typeof parsed.dirty === "boolean" && typeof parsed.built_at === "string") {
|
|
39753
40492
|
return { sha: parsed.sha, dirty: parsed.dirty, built_at: parsed.built_at };
|
|
39754
40493
|
}
|
|
@@ -39812,9 +40551,10 @@ function checkBuildStaleness(deps = {}) {
|
|
|
39812
40551
|
init_output();
|
|
39813
40552
|
|
|
39814
40553
|
// src/lib/pending-writes.ts
|
|
39815
|
-
import { readFileSync as
|
|
40554
|
+
import { readFileSync as readFileSync25, statSync as statSync16 } from "node:fs";
|
|
39816
40555
|
import { relative as relative13 } from "node:path";
|
|
39817
|
-
var
|
|
40556
|
+
var import_gray_matter13 = __toESM(require_gray_matter(), 1);
|
|
40557
|
+
var EMPTY2 = { writes: [], count: 0 };
|
|
39818
40558
|
function reportedElsewhere(state) {
|
|
39819
40559
|
const entities = new Set;
|
|
39820
40560
|
const files = new Set;
|
|
@@ -39832,13 +40572,14 @@ function scanPendingWrites(args) {
|
|
|
39832
40572
|
try {
|
|
39833
40573
|
mirrorRootPath = workspaceMirrorPath(workspaceSlug);
|
|
39834
40574
|
} catch {
|
|
39835
|
-
return
|
|
40575
|
+
return EMPTY2;
|
|
39836
40576
|
}
|
|
39837
40577
|
const ignored = watcherIgnoreMatcher(mirrorRootPath);
|
|
39838
40578
|
const reported = reportedElsewhere(state);
|
|
39839
40579
|
const frozenEntities = computeFrozen(workspaceSlug);
|
|
39840
40580
|
const frozenFiles = computeFrozenFiles(workspaceSlug);
|
|
39841
40581
|
const writes = [];
|
|
40582
|
+
const discovered = [];
|
|
39842
40583
|
for (const absolutePath of walkSubtreeFiles(mirrorRootPath, ignored)) {
|
|
39843
40584
|
const relPath = relative13(mirrorRootPath, absolutePath);
|
|
39844
40585
|
const fileMatch = classifyFilesPath({ workspaceSlug, absolutePath });
|
|
@@ -39850,7 +40591,7 @@ function scanPendingWrites(args) {
|
|
|
39850
40591
|
const entry3 = getFileEntry(state, fileMatch.relPath);
|
|
39851
40592
|
let size;
|
|
39852
40593
|
try {
|
|
39853
|
-
size =
|
|
40594
|
+
size = statSync16(absolutePath).size;
|
|
39854
40595
|
} catch {
|
|
39855
40596
|
continue;
|
|
39856
40597
|
}
|
|
@@ -39858,7 +40599,7 @@ function scanPendingWrites(args) {
|
|
|
39858
40599
|
continue;
|
|
39859
40600
|
let hash2;
|
|
39860
40601
|
try {
|
|
39861
|
-
hash2 = sha256Bytes(
|
|
40602
|
+
hash2 = sha256Bytes(readFileSync25(absolutePath));
|
|
39862
40603
|
} catch {
|
|
39863
40604
|
continue;
|
|
39864
40605
|
}
|
|
@@ -39879,21 +40620,42 @@ function scanPendingWrites(args) {
|
|
|
39879
40620
|
continue;
|
|
39880
40621
|
let liveInode;
|
|
39881
40622
|
try {
|
|
39882
|
-
liveInode =
|
|
40623
|
+
liveInode = statSync16(absolutePath).ino;
|
|
39883
40624
|
} catch {
|
|
39884
40625
|
liveInode = undefined;
|
|
39885
40626
|
}
|
|
39886
|
-
const
|
|
40627
|
+
const byInode = liveInode !== undefined ? getEntryByInode(state, entityType, liveInode) : undefined;
|
|
40628
|
+
const entry2 = byInode ?? getEntryBySlug(state, entityType, entitySlug);
|
|
39887
40629
|
if (entry2 && frozenEntities.has(`${entityType}:${entry2.id}`))
|
|
39888
40630
|
continue;
|
|
39889
40631
|
let content;
|
|
39890
40632
|
try {
|
|
39891
|
-
content =
|
|
40633
|
+
content = readFileSync25(absolutePath, "utf8");
|
|
39892
40634
|
} catch {
|
|
39893
40635
|
continue;
|
|
39894
40636
|
}
|
|
39895
40637
|
if (!entry2) {
|
|
39896
|
-
|
|
40638
|
+
let frontmatter;
|
|
40639
|
+
try {
|
|
40640
|
+
frontmatter = import_gray_matter13.default(content).data ?? {};
|
|
40641
|
+
} catch {
|
|
40642
|
+
continue;
|
|
40643
|
+
}
|
|
40644
|
+
const verdict = classifyUntrackedEntity({
|
|
40645
|
+
entityType,
|
|
40646
|
+
entitySlug,
|
|
40647
|
+
frontmatter
|
|
40648
|
+
});
|
|
40649
|
+
if (verdict.kind === "hold") {
|
|
40650
|
+
writes.push({
|
|
40651
|
+
path: relPath,
|
|
40652
|
+
kind: "new",
|
|
40653
|
+
clears: "resave",
|
|
40654
|
+
held: verdict.held
|
|
40655
|
+
});
|
|
40656
|
+
} else {
|
|
40657
|
+
discovered.push({ path: relPath, kind: "new" });
|
|
40658
|
+
}
|
|
39897
40659
|
continue;
|
|
39898
40660
|
}
|
|
39899
40661
|
let hashes;
|
|
@@ -39902,10 +40664,38 @@ function scanPendingWrites(args) {
|
|
|
39902
40664
|
} catch {
|
|
39903
40665
|
continue;
|
|
39904
40666
|
}
|
|
39905
|
-
|
|
40667
|
+
const edited = hashes.bodyHash !== entry2.body_hash || hashes.frontmatterHash !== entry2.frontmatter_hash;
|
|
40668
|
+
if (byInode && byInode.slug !== entitySlug) {
|
|
40669
|
+
const destination = classifyRenameDestination({
|
|
40670
|
+
state,
|
|
40671
|
+
entityType,
|
|
40672
|
+
entitySlug,
|
|
40673
|
+
movedEntryId: byInode.id
|
|
40674
|
+
});
|
|
40675
|
+
if (destination.kind === "hold") {
|
|
40676
|
+
writes.push({
|
|
40677
|
+
path: relPath,
|
|
40678
|
+
kind: edited ? "edited" : "renamed",
|
|
40679
|
+
clears: "resave",
|
|
40680
|
+
held: destination.held
|
|
40681
|
+
});
|
|
40682
|
+
} else {
|
|
40683
|
+
writes.push({
|
|
40684
|
+
path: relPath,
|
|
40685
|
+
kind: edited ? "edited" : "renamed",
|
|
40686
|
+
clears: "boot"
|
|
40687
|
+
});
|
|
40688
|
+
}
|
|
40689
|
+
continue;
|
|
40690
|
+
}
|
|
40691
|
+
if (edited) {
|
|
39906
40692
|
writes.push({ path: relPath, kind: "edited", clears: "boot" });
|
|
39907
40693
|
}
|
|
39908
40694
|
}
|
|
40695
|
+
const overCap = discovered.length > discoveryWriteCap();
|
|
40696
|
+
for (const d of discovered) {
|
|
40697
|
+
writes.push(overCap ? { path: d.path, kind: d.kind, clears: "resave", held: "cap" } : { path: d.path, kind: d.kind, clears: "boot" });
|
|
40698
|
+
}
|
|
39909
40699
|
for (const lane of [
|
|
39910
40700
|
{
|
|
39911
40701
|
filename: WORKSPACE_RULES_FILENAME,
|
|
@@ -39982,6 +40772,15 @@ function row(label, value) {
|
|
|
39982
40772
|
function cont(value) {
|
|
39983
40773
|
return `${" ".repeat(LABEL_WIDTH)} ${value}`;
|
|
39984
40774
|
}
|
|
40775
|
+
function uncheckedLines(items) {
|
|
40776
|
+
const n = items.length;
|
|
40777
|
+
const head = `${n} generated command file${n === 1 ? "" : "s"} could not be checked, ` + `so ${n === 1 ? "its block was" : "their blocks were"} not compared.`;
|
|
40778
|
+
const why = {
|
|
40779
|
+
unreadable: "could not be opened; check its permissions and that it is a file",
|
|
40780
|
+
"no-markers": "its BEGIN/END markers are gone; the next sync backs your copy up and restores them"
|
|
40781
|
+
};
|
|
40782
|
+
return [head, ...items.map((i) => `${i.commandRel} — ${why[i.reason]}`)];
|
|
40783
|
+
}
|
|
39985
40784
|
function formatRelative(deltaMs) {
|
|
39986
40785
|
const s = Math.floor(Math.max(0, deltaMs) / 1000);
|
|
39987
40786
|
if (s < 1)
|
|
@@ -40021,7 +40820,7 @@ function renderBuild(out, staleness) {
|
|
|
40021
40820
|
}
|
|
40022
40821
|
function lastActivity(now) {
|
|
40023
40822
|
try {
|
|
40024
|
-
const mtimeMs =
|
|
40823
|
+
const mtimeMs = statSync17(daemonLogPath()).mtimeMs;
|
|
40025
40824
|
return formatRelative(now.getTime() - mtimeMs);
|
|
40026
40825
|
} catch {
|
|
40027
40826
|
return null;
|
|
@@ -40127,7 +40926,10 @@ async function runStatus(args, deps = {}) {
|
|
|
40127
40926
|
out.info(row("Unsynced", "All local writes synced."));
|
|
40128
40927
|
} else {
|
|
40129
40928
|
const bootable = pending.writes.filter((w) => w.clears === "boot");
|
|
40130
|
-
const
|
|
40929
|
+
const heldByCap = pending.writes.filter((w) => w.held === "cap");
|
|
40930
|
+
const heldArtists = pending.writes.filter((w) => w.held === "artist");
|
|
40931
|
+
const heldCloudRows = pending.writes.filter((w) => w.held === "cloud-row");
|
|
40932
|
+
const heldRenames = pending.writes.filter((w) => w.held === "rename-taken");
|
|
40131
40933
|
const MAX_SHOWN = 10;
|
|
40132
40934
|
const listPaths = (writes) => {
|
|
40133
40935
|
for (const w of writes.slice(0, MAX_SHOWN)) {
|
|
@@ -40148,10 +40950,25 @@ async function runStatus(args, deps = {}) {
|
|
|
40148
40950
|
listPaths(bootable);
|
|
40149
40951
|
out.info(cont(pushing ? "A file you just saved clears within seconds. If something " + "stays listed, save it again or restart the daemon." : "Start the daemon so it can push them — the Plinth desktop app " + "runs it for you; in a terminal, `plinth start`."));
|
|
40150
40952
|
}
|
|
40151
|
-
if (
|
|
40152
|
-
head(`${
|
|
40153
|
-
listPaths(
|
|
40154
|
-
out.info(cont(
|
|
40953
|
+
if (heldByCap.length > 0) {
|
|
40954
|
+
head(`${heldByCap.length} file${heldByCap.length === 1 ? "" : "s"} ` + `Plinth has never seen — more than it adds unattended in one go.`);
|
|
40955
|
+
listPaths(heldByCap);
|
|
40956
|
+
out.info(cont(`Plinth adds at most ${discoveryWriteCap()} new files per start. ` + "With the daemon running, open and save the ones you want now — or " + "set PLINTH_DISCOVERY_WRITE_CAP higher and restart it."));
|
|
40957
|
+
}
|
|
40958
|
+
if (heldArtists.length > 0) {
|
|
40959
|
+
head(`${heldArtists.length} artist file${heldArtists.length === 1 ? "" : "s"} ` + `with no artist in Plinth.`);
|
|
40960
|
+
listPaths(heldArtists);
|
|
40961
|
+
out.info(cont("Plinth never adds an artist on its own. Create the artist in Plinth, " + "or open and save this file with the daemon running to add them."));
|
|
40962
|
+
}
|
|
40963
|
+
if (heldRenames.length > 0) {
|
|
40964
|
+
head(`${heldRenames.length} renamed file${heldRenames.length === 1 ? "" : "s"} ` + `whose new name is already taken in Plinth.`);
|
|
40965
|
+
listPaths(heldRenames);
|
|
40966
|
+
out.info(cont("Two files cannot share a name in Plinth. Rename this one to something " + "free and it will sync."));
|
|
40967
|
+
}
|
|
40968
|
+
if (heldCloudRows.length > 0) {
|
|
40969
|
+
head(`${heldCloudRows.length} file${heldCloudRows.length === 1 ? "" : "s"} ` + `that came from Plinth but this copy has lost track of.`);
|
|
40970
|
+
listPaths(heldCloudRows);
|
|
40971
|
+
out.info(cont("If this copy simply lost track of them, re-sync with `plinth login " + `--workspace ${slug} --reset-sync\`. If you made this file by ` + "duplicating another, delete its `revision:` line and Plinth will " + "add it as new."));
|
|
40155
40972
|
}
|
|
40156
40973
|
if (unsynced > 0) {
|
|
40157
40974
|
head(`${unsynced} write${unsynced === 1 ? "" : "s"} awaiting retry (rate-limited or offline).`);
|
|
@@ -40186,6 +41003,33 @@ async function runStatus(args, deps = {}) {
|
|
|
40186
41003
|
out.info(cont(`Full list with restore commands: ${CONFLICTS_VIEW_REL_PATH} (written by the sync, so this line is the fresher of the two)`));
|
|
40187
41004
|
}
|
|
40188
41005
|
out.info("");
|
|
41006
|
+
const commandScan = scanStaleUserBlocks({ workspaceSlug: slug });
|
|
41007
|
+
const unchecked = commandScan.unchecked;
|
|
41008
|
+
if (commandScan.stale.length > 0) {
|
|
41009
|
+
const n = commandScan.stale.length;
|
|
41010
|
+
out.info(row("Commands", n === 1 ? `1 customisable block names machinery ${commandScan.stale[0].invocation} no longer uses.` : `${n} customisable blocks name machinery their commands no longer use.`));
|
|
41011
|
+
for (const f of commandScan.stale) {
|
|
41012
|
+
const versions2 = new Set(f.tokens.map((t) => t.version));
|
|
41013
|
+
const names = f.tokens.map((t) => versions2.size === 1 ? `\`${t.token}\`` : `\`${t.token}\` (${t.version})`);
|
|
41014
|
+
const joined = names.length === 1 ? names[0] : `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
|
|
41015
|
+
const inVersion = versions2.size === 1 ? ` in ${f.tokens[0].version}` : "";
|
|
41016
|
+
out.info(cont(`${f.commandRel} names ${joined}, retired from ${f.invocation}` + `${inVersion}. Review it.`));
|
|
41017
|
+
}
|
|
41018
|
+
if (unchecked.length > 0) {
|
|
41019
|
+
for (const line of uncheckedLines(unchecked))
|
|
41020
|
+
out.info(cont(line));
|
|
41021
|
+
}
|
|
41022
|
+
} else if (unchecked.length > 0) {
|
|
41023
|
+
const [head, ...rest] = uncheckedLines(unchecked);
|
|
41024
|
+
out.info(row("Commands", head));
|
|
41025
|
+
for (const line of rest)
|
|
41026
|
+
out.info(cont(line));
|
|
41027
|
+
} else if (commandScan.checked === 0) {
|
|
41028
|
+
out.info(row("Commands", "No generated commands yet. They arrive on the next sync."));
|
|
41029
|
+
} else {
|
|
41030
|
+
out.info(row("Commands", "Your customisable blocks match the current commands."));
|
|
41031
|
+
}
|
|
41032
|
+
out.info("");
|
|
40189
41033
|
const voiceRecord = localState.user_voice;
|
|
40190
41034
|
let voiceRaw = null;
|
|
40191
41035
|
try {
|
|
@@ -40207,6 +41051,59 @@ async function runStatus(args, deps = {}) {
|
|
|
40207
41051
|
} else {
|
|
40208
41052
|
out.info(row("Voice", `${VOICE_FILENAME} is present; Plinth has not checked it yet.`));
|
|
40209
41053
|
}
|
|
41054
|
+
out.info("");
|
|
41055
|
+
let report = null;
|
|
41056
|
+
try {
|
|
41057
|
+
report = runGroundingReport({ workspaceSlug: slug });
|
|
41058
|
+
} catch {
|
|
41059
|
+
report = null;
|
|
41060
|
+
}
|
|
41061
|
+
if (report === null) {
|
|
41062
|
+
out.info(row("Injection", "Could not render the session grounding to measure it."));
|
|
41063
|
+
} else {
|
|
41064
|
+
const injected = report.block;
|
|
41065
|
+
const bytes = Buffer.byteLength(injected);
|
|
41066
|
+
const overBudget = bytes > GROUNDING_BUDGET_BYTES;
|
|
41067
|
+
out.info(row("Injection", `${bytes.toLocaleString("en-GB")} bytes at every session start` + (overBudget ? "" : ` (budget ${GROUNDING_BUDGET_BYTES.toLocaleString("en-GB")})`)));
|
|
41068
|
+
const contributors = [];
|
|
41069
|
+
const rides = (name, reduce) => {
|
|
41070
|
+
let raw = null;
|
|
41071
|
+
try {
|
|
41072
|
+
raw = readContextFile(slug, name);
|
|
41073
|
+
} catch {
|
|
41074
|
+
raw = null;
|
|
41075
|
+
}
|
|
41076
|
+
const body = reduce(raw);
|
|
41077
|
+
if (body !== null)
|
|
41078
|
+
contributors.push({ name, bytes: Buffer.byteLength(body) });
|
|
41079
|
+
};
|
|
41080
|
+
rides(WORKSPACE_MEMORY_FILENAME, extractWorkspaceMemory);
|
|
41081
|
+
rides(USER_MEMORY_FILENAME, extractUserMemory);
|
|
41082
|
+
contributors.sort((a, b) => b.bytes - a.bytes);
|
|
41083
|
+
const largest = contributors[0];
|
|
41084
|
+
const indexes = [
|
|
41085
|
+
{ name: WORKSPACE_MEMORY_FILENAME, pick: report.picks.wmPick },
|
|
41086
|
+
{ name: USER_MEMORY_FILENAME, pick: report.picks.umPick }
|
|
41087
|
+
].flatMap((i) => i.pick === null ? [] : [{ name: i.name, ...i.pick }]);
|
|
41088
|
+
const dropped = indexes.filter((i) => i.kept === 0 && i.total > 0);
|
|
41089
|
+
const partial2 = indexes.filter((i) => i.kept > 0 && i.kept < i.total);
|
|
41090
|
+
if (overBudget) {
|
|
41091
|
+
out.info(cont(`Over the ${GROUNDING_BUDGET_BYTES.toLocaleString("en-GB")}-byte budget.` + (indexes.length > 0 ? " Your memory indexes are already trimmed as far as they go, so the rest is Plinth's own grounding." : " There is no memory index to trim, so all of it is Plinth's own grounding and this workspace's rules.") + " Report it."));
|
|
41092
|
+
} else if (dropped.length > 0 || partial2.length > 0) {
|
|
41093
|
+
for (const i of dropped) {
|
|
41094
|
+
out.info(cont(`NONE of ${i.name} fits in the injection: all ${i.total} facts are read from the file on demand, not recalled.`));
|
|
41095
|
+
}
|
|
41096
|
+
for (const i of partial2) {
|
|
41097
|
+
out.info(cont(`Only ${i.kept} of ${i.total} facts in ${i.name} fit in the injection. The rest stay in the file and are read on demand.`));
|
|
41098
|
+
}
|
|
41099
|
+
if (largest !== undefined) {
|
|
41100
|
+
out.info(cont(`${largest.name} is the largest of these at ${largest.bytes.toLocaleString("en-GB")} bytes. Shorter lines fit more facts: the convention is one line per fact, a hook and a [[pointer]], not a paragraph.`));
|
|
41101
|
+
}
|
|
41102
|
+
} else {
|
|
41103
|
+
out.info(cont("Every memory fact fits; nothing is being trimmed."));
|
|
41104
|
+
}
|
|
41105
|
+
}
|
|
41106
|
+
out.info("");
|
|
40210
41107
|
return { exitCode: 0 };
|
|
40211
41108
|
}
|
|
40212
41109
|
var statusCommand = defineCommand({
|
|
@@ -40246,7 +41143,7 @@ var versionCommand = defineCommand({
|
|
|
40246
41143
|
});
|
|
40247
41144
|
|
|
40248
41145
|
// src/commands/voice-gate.ts
|
|
40249
|
-
import { readFileSync as
|
|
41146
|
+
import { readFileSync as readFileSync28 } from "node:fs";
|
|
40250
41147
|
|
|
40251
41148
|
// src/lib/voice-gate/api.ts
|
|
40252
41149
|
var VoiceGateSurfaces = [
|
|
@@ -40351,7 +41248,7 @@ async function callVoiceGate(req) {
|
|
|
40351
41248
|
import { createHash as createHash4 } from "node:crypto";
|
|
40352
41249
|
import {
|
|
40353
41250
|
mkdirSync as mkdirSync12,
|
|
40354
|
-
readFileSync as
|
|
41251
|
+
readFileSync as readFileSync26,
|
|
40355
41252
|
renameSync as renameSync8,
|
|
40356
41253
|
unlinkSync as unlinkSync8,
|
|
40357
41254
|
writeFileSync as writeFileSync9
|
|
@@ -40382,7 +41279,7 @@ function emptyFile() {
|
|
|
40382
41279
|
}
|
|
40383
41280
|
function load2(workspaceSlug) {
|
|
40384
41281
|
try {
|
|
40385
|
-
const raw =
|
|
41282
|
+
const raw = readFileSync26(attemptsPath(workspaceSlug), "utf8");
|
|
40386
41283
|
const parsed = AttemptsFileSchema.safeParse(JSON.parse(raw));
|
|
40387
41284
|
return parsed.success ? parsed.data : emptyFile();
|
|
40388
41285
|
} catch {
|
|
@@ -40545,7 +41442,7 @@ function denyUngated(detail) {
|
|
|
40545
41442
|
}
|
|
40546
41443
|
|
|
40547
41444
|
// src/lib/voice-gate/payload.ts
|
|
40548
|
-
import { readFileSync as
|
|
41445
|
+
import { readFileSync as readFileSync27, statSync as statSync18 } from "node:fs";
|
|
40549
41446
|
import { homedir as homedir2 } from "node:os";
|
|
40550
41447
|
import { isAbsolute as isAbsolute3, resolve as resolve4 } from "node:path";
|
|
40551
41448
|
var MAX_BODY_BYTES = 256 * 1024;
|
|
@@ -40702,11 +41599,11 @@ function isHelperInvocation(tokens) {
|
|
|
40702
41599
|
return helperIndex === start + 1 && /(^|\/)python[0-9.]*$/.test(prev);
|
|
40703
41600
|
}
|
|
40704
41601
|
function defaultReadBodyFile(path5) {
|
|
40705
|
-
const stat4 =
|
|
41602
|
+
const stat4 = statSync18(path5);
|
|
40706
41603
|
if (stat4.size > MAX_BODY_BYTES) {
|
|
40707
41604
|
throw new Error(`draft body is ${stat4.size} bytes, over the ${MAX_BODY_BYTES}-byte gate limit`);
|
|
40708
41605
|
}
|
|
40709
|
-
return
|
|
41606
|
+
return readFileSync27(path5, "utf8");
|
|
40710
41607
|
}
|
|
40711
41608
|
function parseRecipients(...raw) {
|
|
40712
41609
|
const all = raw.filter((v) => typeof v === "string").flatMap((v) => v.split(",")).map((v) => v.trim().toLowerCase()).filter((v) => v.length > 0);
|
|
@@ -41066,7 +41963,7 @@ async function runVoiceGateDirect(args, deps = {}) {
|
|
|
41066
41963
|
let text;
|
|
41067
41964
|
if (args.file) {
|
|
41068
41965
|
try {
|
|
41069
|
-
text =
|
|
41966
|
+
text = readFileSync28(args.file, "utf8");
|
|
41070
41967
|
} catch (err) {
|
|
41071
41968
|
const msg = err instanceof Error ? err.message : "unknown error";
|
|
41072
41969
|
out.error(`Could not read ${args.file}: ${msg}`);
|