@plinth-music/cli 0.12.0 → 0.14.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 +84 -0
- package/README.md +10 -6
- package/dist/build-stamp.json +2 -2
- package/dist/cli.js +1350 -257
- 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.14.0",
|
|
7588
7588
|
description: "Plinth capstone sync daemon and CLI",
|
|
7589
7589
|
type: "module",
|
|
7590
7590
|
private: false,
|
|
@@ -22434,7 +22434,8 @@ function stripFm(fm) {
|
|
|
22434
22434
|
}
|
|
22435
22435
|
return out;
|
|
22436
22436
|
}
|
|
22437
|
-
var
|
|
22437
|
+
var ARTIST_ANCHOR_FILENAME = "_artist.md";
|
|
22438
|
+
var MIRROR_ANCHOR = ARTIST_ANCHOR_FILENAME;
|
|
22438
22439
|
var artistsStrategy = {
|
|
22439
22440
|
type: "artists",
|
|
22440
22441
|
apiSegment: "artists",
|
|
@@ -25892,10 +25893,39 @@ function invocationFor(relPath) {
|
|
|
25892
25893
|
const namespace = parts.pop() ?? "";
|
|
25893
25894
|
return `/${namespace}:${name}`;
|
|
25894
25895
|
}
|
|
25896
|
+
var GENERATED_COMMAND_RELS = [
|
|
25897
|
+
START_COMMAND_REL,
|
|
25898
|
+
CLOSE_COMMAND_REL,
|
|
25899
|
+
ONBOARD_COMMAND_REL
|
|
25900
|
+
];
|
|
25895
25901
|
var START_COMMAND_INVOCATION = invocationFor(START_COMMAND_REL);
|
|
25896
25902
|
var CLOSE_COMMAND_INVOCATION = invocationFor(CLOSE_COMMAND_REL);
|
|
25897
25903
|
var ONBOARD_COMMAND_INVOCATION = invocationFor(ONBOARD_COMMAND_REL);
|
|
25898
25904
|
|
|
25905
|
+
// src/lib/context/bars.ts
|
|
25906
|
+
var RULE_FILE_BAR_BYTES = 6000;
|
|
25907
|
+
var FACT_STORE_BAR_BYTES = 12000;
|
|
25908
|
+
var ROOT_CLAUDE_MD_LINE_BAR = 140;
|
|
25909
|
+
var COMMAND_BAR_BYTES = 24000;
|
|
25910
|
+
var INDEX_LINE_MAX_BYTES = 300;
|
|
25911
|
+
var MAX_NAMED_FACT_STORES = 5;
|
|
25912
|
+
var MAX_NAMED_LONG_INDEX_LINES = 3;
|
|
25913
|
+
function countRuleShapedLines(body) {
|
|
25914
|
+
let count = 0;
|
|
25915
|
+
for (const line of body.split(`
|
|
25916
|
+
`)) {
|
|
25917
|
+
const trimmed = line.trim();
|
|
25918
|
+
if (/^[-*+]\s/.test(trimmed))
|
|
25919
|
+
count += 1;
|
|
25920
|
+
else if (/^\*\*[^*]+\*\*/.test(trimmed))
|
|
25921
|
+
count += 1;
|
|
25922
|
+
}
|
|
25923
|
+
return count;
|
|
25924
|
+
}
|
|
25925
|
+
function countParagraphs(body) {
|
|
25926
|
+
return body.split(/\n\s*\n/).map((p) => p.trim()).filter((p) => p.length > 0).length;
|
|
25927
|
+
}
|
|
25928
|
+
|
|
25899
25929
|
// src/lib/context/grounding.ts
|
|
25900
25930
|
var WORKSPACE_RULES_FILENAME = "workspace-rules.md";
|
|
25901
25931
|
var HOUSE_STYLE_LEGACY_FILENAME = "house-style.md";
|
|
@@ -25959,6 +25989,7 @@ var GROUNDING_CATEGORY_ORDER = [
|
|
|
25959
25989
|
"grounding",
|
|
25960
25990
|
"identity",
|
|
25961
25991
|
"budgetNote",
|
|
25992
|
+
"sizeNote",
|
|
25962
25993
|
"voice",
|
|
25963
25994
|
"rules",
|
|
25964
25995
|
"workspaceFacts",
|
|
@@ -25968,6 +25999,7 @@ var GROUNDING_CATEGORY_LABELS = {
|
|
|
25968
25999
|
grounding: "Plinth's own grounding",
|
|
25969
26000
|
identity: "who you are",
|
|
25970
26001
|
budgetNote: "the trim notes",
|
|
26002
|
+
sizeNote: "the size warning",
|
|
25971
26003
|
voice: "voice pointer",
|
|
25972
26004
|
rules: "this workspace's rules",
|
|
25973
26005
|
workspaceFacts: "workspace facts",
|
|
@@ -25986,7 +26018,8 @@ function segmentBytes(lines, cats) {
|
|
|
25986
26018
|
workspaceFacts: 0,
|
|
25987
26019
|
personalFacts: 0,
|
|
25988
26020
|
voice: 0,
|
|
25989
|
-
budgetNote: 0
|
|
26021
|
+
budgetNote: 0,
|
|
26022
|
+
sizeNote: 0
|
|
25990
26023
|
};
|
|
25991
26024
|
lines.forEach((text, i) => {
|
|
25992
26025
|
const cat = cats[i];
|
|
@@ -26156,7 +26189,7 @@ function titlesLeadIn(kept, named, unnamed, total, filename) {
|
|
|
26156
26189
|
parts.push(`${unnamed} not shown at all`);
|
|
26157
26190
|
return `Not all of this index fits. It holds ${total} facts: ${parts.join(", ")}. ` + `Every line below ends \`…\` because it is an opening and not the whole ` + `fact. They are all in \`${filename}\` at the mirror root: READ THAT FILE ` + `IN FULL before you answer that no fact covers something.`;
|
|
26158
26191
|
}
|
|
26159
|
-
function composeGroundingBlock(today, workspaceRules, workspaceMemory, userMemory, freshness, rosterIsEmpty, voiceProfile, voiceMalformed, workspaceSlug, workspaceMemoryNote, userMemoryNote, member, workspaceTitles = null, userTitles = null) {
|
|
26192
|
+
function composeGroundingBlock(today, workspaceRules, workspaceMemory, userMemory, freshness, rosterIsEmpty, voiceProfile, voiceMalformed, workspaceSlug, workspaceMemoryNote, userMemoryNote, member, workspaceTitles = null, userTitles = null, filesPastBar = 0) {
|
|
26160
26193
|
const gateFlag = workspaceSlug === null ? `--workspace="<this workspace>"` : `--workspace ${workspaceSlug}`;
|
|
26161
26194
|
const identity = renderMemberIdentity(member, workspaceSlug);
|
|
26162
26195
|
const block = [];
|
|
@@ -26170,7 +26203,7 @@ function composeGroundingBlock(today, workspaceRules, workspaceMemory, userMemor
|
|
|
26170
26203
|
add("grounding", `Plinth session grounding:`, ``, `Today's date: ${today}. You have no inherent sense of the current date and ` + `your training has a knowledge cutoff, so treat this injected date as ` + `authoritative for anything time-relative: deadlines, what counts as ` + `"recent" or "upcoming", and the age of any data or of your own knowledge.`);
|
|
26171
26204
|
if (identity !== null)
|
|
26172
26205
|
add("identity", ``, identity);
|
|
26173
|
-
add("grounding", ``, `Resolve Plinth entities before answering about them. For any answer grounded ` + `in a specific artist, project, task, or other workspace entity, first look ` + `the name up in THIS workspace with the Plinth MCP tools ` + `(\`mcp__plinth__search_entities\`, \`mcp__plinth__list_artists\`, or ` + `\`mcp__plinth__list_projects\`) and ground your answer in what they return. ` + `Do not answer from name recognition alone: a globally famous name may not ` + `exist in this workspace, and an entity here may differ from the public ` + `figure of the same name. If the lookup returns nothing, say so instead of ` + `inventing details.`, ``, `Plan before you write. The first time you call any mutating Plinth MCP tool ` + `in a session (create_*, update_*, delete_*, mark_*: anything that changes ` + `workspace data), do not commit silently: state exactly what you would write, ` + `naming the tool, the entity, and the fields and values, and proceed only after ` + `the user confirms or amends.`, ``, `Write through the canonical surface. In this cockpit the file tree is your edit ` + `surface, so reach for it before the most direct tool. Change prose (document ` + `bodies, free-form notes, anything textual) by editing the mirror markdown ` + `file; the daemon syncs it up. An artist's master context is the clearest case: ` + `it lives in the body of that artist's \`artists/<slug>/_artist.md\` mirror file, ` + `the file the web context pane renders, and one of only two files under ` + `\`artists/<slug>/\` the daemon syncs both ways (\`memory.md\` is the other), ` + `so change it by editing that body, never by creating ` + `a separate document or context entity (e.g. \`create_document\`) the pane won't ` + `render. One carve-out inside that file: a block labelled as rendered from ` + `Plinth is regenerated on every sync and your edits to it are discarded, so ` + `write those through the tool the block names. Change structured fields (task ` + `status, priority, assignees, due dates, a project's category) through the ` + `Plinth MCP (\`mcp__plinth__*\`) or the task file's frontmatter, which is ` + `row-canonical but still written through the validated API. Make a bulk or ` + `destructive change (a mass delete, a gutting overwrite) by editing the file ` + `path, so the daemon's confirm-gate can quarantine it for \`plinth confirm\`. ` + `Never write the database directly, which rules out raw SQL and ` + `\`execute_sql\`, \`apply_migration\` and \`psql\`: those bypass the daemon, the ` + `mirror, the confirm-gate, canonicality, and API validation.`, ``, `Dates belong on the calendar, not in \`key_dates\`. The date surface for an artist ` + `is their mapped Google Calendar; the \`key_dates\` table is retired. Reading ` + `dates is yours, and this mirror already holds them: the daemon projects every ` + `mapped calendar into \`views/calendar/upcoming.md\` and ` + `\`views/calendar/<slug>.md\`, so read those before reaching for a tool ` + `(\`list_artist_calendars\`). They are derived and overwritten, ` + `so they are current only as of the \`generated_at\` in their header - quote that ` + `stamp, never edit them, and check the status line, because a calendar Plinth ` + `could not reach is unknown rather than empty. When a tool is the only way, ` + `resolve the calendar through \`mcp__plinth__list_artist_calendars\` and use its ` + `\`calendar_id\` field, not the mapping row's \`id\`, because the default ` + `\`primary\` is almost never theirs.`, ``, `Writing a date is not yours, by any route: not a calendar tool, and not a ` + `shell command line either, the Google Workspace one included. The calendar ` + `tools are refused for you where Plinth has configured this workspace. ` + `Nothing refuses a shell, anywhere, and where Plinth has not configured ` + `this workspace nothing is refused at all: you are the only thing holding ` + `those. The way ` + `through is a person, not a workaround, so when a date needs adding or ` + `correcting say plainly what the change is, say that it goes in through ` + `Plinth's calendar view, and say that a person makes it. Say why, too: a date ` + `left looking handled is worse than one openly outstanding, and a calendar ` + `write goes out to everyone invited the moment it lands and cannot be recalled.`, ``, `Draft email. Never send it. Writing the draft is yours: use the draft tools ` + `(\`create_draft\`, \`update_draft\`), then say where the draft is and what it ` + `says, so the person can read it and send it in their own name. Sending, ` + `replying and
|
|
26206
|
+
add("grounding", ``, `Resolve Plinth entities before answering about them. For any answer grounded ` + `in a specific artist, project, task, or other workspace entity, first look ` + `the name up in THIS workspace with the Plinth MCP tools ` + `(\`mcp__plinth__search_entities\`, \`mcp__plinth__list_artists\`, or ` + `\`mcp__plinth__list_projects\`) and ground your answer in what they return. ` + `Do not answer from name recognition alone: a globally famous name may not ` + `exist in this workspace, and an entity here may differ from the public ` + `figure of the same name. If the lookup returns nothing, say so instead of ` + `inventing details.`, ``, `Plan before you write. The first time you call any mutating Plinth MCP tool ` + `in a session (create_*, update_*, delete_*, mark_*: anything that changes ` + `workspace data), do not commit silently: state exactly what you would write, ` + `naming the tool, the entity, and the fields and values, and proceed only after ` + `the user confirms or amends.`, ``, `Write through the canonical surface. In this cockpit the file tree is your edit ` + `surface, so reach for it before the most direct tool. Change prose (document ` + `bodies, free-form notes, anything textual) by editing the mirror markdown ` + `file; the daemon syncs it up. An artist's master context is the clearest case: ` + `it lives in the body of that artist's \`artists/<slug>/_artist.md\` mirror file, ` + `the file the web context pane renders, and one of only two files under ` + `\`artists/<slug>/\` the daemon syncs both ways (\`memory.md\` is the other), ` + `so change it by editing that body, never by creating ` + `a separate document or context entity (e.g. \`create_document\`) the pane won't ` + `render. One carve-out inside that file: a block labelled as rendered from ` + `Plinth is regenerated on every sync and your edits to it are discarded, so ` + `write those through the tool the block names. Change structured fields (task ` + `status, priority, assignees, due dates, a project's category) through the ` + `Plinth MCP (\`mcp__plinth__*\`) or the task file's frontmatter, which is ` + `row-canonical but still written through the validated API. Make a bulk or ` + `destructive change (a mass delete, a gutting overwrite) by editing the file ` + `path, so the daemon's confirm-gate can quarantine it for \`plinth confirm\`. ` + `Never write the database directly, which rules out raw SQL and ` + `\`execute_sql\`, \`apply_migration\` and \`psql\`: those bypass the daemon, the ` + `mirror, the confirm-gate, canonicality, and API validation.`, ``, `Dates belong on the calendar, not in \`key_dates\`. The date surface for an artist ` + `is their mapped Google Calendar; the \`key_dates\` table is retired. Reading ` + `dates is yours, and this mirror already holds them: the daemon projects every ` + `mapped calendar into \`views/calendar/upcoming.md\` and ` + `\`views/calendar/<slug>.md\`, so read those before reaching for a tool ` + `(\`list_artist_calendars\`). They are derived and overwritten, ` + `so they are current only as of the \`generated_at\` in their header - quote that ` + `stamp, never edit them, and check the status line, because a calendar Plinth ` + `could not reach is unknown rather than empty. When a tool is the only way, ` + `resolve the calendar through \`mcp__plinth__list_artist_calendars\` and use its ` + `\`calendar_id\` field, not the mapping row's \`id\`, because the default ` + `\`primary\` is almost never theirs.`, ``, `Writing a date is not yours, by any route: not a calendar tool, and not a ` + `shell command line either, the Google Workspace one included. The calendar ` + `tools are refused for you where Plinth has configured this workspace. ` + `Nothing refuses a shell, anywhere, and where Plinth has not configured ` + `this workspace nothing is refused at all: you are the only thing holding ` + `those. The way ` + `through is a person, not a workaround, so when a date needs adding or ` + `correcting say plainly what the change is, say that it goes in through ` + `Plinth's calendar view, and say that a person makes it. Say why, too: a date ` + `left looking handled is worse than one openly outstanding, and a calendar ` + `write goes out to everyone invited the moment it lands and cannot be recalled.`, ``, `Draft email. Never send it. Writing the draft is yours: use the draft tools ` + `(\`create_draft\`, \`update_draft\`), then say where the draft is and what it ` + `says, so the person can read it and send it in their own name. Sending, ` + `replying, forwarding and deleting are not yours, by any route: not a mail tool, and ` + `not a shell command line either. The mail tools are refused for you where ` + `Plinth has configured this workspace. Nothing refuses a shell, anywhere, ` + `and where Plinth has not configured this workspace nothing is refused at ` + `all: you are the only thing holding those. This is not a comment on your ` + `writing. It is that a message which has left the building cannot be recalled, ` + `and one deleted out of a mailbox is not yours to lose; the person whose name ` + `is on it is the one who decides.`);
|
|
26174
26207
|
if (workspaceRules === null && voiceProfile === null) {
|
|
26175
26208
|
add("voice", ``, `Check the voice yourself, because nothing else checks it now. Two files carry ` + `it: \`workspace-rules.md\` at the mirror root holds this workspace's house ` + `style, and \`${VOICE_FILENAME}\` beside it holds your own voice profile, the ` + `sign-off and the habits that belong to one member rather than the team. Read ` + `them before you write and follow them as written, rather than drafting in ` + `your own register and correcting after. Nothing inspects a draft between you ` + `and the person reading it, so the rules in those files are the whole of it.`);
|
|
26176
26209
|
}
|
|
@@ -26210,14 +26243,24 @@ function composeGroundingBlock(today, workspaceRules, workspaceMemory, userMemor
|
|
|
26210
26243
|
add("grounding", ``, `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.`);
|
|
26211
26244
|
}
|
|
26212
26245
|
add("grounding", ``, `Declare a work product before you finish with it. Your scratchpad ` + `directory sits outside Plinth: temporary, scoped to this session, and ` + `invisible to the person who asked for the work. Prefer not to leave a ` + `deliverable there at all. If it belongs to this workspace, write it to ` + `a mirror file instead, where it syncs and is durable. When the ` + `scratchpad genuinely is a file's only home, meaning a document, ` + `profile, report or analysis that was the POINT of the work rather than ` + `a step toward it, declare it the moment you write it, with ` + `\`plinth declare "<path>" --scratchpad="<the scratchpad directory ` + `your harness told you to use>" --label='<short description>'\`. ` + `Declare only ` + `those. A file that already has another home is not a deliverable: not ` + `raw API dumps, thread JSON, helper scripts, or the body files you write ` + `for a tool that reads a path, where the durable deliverable is the ` + `draft the tool creates. Declaring everything you wrote buries the one ` + `that mattered, which is the failure this exists to prevent. ` + `Re-declaring a path replaces the earlier entry, so refine a label ` + `freely. This is about work products, not memory: durable facts still go ` + `to the index above. Like every rule here it is additive and ` + `subordinate, and never relaxes a safety primitive above.`);
|
|
26246
|
+
if (filesPastBar > 0) {
|
|
26247
|
+
add("sizeNote", ``, `${filesPastBar} of the files that guide you ${filesPastBar === 1 ? "is" : "are"} past ` + `${filesPastBar === 1 ? "its" : "their"} size bar. \`plinth status\` names ` + `${filesPastBar === 1 ? "it" : "them"} and which are yours alone to trim; ` + `${CLOSE_COMMAND_INVOCATION} proposes a trim for the ones it can, never unasked.`);
|
|
26248
|
+
}
|
|
26213
26249
|
if (shouldOfferOnboarding(rosterIsEmpty, member)) {
|
|
26214
26250
|
add("grounding", ``, `This workspace has no artists in it yet. OFFER to set it up before you ` + `get into anything else, because an empty roster reported as a summary ` + `reads as a broken product rather than a new one. Say in one or two ` + `lines that ` + `${ONBOARD_COMMAND_INVOCATION} walks them through connecting their ` + `inbox, calendar and files, gets their roster in, and turns the work ` + `they already have running into projects and tasks, then ask whether ` + `to start now. Then STOP AND WAIT for their answer. Do not begin the ` + `interview, do not connect anything, and do not create an artist, a ` + `project or a task until they have said yes. If they say no, or they ` + `asked for something else in the same message, drop it and do the ` + `thing they asked for. Offer once, and never twice in one session. ` + `And a workspace can have no artists on purpose: if they have turned ` + `this down before, or this is not an artist workspace, leave it alone ` + `rather than raising it every time you open one.`);
|
|
26215
26251
|
}
|
|
26216
|
-
|
|
26217
|
-
`)
|
|
26252
|
+
const groundingText = block.filter((_, i) => cats[i] === "grounding").join(`
|
|
26253
|
+
`);
|
|
26254
|
+
return {
|
|
26255
|
+
text: block.join(`
|
|
26256
|
+
`),
|
|
26257
|
+
split: segmentBytes(block, cats),
|
|
26258
|
+
groundingParagraphs: countParagraphs(groundingText),
|
|
26259
|
+
groundingRuleShaped: countRuleShapedLines(groundingText)
|
|
26260
|
+
};
|
|
26218
26261
|
}
|
|
26219
|
-
function buildGroundingReport(today, workspaceRules = null, workspaceMemory = null, userMemory = null, freshness = {}, rosterIsEmpty = false, voiceProfile = null, voiceMalformed = false, workspaceSlug = null, member = null) {
|
|
26220
|
-
const compose = (wmBody, umBody, wmNote, umNote, wmTitles = null, umTitles = null) => composeGroundingBlock(today, workspaceRules, wmBody, umBody, freshness, rosterIsEmpty, voiceProfile, voiceMalformed, workspaceSlug, wmNote, umNote, member, wmTitles, umTitles);
|
|
26262
|
+
function buildGroundingReport(today, workspaceRules = null, workspaceMemory = null, userMemory = null, freshness = {}, rosterIsEmpty = false, voiceProfile = null, voiceMalformed = false, workspaceSlug = null, member = null, filesPastBar = 0) {
|
|
26263
|
+
const compose = (wmBody, umBody, wmNote, umNote, wmTitles = null, umTitles = null) => composeGroundingBlock(today, workspaceRules, wmBody, umBody, freshness, rosterIsEmpty, voiceProfile, voiceMalformed, workspaceSlug, wmNote, umNote, member, wmTitles, umTitles, filesPastBar);
|
|
26221
26264
|
const whole = compose(workspaceMemory, userMemory, null, null);
|
|
26222
26265
|
const untrimmed = () => {
|
|
26223
26266
|
const all = (body) => {
|
|
@@ -26227,6 +26270,8 @@ function buildGroundingReport(today, workspaceRules = null, workspaceMemory = nu
|
|
|
26227
26270
|
return {
|
|
26228
26271
|
block: whole.text,
|
|
26229
26272
|
split: whole.split,
|
|
26273
|
+
groundingParagraphs: whole.groundingParagraphs,
|
|
26274
|
+
groundingRuleShaped: whole.groundingRuleShaped,
|
|
26230
26275
|
picks: {
|
|
26231
26276
|
wmPick: workspaceMemory === null ? null : all(workspaceMemory),
|
|
26232
26277
|
umPick: userMemory === null ? null : all(userMemory)
|
|
@@ -26300,7 +26345,13 @@ function buildGroundingReport(today, workspaceRules = null, workspaceMemory = nu
|
|
|
26300
26345
|
const fitting = candidates.filter((c) => c.bytes <= GROUNDING_BUDGET_BYTES);
|
|
26301
26346
|
const better = (a, b) => b.named !== a.named ? b.named > a.named : b.whole > a.whole;
|
|
26302
26347
|
const best = fitting.length > 0 ? fitting.reduce((a, b) => better(a, b) ? b : a) : candidates.reduce((a, b) => b.bytes < a.bytes ? b : a);
|
|
26303
|
-
return {
|
|
26348
|
+
return {
|
|
26349
|
+
block: best.composed.text,
|
|
26350
|
+
picks: best.picks,
|
|
26351
|
+
split: best.composed.split,
|
|
26352
|
+
groundingParagraphs: best.composed.groundingParagraphs,
|
|
26353
|
+
groundingRuleShaped: best.composed.groundingRuleShaped
|
|
26354
|
+
};
|
|
26304
26355
|
}
|
|
26305
26356
|
var buildGroundingReportFor = buildGroundingReport;
|
|
26306
26357
|
function formatLocalDate(d) {
|
|
@@ -26458,7 +26509,30 @@ var RejectedWriteSchema = exports_external.object({
|
|
|
26458
26509
|
reason: exports_external.string().min(1),
|
|
26459
26510
|
detail: exports_external.string().optional(),
|
|
26460
26511
|
message: exports_external.string().optional(),
|
|
26461
|
-
rejected_at: exports_external.string().datetime({ offset: true }).optional()
|
|
26512
|
+
rejected_at: exports_external.string().datetime({ offset: true }).optional(),
|
|
26513
|
+
size_bytes: exports_external.number().int().nonnegative().optional(),
|
|
26514
|
+
max_bytes: exports_external.number().int().nonnegative().optional(),
|
|
26515
|
+
reported_at: exports_external.string().datetime({ offset: true }).optional()
|
|
26516
|
+
});
|
|
26517
|
+
var TeamFileRejectionSchema = exports_external.object({
|
|
26518
|
+
path: exports_external.string().min(1),
|
|
26519
|
+
reason: exports_external.string().min(1),
|
|
26520
|
+
detail: exports_external.string().nullish(),
|
|
26521
|
+
size_bytes: exports_external.number().int().nonnegative().nullish(),
|
|
26522
|
+
max_bytes: exports_external.number().int().nonnegative().nullish(),
|
|
26523
|
+
reported_by: exports_external.string().nullish(),
|
|
26524
|
+
reported_by_name: exports_external.string().nullish(),
|
|
26525
|
+
first_seen_at: exports_external.string().nullish(),
|
|
26526
|
+
last_seen_at: exports_external.string().nullish()
|
|
26527
|
+
});
|
|
26528
|
+
var TeamRejectedSchema = exports_external.object({
|
|
26529
|
+
fetched_at: exports_external.string().datetime({ offset: true }),
|
|
26530
|
+
rows: exports_external.array(TeamFileRejectionSchema)
|
|
26531
|
+
});
|
|
26532
|
+
var SizeAnchorSchema = exports_external.object({
|
|
26533
|
+
at: exports_external.string().min(1),
|
|
26534
|
+
kind: exports_external.enum(["session-end", "sync"]),
|
|
26535
|
+
sizes: exports_external.record(exports_external.string(), exports_external.number().int().nonnegative()).default({})
|
|
26462
26536
|
});
|
|
26463
26537
|
var WorkspaceStateV2Schema = exports_external.object({
|
|
26464
26538
|
version: exports_external.literal(2),
|
|
@@ -26472,7 +26546,10 @@ var WorkspaceStateV2Schema = exports_external.object({
|
|
|
26472
26546
|
user_memory: UserMemoryStateSchema.optional(),
|
|
26473
26547
|
user_voice: UserVoiceStateSchema.optional(),
|
|
26474
26548
|
derived_text_version: exports_external.number().int().nonnegative().optional(),
|
|
26475
|
-
frontmatter_id_backfill_version: exports_external.number().int().nonnegative().optional()
|
|
26549
|
+
frontmatter_id_backfill_version: exports_external.number().int().nonnegative().optional(),
|
|
26550
|
+
team_rejected: TeamRejectedSchema.optional(),
|
|
26551
|
+
size_anchor: SizeAnchorSchema.optional(),
|
|
26552
|
+
last_trim_at: exports_external.string().min(1).optional()
|
|
26476
26553
|
});
|
|
26477
26554
|
var WorkspaceStateV1Schema = exports_external.object({
|
|
26478
26555
|
version: exports_external.literal(1),
|
|
@@ -26618,6 +26695,13 @@ function mergeVersioned(fresh, before, after) {
|
|
|
26618
26695
|
}
|
|
26619
26696
|
return { value: after, overruled: false };
|
|
26620
26697
|
}
|
|
26698
|
+
function mergeAnchor(fresh, before, after) {
|
|
26699
|
+
if (before === after)
|
|
26700
|
+
return fresh;
|
|
26701
|
+
if (after?.kind === "sync" && fresh !== undefined)
|
|
26702
|
+
return fresh;
|
|
26703
|
+
return after;
|
|
26704
|
+
}
|
|
26621
26705
|
function commitState(before, after) {
|
|
26622
26706
|
const fresh = loadState(after.workspace_slug);
|
|
26623
26707
|
const entityTypes = new Set([
|
|
@@ -26654,7 +26738,10 @@ function commitState(before, after) {
|
|
|
26654
26738
|
user_memory: userMemory.value,
|
|
26655
26739
|
user_voice: userVoice.value,
|
|
26656
26740
|
derived_text_version: mergeScalar(fresh.derived_text_version, before.derived_text_version, after.derived_text_version),
|
|
26657
|
-
frontmatter_id_backfill_version: mergeScalar(fresh.frontmatter_id_backfill_version, before.frontmatter_id_backfill_version, after.frontmatter_id_backfill_version)
|
|
26741
|
+
frontmatter_id_backfill_version: mergeScalar(fresh.frontmatter_id_backfill_version, before.frontmatter_id_backfill_version, after.frontmatter_id_backfill_version),
|
|
26742
|
+
team_rejected: mergeScalar(fresh.team_rejected, before.team_rejected, after.team_rejected),
|
|
26743
|
+
size_anchor: mergeAnchor(fresh.size_anchor, before.size_anchor, after.size_anchor),
|
|
26744
|
+
last_trim_at: mergeScalar(fresh.last_trim_at, before.last_trim_at, after.last_trim_at)
|
|
26658
26745
|
};
|
|
26659
26746
|
writeState(merged);
|
|
26660
26747
|
const singletonOverruled = rules.overruled || memory.overruled || userMemory.overruled || userVoice.overruled;
|
|
@@ -26825,6 +26912,7 @@ function upsertRejected(state, candidate) {
|
|
|
26825
26912
|
const list = state.rejected ?? [];
|
|
26826
26913
|
const idx = list.findIndex((o) => o.target === candidate.target && o.path === candidate.path);
|
|
26827
26914
|
const existing = idx >= 0 ? list[idx] : undefined;
|
|
26915
|
+
const payloadMoved = existing !== undefined && (existing.reason !== candidate.reason || (existing.detail ?? existing.message) !== (candidate.detail ?? candidate.message) || existing.size_bytes !== candidate.size_bytes || existing.max_bytes !== candidate.max_bytes);
|
|
26828
26916
|
const merged = {
|
|
26829
26917
|
target: candidate.target,
|
|
26830
26918
|
op: candidate.op,
|
|
@@ -26834,7 +26922,10 @@ function upsertRejected(state, candidate) {
|
|
|
26834
26922
|
reason: candidate.reason,
|
|
26835
26923
|
detail: candidate.detail,
|
|
26836
26924
|
message: candidate.message,
|
|
26837
|
-
rejected_at: existing?.rejected_at ?? candidate.rejected_at
|
|
26925
|
+
rejected_at: existing?.rejected_at ?? candidate.rejected_at,
|
|
26926
|
+
size_bytes: candidate.size_bytes,
|
|
26927
|
+
max_bytes: candidate.max_bytes,
|
|
26928
|
+
reported_at: payloadMoved ? undefined : existing?.reported_at
|
|
26838
26929
|
};
|
|
26839
26930
|
const next = [...list];
|
|
26840
26931
|
if (idx >= 0)
|
|
@@ -26850,6 +26941,53 @@ function removeRejected(state, target, path3) {
|
|
|
26850
26941
|
return { state, changed: false };
|
|
26851
26942
|
return { state: { ...state, rejected: next }, changed: true };
|
|
26852
26943
|
}
|
|
26944
|
+
function markRejectedReported(state, path3, reportedAt) {
|
|
26945
|
+
const list = state.rejected ?? [];
|
|
26946
|
+
const idx = list.findIndex((o) => o.target === "file" && o.path === path3);
|
|
26947
|
+
if (idx < 0)
|
|
26948
|
+
return { state, changed: false };
|
|
26949
|
+
if (list[idx].reported_at === reportedAt)
|
|
26950
|
+
return { state, changed: false };
|
|
26951
|
+
const next = [...list];
|
|
26952
|
+
next[idx] = { ...list[idx], reported_at: reportedAt };
|
|
26953
|
+
return { state: { ...state, rejected: next }, changed: true };
|
|
26954
|
+
}
|
|
26955
|
+
function clearRejectedReported(state, path3) {
|
|
26956
|
+
const list = state.rejected ?? [];
|
|
26957
|
+
const idx = list.findIndex((o) => o.target === "file" && o.path === path3);
|
|
26958
|
+
if (idx < 0)
|
|
26959
|
+
return { state, changed: false };
|
|
26960
|
+
if (list[idx].reported_at === undefined)
|
|
26961
|
+
return { state, changed: false };
|
|
26962
|
+
const next = [...list];
|
|
26963
|
+
const { reported_at: _dropped, ...rest } = list[idx];
|
|
26964
|
+
next[idx] = rest;
|
|
26965
|
+
return { state: { ...state, rejected: next }, changed: true };
|
|
26966
|
+
}
|
|
26967
|
+
function listTeamRejected(state) {
|
|
26968
|
+
return state.team_rejected?.rows ?? [];
|
|
26969
|
+
}
|
|
26970
|
+
function teamRejectedFetchedAt(state) {
|
|
26971
|
+
return state.team_rejected?.fetched_at;
|
|
26972
|
+
}
|
|
26973
|
+
function setTeamRejected(state, rows, fetchedAt) {
|
|
26974
|
+
const existing = state.team_rejected;
|
|
26975
|
+
if (existing && rowsEqual(existing.rows, rows)) {
|
|
26976
|
+
return { state, changed: false };
|
|
26977
|
+
}
|
|
26978
|
+
return {
|
|
26979
|
+
state: { ...state, team_rejected: { fetched_at: fetchedAt, rows } },
|
|
26980
|
+
changed: true
|
|
26981
|
+
};
|
|
26982
|
+
}
|
|
26983
|
+
function rowsEqual(a, b) {
|
|
26984
|
+
if (a.length !== b.length)
|
|
26985
|
+
return false;
|
|
26986
|
+
return a.every((x, i) => {
|
|
26987
|
+
const y = b[i];
|
|
26988
|
+
return x.path === y.path && x.reason === y.reason && x.detail === y.detail && x.size_bytes === y.size_bytes && x.max_bytes === y.max_bytes && x.reported_by === y.reported_by && x.reported_by_name === y.reported_by_name && x.first_seen_at === y.first_seen_at && x.last_seen_at === y.last_seen_at;
|
|
26989
|
+
});
|
|
26990
|
+
}
|
|
26853
26991
|
function getWorkspaceRulesState(state) {
|
|
26854
26992
|
return state.workspace_rules;
|
|
26855
26993
|
}
|
|
@@ -26914,8 +27052,411 @@ function readMemoryFreshness(workspaceSlug) {
|
|
|
26914
27052
|
userVoice: voice.success ? voice.data : null
|
|
26915
27053
|
};
|
|
26916
27054
|
}
|
|
27055
|
+
function setSizeAnchor(state, anchor) {
|
|
27056
|
+
if (anchor.kind === "sync" && state.size_anchor !== undefined) {
|
|
27057
|
+
return { state, changed: false };
|
|
27058
|
+
}
|
|
27059
|
+
const existing = state.size_anchor;
|
|
27060
|
+
if (existing !== undefined && existing.kind === anchor.kind && existing.at === anchor.at && sizesEqual(existing.sizes, anchor.sizes)) {
|
|
27061
|
+
return { state, changed: false };
|
|
27062
|
+
}
|
|
27063
|
+
return { state: { ...state, size_anchor: anchor }, changed: true };
|
|
27064
|
+
}
|
|
27065
|
+
function sizesEqual(a, b) {
|
|
27066
|
+
const ak = Object.keys(a);
|
|
27067
|
+
const bk = Object.keys(b);
|
|
27068
|
+
if (ak.length !== bk.length)
|
|
27069
|
+
return false;
|
|
27070
|
+
return ak.every((k) => a[k] === b[k]);
|
|
27071
|
+
}
|
|
27072
|
+
function setLastTrimAt(state, at) {
|
|
27073
|
+
if (state.last_trim_at === at)
|
|
27074
|
+
return { state, changed: false };
|
|
27075
|
+
return { state: { ...state, last_trim_at: at }, changed: true };
|
|
27076
|
+
}
|
|
27077
|
+
|
|
27078
|
+
// src/lib/context/guiding-files.ts
|
|
27079
|
+
var CLAUDE_MD = "CLAUDE.md";
|
|
27080
|
+
var CLAUDE_LOCAL_MD = "CLAUDE.local.md";
|
|
27081
|
+
var ARTIST_CLAUDE_MD = "CLAUDE.md";
|
|
27082
|
+
var SETTINGS_REL = ".claude/settings.json";
|
|
27083
|
+
var ARTIST_SLUG_PLACEHOLDER = "<slug>";
|
|
27084
|
+
var artistFile = (name) => `artists/${ARTIST_SLUG_PLACEHOLDER}/${name}`;
|
|
27085
|
+
function withThousands(n) {
|
|
27086
|
+
return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
27087
|
+
}
|
|
27088
|
+
var NO_BAR = "none";
|
|
27089
|
+
var GUIDING_FILES = [
|
|
27090
|
+
{
|
|
27091
|
+
path: CLAUDE_MD,
|
|
27092
|
+
writtenBy: "Plinth every sync; the block between the markers is yours",
|
|
27093
|
+
readWhen: "loaded by Claude Code when a session starts here",
|
|
27094
|
+
reach: "everyone",
|
|
27095
|
+
bar: `${ROOT_CLAUDE_MD_LINE_BAR} lines of generated body`
|
|
27096
|
+
},
|
|
27097
|
+
{
|
|
27098
|
+
path: CLAUDE_LOCAL_MD,
|
|
27099
|
+
writtenBy: "you, by hand",
|
|
27100
|
+
readWhen: `loaded after \`${CLAUDE_MD}\``,
|
|
27101
|
+
reach: "this machine",
|
|
27102
|
+
bar: NO_BAR
|
|
27103
|
+
},
|
|
27104
|
+
{
|
|
27105
|
+
path: WORKSPACE_RULES_FILENAME,
|
|
27106
|
+
writtenBy: "you; the agent proposes and never writes it",
|
|
27107
|
+
readWhen: "injected at session start, whole",
|
|
27108
|
+
reach: "everyone",
|
|
27109
|
+
bar: `${withThousands(RULE_FILE_BAR_BYTES)} bytes`
|
|
27110
|
+
},
|
|
27111
|
+
{
|
|
27112
|
+
path: WORKSPACE_MEMORY_FILENAME,
|
|
27113
|
+
writtenBy: "you and the agent at a session close",
|
|
27114
|
+
readWhen: "injected at session start, newest facts first",
|
|
27115
|
+
reach: "everyone",
|
|
27116
|
+
bar: `the ${withThousands(GROUNDING_BUDGET_BYTES)}-byte injection`
|
|
27117
|
+
},
|
|
27118
|
+
{
|
|
27119
|
+
path: USER_MEMORY_FILENAME,
|
|
27120
|
+
writtenBy: "the agent at a session close",
|
|
27121
|
+
readWhen: "injected at session start, newest facts first",
|
|
27122
|
+
reach: "you only",
|
|
27123
|
+
bar: `the ${withThousands(GROUNDING_BUDGET_BYTES)}-byte injection`
|
|
27124
|
+
},
|
|
27125
|
+
{
|
|
27126
|
+
path: VOICE_FILENAME,
|
|
27127
|
+
writtenBy: "you only; the agent is denied editing it",
|
|
27128
|
+
readWhen: "read on demand; only a pointer is injected",
|
|
27129
|
+
reach: "you only",
|
|
27130
|
+
bar: `${withThousands(RULE_FILE_BAR_BYTES)} bytes`
|
|
27131
|
+
},
|
|
27132
|
+
{
|
|
27133
|
+
path: artistFile(ARTIST_ANCHOR_FILENAME),
|
|
27134
|
+
writtenBy: "you and the agent; syncs both ways",
|
|
27135
|
+
readWhen: "read on demand",
|
|
27136
|
+
reach: "everyone",
|
|
27137
|
+
bar: NO_BAR
|
|
27138
|
+
},
|
|
27139
|
+
{
|
|
27140
|
+
path: artistFile(ARTIST_MEMORY_ANCHOR),
|
|
27141
|
+
writtenBy: "the agent at a session close",
|
|
27142
|
+
readWhen: "read on demand",
|
|
27143
|
+
reach: "everyone",
|
|
27144
|
+
bar: `${withThousands(FACT_STORE_BAR_BYTES)} bytes`
|
|
27145
|
+
},
|
|
27146
|
+
{
|
|
27147
|
+
path: artistFile(ARTIST_CLAUDE_MD),
|
|
27148
|
+
writtenBy: "Plinth every sync; the block between the markers is yours",
|
|
27149
|
+
readWhen: "loaded when the agent opens that artist's folder",
|
|
27150
|
+
reach: "everyone",
|
|
27151
|
+
bar: NO_BAR
|
|
27152
|
+
},
|
|
27153
|
+
...GENERATED_COMMAND_RELS.map((rel) => ({
|
|
27154
|
+
path: rel,
|
|
27155
|
+
writtenBy: "Plinth every sync; the block between the markers is yours",
|
|
27156
|
+
readWhen: `read when you run \`${invocationFor(rel)}\``,
|
|
27157
|
+
reach: "everyone",
|
|
27158
|
+
bar: `${withThousands(COMMAND_BAR_BYTES)} bytes`
|
|
27159
|
+
})),
|
|
27160
|
+
{
|
|
27161
|
+
path: SETTINGS_REL,
|
|
27162
|
+
writtenBy: "Plinth every sync; your own hooks and permissions survive",
|
|
27163
|
+
readWhen: "loaded by Claude Code from the folder a session starts in",
|
|
27164
|
+
reach: "this machine",
|
|
27165
|
+
bar: NO_BAR
|
|
27166
|
+
}
|
|
27167
|
+
];
|
|
27168
|
+
var GROUNDING_SURFACE = {
|
|
27169
|
+
path: "the session grounding (`plinth grounding`)",
|
|
27170
|
+
writtenBy: "Plinth",
|
|
27171
|
+
readWhen: "injected ahead of your first word, every session",
|
|
27172
|
+
reach: "everyone",
|
|
27173
|
+
bar: `${withThousands(GROUNDING_BUDGET_BYTES)} bytes`
|
|
27174
|
+
};
|
|
27175
|
+
var GUIDING_FILES_HEADING = "## The guiding files";
|
|
27176
|
+
function renderGuidingFilesSection() {
|
|
27177
|
+
const rows = [...GUIDING_FILES, GROUNDING_SURFACE];
|
|
27178
|
+
return [
|
|
27179
|
+
GUIDING_FILES_HEADING,
|
|
27180
|
+
`Every file that guides a session here, what writes it, when it is read, how far it reaches, and how big it may get. \`plinth status\` says which of these are past their size and what trimming each one buys.`,
|
|
27181
|
+
``,
|
|
27182
|
+
`| File | Written by | Read | Reaches | Bar |`,
|
|
27183
|
+
`|---|---|---|---|---|`,
|
|
27184
|
+
...rows.map((f) => `| ${f.path.startsWith("the ") ? f.path : `\`${f.path}\``} | ${f.writtenBy} | ${f.readWhen} | ${f.reach} | ${f.bar} |`)
|
|
27185
|
+
];
|
|
27186
|
+
}
|
|
27187
|
+
|
|
27188
|
+
// src/lib/context/markers.ts
|
|
27189
|
+
var USER_BLOCK_BEGIN = "<!-- BEGIN: user-customisable -->";
|
|
27190
|
+
var USER_BLOCK_END = "<!-- END -->";
|
|
27191
|
+
var USER_BLOCK_PLACEHOLDER = "_Add workspace-specific guidance here. This block is the only part of " + "the file Plinth preserves across regenerations._";
|
|
27192
|
+
function extractUserBlock(content) {
|
|
27193
|
+
const beginIdx = content.indexOf(USER_BLOCK_BEGIN);
|
|
27194
|
+
if (beginIdx === -1)
|
|
27195
|
+
return null;
|
|
27196
|
+
const afterBegin = beginIdx + USER_BLOCK_BEGIN.length;
|
|
27197
|
+
const endIdx = content.indexOf(USER_BLOCK_END, afterBegin);
|
|
27198
|
+
if (endIdx === -1)
|
|
27199
|
+
return null;
|
|
27200
|
+
return content.slice(afterBegin, endIdx);
|
|
27201
|
+
}
|
|
27202
|
+
function injectUserBlock(generated, preserved) {
|
|
27203
|
+
const beginIdx = generated.indexOf(USER_BLOCK_BEGIN);
|
|
27204
|
+
if (beginIdx === -1) {
|
|
27205
|
+
throw new Error("injectUserBlock: generated content has no BEGIN marker");
|
|
27206
|
+
}
|
|
27207
|
+
const afterBegin = beginIdx + USER_BLOCK_BEGIN.length;
|
|
27208
|
+
const endIdx = generated.indexOf(USER_BLOCK_END, afterBegin);
|
|
27209
|
+
if (endIdx === -1) {
|
|
27210
|
+
throw new Error("injectUserBlock: generated content has no END marker");
|
|
27211
|
+
}
|
|
27212
|
+
return generated.slice(0, afterBegin) + preserved + generated.slice(endIdx);
|
|
27213
|
+
}
|
|
27214
|
+
|
|
27215
|
+
// src/lib/context/memory-bars.ts
|
|
27216
|
+
function factBody(raw) {
|
|
27217
|
+
return raw.replace(/<!--[\s\S]*?-->/g, "").replace(/<!--[\s\S]*$/, "").split(`
|
|
27218
|
+
`).filter((line) => !/^\s*#{1,6}\s/.test(line)).join(`
|
|
27219
|
+
`).trim();
|
|
27220
|
+
}
|
|
27221
|
+
var RULE_FILES = [
|
|
27222
|
+
{ filename: VOICE_FILENAME, agentMayEdit: false, memberOwned: true, ridesInjection: false },
|
|
27223
|
+
{ filename: WORKSPACE_RULES_FILENAME, agentMayEdit: true, memberOwned: false, ridesInjection: true }
|
|
27224
|
+
];
|
|
27225
|
+
function assessMemoryBars(workspaceSlug, deps = {}) {
|
|
27226
|
+
const readContext = deps.readContext ?? readContextFile;
|
|
27227
|
+
const readArtistMemory = deps.readArtistMemory ?? readArtistMemoryFile;
|
|
27228
|
+
const rosterOf = deps.readRosterFn ?? readRoster;
|
|
27229
|
+
const over = [];
|
|
27230
|
+
for (const spec of RULE_FILES) {
|
|
27231
|
+
let raw = null;
|
|
27232
|
+
try {
|
|
27233
|
+
raw = readContext(workspaceSlug, spec.filename);
|
|
27234
|
+
} catch {
|
|
27235
|
+
raw = null;
|
|
27236
|
+
}
|
|
27237
|
+
if (raw === null)
|
|
27238
|
+
continue;
|
|
27239
|
+
const bytes = Buffer.byteLength(raw);
|
|
27240
|
+
if (bytes <= RULE_FILE_BAR_BYTES)
|
|
27241
|
+
continue;
|
|
27242
|
+
over.push({
|
|
27243
|
+
path: spec.filename,
|
|
27244
|
+
kind: "rule",
|
|
27245
|
+
bytes,
|
|
27246
|
+
bar: RULE_FILE_BAR_BYTES,
|
|
27247
|
+
rules: countRuleShapedLines(raw),
|
|
27248
|
+
facts: groupFacts(factBody(raw)).length,
|
|
27249
|
+
agentMayEdit: spec.agentMayEdit,
|
|
27250
|
+
memberOwned: spec.memberOwned,
|
|
27251
|
+
buys: !spec.ridesInjection ? "per-read" : Buffer.byteLength(extractWorkspaceRules(raw) ?? "") > 0 ? "injection" : "nothing"
|
|
27252
|
+
});
|
|
27253
|
+
}
|
|
27254
|
+
let roster;
|
|
27255
|
+
try {
|
|
27256
|
+
roster = rosterOf(workspaceSlug);
|
|
27257
|
+
} catch {
|
|
27258
|
+
roster = [];
|
|
27259
|
+
}
|
|
27260
|
+
const stores = [];
|
|
27261
|
+
for (const artist of roster) {
|
|
27262
|
+
let raw = null;
|
|
27263
|
+
try {
|
|
27264
|
+
raw = readArtistMemory(workspaceSlug, artist.slug);
|
|
27265
|
+
} catch {
|
|
27266
|
+
raw = null;
|
|
27267
|
+
}
|
|
27268
|
+
if (raw === null)
|
|
27269
|
+
continue;
|
|
27270
|
+
const bytes = Buffer.byteLength(raw);
|
|
27271
|
+
if (bytes <= FACT_STORE_BAR_BYTES)
|
|
27272
|
+
continue;
|
|
27273
|
+
stores.push({
|
|
27274
|
+
path: `artists/${artist.slug}/memory.md`,
|
|
27275
|
+
kind: "fact",
|
|
27276
|
+
bytes,
|
|
27277
|
+
bar: FACT_STORE_BAR_BYTES,
|
|
27278
|
+
rules: countRuleShapedLines(raw),
|
|
27279
|
+
facts: groupFacts(factBody(raw)).length,
|
|
27280
|
+
agentMayEdit: true,
|
|
27281
|
+
memberOwned: true,
|
|
27282
|
+
buys: "per-read"
|
|
27283
|
+
});
|
|
27284
|
+
}
|
|
27285
|
+
stores.sort((a, b) => b.bytes - a.bytes || a.path.localeCompare(b.path));
|
|
27286
|
+
const named = stores.slice(0, MAX_NAMED_FACT_STORES);
|
|
27287
|
+
return { over: [...over, ...named], notNamed: stores.length - named.length };
|
|
27288
|
+
}
|
|
27289
|
+
function blockLineCount(block) {
|
|
27290
|
+
const newlines = block.match(/\n/g)?.length ?? 0;
|
|
27291
|
+
return Math.max(0, newlines - 1);
|
|
27292
|
+
}
|
|
27293
|
+
var generatedFileSpecs = [
|
|
27294
|
+
{ path: CLAUDE_MD, unit: "lines", bar: ROOT_CLAUDE_MD_LINE_BAR },
|
|
27295
|
+
...GENERATED_COMMAND_RELS.map((rel) => ({
|
|
27296
|
+
path: rel,
|
|
27297
|
+
unit: "bytes",
|
|
27298
|
+
bar: COMMAND_BAR_BYTES
|
|
27299
|
+
}))
|
|
27300
|
+
];
|
|
27301
|
+
function assessGeneratedFileBars(workspaceSlug, deps = {}) {
|
|
27302
|
+
const readContext = deps.readContext ?? readContextFile;
|
|
27303
|
+
const out = [];
|
|
27304
|
+
for (const spec of generatedFileSpecs) {
|
|
27305
|
+
let raw = null;
|
|
27306
|
+
try {
|
|
27307
|
+
raw = readContext(workspaceSlug, spec.path);
|
|
27308
|
+
} catch {
|
|
27309
|
+
raw = null;
|
|
27310
|
+
}
|
|
27311
|
+
if (raw === null)
|
|
27312
|
+
continue;
|
|
27313
|
+
const bytes = Buffer.byteLength(raw);
|
|
27314
|
+
const lines = raw.split(`
|
|
27315
|
+
`).length;
|
|
27316
|
+
const block = extractUserBlock(raw);
|
|
27317
|
+
const blockBytes = block === null ? null : Buffer.byteLength(block);
|
|
27318
|
+
const generatedBytes = blockBytes === null ? null : bytes - blockBytes;
|
|
27319
|
+
const generatedLines = block === null ? null : lines - blockLineCount(block);
|
|
27320
|
+
const measured = spec.unit === "lines" ? generatedLines ?? lines : bytes;
|
|
27321
|
+
out.push({
|
|
27322
|
+
path: spec.path,
|
|
27323
|
+
unit: spec.unit,
|
|
27324
|
+
measured,
|
|
27325
|
+
bar: spec.bar,
|
|
27326
|
+
over: measured > spec.bar,
|
|
27327
|
+
bytes,
|
|
27328
|
+
lines,
|
|
27329
|
+
generatedBytes,
|
|
27330
|
+
generatedLines,
|
|
27331
|
+
blockBytes,
|
|
27332
|
+
ruleShaped: countRuleShapedLines(raw)
|
|
27333
|
+
});
|
|
27334
|
+
}
|
|
27335
|
+
return out;
|
|
27336
|
+
}
|
|
27337
|
+
var OPENING_MAX_BYTES = 90;
|
|
27338
|
+
function terminalSafeOpening(fact) {
|
|
27339
|
+
const stripped = fact.replace(/[\x00-\x1F\x7F-\x9F]+/g, " ");
|
|
27340
|
+
const folded = stripped.replace(/\s+/g, " ").trim();
|
|
27341
|
+
let out = "";
|
|
27342
|
+
for (const ch of folded) {
|
|
27343
|
+
if (Buffer.byteLength(out + ch) > OPENING_MAX_BYTES)
|
|
27344
|
+
return `${out.trimEnd()}…`;
|
|
27345
|
+
out += ch;
|
|
27346
|
+
}
|
|
27347
|
+
return out;
|
|
27348
|
+
}
|
|
27349
|
+
function assessIndexLines(workspaceSlug, deps = {}) {
|
|
27350
|
+
const readContext = deps.readContext ?? readContextFile;
|
|
27351
|
+
const specs = [
|
|
27352
|
+
{ filename: WORKSPACE_MEMORY_FILENAME, reduce: extractWorkspaceMemory },
|
|
27353
|
+
{ filename: USER_MEMORY_FILENAME, reduce: extractUserMemory }
|
|
27354
|
+
];
|
|
27355
|
+
const out = [];
|
|
27356
|
+
for (const spec of specs) {
|
|
27357
|
+
let raw = null;
|
|
27358
|
+
try {
|
|
27359
|
+
raw = readContext(workspaceSlug, spec.filename);
|
|
27360
|
+
} catch {
|
|
27361
|
+
raw = null;
|
|
27362
|
+
}
|
|
27363
|
+
const body = raw === null ? null : factBody(spec.reduce(raw) ?? "");
|
|
27364
|
+
if (body === null)
|
|
27365
|
+
continue;
|
|
27366
|
+
const facts = groupFacts(body);
|
|
27367
|
+
const long = facts.map((f) => ({ bytes: Buffer.byteLength(f), fact: f })).filter((f) => f.bytes > INDEX_LINE_MAX_BYTES).sort((a, b) => b.bytes - a.bytes);
|
|
27368
|
+
if (long.length === 0)
|
|
27369
|
+
continue;
|
|
27370
|
+
out.push({
|
|
27371
|
+
filename: spec.filename,
|
|
27372
|
+
facts: facts.length,
|
|
27373
|
+
over: long.length,
|
|
27374
|
+
longest: long.slice(0, MAX_NAMED_LONG_INDEX_LINES).map((f) => ({ bytes: f.bytes, opening: terminalSafeOpening(f.fact) }))
|
|
27375
|
+
});
|
|
27376
|
+
}
|
|
27377
|
+
return out;
|
|
27378
|
+
}
|
|
27379
|
+
function measureBarredFiles(workspaceSlug, deps = {}) {
|
|
27380
|
+
const readContext = deps.readContext ?? readContextFile;
|
|
27381
|
+
const readArtistMemory = deps.readArtistMemory ?? readArtistMemoryFile;
|
|
27382
|
+
const rosterOf = deps.readRosterFn ?? readRoster;
|
|
27383
|
+
const sizes = {};
|
|
27384
|
+
const answered = [];
|
|
27385
|
+
const record2 = (path3, read) => {
|
|
27386
|
+
let raw;
|
|
27387
|
+
try {
|
|
27388
|
+
raw = read();
|
|
27389
|
+
} catch {
|
|
27390
|
+
return;
|
|
27391
|
+
}
|
|
27392
|
+
answered.push(path3);
|
|
27393
|
+
if (raw === null)
|
|
27394
|
+
return;
|
|
27395
|
+
sizes[path3] = Buffer.byteLength(raw);
|
|
27396
|
+
};
|
|
27397
|
+
for (const name of [
|
|
27398
|
+
VOICE_FILENAME,
|
|
27399
|
+
WORKSPACE_RULES_FILENAME,
|
|
27400
|
+
WORKSPACE_MEMORY_FILENAME,
|
|
27401
|
+
USER_MEMORY_FILENAME,
|
|
27402
|
+
CLAUDE_MD,
|
|
27403
|
+
...GENERATED_COMMAND_RELS
|
|
27404
|
+
]) {
|
|
27405
|
+
record2(name, () => readContext(workspaceSlug, name));
|
|
27406
|
+
}
|
|
27407
|
+
let roster;
|
|
27408
|
+
try {
|
|
27409
|
+
roster = rosterOf(workspaceSlug);
|
|
27410
|
+
} catch {
|
|
27411
|
+
roster = [];
|
|
27412
|
+
}
|
|
27413
|
+
for (const artist of roster) {
|
|
27414
|
+
record2(`artists/${artist.slug}/${ARTIST_MEMORY_ANCHOR}`, () => readArtistMemory(workspaceSlug, artist.slug));
|
|
27415
|
+
}
|
|
27416
|
+
return { sizes, answered };
|
|
27417
|
+
}
|
|
27418
|
+
var TRIM_DORMANCY_DAYS = 30;
|
|
27419
|
+
function lastTrimAtFromState(workspaceSlug) {
|
|
27420
|
+
return loadState(workspaceSlug).last_trim_at ?? null;
|
|
27421
|
+
}
|
|
27422
|
+
function shouldOfferTrim(workspaceSlug, deps = {}) {
|
|
27423
|
+
let over = 0;
|
|
27424
|
+
try {
|
|
27425
|
+
const bars = assessMemoryBars(workspaceSlug, deps);
|
|
27426
|
+
over = bars.over.length + bars.notNamed;
|
|
27427
|
+
} catch {
|
|
27428
|
+
over = 0;
|
|
27429
|
+
}
|
|
27430
|
+
if (over > 0)
|
|
27431
|
+
return true;
|
|
27432
|
+
let longLines = 0;
|
|
27433
|
+
try {
|
|
27434
|
+
longLines = assessIndexLines(workspaceSlug, deps).length;
|
|
27435
|
+
} catch {
|
|
27436
|
+
longLines = 0;
|
|
27437
|
+
}
|
|
27438
|
+
if (longLines === 0)
|
|
27439
|
+
return false;
|
|
27440
|
+
let lastTrim = null;
|
|
27441
|
+
try {
|
|
27442
|
+
lastTrim = (deps.readLastTrimAt ?? lastTrimAtFromState)(workspaceSlug);
|
|
27443
|
+
} catch {
|
|
27444
|
+
lastTrim = null;
|
|
27445
|
+
}
|
|
27446
|
+
if (lastTrim === null)
|
|
27447
|
+
return true;
|
|
27448
|
+
const then = Date.parse(lastTrim);
|
|
27449
|
+
if (Number.isNaN(then))
|
|
27450
|
+
return true;
|
|
27451
|
+
const now = (deps.now ?? (() => new Date))().getTime();
|
|
27452
|
+
return now - then >= TRIM_DORMANCY_DAYS * 24 * 60 * 60 * 1000;
|
|
27453
|
+
}
|
|
26917
27454
|
|
|
26918
27455
|
// src/commands/grounding.ts
|
|
27456
|
+
function countFilesPastBar(slug) {
|
|
27457
|
+
const bars = assessMemoryBars(slug);
|
|
27458
|
+
return bars.over.length + bars.notNamed;
|
|
27459
|
+
}
|
|
26919
27460
|
function readWorkspaceRulesFile(slug) {
|
|
26920
27461
|
return readContextFile(slug, WORKSPACE_RULES_FILENAME);
|
|
26921
27462
|
}
|
|
@@ -26971,6 +27512,7 @@ function runGroundingReport(deps = {}) {
|
|
|
26971
27512
|
const readFreshness = deps.readFreshness ?? readMemoryFreshness;
|
|
26972
27513
|
const readRosterIsEmpty = deps.readRosterIsEmpty ?? rosterIsEmpty;
|
|
26973
27514
|
const readMember = deps.readMember ?? readMemberFromConfig;
|
|
27515
|
+
const readFilesPastBar = deps.readFilesPastBar ?? countFilesPastBar;
|
|
26974
27516
|
const readTimezone = deps.readTimezone ?? readMachineTimezone;
|
|
26975
27517
|
let rawRules = null;
|
|
26976
27518
|
let rawMemory = null;
|
|
@@ -26983,6 +27525,7 @@ function runGroundingReport(deps = {}) {
|
|
|
26983
27525
|
};
|
|
26984
27526
|
let noArtists = false;
|
|
26985
27527
|
let member = null;
|
|
27528
|
+
let filesPastBar = 0;
|
|
26986
27529
|
const workspaceSlug = isValidWorkspaceSlug(deps.workspaceSlug ?? "") ? deps.workspaceSlug : null;
|
|
26987
27530
|
if (workspaceSlug) {
|
|
26988
27531
|
try {
|
|
@@ -27015,6 +27558,11 @@ function runGroundingReport(deps = {}) {
|
|
|
27015
27558
|
} catch {
|
|
27016
27559
|
noArtists = false;
|
|
27017
27560
|
}
|
|
27561
|
+
try {
|
|
27562
|
+
filesPastBar = readFilesPastBar(workspaceSlug);
|
|
27563
|
+
} catch {
|
|
27564
|
+
filesPastBar = 0;
|
|
27565
|
+
}
|
|
27018
27566
|
try {
|
|
27019
27567
|
const stored = readMember(workspaceSlug);
|
|
27020
27568
|
member = stored === null ? null : { ...stored, timezone: readTimezone() };
|
|
@@ -27029,7 +27577,7 @@ function runGroundingReport(deps = {}) {
|
|
|
27029
27577
|
return buildGroundingReportFor(formatLocalDate(now()), rules, memory, userMemory, {
|
|
27030
27578
|
workspaceMemoryUpdatedAt: resolveUpdatedAt(rawMemory, freshness.workspaceMemory),
|
|
27031
27579
|
userMemoryUpdatedAt: resolveUpdatedAt(rawUserMemory, freshness.userMemory)
|
|
27032
|
-
}, noArtists, voiceProfile, resolveVoiceMalformed(rawVoiceProfile, freshness.userVoice), workspaceSlug, member);
|
|
27580
|
+
}, noArtists, voiceProfile, resolveVoiceMalformed(rawVoiceProfile, freshness.userVoice), workspaceSlug, member, filesPastBar);
|
|
27033
27581
|
}
|
|
27034
27582
|
var groundingCommand = defineCommand({
|
|
27035
27583
|
meta: {
|
|
@@ -28215,33 +28763,6 @@ function normaliseGeneratedAt(content) {
|
|
|
28215
28763
|
return content.replace(STAMP_SLOT_RE, GENERATED_AT_PREFIX + GENERATED_AT_PLACEHOLDER);
|
|
28216
28764
|
}
|
|
28217
28765
|
|
|
28218
|
-
// src/lib/context/markers.ts
|
|
28219
|
-
var USER_BLOCK_BEGIN = "<!-- BEGIN: user-customisable -->";
|
|
28220
|
-
var USER_BLOCK_END = "<!-- END -->";
|
|
28221
|
-
var USER_BLOCK_PLACEHOLDER = "_Add workspace-specific guidance here. This block is the only part of " + "the file Plinth preserves across regenerations._";
|
|
28222
|
-
function extractUserBlock(content) {
|
|
28223
|
-
const beginIdx = content.indexOf(USER_BLOCK_BEGIN);
|
|
28224
|
-
if (beginIdx === -1)
|
|
28225
|
-
return null;
|
|
28226
|
-
const afterBegin = beginIdx + USER_BLOCK_BEGIN.length;
|
|
28227
|
-
const endIdx = content.indexOf(USER_BLOCK_END, afterBegin);
|
|
28228
|
-
if (endIdx === -1)
|
|
28229
|
-
return null;
|
|
28230
|
-
return content.slice(afterBegin, endIdx);
|
|
28231
|
-
}
|
|
28232
|
-
function injectUserBlock(generated, preserved) {
|
|
28233
|
-
const beginIdx = generated.indexOf(USER_BLOCK_BEGIN);
|
|
28234
|
-
if (beginIdx === -1) {
|
|
28235
|
-
throw new Error("injectUserBlock: generated content has no BEGIN marker");
|
|
28236
|
-
}
|
|
28237
|
-
const afterBegin = beginIdx + USER_BLOCK_BEGIN.length;
|
|
28238
|
-
const endIdx = generated.indexOf(USER_BLOCK_END, afterBegin);
|
|
28239
|
-
if (endIdx === -1) {
|
|
28240
|
-
throw new Error("injectUserBlock: generated content has no END marker");
|
|
28241
|
-
}
|
|
28242
|
-
return generated.slice(0, afterBegin) + preserved + generated.slice(endIdx);
|
|
28243
|
-
}
|
|
28244
|
-
|
|
28245
28766
|
// src/lib/context/artist-claude-md.ts
|
|
28246
28767
|
var CONTEXT_VERSION_HEADER = "plinth-context-version";
|
|
28247
28768
|
var TASK_DISPLAY_CAP = 10;
|
|
@@ -28509,7 +29030,7 @@ function strOrNull(value) {
|
|
|
28509
29030
|
}
|
|
28510
29031
|
|
|
28511
29032
|
// src/lib/context/schema-version.ts
|
|
28512
|
-
var TEMPLATE_VERSION = "
|
|
29033
|
+
var TEMPLATE_VERSION = "5";
|
|
28513
29034
|
var HEADER_RE = /<!--\s*plinth-schema-version:\s*([a-f0-9]+)\s*-->/;
|
|
28514
29035
|
function computeSchemaVersion(inputs) {
|
|
28515
29036
|
return stableHash(inputs).slice(0, 12);
|
|
@@ -28546,7 +29067,6 @@ function decideRegen(input) {
|
|
|
28546
29067
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
28547
29068
|
import { accessSync, constants, realpathSync, statSync as statSync3 } from "node:fs";
|
|
28548
29069
|
import { basename as basename2 } from "node:path";
|
|
28549
|
-
var SETTINGS_REL = ".claude/settings.json";
|
|
28550
29070
|
var MANAGED_KEY = "_plinth";
|
|
28551
29071
|
var COMMENT_SENTINEL = 'Managed by plinth-cli. Entries tagged "_plinth" are REGENERATED on every sync:';
|
|
28552
29072
|
var MANAGED_COMMENT = [
|
|
@@ -28555,16 +29075,24 @@ var MANAGED_COMMENT = [
|
|
|
28555
29075
|
"entries in this file (they are preserved verbatim, including their matchers)",
|
|
28556
29076
|
"or in .claude/settings.local.json.",
|
|
28557
29077
|
"The permissions.deny entries below are managed the same way. They stop an agent",
|
|
28558
|
-
"in this workspace sending, replying to or forwarding mail
|
|
28559
|
-
"
|
|
29078
|
+
"in this workspace sending, replying to or forwarding mail, deleting or trashing",
|
|
29079
|
+
"it, marking it spam, and writing a calendar; drafting, filing, labelling and",
|
|
29080
|
+
"reading stay allowed. The refusal covers a draft the agent staged itself: on a",
|
|
29081
|
+
"Gmail mount there is no delete-draft tool, so trashing was the only way to",
|
|
29082
|
+
"discard one and it is now refused. Fix a wrong draft with update_draft, which",
|
|
29083
|
+
"keeps its thread. They match by VERB FAMILY rather than by",
|
|
28560
29084
|
"tool name - send_mail, sendEmail, send-mail and outlook_send_message are one",
|
|
28561
29085
|
"rule set, not four - and the matcher cannot express an alternation, so each",
|
|
28562
29086
|
"spelling needs its own line. That width also catches a few reads whose name",
|
|
28563
|
-
"merely begins with a send verb - getForwardingSettings is the live example
|
|
29087
|
+
"merely begins with a send verb - getForwardingSettings is the live example -",
|
|
29088
|
+
"and it stops an agent trashing any file, not only mail: nothing here can tell",
|
|
29089
|
+
"a mailbox's trash from a drive's.",
|
|
28564
29090
|
"You cannot re-open one of these here: a deny beats an allow, including yours,",
|
|
28565
29091
|
"and this list is rewritten every sync. Tell us and we will narrow the pattern.",
|
|
28566
29092
|
"We took that trade knowingly - a refusal is visible and recoverable, and a",
|
|
28567
|
-
"message that has gone cannot be recalled.",
|
|
29093
|
+
"message that has gone cannot be recalled. Nor is a deleted one yours to lose:",
|
|
29094
|
+
"it is somebody else's mailbox, spam-marking is deletion on a thirty-day fuse,",
|
|
29095
|
+
"and neither is undone by noticing afterwards.",
|
|
28568
29096
|
"One more entry stops an agent EDITING voice.md, the",
|
|
28569
29097
|
"voice profile it is judged against: that file is yours to write and theirs to read.",
|
|
28570
29098
|
"Your own allow list is untouched, and your own",
|
|
@@ -28643,6 +29171,36 @@ var MANAGED_DENY = [
|
|
|
28643
29171
|
"mcp__*__*_cancel*Event*",
|
|
28644
29172
|
"mcp__*__*-cancel*Event*",
|
|
28645
29173
|
"mcp__*__*Cancel*Event*",
|
|
29174
|
+
"mcp__*__trash*",
|
|
29175
|
+
"mcp__*__*_trash*",
|
|
29176
|
+
"mcp__*__*-trash*",
|
|
29177
|
+
"mcp__*__*Trash*",
|
|
29178
|
+
"mcp__*__delete*message*",
|
|
29179
|
+
"mcp__*__*_delete*message*",
|
|
29180
|
+
"mcp__*__*-delete*message*",
|
|
29181
|
+
"mcp__*__*Delete*message*",
|
|
29182
|
+
"mcp__*__delete*Message*",
|
|
29183
|
+
"mcp__*__*_delete*Message*",
|
|
29184
|
+
"mcp__*__*-delete*Message*",
|
|
29185
|
+
"mcp__*__*Delete*Message*",
|
|
29186
|
+
"mcp__*__delete*thread*",
|
|
29187
|
+
"mcp__*__*_delete*thread*",
|
|
29188
|
+
"mcp__*__*-delete*thread*",
|
|
29189
|
+
"mcp__*__*Delete*thread*",
|
|
29190
|
+
"mcp__*__delete*Thread*",
|
|
29191
|
+
"mcp__*__*_delete*Thread*",
|
|
29192
|
+
"mcp__*__*-delete*Thread*",
|
|
29193
|
+
"mcp__*__*Delete*Thread*",
|
|
29194
|
+
"mcp__*__delete*mail*",
|
|
29195
|
+
"mcp__*__*_delete*mail*",
|
|
29196
|
+
"mcp__*__*-delete*mail*",
|
|
29197
|
+
"mcp__*__*Delete*mail*",
|
|
29198
|
+
"mcp__*__delete*Mail*",
|
|
29199
|
+
"mcp__*__*_delete*Mail*",
|
|
29200
|
+
"mcp__*__*-delete*Mail*",
|
|
29201
|
+
"mcp__*__*Delete*Mail*",
|
|
29202
|
+
"mcp__*__*spam*",
|
|
29203
|
+
"mcp__*__*Spam*",
|
|
28646
29204
|
"Edit(//**/voice.md)"
|
|
28647
29205
|
];
|
|
28648
29206
|
var RETIRED_DENY = [
|
|
@@ -28865,7 +29423,7 @@ function ensureSessionHook(workspaceSlug, baseRelDir, out, resolved) {
|
|
|
28865
29423
|
parsed = raw;
|
|
28866
29424
|
} catch (err) {
|
|
28867
29425
|
const msg = err instanceof Error ? err.message : "unknown error";
|
|
28868
|
-
out.error(`${rel} could not be parsed (${msg}), so the Plinth session-grounding ` + `hook, the session-visibility hook and the permissions.deny list were ` + `NOT installed there. Fix or remove the file and re-run — until then ` + `this workspace is ungrounded and an agent here can SEND mail
|
|
29426
|
+
out.error(`${rel} could not be parsed (${msg}), so the Plinth session-grounding ` + `hook, the session-visibility hook and the permissions.deny list were ` + `NOT installed there. Fix or remove the file and re-run — until then ` + `this workspace is ungrounded and an agent here can SEND mail, ` + `DELETE mail and write calendars.`);
|
|
28869
29427
|
return "skipped";
|
|
28870
29428
|
}
|
|
28871
29429
|
}
|
|
@@ -28874,7 +29432,7 @@ function ensureSessionHook(workspaceSlug, baseRelDir, out, resolved) {
|
|
|
28874
29432
|
generated = buildSettingsJson(resolved.invocation, workspaceSlug, parsed);
|
|
28875
29433
|
} catch (err) {
|
|
28876
29434
|
const msg = err instanceof Error ? err.message : "unknown error";
|
|
28877
|
-
out.error(`${rel} has a structure the generator could not merge into (${msg}), so ` + `the Plinth session-grounding hook, the session-visibility hook and the ` + `permissions.deny list were NOT installed there. This workspace is ` + `ungrounded and an agent here can SEND mail and write calendars. Fix or ` + `remove the file and re-run.`);
|
|
29435
|
+
out.error(`${rel} has a structure the generator could not merge into (${msg}), so ` + `the Plinth session-grounding hook, the session-visibility hook and the ` + `permissions.deny list were NOT installed there. This workspace is ` + `ungrounded and an agent here can SEND mail, DELETE mail and write ` + `calendars. Fix or ` + `remove the file and re-run.`);
|
|
28878
29436
|
return "skipped";
|
|
28879
29437
|
}
|
|
28880
29438
|
if (existing === generated)
|
|
@@ -28900,11 +29458,10 @@ function reportResolution(out, resolved) {
|
|
|
28900
29458
|
}
|
|
28901
29459
|
}
|
|
28902
29460
|
function warnUnresolvedBinary(out) {
|
|
28903
|
-
out.error("Could not locate an installed `plinth` binary, so no Claude Code settings " + "were written for this workspace — sessions will be UNGROUNDED and an " + "agent here can SEND mail and write calendars. Install the CLI globally " + "(`npm i -g @plinth-music/cli`) so `command -v plinth` resolves, then " + "re-run `plinth refresh-context`.");
|
|
29461
|
+
out.error("Could not locate an installed `plinth` binary, so no Claude Code settings " + "were written for this workspace — sessions will be UNGROUNDED and an " + "agent here can SEND mail, DELETE mail and write calendars. Install the " + "CLI globally " + "(`npm i -g @plinth-music/cli`) so `command -v plinth` resolves, then " + "re-run `plinth refresh-context`.");
|
|
28904
29462
|
}
|
|
28905
29463
|
|
|
28906
29464
|
// src/lib/context/artist-context.ts
|
|
28907
|
-
var ARTIST_CLAUDE_MD = "CLAUDE.md";
|
|
28908
29465
|
var ARTIST_DAILY_LOG_MD = "daily-log.md";
|
|
28909
29466
|
function ensurePerArtistContext(deps) {
|
|
28910
29467
|
const { workspaceSlug, force, out, now } = deps;
|
|
@@ -29060,7 +29617,7 @@ function withUserBlock(lines) {
|
|
|
29060
29617
|
`) + `
|
|
29061
29618
|
`;
|
|
29062
29619
|
}
|
|
29063
|
-
function buildStartCommandMd(rosterIsEmpty2) {
|
|
29620
|
+
function buildStartCommandMd(rosterIsEmpty2, offerTrim = false) {
|
|
29064
29621
|
return withUserBlock([
|
|
29065
29622
|
"---",
|
|
29066
29623
|
"description: Open a Plinth session in this workspace. Reads both memory files, pulls what is unread, reads the projected calendar and anything the sync set aside, gives a three-line brief, then asks what you want to work on.",
|
|
@@ -29080,6 +29637,14 @@ function buildStartCommandMd(rosterIsEmpty2) {
|
|
|
29080
29637
|
"**Offer once, and never twice in one session.** And a workspace can have no artists on purpose: if they have declined before, or this is not an artist workspace, drop the offer and brief normally rather than raising it every time you open one.",
|
|
29081
29638
|
""
|
|
29082
29639
|
] : [],
|
|
29640
|
+
...offerTrim ? [
|
|
29641
|
+
`**Some of the files that guide this workspace have outgrown what they are for.** Say so in ONE line beside the brief, say \`plinth status\` lists them and which are theirs alone to trim, and name ${CLOSE_COMMAND_INVOCATION} as the thing that proposes a trim for the ones it can. Then carry on.`,
|
|
29642
|
+
"",
|
|
29643
|
+
"**Offer it and wait; never trim anything here.** This command reads. If they want it now, run `plinth status` so they can see which files and by how much, and leave the trim itself to the close, which proposes each line and writes only what they accept.",
|
|
29644
|
+
"",
|
|
29645
|
+
"**Offer once, and never twice in one session.** If they say no, or they came here to do something else, drop it and do not raise it again in this session.",
|
|
29646
|
+
""
|
|
29647
|
+
] : [],
|
|
29083
29648
|
"**Do not re-run `date`, and do not restate the grounding rules.** The Plinth SessionStart hook has already injected today's date, the entity-resolution rule and the write-confirmation rule, and the workspace `CLAUDE.md` (roster and folder map) is already in context. Repeating them spends the budget this command exists to protect.",
|
|
29084
29649
|
"",
|
|
29085
29650
|
"## Step 1: use the two memory indexes you were already given",
|
|
@@ -29208,6 +29773,8 @@ function buildCloseCommandMd(workspaceSlug) {
|
|
|
29208
29773
|
"",
|
|
29209
29774
|
"**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.",
|
|
29210
29775
|
"",
|
|
29776
|
+
"**Something rule-shaped is not a loose end and is not memory. Sort it here, before Step 4 offers it a memory index. This close writes none of them** - name which of these four it is in the closing summary and write no prose for it. *Mechanical* (a tool to refuse, or a check a program could run): a deny rule or a hook. *Procedural* (a sequence somebody repeats): a skill, or a step in the customisable block at the foot of these commands. *Workspace-wide and stable*: `workspace-rules.md`, and **the member writes it, not you** - it reaches every teammate on every machine. *A preference dressed as a rule*: say so and let it go.",
|
|
29777
|
+
"",
|
|
29211
29778
|
"## Step 4: bank what was learned, by type first and scope second",
|
|
29212
29779
|
"",
|
|
29213
29780
|
"**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.",
|
|
@@ -29229,6 +29796,12 @@ function buildCloseCommandMd(workspaceSlug) {
|
|
|
29229
29796
|
"",
|
|
29230
29797
|
"### Step 4b: two piles, and the artist pile comes first",
|
|
29231
29798
|
"",
|
|
29799
|
+
"**Before either pile, the gate. These four are never banked:** *raw transcript* - what it taught you may be a fact, what was said is not · *a guess, or anything only true today* - live state, which Step 3 routes · *a duplicate of a line already in the index* · *anything the generated `CLAUDE.md` or this session's grounding already states*. **Every line you do bank names its source** - who told you, or which tool read returned it - with an absolute date. **If you cannot name the source, write `source: unknown` - never infer one.**",
|
|
29800
|
+
"",
|
|
29801
|
+
"**Read the index before you add to it, and reconcile what disagrees.** If two lines on your subject contradict each other, **which pair it is decides what you may do**. *One is the line you are adding now*: do not add a third - edit the older line until it reads as superseded, then write yours. **It may well be a teammate's, so mark it superseded, never delete it, and name whose line you changed in the closing summary.** *Both were there before this session*: **propose the supersede and let the member confirm it**, as Step 6 does. Neither is yours and this step has no confirmation gate, so **resolving somebody else's is not yours to do unasked**.",
|
|
29802
|
+
"",
|
|
29803
|
+
"**When two lines disagree, this order decides which is current:** a line the member confirmed outranks a newer one the agent wrote; then the newer line; then one verified against a tool read; then one repeated across sessions; mark the loser superseded in place, never delete it silently.",
|
|
29804
|
+
"",
|
|
29232
29805
|
"**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.",
|
|
29233
29806
|
"",
|
|
29234
29807
|
"**The sorting test: would a teammate who never touches that artist be worse off without this line?** If not, it is the artist pile.",
|
|
@@ -29239,7 +29812,7 @@ function buildCloseCommandMd(workspaceSlug) {
|
|
|
29239
29812
|
"",
|
|
29240
29813
|
"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.**",
|
|
29241
29814
|
"",
|
|
29242
|
-
"**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.",
|
|
29815
|
+
"**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. **The source convention and the conflict order above are the exception**: they reach a header only on a file this command creates, so they hold wherever an existing header is silent.",
|
|
29243
29816
|
"",
|
|
29244
29817
|
"**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.",
|
|
29245
29818
|
"",
|
|
@@ -29251,6 +29824,11 @@ function buildCloseCommandMd(workspaceSlug) {
|
|
|
29251
29824
|
"<!-- Durable facts about <Artist> that hold across sessions. One line per fact, with",
|
|
29252
29825
|
" provenance and an absolute date. SUPERSEDE, do not accumulate - when a fact here",
|
|
29253
29826
|
" becomes wrong, edit that line in place rather than adding a contradicting one.",
|
|
29827
|
+
" If you cannot name the source, write `source: unknown` - never infer one.",
|
|
29828
|
+
" When two lines disagree, this order decides which is current: a line the member",
|
|
29829
|
+
" confirmed outranks a newer one the agent wrote; then the newer line; then one",
|
|
29830
|
+
" verified against a tool read; then one repeated across sessions; mark the loser",
|
|
29831
|
+
" superseded in place, never delete it silently.",
|
|
29254
29832
|
" Live state does NOT belong here - that goes in the body of `_artist.md`. -->",
|
|
29255
29833
|
"```",
|
|
29256
29834
|
"",
|
|
@@ -29278,7 +29856,9 @@ function buildCloseCommandMd(workspaceSlug) {
|
|
|
29278
29856
|
"",
|
|
29279
29857
|
"**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.",
|
|
29280
29858
|
"",
|
|
29281
|
-
"**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.",
|
|
29859
|
+
"**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, **anything rule-shaped you are handing back**, **any contradicting pair you found**, 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.",
|
|
29860
|
+
"",
|
|
29861
|
+
"**When more than seven lines is needed, what this close could NOT do comes first** - a proposal handed back, a contradiction unresolved, a fact with no home. Those exist nowhere else once this session ends.",
|
|
29282
29862
|
"",
|
|
29283
29863
|
"## Step 6: offer to trim what has outgrown its file, and do not trim it",
|
|
29284
29864
|
"",
|
|
@@ -29294,20 +29874,41 @@ function buildCloseCommandMd(workspaceSlug) {
|
|
|
29294
29874
|
"",
|
|
29295
29875
|
"**Running the trim on your own initiative is the failure this step exists to prevent**, and it is the same failure as reporting a sweep nobody asked for. A `no` ends it here, in one line, and the next close offers it again in the same clause without arguing.",
|
|
29296
29876
|
"",
|
|
29297
|
-
"**On yes: propose
|
|
29877
|
+
"**On yes: propose it line by line, and write nothing yet.** Take one file at a time. **This is a list of proposals, not a file operation.** The member accepts or rejects each line on its own, and a line they reject stays exactly as it is. For every line you want to remove or rewrite, give these three fields and no others; a line you leave alone still appears in the listing:",
|
|
29878
|
+
"",
|
|
29879
|
+
"1. **The line itself, verbatim.**",
|
|
29880
|
+
"2. **Its age**, counted from the absolute date the line carries to the date the session hook injected. **A line carrying no date has no age - say so rather than estimating one.**",
|
|
29881
|
+
"3. **What you propose, and one reason.** **To REMOVE**, the reason is one of these four and no others: **superseded** by a later line you name · **duplicate** of another line here · **live state** belonging in the body of `artists/<slug>/_artist.md` · **over-long**, whose remedy is a split and never a cut. **To KEEP but change**, say which rewrite it is: **shortened**, the fact whole and only the wording gone, or **merged** with a line you name. A rewrite is not a removal and needs none of the four.",
|
|
29882
|
+
"",
|
|
29883
|
+
"**Shortening is what actually clears the bar, so reach for it first** - a split makes the file longer, and the four removal reasons are about lines that should not be there rather than about size. **A file whose lines are all correct, current and singular is trimmed by shortening or not at all**: say so rather than manufacturing a removal to reach a number. **An undated line is the member's call** - you cannot show it is over 30 days, so the floor cannot clear it: say it has no date, give your reason, let them decide. **Never treat undated as recent and drop it silently.**",
|
|
29884
|
+
"",
|
|
29885
|
+
"Then the rules the proposal itself obeys:",
|
|
29298
29886
|
"",
|
|
29299
29887
|
"- **Every surviving fact appears either unchanged or beside its rewrite**, so the member is reading a diff rather than trusting a summary of one.",
|
|
29300
29888
|
"- **A fact that is now wrong is edited where it stands, not deleted** - the correction and the date it changed are the record. The file's own header comment states its conventions and is the version that stays current; follow it rather than a copy.",
|
|
29301
29889
|
"- **Provenance and the absolute date survive on every line you keep.** A fact whose source you drop while shortening it has been damaged, not trimmed.",
|
|
29302
29890
|
"- **Nothing banked in the last 30 days is dropped**, measured against the date the session hook injected. Recent facts have not had time to prove whether they were durable, and a trim is not the place to decide.",
|
|
29303
29891
|
"- **Merging two facts into one is a rewrite and shows as one**, with both original lines visible in the proposal.",
|
|
29892
|
+
"- **A contradicting pair is a supersede, not a cut**, resolved by Step 4b's order; the loser stays in the file.",
|
|
29893
|
+
"- **An over-long line is a split wherever it lives.** `plinth status` has an **`Overlong`** row naming index lines past 300 bytes with their length. In an artist store, propose the split here. In either memory index, say it in the closing summary and edit nothing: the workspace one is a person's, and your own is Step 4b's.",
|
|
29304
29894
|
"",
|
|
29305
29895
|
"**Then write only what the member confirms**, and only to the files this command can reach:",
|
|
29306
29896
|
"",
|
|
29307
|
-
"- `artists/<slug>/memory.md` syncs, and a teammate may be in the same file. **Re-read it at the moment you write**, exactly as Step 4b requires, and change only the lines the proposal named. This is the only file this step
|
|
29897
|
+
"- `artists/<slug>/memory.md` syncs, and a teammate may be in the same file. **Re-read it at the moment you write**, exactly as Step 4b requires, and change only the lines the proposal named. **This is the only file this step writes that reaches Plinth.**",
|
|
29308
29898
|
`- \`${VOICE_FILENAME}\` is named and nothing else. You are denied editing it, and that is deliberate: the profile a draft is judged against is not the drafter's to rewrite. Say its size, say the member edits it themselves, and propose nothing.`,
|
|
29899
|
+
"- **`workspace-rules.md` is reported here and never edited, and nothing in the harness stops you.** It is injected whole into every teammate's session, so **it is the member's to change** - Step 3's call, and a size row does not change it. Say how far over it is and propose nothing.",
|
|
29309
29900
|
"- **A memory file the whole workspace shares is not this command's to trim.** `plinth status` names it and says how far over it is; trimming it is a deliberate act by a person who can see who else it belongs to, not a step at the end of somebody's session.",
|
|
29310
29901
|
"",
|
|
29902
|
+
"**Every line the trim changes is preserved, rewrites included.** The ORIGINAL of each line you remove, shorten or merge goes verbatim into `artists/<slug>/memory-archive.md` beside the store, under a heading carrying the date the session hook injected. Create it if absent, with this first line:",
|
|
29903
|
+
"",
|
|
29904
|
+
"```md",
|
|
29905
|
+
"# <Artist> - RETIRED facts, removed from `memory.md`. Not current, and not synced.",
|
|
29906
|
+
"```",
|
|
29907
|
+
"",
|
|
29908
|
+
"**Write the archive first and `artists/<slug>/memory.md` second, never the other way round** - two files cannot be one edit, and in this order a failure between them over-claims rather than losing lines. **Say plainly that the archive stays on this machine**: Plinth never syncs, reads or rewrites it, so a teammate sees the facts gone from `memory.md` and no trace of where they went. Undoing a trim is a copy back by hand. **The proposal is the control; the archive is only the undo.** **Report it when you report the trim, not in the Step 5 summary**, which is already written - name the file, and **if the `memory.md` write was set aside say so**, because the archive then lists lines still in the store.",
|
|
29909
|
+
"",
|
|
29910
|
+
"**A trim you wrote is recorded by `--trimmed` on Step 7's `session end`**, which is what stops `/plinth:start` offering one again for the next 30 days. Leaving it off costs a repeated offer and nothing else.",
|
|
29911
|
+
"",
|
|
29311
29912
|
"**Anything you write here lands after Step 5 looked at the conflicts list**, so it can be set aside within seconds like any other write. If you wrote a trim, run the Step 5 check once more on what you wrote and report it the same way.",
|
|
29312
29913
|
"",
|
|
29313
29914
|
"## Step 7: end the session",
|
|
@@ -29318,6 +29919,8 @@ function buildCloseCommandMd(workspaceSlug) {
|
|
|
29318
29919
|
`plinth session end ${ws} --summary="<one line>"`,
|
|
29319
29920
|
"```",
|
|
29320
29921
|
"",
|
|
29922
|
+
"**If Step 6 wrote a trim, add `--trimmed` to that line.** It is a plain flag with no value, so it can go anywhere after `end`.",
|
|
29923
|
+
"",
|
|
29321
29924
|
"**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.",
|
|
29322
29925
|
"",
|
|
29323
29926
|
"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."
|
|
@@ -29478,8 +30081,14 @@ function ensureWorkspaceCommands(deps) {
|
|
|
29478
30081
|
const created = [];
|
|
29479
30082
|
const refreshed = [];
|
|
29480
30083
|
const noArtists = (deps.readRosterIsEmpty ?? rosterIsEmpty)(workspaceSlug);
|
|
30084
|
+
let offerTrim = false;
|
|
30085
|
+
try {
|
|
30086
|
+
offerTrim = (deps.readShouldOfferTrim ?? shouldOfferTrim)(workspaceSlug);
|
|
30087
|
+
} catch {
|
|
30088
|
+
offerTrim = false;
|
|
30089
|
+
}
|
|
29481
30090
|
const bodies = {
|
|
29482
|
-
[START_COMMAND_REL]: buildStartCommandMd(noArtists),
|
|
30091
|
+
[START_COMMAND_REL]: buildStartCommandMd(noArtists, offerTrim),
|
|
29483
30092
|
[CLOSE_COMMAND_REL]: buildCloseCommandMd(workspaceSlug),
|
|
29484
30093
|
[ONBOARD_COMMAND_REL]: buildOnboardCommandMd(workspaceSlug)
|
|
29485
30094
|
};
|
|
@@ -29513,11 +30122,6 @@ function backupStamp2(d) {
|
|
|
29513
30122
|
var RETIRED_COMMAND_TOKENS = [
|
|
29514
30123
|
{ commandRel: CLOSE_COMMAND_REL, token: "memory-gate", version: "0.11.0" }
|
|
29515
30124
|
];
|
|
29516
|
-
var GENERATED_COMMAND_RELS = [
|
|
29517
|
-
START_COMMAND_REL,
|
|
29518
|
-
CLOSE_COMMAND_REL,
|
|
29519
|
-
ONBOARD_COMMAND_REL
|
|
29520
|
-
];
|
|
29521
30125
|
function scanStaleUserBlocks(deps) {
|
|
29522
30126
|
const retired = deps.retired ?? RETIRED_COMMAND_TOKENS;
|
|
29523
30127
|
const read = deps.read ?? readContextFile;
|
|
@@ -29896,6 +30500,18 @@ function fileOverCapLine(f) {
|
|
|
29896
30500
|
function fileConflictLine(f) {
|
|
29897
30501
|
return `[FILE_CONFLICT path=${f.path} stash=${f.stash}]`;
|
|
29898
30502
|
}
|
|
30503
|
+
function fileRejectionSyncFailedLine(f) {
|
|
30504
|
+
const parts = [`[FILE_REJECTION_SYNC_FAILED verb=${f.verb}`];
|
|
30505
|
+
if (f.path !== undefined)
|
|
30506
|
+
parts.push(`path=${f.path}`);
|
|
30507
|
+
parts.push(`reason=${f.reason}`);
|
|
30508
|
+
if (f.status !== undefined)
|
|
30509
|
+
parts.push(`status=${f.status}`);
|
|
30510
|
+
return `${parts.join(" ")}]`;
|
|
30511
|
+
}
|
|
30512
|
+
function fileRejectionWithdrawnLine(f) {
|
|
30513
|
+
return `[FILE_REJECTION_WITHDRAWN path=${f.path} disposition=${f.disposition}]`;
|
|
30514
|
+
}
|
|
29899
30515
|
function conflictsViewFailedLine(f) {
|
|
29900
30516
|
const msg = f.message === undefined ? "" : sanitizeServerMessage(f.message);
|
|
29901
30517
|
const msgPart = msg === "" ? "" : ` message="${msg.replace(/"/g, "'")}"`;
|
|
@@ -30005,7 +30621,7 @@ function humanBytes(bytes) {
|
|
|
30005
30621
|
function recordFileOverCap(args) {
|
|
30006
30622
|
const withoutStale = removeUnsynced(args.state, "file", args.relPath).state;
|
|
30007
30623
|
const ceiling = args.source === "server" ? `the ${humanBytes(args.max)} server limit` : `the ${humanBytes(args.max)} upload ceiling (measured platform limit)`;
|
|
30008
|
-
const message = args.tracked ? `
|
|
30624
|
+
const message = args.tracked ? `A local change to this file (${humanBytes(args.size)}) exceeds ${ceiling}, ` + `so it cannot be pushed and the copy in Plinth is unchanged. ` + `Your edit is on disk here only.` : `${humanBytes(args.size)} exceeds ${ceiling}. The file is still on disk ` + `here but is not in Plinth; shrink or split it to sync it.`;
|
|
30009
30625
|
return persist(withoutStale, upsertRejected(withoutStale, {
|
|
30010
30626
|
target: "file",
|
|
30011
30627
|
op: "upsert",
|
|
@@ -30013,6 +30629,8 @@ function recordFileOverCap(args) {
|
|
|
30013
30629
|
status: 413,
|
|
30014
30630
|
reason: "over_cap",
|
|
30015
30631
|
message,
|
|
30632
|
+
size_bytes: args.size,
|
|
30633
|
+
max_bytes: args.max,
|
|
30016
30634
|
rejected_at: args.now().toISOString()
|
|
30017
30635
|
}));
|
|
30018
30636
|
}
|
|
@@ -30066,6 +30684,12 @@ function clearFileRejected(state, relPath) {
|
|
|
30066
30684
|
return state;
|
|
30067
30685
|
return commitState(state, next.state);
|
|
30068
30686
|
}
|
|
30687
|
+
function clearReportedSafeFileRejected(state, relPath) {
|
|
30688
|
+
const existing = (state.rejected ?? []).find((o) => o.target === "file" && o.path === relPath);
|
|
30689
|
+
if (existing?.reported_at !== undefined)
|
|
30690
|
+
return state;
|
|
30691
|
+
return clearFileRejected(state, relPath);
|
|
30692
|
+
}
|
|
30069
30693
|
|
|
30070
30694
|
// src/lib/context/workspace-claude-md.ts
|
|
30071
30695
|
var EXTRACTED_TEXT_DIR = extractedTextRelPath("x").replace(/x\.md$/, "");
|
|
@@ -30121,6 +30745,8 @@ function buildWorkspaceClaudeMd(input) {
|
|
|
30121
30745
|
`- \`${extractedTextRelPath("<path>")}\` where \`<path>\` is the file's path under \`files/\`: the text Plinth extracted from that file, so a PDF's contents can be searched. Not every file has one - text files are already their own text, and extraction can fail. Where it produced junk the file exists and says so instead of carrying it, so read what is there before trusting a silence. Derived: edits here are overwritten and never reach Plinth`,
|
|
30122
30746
|
`- \`.claude/commands/\` local skills, incl. generated \`${START_COMMAND_INVOCATION}\` + \`${CLOSE_COMMAND_INVOCATION}\` + \`${ONBOARD_COMMAND_INVOCATION}\` · at the mirror root: \`workspace-memory.md\` \`workspace-rules.md\` (workspace-wide) + \`user-memory.md\` (yours), synced to Plinth · \`voice.md\` (yours: how you write, read by the agent, edited only by you) · \`CLAUDE.local.md\` private, gitignored`,
|
|
30123
30747
|
``,
|
|
30748
|
+
...renderGuidingFilesSection(),
|
|
30749
|
+
``,
|
|
30124
30750
|
`## Where key context lives: look here before hunting`,
|
|
30125
30751
|
`- 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`,
|
|
30126
30752
|
`- **Contracts, executed agreements & any PDF** → \`files/\` (search here first; \`documents/\` holds markdown bodies, not attachments)`,
|
|
@@ -30152,8 +30778,6 @@ function buildWorkspaceClaudeMd(input) {
|
|
|
30152
30778
|
}
|
|
30153
30779
|
|
|
30154
30780
|
// src/lib/context/generate.ts
|
|
30155
|
-
var CLAUDE_MD = "CLAUDE.md";
|
|
30156
|
-
var CLAUDE_LOCAL_MD = "CLAUDE.local.md";
|
|
30157
30781
|
var GITIGNORE = ".gitignore";
|
|
30158
30782
|
var GITIGNORE_ENTRIES = [
|
|
30159
30783
|
CLAUDE_LOCAL_MD,
|
|
@@ -30817,6 +31441,27 @@ async function runSessionEnd(args, deps = {}) {
|
|
|
30817
31441
|
} catch {}
|
|
30818
31442
|
}
|
|
30819
31443
|
}
|
|
31444
|
+
const anchorSlug = resolveSlug(args.workspace);
|
|
31445
|
+
if (anchorSlug.ok) {
|
|
31446
|
+
try {
|
|
31447
|
+
const before = loadState(anchorSlug.slug);
|
|
31448
|
+
let next = before;
|
|
31449
|
+
const anchored = setSizeAnchor(next, {
|
|
31450
|
+
at: now.toISOString(),
|
|
31451
|
+
kind: "session-end",
|
|
31452
|
+
sizes: measureBarredFiles(anchorSlug.slug).sizes
|
|
31453
|
+
});
|
|
31454
|
+
next = anchored.state;
|
|
31455
|
+
let changed = anchored.changed;
|
|
31456
|
+
if (args.trimmed === true) {
|
|
31457
|
+
const stamped = setLastTrimAt(next, now.toISOString());
|
|
31458
|
+
next = stamped.state;
|
|
31459
|
+
changed = changed || stamped.changed;
|
|
31460
|
+
}
|
|
31461
|
+
if (changed)
|
|
31462
|
+
commitState(before, next);
|
|
31463
|
+
} catch {}
|
|
31464
|
+
}
|
|
30820
31465
|
const dock = readDockSignalEnv(env2);
|
|
30821
31466
|
if (dock === null)
|
|
30822
31467
|
return { exitCode: 0 };
|
|
@@ -30918,6 +31563,10 @@ var sessionCommand = defineCommand({
|
|
|
30918
31563
|
},
|
|
30919
31564
|
args: {
|
|
30920
31565
|
workspace: { type: "string", description: "Workspace slug" },
|
|
31566
|
+
trimmed: {
|
|
31567
|
+
type: "boolean",
|
|
31568
|
+
description: "This close wrote a memory trim; records the date so /plinth:start stops offering one"
|
|
31569
|
+
},
|
|
30921
31570
|
summary: {
|
|
30922
31571
|
type: "string",
|
|
30923
31572
|
description: "One-line summary of the session, forwarded to the dock",
|
|
@@ -30927,7 +31576,8 @@ var sessionCommand = defineCommand({
|
|
|
30927
31576
|
async run({ args }) {
|
|
30928
31577
|
const result = await runSessionEnd({
|
|
30929
31578
|
workspace: args.workspace,
|
|
30930
|
-
summary: args.summary
|
|
31579
|
+
summary: args.summary,
|
|
31580
|
+
trimmed: args.trimmed === true
|
|
30931
31581
|
});
|
|
30932
31582
|
process.exitCode = result.exitCode;
|
|
30933
31583
|
}
|
|
@@ -30937,7 +31587,7 @@ var sessionCommand = defineCommand({
|
|
|
30937
31587
|
|
|
30938
31588
|
// src/commands/start.ts
|
|
30939
31589
|
var import_gray_matter13 = __toESM(require_gray_matter(), 1);
|
|
30940
|
-
import { existsSync as existsSync8, readFileSync as
|
|
31590
|
+
import { existsSync as existsSync8, readFileSync as readFileSync25, statSync as statSync17 } from "node:fs";
|
|
30941
31591
|
|
|
30942
31592
|
// src/lib/daemon-log.ts
|
|
30943
31593
|
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync9, renameSync as renameSync7, statSync as statSync4 } from "node:fs";
|
|
@@ -31190,9 +31840,9 @@ function conflictsRoot(workspaceSlug) {
|
|
|
31190
31840
|
return join11(plinthHome(), "_conflicts", workspaceSlug);
|
|
31191
31841
|
}
|
|
31192
31842
|
function conflictFilePath(args) {
|
|
31193
|
-
const { workspaceSlug, entityType, entitySlug, timestamp } = args;
|
|
31843
|
+
const { workspaceSlug, entityType, entitySlug, timestamp, side } = args;
|
|
31194
31844
|
const iso = timestamp.toISOString().replace(/[:.]/g, "-");
|
|
31195
|
-
return join11(conflictsRoot(workspaceSlug), `${entityType}.${entitySlug}.${iso}.md`);
|
|
31845
|
+
return join11(conflictsRoot(workspaceSlug), `${entityType}.${entitySlug}.${iso}.${side}.md`);
|
|
31196
31846
|
}
|
|
31197
31847
|
function stashConflict(args) {
|
|
31198
31848
|
const target = conflictFilePath(args);
|
|
@@ -31201,10 +31851,10 @@ function stashConflict(args) {
|
|
|
31201
31851
|
return target;
|
|
31202
31852
|
}
|
|
31203
31853
|
function fileConflictPath(args) {
|
|
31204
|
-
const { workspaceSlug, relPath, timestamp } = args;
|
|
31854
|
+
const { workspaceSlug, relPath, timestamp, side } = args;
|
|
31205
31855
|
const iso = timestamp.toISOString().replace(/[:.]/g, "-");
|
|
31206
31856
|
const safe = relPath.replace(/[/\\]/g, "__");
|
|
31207
|
-
return join11(conflictsRoot(workspaceSlug), `files.${safe}.${iso}`);
|
|
31857
|
+
return join11(conflictsRoot(workspaceSlug), `files.${safe}.${iso}.${side}`);
|
|
31208
31858
|
}
|
|
31209
31859
|
function stashFileConflict(args) {
|
|
31210
31860
|
const target = fileConflictPath(args);
|
|
@@ -31212,7 +31862,7 @@ function stashFileConflict(args) {
|
|
|
31212
31862
|
writeFileSync7(target, args.bytes, { mode: 384 });
|
|
31213
31863
|
return target;
|
|
31214
31864
|
}
|
|
31215
|
-
var STAMP_RE2 = /\.(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z)(
|
|
31865
|
+
var STAMP_RE2 = /\.(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z)(?:\.(local|cloud))?(?:\.md)?$/;
|
|
31216
31866
|
function parseStamp(stamp) {
|
|
31217
31867
|
const iso = stamp.replace(/T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z$/, "T$1:$2:$3.$4Z");
|
|
31218
31868
|
const d = new Date(iso);
|
|
@@ -31253,11 +31903,13 @@ function listConflictStashes(workspaceSlug) {
|
|
|
31253
31903
|
if (file2.startsWith("."))
|
|
31254
31904
|
continue;
|
|
31255
31905
|
const m = STAMP_RE2.exec(file2);
|
|
31906
|
+
const side = m?.[2];
|
|
31256
31907
|
out.push({
|
|
31257
31908
|
file: file2,
|
|
31258
31909
|
absPath,
|
|
31259
31910
|
label: labelFor(file2),
|
|
31260
31911
|
stashedAt: m ? parseStamp(m[1]) : null,
|
|
31912
|
+
side: side === "local" || side === "cloud" ? side : null,
|
|
31261
31913
|
mtimeMs: st.mtimeMs
|
|
31262
31914
|
});
|
|
31263
31915
|
}
|
|
@@ -31512,7 +32164,7 @@ async function pushFilesUpsert(args) {
|
|
|
31512
32164
|
cloudRow = undefined;
|
|
31513
32165
|
}
|
|
31514
32166
|
}
|
|
31515
|
-
const stashPath = stashFileConflict({ workspaceSlug, relPath, timestamp: now(), bytes });
|
|
32167
|
+
const stashPath = stashFileConflict({ workspaceSlug, relPath, timestamp: now(), bytes, side: "local" });
|
|
31516
32168
|
output?.log(fileConflictLine({ path: relPath, stash: stashPath }));
|
|
31517
32169
|
if (existing) {
|
|
31518
32170
|
nextState = await restoreFileFromCloud({
|
|
@@ -32002,7 +32654,8 @@ async function reconcileContextDocConflict(args) {
|
|
|
32002
32654
|
entityType: strategy.type,
|
|
32003
32655
|
entitySlug: fileSlug,
|
|
32004
32656
|
timestamp: now(),
|
|
32005
|
-
contents: raw
|
|
32657
|
+
contents: raw,
|
|
32658
|
+
side: "local"
|
|
32006
32659
|
});
|
|
32007
32660
|
const nextState = await pullAndWriteMirror({
|
|
32008
32661
|
strategy,
|
|
@@ -32034,12 +32687,13 @@ async function reconcileContextDocConflict(args) {
|
|
|
32034
32687
|
state: nextState
|
|
32035
32688
|
};
|
|
32036
32689
|
}
|
|
32037
|
-
const
|
|
32690
|
+
const stashPreLandCloudCopy = () => stashConflict({
|
|
32038
32691
|
workspaceSlug,
|
|
32039
32692
|
entityType: strategy.type,
|
|
32040
32693
|
entitySlug: existingSlug,
|
|
32041
32694
|
timestamp: now(),
|
|
32042
|
-
contents: strategy.serialise(existing)
|
|
32695
|
+
contents: strategy.serialise(existing),
|
|
32696
|
+
side: "cloud"
|
|
32043
32697
|
});
|
|
32044
32698
|
const fm = parsed.data ?? {};
|
|
32045
32699
|
const lifted = splitUserFrontmatter(strategy, fm);
|
|
@@ -32073,6 +32727,7 @@ async function reconcileContextDocConflict(args) {
|
|
|
32073
32727
|
body_len: hashBody.length
|
|
32074
32728
|
};
|
|
32075
32729
|
state = persistEntry(state, strategy.type, updated);
|
|
32730
|
+
const preLandStash = stashPreLandCloudCopy();
|
|
32076
32731
|
output?.log(contextDocConflictLine({
|
|
32077
32732
|
entityType: strategy.type,
|
|
32078
32733
|
localSlug: fileSlug,
|
|
@@ -32088,7 +32743,8 @@ async function reconcileContextDocConflict(args) {
|
|
|
32088
32743
|
outcome: {
|
|
32089
32744
|
kind: "pushed",
|
|
32090
32745
|
entityType: strategy.type,
|
|
32091
|
-
entitySlug: updated.slug
|
|
32746
|
+
entitySlug: updated.slug,
|
|
32747
|
+
stashPath: preLandStash
|
|
32092
32748
|
},
|
|
32093
32749
|
state
|
|
32094
32750
|
};
|
|
@@ -32112,7 +32768,8 @@ async function reconcileContextDocConflict(args) {
|
|
|
32112
32768
|
entityType: strategy.type,
|
|
32113
32769
|
entitySlug: fileSlug,
|
|
32114
32770
|
timestamp: now(),
|
|
32115
|
-
contents: raw
|
|
32771
|
+
contents: raw,
|
|
32772
|
+
side: "local"
|
|
32116
32773
|
});
|
|
32117
32774
|
if (staleEntryId && staleEntryId !== existingId) {
|
|
32118
32775
|
state = dropEntry(state, strategy.type, staleEntryId);
|
|
@@ -32384,7 +33041,8 @@ async function runPushUpsert(args) {
|
|
|
32384
33041
|
entityType: strategy.type,
|
|
32385
33042
|
entitySlug: liveEntry.slug,
|
|
32386
33043
|
timestamp: args.now(),
|
|
32387
|
-
contents: args.raw
|
|
33044
|
+
contents: args.raw,
|
|
33045
|
+
side: "local"
|
|
32388
33046
|
});
|
|
32389
33047
|
const nextState = await pullAndWriteMirror({
|
|
32390
33048
|
strategy,
|
|
@@ -32422,16 +33080,18 @@ async function reconcileTaskConflict(args) {
|
|
|
32422
33080
|
output
|
|
32423
33081
|
} = args;
|
|
32424
33082
|
let state = args.state;
|
|
32425
|
-
const
|
|
33083
|
+
const stashLocalSide = () => stashConflict({
|
|
32426
33084
|
workspaceSlug,
|
|
32427
33085
|
entityType: strategy.type,
|
|
32428
33086
|
entitySlug: liveEntry.slug,
|
|
32429
33087
|
timestamp: now(),
|
|
32430
|
-
contents: raw
|
|
33088
|
+
contents: raw,
|
|
33089
|
+
side: "local"
|
|
32431
33090
|
});
|
|
32432
33091
|
const localProse = stripSyncErrorBlock(stripCommentsBlock(parsed.content));
|
|
32433
33092
|
const wireBody = normaliseWireBody(strategy.bodyToWire(localProse).body);
|
|
32434
33093
|
if (wireBody == null) {
|
|
33094
|
+
const stashPath2 = stashLocalSide();
|
|
32435
33095
|
state = await pullAndWriteMirror({
|
|
32436
33096
|
strategy,
|
|
32437
33097
|
workspaceSlug,
|
|
@@ -32446,11 +33106,37 @@ async function reconcileTaskConflict(args) {
|
|
|
32446
33106
|
kind: "conflict",
|
|
32447
33107
|
entityType: strategy.type,
|
|
32448
33108
|
entitySlug: liveEntry.slug,
|
|
32449
|
-
stashPath
|
|
33109
|
+
stashPath: stashPath2
|
|
32450
33110
|
},
|
|
32451
33111
|
state
|
|
32452
33112
|
};
|
|
32453
33113
|
}
|
|
33114
|
+
let cloudRow = null;
|
|
33115
|
+
try {
|
|
33116
|
+
cloudRow = await getEntity({ strategy, baseUrl, pat, id: liveEntry.id, fetchImpl });
|
|
33117
|
+
} catch (err) {
|
|
33118
|
+
if (!(err instanceof ApiError) || err.status !== 404)
|
|
33119
|
+
throw err;
|
|
33120
|
+
}
|
|
33121
|
+
if (cloudRow === null) {
|
|
33122
|
+
stashLocalSide();
|
|
33123
|
+
return {
|
|
33124
|
+
outcome: {
|
|
33125
|
+
kind: "not_found",
|
|
33126
|
+
entityType: strategy.type,
|
|
33127
|
+
entitySlug: liveEntry.slug
|
|
33128
|
+
},
|
|
33129
|
+
state
|
|
33130
|
+
};
|
|
33131
|
+
}
|
|
33132
|
+
const stashCloudSide = () => stashConflict({
|
|
33133
|
+
workspaceSlug,
|
|
33134
|
+
entityType: strategy.type,
|
|
33135
|
+
entitySlug: liveEntry.slug,
|
|
33136
|
+
timestamp: now(),
|
|
33137
|
+
contents: strategy.serialise(cloudRow),
|
|
33138
|
+
side: "cloud"
|
|
33139
|
+
});
|
|
32454
33140
|
const patchResult = await patchEntity({
|
|
32455
33141
|
strategy,
|
|
32456
33142
|
baseUrl,
|
|
@@ -32461,6 +33147,7 @@ async function reconcileTaskConflict(args) {
|
|
|
32461
33147
|
fetchImpl
|
|
32462
33148
|
});
|
|
32463
33149
|
if (patchResult.ok) {
|
|
33150
|
+
const stashPath2 = stashCloudSide();
|
|
32464
33151
|
writeMirrorWithBodyPreserve(workspaceSlug, strategy, patchResult.doc);
|
|
32465
33152
|
state = stampStateFromMirror({
|
|
32466
33153
|
strategy,
|
|
@@ -32474,12 +33161,14 @@ async function reconcileTaskConflict(args) {
|
|
|
32474
33161
|
outcome: {
|
|
32475
33162
|
kind: "pushed",
|
|
32476
33163
|
entityType: strategy.type,
|
|
32477
|
-
entitySlug: patchResult.doc.slug
|
|
33164
|
+
entitySlug: patchResult.doc.slug,
|
|
33165
|
+
stashPath: stashPath2
|
|
32478
33166
|
},
|
|
32479
33167
|
state
|
|
32480
33168
|
};
|
|
32481
33169
|
}
|
|
32482
33170
|
if (patchResult.code === 404) {
|
|
33171
|
+
stashLocalSide();
|
|
32483
33172
|
return {
|
|
32484
33173
|
outcome: {
|
|
32485
33174
|
kind: "not_found",
|
|
@@ -32489,6 +33178,7 @@ async function reconcileTaskConflict(args) {
|
|
|
32489
33178
|
state
|
|
32490
33179
|
};
|
|
32491
33180
|
}
|
|
33181
|
+
const stashPath = stashLocalSide();
|
|
32492
33182
|
state = await pullAndWriteMirror({
|
|
32493
33183
|
strategy,
|
|
32494
33184
|
workspaceSlug,
|
|
@@ -32650,7 +33340,8 @@ async function runPushCreate(args) {
|
|
|
32650
33340
|
entityType: strategy.type,
|
|
32651
33341
|
entitySlug: args.slug,
|
|
32652
33342
|
timestamp: args.now(),
|
|
32653
|
-
contents: args.raw
|
|
33343
|
+
contents: args.raw,
|
|
33344
|
+
side: "local"
|
|
32654
33345
|
});
|
|
32655
33346
|
const nextState = await pullAndWriteMirror({
|
|
32656
33347
|
strategy,
|
|
@@ -32876,7 +33567,8 @@ async function pushRename(args) {
|
|
|
32876
33567
|
entityType,
|
|
32877
33568
|
entitySlug: fromSlug,
|
|
32878
33569
|
timestamp: now(),
|
|
32879
|
-
contents: contents2
|
|
33570
|
+
contents: contents2,
|
|
33571
|
+
side: "local"
|
|
32880
33572
|
});
|
|
32881
33573
|
const afterColliding = await pullAndWriteMirror({
|
|
32882
33574
|
strategy,
|
|
@@ -32916,7 +33608,8 @@ async function pushRename(args) {
|
|
|
32916
33608
|
entityType,
|
|
32917
33609
|
entitySlug: fromSlug,
|
|
32918
33610
|
timestamp: now(),
|
|
32919
|
-
contents
|
|
33611
|
+
contents,
|
|
33612
|
+
side: "local"
|
|
32920
33613
|
});
|
|
32921
33614
|
const nextState = await pullAndWriteMirror({
|
|
32922
33615
|
strategy,
|
|
@@ -33040,7 +33733,8 @@ async function discardBatchOps(batch, deps) {
|
|
|
33040
33733
|
entityType: op.entityType,
|
|
33041
33734
|
entitySlug: op.slug,
|
|
33042
33735
|
timestamp: (deps.now ?? (() => new Date))(),
|
|
33043
|
-
contents: local
|
|
33736
|
+
contents: local,
|
|
33737
|
+
side: "local"
|
|
33044
33738
|
});
|
|
33045
33739
|
}
|
|
33046
33740
|
}
|
|
@@ -33073,7 +33767,8 @@ async function discardFileOp(op, deps) {
|
|
|
33073
33767
|
workspaceSlug: deps.workspaceSlug,
|
|
33074
33768
|
relPath: op.path,
|
|
33075
33769
|
timestamp: (deps.now ?? (() => new Date))(),
|
|
33076
|
-
bytes: local
|
|
33770
|
+
bytes: local,
|
|
33771
|
+
side: "local"
|
|
33077
33772
|
});
|
|
33078
33773
|
}
|
|
33079
33774
|
const next = await restoreFileFromCloud({
|
|
@@ -35328,6 +36023,7 @@ async function reconcileFilesSubtree(args) {
|
|
|
35328
36023
|
case "created":
|
|
35329
36024
|
case "updated":
|
|
35330
36025
|
countPushed();
|
|
36026
|
+
state = clearFileRejected(state, relPath);
|
|
35331
36027
|
break;
|
|
35332
36028
|
case "conflict":
|
|
35333
36029
|
conflicts++;
|
|
@@ -35567,7 +36263,8 @@ function applyArtistMemoryOnPull(args) {
|
|
|
35567
36263
|
entityType: ENTITY_TYPE,
|
|
35568
36264
|
entitySlug: conflictSlug(slug),
|
|
35569
36265
|
timestamp: now(),
|
|
35570
|
-
contents: local
|
|
36266
|
+
contents: local,
|
|
36267
|
+
side: "local"
|
|
35571
36268
|
});
|
|
35572
36269
|
out.warn(`Conflict on artists/${slug}/memory.md → stashed local edit to ${stashPath} (cloud-wins).`);
|
|
35573
36270
|
}
|
|
@@ -35647,7 +36344,8 @@ async function pushArtistMemory(args) {
|
|
|
35647
36344
|
entityType: ENTITY_TYPE,
|
|
35648
36345
|
entitySlug: conflictSlug(slug),
|
|
35649
36346
|
timestamp: now(),
|
|
35650
|
-
contents: local
|
|
36347
|
+
contents: local,
|
|
36348
|
+
side: "local"
|
|
35651
36349
|
});
|
|
35652
36350
|
const nextState = adoptCloudMemory({ workspaceSlug, slug, cloud, state: base2, suppressor });
|
|
35653
36351
|
output?.log(`[ARTIST_MEMORY_CONFLICT slug=${slug} id=${entry2.id} action=cloud_wins_stash]`);
|
|
@@ -37192,6 +37890,7 @@ async function reconcileDivergedRows(args) {
|
|
|
37192
37890
|
let scanned = 0;
|
|
37193
37891
|
let diverged = 0;
|
|
37194
37892
|
let pushed = 0;
|
|
37893
|
+
let merged = 0;
|
|
37195
37894
|
let conflicts = 0;
|
|
37196
37895
|
let noop = 0;
|
|
37197
37896
|
let failed = 0;
|
|
@@ -37214,7 +37913,10 @@ async function reconcileDivergedRows(args) {
|
|
|
37214
37913
|
state = result.state;
|
|
37215
37914
|
switch (result.outcome.kind) {
|
|
37216
37915
|
case "pushed":
|
|
37217
|
-
|
|
37916
|
+
if (result.outcome.stashPath)
|
|
37917
|
+
merged++;
|
|
37918
|
+
else
|
|
37919
|
+
pushed++;
|
|
37218
37920
|
break;
|
|
37219
37921
|
case "conflict":
|
|
37220
37922
|
case "create_slug_conflict":
|
|
@@ -37382,6 +38084,7 @@ async function reconcileDivergedRows(args) {
|
|
|
37382
38084
|
scanned,
|
|
37383
38085
|
diverged,
|
|
37384
38086
|
pushed,
|
|
38087
|
+
merged,
|
|
37385
38088
|
conflicts,
|
|
37386
38089
|
noop,
|
|
37387
38090
|
failed,
|
|
@@ -37578,7 +38281,8 @@ async function pullWorkspaceRules(args) {
|
|
|
37578
38281
|
entityType: CONFLICT_ENTITY,
|
|
37579
38282
|
entitySlug: CONFLICT_SLUG,
|
|
37580
38283
|
timestamp: now(),
|
|
37581
|
-
contents: local
|
|
38284
|
+
contents: local,
|
|
38285
|
+
side: "local"
|
|
37582
38286
|
});
|
|
37583
38287
|
output?.warn(`Conflict on ${WORKSPACE_RULES_FILENAME} -> stashed local edit to ${stashPath} (cloud-wins).`);
|
|
37584
38288
|
output?.log(`[WORKSPACE_RULES_PULL_CONFLICT ws=${workspaceSlug} stashed]`);
|
|
@@ -37664,7 +38368,8 @@ async function pushWorkspaceRules(args) {
|
|
|
37664
38368
|
entityType: CONFLICT_ENTITY,
|
|
37665
38369
|
entitySlug: CONFLICT_SLUG,
|
|
37666
38370
|
timestamp: now(),
|
|
37667
|
-
contents: local
|
|
38371
|
+
contents: local,
|
|
38372
|
+
side: "local"
|
|
37668
38373
|
});
|
|
37669
38374
|
const next = await restoreFromCloud(args);
|
|
37670
38375
|
output?.warn(`Conflict on ${WORKSPACE_RULES_FILENAME} -> stashed local edit to ${stashPath} (cloud-wins; cloud copy restored).`);
|
|
@@ -37844,7 +38549,8 @@ async function pullWorkspaceMemory(args) {
|
|
|
37844
38549
|
entityType: CONFLICT_ENTITY2,
|
|
37845
38550
|
entitySlug: CONFLICT_SLUG2,
|
|
37846
38551
|
timestamp: now(),
|
|
37847
|
-
contents: local
|
|
38552
|
+
contents: local,
|
|
38553
|
+
side: "local"
|
|
37848
38554
|
});
|
|
37849
38555
|
output?.warn(`Conflict on ${WORKSPACE_MEMORY_FILENAME} -> stashed local edit to ${stashPath} (cloud-wins).`);
|
|
37850
38556
|
output?.log(`[WORKSPACE_MEMORY_PULL_CONFLICT ws=${workspaceSlug} stashed]`);
|
|
@@ -37930,7 +38636,8 @@ async function pushWorkspaceMemory(args) {
|
|
|
37930
38636
|
entityType: CONFLICT_ENTITY2,
|
|
37931
38637
|
entitySlug: CONFLICT_SLUG2,
|
|
37932
38638
|
timestamp: now(),
|
|
37933
|
-
contents: local
|
|
38639
|
+
contents: local,
|
|
38640
|
+
side: "local"
|
|
37934
38641
|
});
|
|
37935
38642
|
const next = await restoreFromCloud2(args);
|
|
37936
38643
|
output?.warn(`Conflict on ${WORKSPACE_MEMORY_FILENAME} -> stashed local edit to ${stashPath} (cloud-wins; cloud copy restored).`);
|
|
@@ -38118,7 +38825,8 @@ async function pullSingleton(descriptor, args) {
|
|
|
38118
38825
|
entityType: descriptor.conflictEntity,
|
|
38119
38826
|
entitySlug: descriptor.conflictSlug,
|
|
38120
38827
|
timestamp: now(),
|
|
38121
|
-
contents: local
|
|
38828
|
+
contents: local,
|
|
38829
|
+
side: "local"
|
|
38122
38830
|
});
|
|
38123
38831
|
output?.warn(`Conflict on ${descriptor.filename} -> stashed local edit to ${stashPath} (cloud-wins).`);
|
|
38124
38832
|
output?.log(`[${descriptor.logTag}_PULL_CONFLICT ws=${workspaceSlug} stashed]`);
|
|
@@ -38192,7 +38900,8 @@ async function pushSingleton(descriptor, args) {
|
|
|
38192
38900
|
entityType: descriptor.conflictEntity,
|
|
38193
38901
|
entitySlug: descriptor.conflictSlug,
|
|
38194
38902
|
timestamp: now(),
|
|
38195
|
-
contents: local
|
|
38903
|
+
contents: local,
|
|
38904
|
+
side: "local"
|
|
38196
38905
|
});
|
|
38197
38906
|
const next = await restoreFromCloud3(descriptor, args);
|
|
38198
38907
|
output?.warn(`Conflict on ${descriptor.filename} -> stashed local edit to ${stashPath} (cloud-wins; cloud copy restored).`);
|
|
@@ -38385,8 +39094,8 @@ function createThrottledFetch(baseFetch, opts = {}) {
|
|
|
38385
39094
|
import {
|
|
38386
39095
|
existsSync as existsSync7,
|
|
38387
39096
|
readdirSync as readdirSync6,
|
|
38388
|
-
readFileSync as
|
|
38389
|
-
statSync as
|
|
39097
|
+
readFileSync as readFileSync24,
|
|
39098
|
+
statSync as statSync16,
|
|
38390
39099
|
unlinkSync as unlinkSync8
|
|
38391
39100
|
} from "node:fs";
|
|
38392
39101
|
import { join as join16 } from "node:path";
|
|
@@ -38405,8 +39114,8 @@ var FOOTER = [
|
|
|
38405
39114
|
var NOTHING_SET_ASIDE = [
|
|
38406
39115
|
"# Conflicts",
|
|
38407
39116
|
"",
|
|
38408
|
-
"**Nothing has been set aside.**
|
|
38409
|
-
"
|
|
39117
|
+
"**Nothing has been set aside.** Nothing in this workspace has changed in two",
|
|
39118
|
+
"places at once and had one side put aside."
|
|
38410
39119
|
].join(`
|
|
38411
39120
|
`);
|
|
38412
39121
|
function absolute(date5) {
|
|
@@ -38443,9 +39152,47 @@ function resolveConflictRows(workspaceSlug, stashes) {
|
|
|
38443
39152
|
};
|
|
38444
39153
|
});
|
|
38445
39154
|
}
|
|
39155
|
+
function entityNoun(label) {
|
|
39156
|
+
const head = label.slice(0, label.indexOf("/"));
|
|
39157
|
+
switch (head) {
|
|
39158
|
+
case "tasks":
|
|
39159
|
+
return "task";
|
|
39160
|
+
case "documents":
|
|
39161
|
+
return "document";
|
|
39162
|
+
case "projects":
|
|
39163
|
+
return "project";
|
|
39164
|
+
case "artists":
|
|
39165
|
+
return "artist";
|
|
39166
|
+
default:
|
|
39167
|
+
return "record";
|
|
39168
|
+
}
|
|
39169
|
+
}
|
|
38446
39170
|
function renderRow(row) {
|
|
38447
39171
|
const { stash } = row;
|
|
38448
39172
|
const when = stash.stashedAt === null ? "at a time its filename does not record" : `on ${absolute(stash.stashedAt)}`;
|
|
39173
|
+
if (stash.side === "cloud") {
|
|
39174
|
+
const noun = entityNoun(stash.label);
|
|
39175
|
+
return [
|
|
39176
|
+
`## ${stash.label}`,
|
|
39177
|
+
"",
|
|
39178
|
+
`Set aside ${when}.`,
|
|
39179
|
+
"",
|
|
39180
|
+
"**This one is the cloud's copy, not yours.** This " + noun + " changed in",
|
|
39181
|
+
"two places at once and Plinth merged it field by field: your writing won",
|
|
39182
|
+
"and is in your file now, the cloud's other fields won, and what is set",
|
|
39183
|
+
"aside below is the version your writing replaced. That is someone else's",
|
|
39184
|
+
"work, and it is nowhere else.",
|
|
39185
|
+
"",
|
|
39186
|
+
"It is at:",
|
|
39187
|
+
"",
|
|
39188
|
+
"```",
|
|
39189
|
+
stash.absPath,
|
|
39190
|
+
"```",
|
|
39191
|
+
"",
|
|
39192
|
+
`Read it, fold anything worth keeping into the ${noun}, then delete the stash.`
|
|
39193
|
+
].join(`
|
|
39194
|
+
`);
|
|
39195
|
+
}
|
|
38449
39196
|
const out = [`## ${stash.label}`, "", `Set aside ${when}.`, ""];
|
|
38450
39197
|
if (row.kind !== "restorable") {
|
|
38451
39198
|
out.push("Your version is at:", "", "```", stash.absPath, "```", "");
|
|
@@ -38471,15 +39218,24 @@ function renderConflictsView(rows) {
|
|
|
38471
39218
|
${FOOTER}
|
|
38472
39219
|
`;
|
|
38473
39220
|
const n = rows.length;
|
|
39221
|
+
const cloudSide = rows.filter((r) => r.stash.side === "cloud").length;
|
|
39222
|
+
const localSide = n - cloudSide;
|
|
38474
39223
|
const head = [
|
|
38475
39224
|
"# Conflicts",
|
|
38476
39225
|
"",
|
|
38477
|
-
`**${n}
|
|
38478
|
-
`
|
|
39226
|
+
`**${n} ${n === 1 ? "copy was" : "copies were"} set aside when the same thing changed in two places at once.**`,
|
|
39227
|
+
`Nothing here is lost. ${n === 1 ? "It is" : "They are"} listed below, each saying which side it holds.`,
|
|
38479
39228
|
"",
|
|
38480
|
-
|
|
38481
|
-
|
|
38482
|
-
|
|
39229
|
+
...localSide > 0 ? [
|
|
39230
|
+
"For most files Plinth keeps the cloud version in your mirror and puts your",
|
|
39231
|
+
"version aside rather than choosing between them."
|
|
39232
|
+
] : [],
|
|
39233
|
+
...localSide > 0 && cloudSide > 0 ? [""] : [],
|
|
39234
|
+
...cloudSide > 0 ? [
|
|
39235
|
+
"Some records are merged field by field instead: your writing wins and the",
|
|
39236
|
+
"cloud's other fields win. There the copy set aside is the CLOUD's, which",
|
|
39237
|
+
"your version replaced. Each of those says so in its own section."
|
|
39238
|
+
] : []
|
|
38483
39239
|
].join(`
|
|
38484
39240
|
`);
|
|
38485
39241
|
return `${head}
|
|
@@ -38882,7 +39638,7 @@ async function applyFileRow(args) {
|
|
|
38882
39638
|
try {
|
|
38883
39639
|
const localBytes = readFileSync22(abs);
|
|
38884
39640
|
if (sha256Bytes(localBytes) !== existing.content_hash) {
|
|
38885
|
-
const stashPath = stashFileConflict({ workspaceSlug, relPath, timestamp: now(), bytes: localBytes });
|
|
39641
|
+
const stashPath = stashFileConflict({ workspaceSlug, relPath, timestamp: now(), bytes: localBytes, side: "local" });
|
|
38886
39642
|
output?.warn(`Conflict on files/${relPath} → stashed local edit to ${stashPath} (cloud-wins on boot).`);
|
|
38887
39643
|
}
|
|
38888
39644
|
} catch {}
|
|
@@ -38988,6 +39744,203 @@ function textIsCurrent(existing, row, workspaceSlug, relPath) {
|
|
|
38988
39744
|
return extractedTextHash(onDisk) === existing.text_hash;
|
|
38989
39745
|
}
|
|
38990
39746
|
|
|
39747
|
+
// src/lib/files/rejection-sync.ts
|
|
39748
|
+
import { readFileSync as readFileSync23, statSync as statSync15 } from "node:fs";
|
|
39749
|
+
|
|
39750
|
+
// src/lib/files/rejections-api.ts
|
|
39751
|
+
var RejectionsFeedSchema = exports_external.object({
|
|
39752
|
+
rejections: exports_external.array(TeamFileRejectionSchema)
|
|
39753
|
+
});
|
|
39754
|
+
|
|
39755
|
+
class RejectionFeedShapeError extends Error {
|
|
39756
|
+
constructor(message) {
|
|
39757
|
+
super(message);
|
|
39758
|
+
this.name = "RejectionFeedShapeError";
|
|
39759
|
+
}
|
|
39760
|
+
}
|
|
39761
|
+
var WithdrawResponseSchema = exports_external.object({
|
|
39762
|
+
cleared: exports_external.number().int().nonnegative()
|
|
39763
|
+
});
|
|
39764
|
+
function headers(pat) {
|
|
39765
|
+
return {
|
|
39766
|
+
authorization: `Bearer ${pat}`,
|
|
39767
|
+
"content-type": "application/json"
|
|
39768
|
+
};
|
|
39769
|
+
}
|
|
39770
|
+
function endpoint(baseUrl) {
|
|
39771
|
+
return `${baseUrl.replace(/\/$/, "")}/api/sync/files/rejections`;
|
|
39772
|
+
}
|
|
39773
|
+
async function listFileRejections(args) {
|
|
39774
|
+
const doFetch = args.fetchImpl ?? fetch;
|
|
39775
|
+
const res = await doFetch(endpoint(args.baseUrl), {
|
|
39776
|
+
method: "GET",
|
|
39777
|
+
headers: headers(args.pat)
|
|
39778
|
+
});
|
|
39779
|
+
if (!res.ok) {
|
|
39780
|
+
throw new ApiError(res.status, `file-rejections feed failed with HTTP ${res.status}`);
|
|
39781
|
+
}
|
|
39782
|
+
const parsed = RejectionsFeedSchema.safeParse(await res.json());
|
|
39783
|
+
if (!parsed.success) {
|
|
39784
|
+
throw new RejectionFeedShapeError("file-rejections feed did not match the expected shape");
|
|
39785
|
+
}
|
|
39786
|
+
return parsed.data.rejections;
|
|
39787
|
+
}
|
|
39788
|
+
async function postFileRejection(args) {
|
|
39789
|
+
const doFetch = args.fetchImpl ?? fetch;
|
|
39790
|
+
const res = await doFetch(endpoint(args.baseUrl), {
|
|
39791
|
+
method: "POST",
|
|
39792
|
+
headers: headers(args.pat),
|
|
39793
|
+
body: JSON.stringify({
|
|
39794
|
+
path: args.path,
|
|
39795
|
+
reason: args.reason,
|
|
39796
|
+
detail: args.detail ?? null,
|
|
39797
|
+
size_bytes: args.sizeBytes ?? null,
|
|
39798
|
+
max_bytes: args.maxBytes ?? null
|
|
39799
|
+
})
|
|
39800
|
+
});
|
|
39801
|
+
if (!res.ok) {
|
|
39802
|
+
throw new ApiError(res.status, `recording the file rejection failed with HTTP ${res.status}`);
|
|
39803
|
+
}
|
|
39804
|
+
}
|
|
39805
|
+
async function deleteFileRejection(args) {
|
|
39806
|
+
const doFetch = args.fetchImpl ?? fetch;
|
|
39807
|
+
const url2 = `${endpoint(args.baseUrl)}?path=${encodeURIComponent(args.path)}`;
|
|
39808
|
+
const res = await doFetch(url2, {
|
|
39809
|
+
method: "DELETE",
|
|
39810
|
+
headers: headers(args.pat)
|
|
39811
|
+
});
|
|
39812
|
+
if (!res.ok) {
|
|
39813
|
+
throw new ApiError(res.status, `withdrawing the file rejection failed with HTTP ${res.status}`);
|
|
39814
|
+
}
|
|
39815
|
+
const parsed = WithdrawResponseSchema.safeParse(await res.json());
|
|
39816
|
+
return parsed.success ? parsed.data.cleared : 0;
|
|
39817
|
+
}
|
|
39818
|
+
|
|
39819
|
+
// src/lib/files/rejection-sync.ts
|
|
39820
|
+
var TEAM_VISIBLE_REJECT_REASONS = new Set([
|
|
39821
|
+
"over_cap",
|
|
39822
|
+
"bad_request",
|
|
39823
|
+
"invalid_frontmatter"
|
|
39824
|
+
]);
|
|
39825
|
+
async function drainFileRejectionReports(args) {
|
|
39826
|
+
let state = args.state;
|
|
39827
|
+
const pending = listRejected(state).filter((r) => r.target === "file" && !r.reported_at && TEAM_VISIBLE_REJECT_REASONS.has(r.reason));
|
|
39828
|
+
for (const r of pending) {
|
|
39829
|
+
try {
|
|
39830
|
+
await postFileRejection({
|
|
39831
|
+
baseUrl: args.baseUrl,
|
|
39832
|
+
pat: args.pat,
|
|
39833
|
+
path: r.path,
|
|
39834
|
+
reason: r.reason,
|
|
39835
|
+
detail: r.detail ?? r.message ?? null,
|
|
39836
|
+
sizeBytes: r.size_bytes ?? null,
|
|
39837
|
+
maxBytes: r.max_bytes ?? null,
|
|
39838
|
+
fetchImpl: args.fetchImpl
|
|
39839
|
+
});
|
|
39840
|
+
const next = markRejectedReported(state, r.path, args.now().toISOString());
|
|
39841
|
+
if (next.changed)
|
|
39842
|
+
state = commitState(state, next.state);
|
|
39843
|
+
} catch (err) {
|
|
39844
|
+
args.onError?.(r.path, err);
|
|
39845
|
+
}
|
|
39846
|
+
}
|
|
39847
|
+
return state;
|
|
39848
|
+
}
|
|
39849
|
+
async function fetchTeamRejections(args) {
|
|
39850
|
+
const rows = await listFileRejections({
|
|
39851
|
+
baseUrl: args.baseUrl,
|
|
39852
|
+
pat: args.pat,
|
|
39853
|
+
fetchImpl: args.fetchImpl
|
|
39854
|
+
});
|
|
39855
|
+
const next = setTeamRejected(args.state, rows, args.now().toISOString());
|
|
39856
|
+
return next.changed ? commitState(args.state, next.state) : args.state;
|
|
39857
|
+
}
|
|
39858
|
+
function settledDisposition(args) {
|
|
39859
|
+
const entry2 = getFileEntry(args.state, args.relPath);
|
|
39860
|
+
let absolute2;
|
|
39861
|
+
try {
|
|
39862
|
+
absolute2 = filesSubtreePath(args.workspaceSlug, args.relPath);
|
|
39863
|
+
} catch {
|
|
39864
|
+
return null;
|
|
39865
|
+
}
|
|
39866
|
+
let size = null;
|
|
39867
|
+
try {
|
|
39868
|
+
const st = statSync15(absolute2);
|
|
39869
|
+
size = st.isFile() ? st.size : null;
|
|
39870
|
+
} catch {
|
|
39871
|
+
size = null;
|
|
39872
|
+
}
|
|
39873
|
+
if (size === null)
|
|
39874
|
+
return entry2 === undefined ? "gone" : null;
|
|
39875
|
+
if (entry2 === undefined)
|
|
39876
|
+
return null;
|
|
39877
|
+
if (size !== entry2.size_bytes)
|
|
39878
|
+
return null;
|
|
39879
|
+
let bytes;
|
|
39880
|
+
try {
|
|
39881
|
+
bytes = readFileSync23(absolute2);
|
|
39882
|
+
} catch {
|
|
39883
|
+
return null;
|
|
39884
|
+
}
|
|
39885
|
+
return sha256Bytes(bytes) === entry2.content_hash ? "synced" : null;
|
|
39886
|
+
}
|
|
39887
|
+
async function withdrawSettledRejections(args) {
|
|
39888
|
+
let state = args.state;
|
|
39889
|
+
const live = new Set(listTeamRejected(state).map((r) => r.path));
|
|
39890
|
+
const owned = listRejected(state).filter((r) => r.target === "file" && r.reported_at !== undefined);
|
|
39891
|
+
const withdrawn = new Set;
|
|
39892
|
+
for (const record2 of owned) {
|
|
39893
|
+
if (!live.has(record2.path))
|
|
39894
|
+
continue;
|
|
39895
|
+
const disposition = settledDisposition({
|
|
39896
|
+
state,
|
|
39897
|
+
workspaceSlug: args.workspaceSlug,
|
|
39898
|
+
relPath: record2.path
|
|
39899
|
+
});
|
|
39900
|
+
if (disposition === null)
|
|
39901
|
+
continue;
|
|
39902
|
+
try {
|
|
39903
|
+
await deleteFileRejection({
|
|
39904
|
+
baseUrl: args.baseUrl,
|
|
39905
|
+
pat: args.pat,
|
|
39906
|
+
path: record2.path,
|
|
39907
|
+
fetchImpl: args.fetchImpl
|
|
39908
|
+
});
|
|
39909
|
+
withdrawn.add(record2.path);
|
|
39910
|
+
const cleared = removeRejected(state, "file", record2.path);
|
|
39911
|
+
if (cleared.changed)
|
|
39912
|
+
state = commitState(state, cleared.state);
|
|
39913
|
+
args.onWithdrawn?.(record2.path, disposition);
|
|
39914
|
+
} catch (err) {
|
|
39915
|
+
args.onError?.(record2.path, err);
|
|
39916
|
+
}
|
|
39917
|
+
}
|
|
39918
|
+
if (withdrawn.size > 0) {
|
|
39919
|
+
const fetchedAt = state.team_rejected?.fetched_at;
|
|
39920
|
+
if (fetchedAt) {
|
|
39921
|
+
const kept = listTeamRejected(state).filter((r) => !withdrawn.has(r.path));
|
|
39922
|
+
const pruned = setTeamRejected(state, kept, fetchedAt);
|
|
39923
|
+
if (pruned.changed)
|
|
39924
|
+
state = commitState(state, pruned.state);
|
|
39925
|
+
}
|
|
39926
|
+
}
|
|
39927
|
+
return state;
|
|
39928
|
+
}
|
|
39929
|
+
function rearmMissingReports(state) {
|
|
39930
|
+
const live = new Set(listTeamRejected(state).map((r) => r.path));
|
|
39931
|
+
let next = state;
|
|
39932
|
+
for (const r of listRejected(state)) {
|
|
39933
|
+
if (r.reported_at === undefined)
|
|
39934
|
+
continue;
|
|
39935
|
+
if (live.has(r.path))
|
|
39936
|
+
continue;
|
|
39937
|
+
const cleared = clearRejectedReported(next, r.path);
|
|
39938
|
+
if (cleared.changed)
|
|
39939
|
+
next = commitState(next, cleared.state);
|
|
39940
|
+
}
|
|
39941
|
+
return next;
|
|
39942
|
+
}
|
|
39943
|
+
|
|
38991
39944
|
// src/commands/sync.ts
|
|
38992
39945
|
init_output();
|
|
38993
39946
|
var memberRefreshFailedAt = new Map;
|
|
@@ -39041,7 +39994,7 @@ function unlinkIfProvablyOurs(workspaceSlug, strategy, entry2, suppressor) {
|
|
|
39041
39994
|
}
|
|
39042
39995
|
let raw;
|
|
39043
39996
|
try {
|
|
39044
|
-
raw =
|
|
39997
|
+
raw = readFileSync24(target, "utf8");
|
|
39045
39998
|
} catch (err) {
|
|
39046
39999
|
if (err?.code === "ENOENT") {
|
|
39047
40000
|
return { didUnlink: false };
|
|
@@ -39115,6 +40068,66 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
39115
40068
|
}));
|
|
39116
40069
|
out.warn(`Conflict list could not be written to ${CONFLICTS_VIEW_REL_PATH}: ${res.failed}`);
|
|
39117
40070
|
};
|
|
40071
|
+
const syncFileRejections = async () => {
|
|
40072
|
+
const logFailure = (verb) => (path5, err) => {
|
|
40073
|
+
if (err instanceof RejectionFeedShapeError) {
|
|
40074
|
+
out.log(fileRejectionSyncFailedLine({ verb, path: path5, reason: "shape" }));
|
|
40075
|
+
return;
|
|
40076
|
+
}
|
|
40077
|
+
const cat = categorizeError(err);
|
|
40078
|
+
out.log(fileRejectionSyncFailedLine({
|
|
40079
|
+
verb,
|
|
40080
|
+
path: path5,
|
|
40081
|
+
reason: cat.reason,
|
|
40082
|
+
status: cat.status
|
|
40083
|
+
}));
|
|
40084
|
+
};
|
|
40085
|
+
let feedRead = false;
|
|
40086
|
+
try {
|
|
40087
|
+
await fetchTeamRejections({
|
|
40088
|
+
state: loadState(slug),
|
|
40089
|
+
baseUrl: entry2.api_base_url,
|
|
40090
|
+
pat,
|
|
40091
|
+
now,
|
|
40092
|
+
fetchImpl
|
|
40093
|
+
});
|
|
40094
|
+
feedRead = true;
|
|
40095
|
+
} catch (err) {
|
|
40096
|
+
logFailure("feed")(undefined, err);
|
|
40097
|
+
}
|
|
40098
|
+
if (feedRead) {
|
|
40099
|
+
try {
|
|
40100
|
+
rearmMissingReports(loadState(slug));
|
|
40101
|
+
} catch (err) {
|
|
40102
|
+
logFailure("report")(undefined, err);
|
|
40103
|
+
}
|
|
40104
|
+
}
|
|
40105
|
+
try {
|
|
40106
|
+
await drainFileRejectionReports({
|
|
40107
|
+
state: loadState(slug),
|
|
40108
|
+
baseUrl: entry2.api_base_url,
|
|
40109
|
+
pat,
|
|
40110
|
+
now,
|
|
40111
|
+
fetchImpl,
|
|
40112
|
+
onError: (path5, err) => logFailure("report")(path5, err)
|
|
40113
|
+
});
|
|
40114
|
+
} catch (err) {
|
|
40115
|
+
logFailure("report")(undefined, err);
|
|
40116
|
+
}
|
|
40117
|
+
try {
|
|
40118
|
+
await withdrawSettledRejections({
|
|
40119
|
+
state: loadState(slug),
|
|
40120
|
+
workspaceSlug: slug,
|
|
40121
|
+
baseUrl: entry2.api_base_url,
|
|
40122
|
+
pat,
|
|
40123
|
+
fetchImpl,
|
|
40124
|
+
onError: (path5, err) => logFailure("withdraw")(path5, err),
|
|
40125
|
+
onWithdrawn: (path5, disposition) => out.log(fileRejectionWithdrawnLine({ path: path5, disposition }))
|
|
40126
|
+
});
|
|
40127
|
+
} catch (err) {
|
|
40128
|
+
logFailure("withdraw")(undefined, err);
|
|
40129
|
+
}
|
|
40130
|
+
};
|
|
39118
40131
|
refreshBanner();
|
|
39119
40132
|
out.info(`Syncing ${slug}…`);
|
|
39120
40133
|
const startedAt = now();
|
|
@@ -39330,6 +40343,22 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
39330
40343
|
const msg = err instanceof Error ? err.message : "unknown error";
|
|
39331
40344
|
out.warn(`Context generation skipped: ${msg}`);
|
|
39332
40345
|
}
|
|
40346
|
+
try {
|
|
40347
|
+
const measureSizes = deps.measureSizes ?? measureBarredFiles;
|
|
40348
|
+
const beforeAnchor = loadState(slug);
|
|
40349
|
+
if (beforeAnchor.size_anchor === undefined) {
|
|
40350
|
+
const seeded = setSizeAnchor(beforeAnchor, {
|
|
40351
|
+
at: now().toISOString(),
|
|
40352
|
+
kind: "sync",
|
|
40353
|
+
sizes: measureSizes(slug).sizes
|
|
40354
|
+
});
|
|
40355
|
+
if (seeded.changed)
|
|
40356
|
+
commitState(beforeAnchor, seeded.state);
|
|
40357
|
+
}
|
|
40358
|
+
} catch (err) {
|
|
40359
|
+
const msg = err instanceof Error ? err.message : "unknown error";
|
|
40360
|
+
out.warn(`Could not record the file-size anchor: ${msg}`);
|
|
40361
|
+
}
|
|
39333
40362
|
const lastFailure = memberRefreshFailedAt.get(slug);
|
|
39334
40363
|
const failureCooling = lastFailure !== undefined && now().getTime() - lastFailure < MEMBER_REFRESH_RETRY_AFTER_MS;
|
|
39335
40364
|
if (!failureCooling && memberNeedsRefresh(entry2, now)) {
|
|
@@ -39349,6 +40378,7 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
39349
40378
|
out.warn(`Per-artist context generation skipped: ${msg}`);
|
|
39350
40379
|
}
|
|
39351
40380
|
refreshBanner();
|
|
40381
|
+
await syncFileRejections();
|
|
39352
40382
|
const totalWritten = Object.values(counters).reduce((sum, c) => sum + c.written, 0);
|
|
39353
40383
|
const totalDeleted = Object.values(counters).reduce((sum, c) => sum + c.deleted, 0);
|
|
39354
40384
|
if (totalWritten === 0 && totalDeleted === 0) {
|
|
@@ -39446,7 +40476,7 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
|
|
|
39446
40476
|
if (!suppressor && existingEntry && existingEntry.updated_at !== live.updated_at) {
|
|
39447
40477
|
let localRaw = null;
|
|
39448
40478
|
try {
|
|
39449
|
-
localRaw =
|
|
40479
|
+
localRaw = readFileSync24(entityFilePath(workspaceSlug, strategy.type, live.slug), "utf8");
|
|
39450
40480
|
} catch {
|
|
39451
40481
|
localRaw = null;
|
|
39452
40482
|
}
|
|
@@ -39459,7 +40489,8 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
|
|
|
39459
40489
|
entityType: strategy.type,
|
|
39460
40490
|
entitySlug: live.slug,
|
|
39461
40491
|
timestamp: now(),
|
|
39462
|
-
contents: localRaw
|
|
40492
|
+
contents: localRaw,
|
|
40493
|
+
side: "local"
|
|
39463
40494
|
});
|
|
39464
40495
|
out.warn(`Conflict on ${strategy.type}/${live.slug} → stashed local edit to ${stashPath} (cloud-wins on boot).`);
|
|
39465
40496
|
}
|
|
@@ -39469,7 +40500,7 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
|
|
|
39469
40500
|
const hashes = computeMirrorHashes(strategy, content);
|
|
39470
40501
|
let inode;
|
|
39471
40502
|
try {
|
|
39472
|
-
inode =
|
|
40503
|
+
inode = statSync16(entityFilePath(workspaceSlug, strategy.type, live.slug)).ino;
|
|
39473
40504
|
} catch {
|
|
39474
40505
|
inode = undefined;
|
|
39475
40506
|
}
|
|
@@ -39562,7 +40593,7 @@ var syncCommand = defineCommand({
|
|
|
39562
40593
|
// src/commands/start.ts
|
|
39563
40594
|
var SHUTDOWN_DRAIN_TIMEOUT_MS = 5000;
|
|
39564
40595
|
var PULL_HEARTBEAT_MS = 900000;
|
|
39565
|
-
function backfillStateInodes(workspaceSlug, state, statFn = (p) =>
|
|
40596
|
+
function backfillStateInodes(workspaceSlug, state, statFn = (p) => statSync17(p)) {
|
|
39566
40597
|
let changed = false;
|
|
39567
40598
|
const nextEntries = {};
|
|
39568
40599
|
for (const [entityType, bucket] of Object.entries(state.entries)) {
|
|
@@ -39595,7 +40626,7 @@ function backfillStateInodes(workspaceSlug, state, statFn = (p) => statSync16(p)
|
|
|
39595
40626
|
return { state, changed: false };
|
|
39596
40627
|
return { state: { ...state, entries: nextEntries }, changed: true };
|
|
39597
40628
|
}
|
|
39598
|
-
function backfillBodyLengths(workspaceSlug, state, readFn = (p) =>
|
|
40629
|
+
function backfillBodyLengths(workspaceSlug, state, readFn = (p) => readFileSync25(p, "utf8")) {
|
|
39599
40630
|
let changed = false;
|
|
39600
40631
|
const nextEntries = {};
|
|
39601
40632
|
for (const [entityType, bucket] of Object.entries(state.entries)) {
|
|
@@ -39682,14 +40713,14 @@ function makeWatchHandler(deps) {
|
|
|
39682
40713
|
state: getState(),
|
|
39683
40714
|
deps: { fetchImpl, now, output, suppressor }
|
|
39684
40715
|
});
|
|
39685
|
-
setState(
|
|
40716
|
+
setState(clearReportedSafeFileRejected(result.state, relPath));
|
|
39686
40717
|
reportFileOutcome(spinner, workspaceSlug, getState(), result.outcome);
|
|
39687
40718
|
});
|
|
39688
40719
|
return;
|
|
39689
40720
|
}
|
|
39690
40721
|
let st;
|
|
39691
40722
|
try {
|
|
39692
|
-
st =
|
|
40723
|
+
st = statSync17(absolutePath);
|
|
39693
40724
|
} catch (err) {
|
|
39694
40725
|
if (err?.code === "ENOENT")
|
|
39695
40726
|
return;
|
|
@@ -39720,7 +40751,7 @@ function makeWatchHandler(deps) {
|
|
|
39720
40751
|
spinner.update(`Watching ${workspaceSlug} · ${watchingSummary(getState())}`);
|
|
39721
40752
|
return;
|
|
39722
40753
|
}
|
|
39723
|
-
const bytes =
|
|
40754
|
+
const bytes = readFileSync25(absolutePath);
|
|
39724
40755
|
const hash2 = sha256Bytes(bytes);
|
|
39725
40756
|
if (suppressor?.shouldSuppressUpsert(absolutePath, {
|
|
39726
40757
|
bodyHash: hash2,
|
|
@@ -39778,7 +40809,7 @@ function makeWatchHandler(deps) {
|
|
|
39778
40809
|
if (suppressor) {
|
|
39779
40810
|
let raw = null;
|
|
39780
40811
|
try {
|
|
39781
|
-
raw =
|
|
40812
|
+
raw = readFileSync25(absolutePath, "utf8");
|
|
39782
40813
|
} catch {
|
|
39783
40814
|
raw = null;
|
|
39784
40815
|
}
|
|
@@ -39816,7 +40847,7 @@ function makeWatchHandler(deps) {
|
|
|
39816
40847
|
if (suppressor) {
|
|
39817
40848
|
let raw = null;
|
|
39818
40849
|
try {
|
|
39819
|
-
raw =
|
|
40850
|
+
raw = readFileSync25(absolutePath, "utf8");
|
|
39820
40851
|
} catch {
|
|
39821
40852
|
raw = null;
|
|
39822
40853
|
}
|
|
@@ -39854,7 +40885,7 @@ function makeWatchHandler(deps) {
|
|
|
39854
40885
|
if (suppressor) {
|
|
39855
40886
|
let raw = null;
|
|
39856
40887
|
try {
|
|
39857
|
-
raw =
|
|
40888
|
+
raw = readFileSync25(absolutePath, "utf8");
|
|
39858
40889
|
} catch {
|
|
39859
40890
|
raw = null;
|
|
39860
40891
|
}
|
|
@@ -39892,7 +40923,7 @@ function makeWatchHandler(deps) {
|
|
|
39892
40923
|
if (suppressor) {
|
|
39893
40924
|
let raw = null;
|
|
39894
40925
|
try {
|
|
39895
|
-
raw =
|
|
40926
|
+
raw = readFileSync25(absolutePath, "utf8");
|
|
39896
40927
|
} catch {
|
|
39897
40928
|
raw = null;
|
|
39898
40929
|
}
|
|
@@ -39930,7 +40961,7 @@ function makeWatchHandler(deps) {
|
|
|
39930
40961
|
if (suppressor) {
|
|
39931
40962
|
let raw = null;
|
|
39932
40963
|
try {
|
|
39933
|
-
raw =
|
|
40964
|
+
raw = readFileSync25(absolutePath, "utf8");
|
|
39934
40965
|
} catch {
|
|
39935
40966
|
raw = null;
|
|
39936
40967
|
}
|
|
@@ -40045,7 +41076,7 @@ function makeWatchHandler(deps) {
|
|
|
40045
41076
|
if (strategy) {
|
|
40046
41077
|
let raw = null;
|
|
40047
41078
|
try {
|
|
40048
|
-
raw =
|
|
41079
|
+
raw = readFileSync25(absolutePath, "utf8");
|
|
40049
41080
|
} catch {
|
|
40050
41081
|
raw = null;
|
|
40051
41082
|
}
|
|
@@ -40305,21 +41336,9 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
40305
41336
|
if (backfillWorthReporting(idBackfill)) {
|
|
40306
41337
|
daemonLog.log(frontmatterIdBackfillLine(idBackfill));
|
|
40307
41338
|
}
|
|
40308
|
-
|
|
40309
|
-
|
|
40310
|
-
|
|
40311
|
-
parts.push(`${recon.pushed} pushed`);
|
|
40312
|
-
if (recon.conflicts > 0)
|
|
40313
|
-
parts.push(`${recon.conflicts} stashed to ~/.plinth/_conflicts/`);
|
|
40314
|
-
if (recon.syncErrors > 0)
|
|
40315
|
-
parts.push(`${recon.syncErrors} with sync errors (see # sync-error in the file)`);
|
|
40316
|
-
if (recon.quarantined > 0)
|
|
40317
|
-
parts.push(`${recon.quarantined} held for \`plinth confirm\``);
|
|
40318
|
-
if (recon.failed > 0)
|
|
40319
|
-
parts.push(`${recon.failed} deferred`);
|
|
40320
|
-
const acted = recon.diverged + recon.quarantined;
|
|
40321
|
-
out.info(`Reconciled ${acted} offline edit${acted === 1 ? "" : "s"}${parts.length ? `: ${parts.join(", ")}` : ""}.`);
|
|
40322
|
-
}
|
|
41339
|
+
const reconLine = reconcileSummaryLine(recon);
|
|
41340
|
+
if (reconLine !== null)
|
|
41341
|
+
out.info(reconLine);
|
|
40323
41342
|
const disc = recon.discovery;
|
|
40324
41343
|
if (disc.created > 0 || disc.renamed > 0 || disc.collisions > 0 || disc.renameConflicts > 0) {
|
|
40325
41344
|
const parts = [];
|
|
@@ -40987,11 +42006,34 @@ function watchingSummary(state) {
|
|
|
40987
42006
|
const parts = types.map((t) => `${counts[t] ?? 0} ${t}`);
|
|
40988
42007
|
return parts.join(" · ");
|
|
40989
42008
|
}
|
|
42009
|
+
function reconcileSummaryLine(recon) {
|
|
42010
|
+
if (recon.diverged === 0 && recon.quarantined === 0)
|
|
42011
|
+
return null;
|
|
42012
|
+
const parts = [];
|
|
42013
|
+
if (recon.pushed > 0)
|
|
42014
|
+
parts.push(`${recon.pushed} pushed`);
|
|
42015
|
+
if (recon.merged > 0)
|
|
42016
|
+
parts.push(`${recon.merged} merged field by field (the cloud's description stashed to ~/.plinth/_conflicts/)`);
|
|
42017
|
+
if (recon.conflicts > 0)
|
|
42018
|
+
parts.push(`${recon.conflicts} stashed to ~/.plinth/_conflicts/`);
|
|
42019
|
+
if (recon.syncErrors > 0)
|
|
42020
|
+
parts.push(`${recon.syncErrors} with sync errors (see # sync-error in the file)`);
|
|
42021
|
+
if (recon.quarantined > 0)
|
|
42022
|
+
parts.push(`${recon.quarantined} held for \`plinth confirm\``);
|
|
42023
|
+
if (recon.failed > 0)
|
|
42024
|
+
parts.push(`${recon.failed} deferred`);
|
|
42025
|
+
const acted = recon.diverged + recon.quarantined;
|
|
42026
|
+
return `Reconciled ${acted} offline edit${acted === 1 ? "" : "s"}${parts.length ? `: ${parts.join(", ")}` : ""}.`;
|
|
42027
|
+
}
|
|
40990
42028
|
function reportOutcome(spinner, slug, state, outcome) {
|
|
40991
42029
|
const summary = watchingSummary(state);
|
|
40992
42030
|
switch (outcome.kind) {
|
|
40993
42031
|
case "pushed":
|
|
40994
|
-
|
|
42032
|
+
if (outcome.stashPath) {
|
|
42033
|
+
spinner.warn(`Merged ${outcome.entityType}/${outcome.entitySlug} field by field → the cloud's copy is at ${outcome.stashPath}`);
|
|
42034
|
+
} else {
|
|
42035
|
+
spinner.success(`Pushed ${outcome.entityType}/${outcome.entitySlug}`);
|
|
42036
|
+
}
|
|
40995
42037
|
spinner.update(`Watching ${slug} · ${summary}`);
|
|
40996
42038
|
return;
|
|
40997
42039
|
case "created":
|
|
@@ -41186,101 +42228,11 @@ var startCommand = defineCommand({
|
|
|
41186
42228
|
});
|
|
41187
42229
|
|
|
41188
42230
|
// src/commands/status.ts
|
|
41189
|
-
import { statSync as
|
|
41190
|
-
|
|
41191
|
-
// src/lib/context/memory-bars.ts
|
|
41192
|
-
var RULE_FILE_BAR_BYTES = 6000;
|
|
41193
|
-
var FACT_STORE_BAR_BYTES = 12000;
|
|
41194
|
-
var MAX_NAMED_FACT_STORES = 5;
|
|
41195
|
-
function countRuleShapedLines(body) {
|
|
41196
|
-
let count = 0;
|
|
41197
|
-
for (const line of body.split(`
|
|
41198
|
-
`)) {
|
|
41199
|
-
const trimmed = line.trim();
|
|
41200
|
-
if (/^[-*+]\s/.test(trimmed))
|
|
41201
|
-
count += 1;
|
|
41202
|
-
else if (/^\*\*[^*]+\*\*/.test(trimmed))
|
|
41203
|
-
count += 1;
|
|
41204
|
-
}
|
|
41205
|
-
return count;
|
|
41206
|
-
}
|
|
41207
|
-
function factBody(raw) {
|
|
41208
|
-
return raw.replace(/<!--[\s\S]*?-->/g, "").replace(/<!--[\s\S]*$/, "").split(`
|
|
41209
|
-
`).filter((line) => !/^\s*#{1,6}\s/.test(line)).join(`
|
|
41210
|
-
`).trim();
|
|
41211
|
-
}
|
|
41212
|
-
var RULE_FILES = [
|
|
41213
|
-
{ filename: VOICE_FILENAME, agentMayEdit: false, memberOwned: true, ridesInjection: false },
|
|
41214
|
-
{ filename: WORKSPACE_RULES_FILENAME, agentMayEdit: true, memberOwned: false, ridesInjection: true }
|
|
41215
|
-
];
|
|
41216
|
-
function assessMemoryBars(workspaceSlug, deps = {}) {
|
|
41217
|
-
const readContext = deps.readContext ?? readContextFile;
|
|
41218
|
-
const readArtistMemory = deps.readArtistMemory ?? readArtistMemoryFile;
|
|
41219
|
-
const rosterOf = deps.readRosterFn ?? readRoster;
|
|
41220
|
-
const over = [];
|
|
41221
|
-
for (const spec of RULE_FILES) {
|
|
41222
|
-
let raw = null;
|
|
41223
|
-
try {
|
|
41224
|
-
raw = readContext(workspaceSlug, spec.filename);
|
|
41225
|
-
} catch {
|
|
41226
|
-
raw = null;
|
|
41227
|
-
}
|
|
41228
|
-
if (raw === null)
|
|
41229
|
-
continue;
|
|
41230
|
-
const bytes = Buffer.byteLength(raw);
|
|
41231
|
-
if (bytes <= RULE_FILE_BAR_BYTES)
|
|
41232
|
-
continue;
|
|
41233
|
-
over.push({
|
|
41234
|
-
path: spec.filename,
|
|
41235
|
-
kind: "rule",
|
|
41236
|
-
bytes,
|
|
41237
|
-
bar: RULE_FILE_BAR_BYTES,
|
|
41238
|
-
rules: countRuleShapedLines(raw),
|
|
41239
|
-
facts: groupFacts(factBody(raw)).length,
|
|
41240
|
-
agentMayEdit: spec.agentMayEdit,
|
|
41241
|
-
memberOwned: spec.memberOwned,
|
|
41242
|
-
buys: !spec.ridesInjection ? "per-read" : Buffer.byteLength(extractWorkspaceRules(raw) ?? "") > 0 ? "injection" : "nothing"
|
|
41243
|
-
});
|
|
41244
|
-
}
|
|
41245
|
-
let roster;
|
|
41246
|
-
try {
|
|
41247
|
-
roster = rosterOf(workspaceSlug);
|
|
41248
|
-
} catch {
|
|
41249
|
-
roster = [];
|
|
41250
|
-
}
|
|
41251
|
-
const stores = [];
|
|
41252
|
-
for (const artist of roster) {
|
|
41253
|
-
let raw = null;
|
|
41254
|
-
try {
|
|
41255
|
-
raw = readArtistMemory(workspaceSlug, artist.slug);
|
|
41256
|
-
} catch {
|
|
41257
|
-
raw = null;
|
|
41258
|
-
}
|
|
41259
|
-
if (raw === null)
|
|
41260
|
-
continue;
|
|
41261
|
-
const bytes = Buffer.byteLength(raw);
|
|
41262
|
-
if (bytes <= FACT_STORE_BAR_BYTES)
|
|
41263
|
-
continue;
|
|
41264
|
-
stores.push({
|
|
41265
|
-
path: `artists/${artist.slug}/memory.md`,
|
|
41266
|
-
kind: "fact",
|
|
41267
|
-
bytes,
|
|
41268
|
-
bar: FACT_STORE_BAR_BYTES,
|
|
41269
|
-
rules: countRuleShapedLines(raw),
|
|
41270
|
-
facts: groupFacts(factBody(raw)).length,
|
|
41271
|
-
agentMayEdit: true,
|
|
41272
|
-
memberOwned: true,
|
|
41273
|
-
buys: "per-read"
|
|
41274
|
-
});
|
|
41275
|
-
}
|
|
41276
|
-
stores.sort((a, b) => b.bytes - a.bytes || a.path.localeCompare(b.path));
|
|
41277
|
-
const named = stores.slice(0, MAX_NAMED_FACT_STORES);
|
|
41278
|
-
return { over: [...over, ...named], notNamed: stores.length - named.length };
|
|
41279
|
-
}
|
|
42231
|
+
import { statSync as statSync19 } from "node:fs";
|
|
41280
42232
|
|
|
41281
42233
|
// src/lib/build-stamp.ts
|
|
41282
42234
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
41283
|
-
import { existsSync as existsSync9, mkdirSync as mkdirSync11, readFileSync as
|
|
42235
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync11, readFileSync as readFileSync26, writeFileSync as writeFileSync8 } from "node:fs";
|
|
41284
42236
|
import { dirname as dirname10, join as join17 } from "node:path";
|
|
41285
42237
|
import { fileURLToPath } from "node:url";
|
|
41286
42238
|
var BUILD_STAMP_FILENAME = "build-stamp.json";
|
|
@@ -41304,7 +42256,7 @@ function readHead(repoDir) {
|
|
|
41304
42256
|
}
|
|
41305
42257
|
function readBuildStamp(distDir) {
|
|
41306
42258
|
try {
|
|
41307
|
-
const parsed = JSON.parse(
|
|
42259
|
+
const parsed = JSON.parse(readFileSync26(stampPath(distDir), "utf8"));
|
|
41308
42260
|
if (parsed && typeof parsed.sha === "string" && parsed.sha.length > 0 && typeof parsed.dirty === "boolean" && typeof parsed.built_at === "string") {
|
|
41309
42261
|
return { sha: parsed.sha, dirty: parsed.dirty, built_at: parsed.built_at };
|
|
41310
42262
|
}
|
|
@@ -41365,12 +42317,12 @@ function checkBuildStaleness(deps = {}) {
|
|
|
41365
42317
|
}
|
|
41366
42318
|
|
|
41367
42319
|
// src/lib/memory-links.ts
|
|
41368
|
-
import { readFileSync as
|
|
42320
|
+
import { readFileSync as readFileSync27 } from "node:fs";
|
|
41369
42321
|
import { relative as relative13, sep as sep5 } from "node:path";
|
|
41370
42322
|
var MEMORY_FILE_RE = /^(workspace-memory\.md|artists\/[^/]+\/memory\.md)$/i;
|
|
41371
42323
|
function scanMemoryLinks(deps) {
|
|
41372
42324
|
const listFiles = deps.listFiles ?? ((r) => walkSubtreeFiles(r, isVcsInternal));
|
|
41373
|
-
const readFile = deps.readFile ?? ((p) =>
|
|
42325
|
+
const readFile = deps.readFile ?? ((p) => readFileSync27(p, "utf8"));
|
|
41374
42326
|
let root;
|
|
41375
42327
|
let absPaths;
|
|
41376
42328
|
try {
|
|
@@ -41414,7 +42366,7 @@ function scanMemoryLinks(deps) {
|
|
|
41414
42366
|
init_output();
|
|
41415
42367
|
|
|
41416
42368
|
// src/lib/pending-writes.ts
|
|
41417
|
-
import { readFileSync as
|
|
42369
|
+
import { readFileSync as readFileSync28, statSync as statSync18 } from "node:fs";
|
|
41418
42370
|
import { relative as relative14 } from "node:path";
|
|
41419
42371
|
var import_gray_matter14 = __toESM(require_gray_matter(), 1);
|
|
41420
42372
|
var EMPTY2 = { writes: [], count: 0 };
|
|
@@ -41454,7 +42406,7 @@ function scanPendingWrites(args) {
|
|
|
41454
42406
|
const entry3 = getFileEntry(state, fileMatch.relPath);
|
|
41455
42407
|
let size;
|
|
41456
42408
|
try {
|
|
41457
|
-
size =
|
|
42409
|
+
size = statSync18(absolutePath).size;
|
|
41458
42410
|
} catch {
|
|
41459
42411
|
continue;
|
|
41460
42412
|
}
|
|
@@ -41462,7 +42414,7 @@ function scanPendingWrites(args) {
|
|
|
41462
42414
|
continue;
|
|
41463
42415
|
let hash2;
|
|
41464
42416
|
try {
|
|
41465
|
-
hash2 = sha256Bytes(
|
|
42417
|
+
hash2 = sha256Bytes(readFileSync28(absolutePath));
|
|
41466
42418
|
} catch {
|
|
41467
42419
|
continue;
|
|
41468
42420
|
}
|
|
@@ -41483,7 +42435,7 @@ function scanPendingWrites(args) {
|
|
|
41483
42435
|
continue;
|
|
41484
42436
|
let liveInode;
|
|
41485
42437
|
try {
|
|
41486
|
-
liveInode =
|
|
42438
|
+
liveInode = statSync18(absolutePath).ino;
|
|
41487
42439
|
} catch {
|
|
41488
42440
|
liveInode = undefined;
|
|
41489
42441
|
}
|
|
@@ -41493,7 +42445,7 @@ function scanPendingWrites(args) {
|
|
|
41493
42445
|
continue;
|
|
41494
42446
|
let content;
|
|
41495
42447
|
try {
|
|
41496
|
-
content =
|
|
42448
|
+
content = readFileSync28(absolutePath, "utf8");
|
|
41497
42449
|
} catch {
|
|
41498
42450
|
continue;
|
|
41499
42451
|
}
|
|
@@ -41628,6 +42580,7 @@ function scanPendingWrites(args) {
|
|
|
41628
42580
|
}
|
|
41629
42581
|
|
|
41630
42582
|
// src/commands/status.ts
|
|
42583
|
+
var MAX_NAMED_GROWTH_FILES = 5;
|
|
41631
42584
|
var LABEL_WIDTH = 9;
|
|
41632
42585
|
function row(label, value) {
|
|
41633
42586
|
return `${label.padEnd(LABEL_WIDTH)} ${value}`;
|
|
@@ -41635,6 +42588,40 @@ function row(label, value) {
|
|
|
41635
42588
|
function cont(value) {
|
|
41636
42589
|
return `${" ".repeat(LABEL_WIDTH)} ${value}`;
|
|
41637
42590
|
}
|
|
42591
|
+
function teamRejectedHeadline(count, state) {
|
|
42592
|
+
const noun = count === 1 ? "file is" : "files are";
|
|
42593
|
+
const at = teamRejectedFetchedAt(state);
|
|
42594
|
+
const when = at ? readStamp(at) : undefined;
|
|
42595
|
+
const freshness = when ? `read from Plinth at the last sync, ${when}` : `read from Plinth at an unrecorded time`;
|
|
42596
|
+
return `${count} more ${noun} also rejected on other machines (${freshness} — start the daemon for a fresher list):`;
|
|
42597
|
+
}
|
|
42598
|
+
function teamRejectedDetail(r) {
|
|
42599
|
+
const why = r.detail ? firstSentence(r.detail) : r.reason;
|
|
42600
|
+
const who = r.reported_by_name ?? "another machine";
|
|
42601
|
+
const since = r.first_seen_at ? shortDate(r.first_seen_at) : undefined;
|
|
42602
|
+
return since ? `${why} — ${who}, first seen ${since}` : `${why} — ${who}`;
|
|
42603
|
+
}
|
|
42604
|
+
function firstSentence(text) {
|
|
42605
|
+
const end = text.indexOf(". ");
|
|
42606
|
+
return end === -1 ? text : text.slice(0, end + 1);
|
|
42607
|
+
}
|
|
42608
|
+
function readStamp(iso, today = new Date) {
|
|
42609
|
+
const d = new Date(iso);
|
|
42610
|
+
if (Number.isNaN(d.getTime()))
|
|
42611
|
+
return;
|
|
42612
|
+
const hhmm = `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
42613
|
+
const day = shortDate(iso);
|
|
42614
|
+
return day === localDate(today) ? hhmm : `${day} ${hhmm}`;
|
|
42615
|
+
}
|
|
42616
|
+
function shortDate(iso) {
|
|
42617
|
+
const d = new Date(iso);
|
|
42618
|
+
if (Number.isNaN(d.getTime()))
|
|
42619
|
+
return;
|
|
42620
|
+
return localDate(d);
|
|
42621
|
+
}
|
|
42622
|
+
function localDate(d) {
|
|
42623
|
+
return `${d.getFullYear()}-` + `${String(d.getMonth() + 1).padStart(2, "0")}-` + `${String(d.getDate()).padStart(2, "0")}`;
|
|
42624
|
+
}
|
|
41638
42625
|
function uncheckedLines(items) {
|
|
41639
42626
|
const n = items.length;
|
|
41640
42627
|
const head = `${n} generated command file${n === 1 ? "" : "s"} could not be checked, ` + `so ${n === 1 ? "its block was" : "their blocks were"} not compared.`;
|
|
@@ -41692,7 +42679,7 @@ function renderBuild(out, staleness) {
|
|
|
41692
42679
|
}
|
|
41693
42680
|
function lastActivity(now) {
|
|
41694
42681
|
try {
|
|
41695
|
-
const mtimeMs =
|
|
42682
|
+
const mtimeMs = statSync19(daemonLogPath()).mtimeMs;
|
|
41696
42683
|
return formatRelative(now.getTime() - mtimeMs);
|
|
41697
42684
|
} catch {
|
|
41698
42685
|
return null;
|
|
@@ -41802,13 +42789,13 @@ async function runStatus(args, deps = {}) {
|
|
|
41802
42789
|
const heldArtists = pending.writes.filter((w) => w.held === "artist");
|
|
41803
42790
|
const heldCloudRows = pending.writes.filter((w) => w.held === "cloud-row");
|
|
41804
42791
|
const heldRenames = pending.writes.filter((w) => w.held === "rename-taken");
|
|
41805
|
-
const
|
|
42792
|
+
const MAX_SHOWN2 = 10;
|
|
41806
42793
|
const listPaths = (writes) => {
|
|
41807
|
-
for (const w of writes.slice(0,
|
|
42794
|
+
for (const w of writes.slice(0, MAX_SHOWN2)) {
|
|
41808
42795
|
out.info(cont(`${w.path} (${w.kind})`));
|
|
41809
42796
|
}
|
|
41810
|
-
if (writes.length >
|
|
41811
|
-
out.info(cont(`…and ${writes.length -
|
|
42797
|
+
if (writes.length > MAX_SHOWN2) {
|
|
42798
|
+
out.info(cont(`…and ${writes.length - MAX_SHOWN2} more.`));
|
|
41812
42799
|
}
|
|
41813
42800
|
};
|
|
41814
42801
|
let labelled = false;
|
|
@@ -41849,11 +42836,16 @@ async function runStatus(args, deps = {}) {
|
|
|
41849
42836
|
}
|
|
41850
42837
|
out.info("");
|
|
41851
42838
|
const rejected = listRejected(localState);
|
|
41852
|
-
|
|
42839
|
+
const localRejectedPaths = new Set(rejected.filter((r) => r.target === "file").map((r) => r.path));
|
|
42840
|
+
const teamRejected = listTeamRejected(localState).filter((r) => !localRejectedPaths.has(r.path));
|
|
42841
|
+
const MAX_SHOWN = 10;
|
|
42842
|
+
if (rejected.length === 0 && teamRejected.length === 0) {
|
|
41853
42843
|
out.info(row("Rejected", "No writes rejected."));
|
|
42844
|
+
} else if (rejected.length === 0) {
|
|
42845
|
+
out.info(row("Rejected", "No writes rejected on this machine."));
|
|
41854
42846
|
} else {
|
|
41855
|
-
|
|
41856
|
-
|
|
42847
|
+
const scope = teamRejected.length > 0 ? " on this machine" : "";
|
|
42848
|
+
out.info(row("Rejected", `${rejected.length} write${rejected.length === 1 ? "" : "s"} rejected${scope} and not retried. Each line says what to do.`));
|
|
41857
42849
|
for (const r of rejected.slice(0, MAX_SHOWN)) {
|
|
41858
42850
|
const why = r.detail ?? r.message ?? r.reason;
|
|
41859
42851
|
out.info(cont(`${r.path} — ${why}`));
|
|
@@ -41862,16 +42854,34 @@ async function runStatus(args, deps = {}) {
|
|
|
41862
42854
|
out.info(cont(`…and ${rejected.length - MAX_SHOWN} more (see ~/.plinth/daemon.log).`));
|
|
41863
42855
|
}
|
|
41864
42856
|
}
|
|
42857
|
+
if (teamRejected.length > 0) {
|
|
42858
|
+
out.info(cont(""));
|
|
42859
|
+
out.info(cont(teamRejectedHeadline(teamRejected.length, localState)));
|
|
42860
|
+
for (const r of teamRejected.slice(0, MAX_SHOWN)) {
|
|
42861
|
+
out.info(cont(`${r.path} — ${teamRejectedDetail(r)}`));
|
|
42862
|
+
}
|
|
42863
|
+
if (teamRejected.length > MAX_SHOWN) {
|
|
42864
|
+
out.info(cont(`…and ${teamRejected.length - MAX_SHOWN} more.`));
|
|
42865
|
+
}
|
|
42866
|
+
}
|
|
41865
42867
|
out.info("");
|
|
41866
42868
|
const stashes = listConflictStashes(slug);
|
|
41867
42869
|
if (stashes.length === 0) {
|
|
41868
42870
|
out.info(row("Conflicts", "No conflict stashes."));
|
|
41869
42871
|
} else {
|
|
41870
|
-
|
|
42872
|
+
const cloudSide = stashes.filter((s) => s.side === "cloud");
|
|
42873
|
+
const localSide = stashes.filter((s) => s.side !== "cloud");
|
|
42874
|
+
out.info(row("Conflicts", `${stashes.length} stashed cop${stashes.length === 1 ? "y" : "ies"} in ${conflictsRoot(slug)}`));
|
|
41871
42875
|
const newest = stashes[0];
|
|
41872
42876
|
const when = newest.stashedAt ? `${formatAbsolute(newest.stashedAt)} (${formatRelative(now.getTime() - newest.stashedAt.getTime())})` : "time unknown";
|
|
41873
42877
|
out.info(cont(`Newest: ${newest.label} — ${when}`));
|
|
41874
|
-
|
|
42878
|
+
const lead = (n) => n === stashes.length ? "Each is" : n === 1 ? "1 is" : `${n} are`;
|
|
42879
|
+
if (localSide.length > 0) {
|
|
42880
|
+
out.info(cont(`${lead(localSide.length)} your version of a file that was replaced by the cloud copy. Compare it with the file in your mirror, re-apply anything you still want, then delete the stash.`));
|
|
42881
|
+
}
|
|
42882
|
+
if (cloudSide.length > 0) {
|
|
42883
|
+
out.info(cont(`${lead(cloudSide.length)} the cloud's copy from a merge that went field by field: your writing won and is in your file, the cloud's other fields won, and the stash is what your writing replaced. Read it, fold in anything you still want, then delete the stash.`));
|
|
42884
|
+
}
|
|
41875
42885
|
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)`));
|
|
41876
42886
|
}
|
|
41877
42887
|
out.info("");
|
|
@@ -41989,6 +42999,13 @@ async function runStatus(args, deps = {}) {
|
|
|
41989
42999
|
out.info(cont(` ${GROUNDING_CATEGORY_LABELS[c].padEnd(width)} ` + `${split[c].toLocaleString("en-GB").padStart(6)}` + (MEMBER_EDITABLE_CATEGORIES.includes(c) ? " *" : "")));
|
|
41990
43000
|
}
|
|
41991
43001
|
out.info(cont(`A * marks what is yours to change; the rest is Plinth's own ` + `grounding and is the same on every workspace.`));
|
|
43002
|
+
const groundingBytes = split.grounding;
|
|
43003
|
+
if (groundingBytes > 0) {
|
|
43004
|
+
const paras = report.groundingParagraphs;
|
|
43005
|
+
const shaped = report.groundingRuleShaped;
|
|
43006
|
+
const proxyClause = shaped === 0 ? `It is prose, so the rule-shaped line count used on the files below ` + `scores it zero and is not the measure here.` : `The rule-shaped line count used on the files below scores it ` + `${shaped.toLocaleString("en-GB")}; on prose that count is a floor, ` + `so paragraphs are the measure here.`;
|
|
43007
|
+
out.info(cont(`Plinth's own grounding is ${groundingBytes.toLocaleString("en-GB")} bytes ` + `across ${paras} paragraphs, about one rule each. ${proxyClause}`));
|
|
43008
|
+
}
|
|
41992
43009
|
}
|
|
41993
43010
|
const contributors = [];
|
|
41994
43011
|
const rides = (name, reduce) => {
|
|
@@ -42070,13 +43087,89 @@ async function runStatus(args, deps = {}) {
|
|
|
42070
43087
|
out.info(cont(`Ending a session with ${CLOSE_COMMAND_INVOCATION} offers to propose a trim for the artist stores.`));
|
|
42071
43088
|
}
|
|
42072
43089
|
}
|
|
43090
|
+
let generated = [];
|
|
43091
|
+
try {
|
|
43092
|
+
generated = assessGeneratedFileBars(slug);
|
|
43093
|
+
} catch {
|
|
43094
|
+
generated = [];
|
|
43095
|
+
}
|
|
43096
|
+
const generatedOver = generated.filter((g) => g.over);
|
|
43097
|
+
if (generatedOver.length > 0) {
|
|
43098
|
+
out.info(row("Generated", `${generatedOver.length} file${generatedOver.length === 1 ? " Plinth writes is" : "s Plinth writes are"} ` + `past ${generatedOver.length === 1 ? "its" : "their"} bar. Yours to report, not to fix.`));
|
|
43099
|
+
const n = (v) => v.toLocaleString("en-GB");
|
|
43100
|
+
for (const g of generatedOver) {
|
|
43101
|
+
const unit = g.unit === "lines" ? "lines" : "bytes";
|
|
43102
|
+
const measure = `${g.path} is ${n(g.measured)} ${unit} against a ${n(g.bar)}-${g.unit === "lines" ? "line" : "byte"} bar`;
|
|
43103
|
+
const shares = g.generatedBytes === null || g.blockBytes === null ? `. Its BEGIN/END markers are missing, so Plinth's share and yours cannot be told apart here` : `: ${n(g.generatedBytes)} bytes of it is Plinth's own text and ${n(g.blockBytes)} is your customisable block`;
|
|
43104
|
+
out.info(cont(`${measure}${shares}. About ${g.ruleShaped} rule-shaped lines.`));
|
|
43105
|
+
}
|
|
43106
|
+
const within = generated.filter((g) => !g.over).map((g) => g.path);
|
|
43107
|
+
if (within.length > 0) {
|
|
43108
|
+
out.info(cont(`${within.join(", ")} ${within.length === 1 ? "is" : "are"} within bar.`));
|
|
43109
|
+
}
|
|
43110
|
+
out.info(cont("Plinth's own text here is regenerated on every sync, so trimming it is " + "a change to Plinth and not to this workspace. Only the block between " + "the markers is yours."));
|
|
43111
|
+
}
|
|
43112
|
+
let longLines = [];
|
|
43113
|
+
try {
|
|
43114
|
+
longLines = assessIndexLines(slug);
|
|
43115
|
+
} catch {
|
|
43116
|
+
longLines = [];
|
|
43117
|
+
}
|
|
43118
|
+
if (longLines.length > 0) {
|
|
43119
|
+
const total = longLines.reduce((sum, l) => sum + l.over, 0);
|
|
43120
|
+
out.info(row("Overlong", `${total} memory ${total === 1 ? "fact is" : "facts are"} longer than ` + `${INDEX_LINE_MAX_BYTES} bytes. Split ${total === 1 ? "it" : "them"}, never cut.`));
|
|
43121
|
+
for (const l of longLines) {
|
|
43122
|
+
out.info(cont(`${l.filename}: ${l.over} of ${l.facts}.`));
|
|
43123
|
+
for (const line of l.longest) {
|
|
43124
|
+
out.info(cont(` ${line.bytes.toLocaleString("en-GB")} bytes ${line.opening}`));
|
|
43125
|
+
}
|
|
43126
|
+
}
|
|
43127
|
+
out.info(cont("The convention is one line per fact: a hook and a [[pointer]], not a " + "paragraph. A line this long is usually several facts, so splitting it " + "keeps all of them and cutting it loses some."));
|
|
43128
|
+
}
|
|
43129
|
+
const anchor = localState.size_anchor;
|
|
43130
|
+
if (anchor !== undefined) {
|
|
43131
|
+
let now2 = {
|
|
43132
|
+
sizes: {},
|
|
43133
|
+
answered: []
|
|
43134
|
+
};
|
|
43135
|
+
try {
|
|
43136
|
+
now2 = measureBarredFiles(slug);
|
|
43137
|
+
} catch {
|
|
43138
|
+
now2 = { sizes: {}, answered: [] };
|
|
43139
|
+
}
|
|
43140
|
+
const answered = new Set(now2.answered);
|
|
43141
|
+
const nowSizes = now2.sizes;
|
|
43142
|
+
const deltas = Object.entries(anchor.sizes).flatMap(([path5, then]) => {
|
|
43143
|
+
const bytes = nowSizes[path5];
|
|
43144
|
+
if (bytes === undefined) {
|
|
43145
|
+
return answered.has(path5) ? [{ path: path5, bytes: 0, delta: -then, gone: true }] : [];
|
|
43146
|
+
}
|
|
43147
|
+
const delta = bytes - then;
|
|
43148
|
+
return delta === 0 ? [] : [{ path: path5, bytes, delta, gone: false }];
|
|
43149
|
+
}).sort((a, b) => Math.abs(b.delta) - Math.abs(a.delta));
|
|
43150
|
+
if (deltas.length > 0) {
|
|
43151
|
+
const when = shortDate(anchor.at);
|
|
43152
|
+
const since = anchor.kind === "session-end" ? `since your last session close${when === undefined ? "" : ` on ${when}`}` : `since this workspace first synced on this machine${when === undefined ? "" : ` on ${when}`}, because no session has ended here yet`;
|
|
43153
|
+
const named = deltas.slice(0, MAX_NAMED_GROWTH_FILES);
|
|
43154
|
+
out.info(row("Growth", `${deltas.length} file${deltas.length === 1 ? " has" : "s have"} changed size ${since}.`));
|
|
43155
|
+
for (const d of named) {
|
|
43156
|
+
const sign = d.delta > 0 ? "+" : "-";
|
|
43157
|
+
const size = Math.abs(d.delta).toLocaleString("en-GB");
|
|
43158
|
+
out.info(cont(d.gone ? `${d.path} GONE (was ${size} bytes)` : `${d.path} ${sign}${size} bytes (now ${d.bytes.toLocaleString("en-GB")})`));
|
|
43159
|
+
}
|
|
43160
|
+
if (deltas.length > named.length) {
|
|
43161
|
+
out.info(cont(`And ${deltas.length - named.length} more, not named here.`));
|
|
43162
|
+
}
|
|
43163
|
+
out.info(cont(`Growth is in bytes for every file, including \`${CLAUDE_MD}\`, whose own ` + `bar is a line count on the part Plinth generates.`));
|
|
43164
|
+
}
|
|
43165
|
+
}
|
|
42073
43166
|
out.info("");
|
|
42074
43167
|
return { exitCode: 0 };
|
|
42075
43168
|
}
|
|
42076
43169
|
var statusCommand = defineCommand({
|
|
42077
43170
|
meta: {
|
|
42078
43171
|
name: "status",
|
|
42079
|
-
description: "Print local sync state: daemon liveness, last sync + per-type counts, held destructive batches,
|
|
43172
|
+
description: "Print local sync state: daemon liveness, last sync + per-type counts, held destructive batches, the voice profile's status, and files rejected anywhere in the workspace (rejections are grouped by the member who reported them; two machines signed in as the same person cannot be told apart)"
|
|
42080
43173
|
},
|
|
42081
43174
|
args: {
|
|
42082
43175
|
workspace: {
|
|
@@ -42110,7 +43203,7 @@ var versionCommand = defineCommand({
|
|
|
42110
43203
|
});
|
|
42111
43204
|
|
|
42112
43205
|
// src/commands/voice-gate.ts
|
|
42113
|
-
import { readFileSync as
|
|
43206
|
+
import { readFileSync as readFileSync31 } from "node:fs";
|
|
42114
43207
|
|
|
42115
43208
|
// src/lib/voice-gate/api.ts
|
|
42116
43209
|
var VoiceGateSurfaces = [
|
|
@@ -42215,7 +43308,7 @@ async function callVoiceGate(req) {
|
|
|
42215
43308
|
import { createHash as createHash4 } from "node:crypto";
|
|
42216
43309
|
import {
|
|
42217
43310
|
mkdirSync as mkdirSync12,
|
|
42218
|
-
readFileSync as
|
|
43311
|
+
readFileSync as readFileSync29,
|
|
42219
43312
|
renameSync as renameSync8,
|
|
42220
43313
|
unlinkSync as unlinkSync9,
|
|
42221
43314
|
writeFileSync as writeFileSync9
|
|
@@ -42246,7 +43339,7 @@ function emptyFile() {
|
|
|
42246
43339
|
}
|
|
42247
43340
|
function load2(workspaceSlug) {
|
|
42248
43341
|
try {
|
|
42249
|
-
const raw =
|
|
43342
|
+
const raw = readFileSync29(attemptsPath(workspaceSlug), "utf8");
|
|
42250
43343
|
const parsed = AttemptsFileSchema.safeParse(JSON.parse(raw));
|
|
42251
43344
|
return parsed.success ? parsed.data : emptyFile();
|
|
42252
43345
|
} catch {
|
|
@@ -42409,7 +43502,7 @@ function denyUngated(detail) {
|
|
|
42409
43502
|
}
|
|
42410
43503
|
|
|
42411
43504
|
// src/lib/voice-gate/payload.ts
|
|
42412
|
-
import { readFileSync as
|
|
43505
|
+
import { readFileSync as readFileSync30, statSync as statSync20 } from "node:fs";
|
|
42413
43506
|
import { homedir as homedir2 } from "node:os";
|
|
42414
43507
|
import { isAbsolute as isAbsolute3, resolve as resolve4 } from "node:path";
|
|
42415
43508
|
var MAX_BODY_BYTES = 256 * 1024;
|
|
@@ -42566,11 +43659,11 @@ function isHelperInvocation(tokens) {
|
|
|
42566
43659
|
return helperIndex === start + 1 && /(^|\/)python[0-9.]*$/.test(prev);
|
|
42567
43660
|
}
|
|
42568
43661
|
function defaultReadBodyFile(path5) {
|
|
42569
|
-
const stat4 =
|
|
43662
|
+
const stat4 = statSync20(path5);
|
|
42570
43663
|
if (stat4.size > MAX_BODY_BYTES) {
|
|
42571
43664
|
throw new Error(`draft body is ${stat4.size} bytes, over the ${MAX_BODY_BYTES}-byte gate limit`);
|
|
42572
43665
|
}
|
|
42573
|
-
return
|
|
43666
|
+
return readFileSync30(path5, "utf8");
|
|
42574
43667
|
}
|
|
42575
43668
|
function parseRecipients(...raw) {
|
|
42576
43669
|
const all = raw.filter((v) => typeof v === "string").flatMap((v) => v.split(",")).map((v) => v.trim().toLowerCase()).filter((v) => v.length > 0);
|
|
@@ -42930,7 +44023,7 @@ async function runVoiceGateDirect(args, deps = {}) {
|
|
|
42930
44023
|
let text;
|
|
42931
44024
|
if (args.file) {
|
|
42932
44025
|
try {
|
|
42933
|
-
text =
|
|
44026
|
+
text = readFileSync31(args.file, "utf8");
|
|
42934
44027
|
} catch (err) {
|
|
42935
44028
|
const msg = err instanceof Error ? err.message : "unknown error";
|
|
42936
44029
|
out.error(`Could not read ${args.file}: ${msg}`);
|