@plinth-music/cli 0.13.0 → 0.15.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 +52 -0
- package/dist/build-stamp.json +2 -2
- package/dist/cli.js +891 -160
- 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.15.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 = [];
|
|
@@ -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) {
|
|
@@ -26478,6 +26529,11 @@ var TeamRejectedSchema = exports_external.object({
|
|
|
26478
26529
|
fetched_at: exports_external.string().datetime({ offset: true }),
|
|
26479
26530
|
rows: exports_external.array(TeamFileRejectionSchema)
|
|
26480
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({})
|
|
26536
|
+
});
|
|
26481
26537
|
var WorkspaceStateV2Schema = exports_external.object({
|
|
26482
26538
|
version: exports_external.literal(2),
|
|
26483
26539
|
workspace_slug: exports_external.string().min(1),
|
|
@@ -26491,7 +26547,9 @@ var WorkspaceStateV2Schema = exports_external.object({
|
|
|
26491
26547
|
user_voice: UserVoiceStateSchema.optional(),
|
|
26492
26548
|
derived_text_version: exports_external.number().int().nonnegative().optional(),
|
|
26493
26549
|
frontmatter_id_backfill_version: exports_external.number().int().nonnegative().optional(),
|
|
26494
|
-
team_rejected: TeamRejectedSchema.optional()
|
|
26550
|
+
team_rejected: TeamRejectedSchema.optional(),
|
|
26551
|
+
size_anchor: SizeAnchorSchema.optional(),
|
|
26552
|
+
last_trim_at: exports_external.string().min(1).optional()
|
|
26495
26553
|
});
|
|
26496
26554
|
var WorkspaceStateV1Schema = exports_external.object({
|
|
26497
26555
|
version: exports_external.literal(1),
|
|
@@ -26637,6 +26695,13 @@ function mergeVersioned(fresh, before, after) {
|
|
|
26637
26695
|
}
|
|
26638
26696
|
return { value: after, overruled: false };
|
|
26639
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
|
+
}
|
|
26640
26705
|
function commitState(before, after) {
|
|
26641
26706
|
const fresh = loadState(after.workspace_slug);
|
|
26642
26707
|
const entityTypes = new Set([
|
|
@@ -26674,7 +26739,9 @@ function commitState(before, after) {
|
|
|
26674
26739
|
user_voice: userVoice.value,
|
|
26675
26740
|
derived_text_version: mergeScalar(fresh.derived_text_version, before.derived_text_version, after.derived_text_version),
|
|
26676
26741
|
frontmatter_id_backfill_version: mergeScalar(fresh.frontmatter_id_backfill_version, before.frontmatter_id_backfill_version, after.frontmatter_id_backfill_version),
|
|
26677
|
-
team_rejected: mergeScalar(fresh.team_rejected, before.team_rejected, after.team_rejected)
|
|
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)
|
|
26678
26745
|
};
|
|
26679
26746
|
writeState(merged);
|
|
26680
26747
|
const singletonOverruled = rules.overruled || memory.overruled || userMemory.overruled || userVoice.overruled;
|
|
@@ -26985,8 +27052,411 @@ function readMemoryFreshness(workspaceSlug) {
|
|
|
26985
27052
|
userVoice: voice.success ? voice.data : null
|
|
26986
27053
|
};
|
|
26987
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
|
+
}
|
|
26988
27454
|
|
|
26989
27455
|
// src/commands/grounding.ts
|
|
27456
|
+
function countFilesPastBar(slug) {
|
|
27457
|
+
const bars = assessMemoryBars(slug);
|
|
27458
|
+
return bars.over.length + bars.notNamed;
|
|
27459
|
+
}
|
|
26990
27460
|
function readWorkspaceRulesFile(slug) {
|
|
26991
27461
|
return readContextFile(slug, WORKSPACE_RULES_FILENAME);
|
|
26992
27462
|
}
|
|
@@ -27042,6 +27512,7 @@ function runGroundingReport(deps = {}) {
|
|
|
27042
27512
|
const readFreshness = deps.readFreshness ?? readMemoryFreshness;
|
|
27043
27513
|
const readRosterIsEmpty = deps.readRosterIsEmpty ?? rosterIsEmpty;
|
|
27044
27514
|
const readMember = deps.readMember ?? readMemberFromConfig;
|
|
27515
|
+
const readFilesPastBar = deps.readFilesPastBar ?? countFilesPastBar;
|
|
27045
27516
|
const readTimezone = deps.readTimezone ?? readMachineTimezone;
|
|
27046
27517
|
let rawRules = null;
|
|
27047
27518
|
let rawMemory = null;
|
|
@@ -27054,6 +27525,7 @@ function runGroundingReport(deps = {}) {
|
|
|
27054
27525
|
};
|
|
27055
27526
|
let noArtists = false;
|
|
27056
27527
|
let member = null;
|
|
27528
|
+
let filesPastBar = 0;
|
|
27057
27529
|
const workspaceSlug = isValidWorkspaceSlug(deps.workspaceSlug ?? "") ? deps.workspaceSlug : null;
|
|
27058
27530
|
if (workspaceSlug) {
|
|
27059
27531
|
try {
|
|
@@ -27086,6 +27558,11 @@ function runGroundingReport(deps = {}) {
|
|
|
27086
27558
|
} catch {
|
|
27087
27559
|
noArtists = false;
|
|
27088
27560
|
}
|
|
27561
|
+
try {
|
|
27562
|
+
filesPastBar = readFilesPastBar(workspaceSlug);
|
|
27563
|
+
} catch {
|
|
27564
|
+
filesPastBar = 0;
|
|
27565
|
+
}
|
|
27089
27566
|
try {
|
|
27090
27567
|
const stored = readMember(workspaceSlug);
|
|
27091
27568
|
member = stored === null ? null : { ...stored, timezone: readTimezone() };
|
|
@@ -27100,7 +27577,7 @@ function runGroundingReport(deps = {}) {
|
|
|
27100
27577
|
return buildGroundingReportFor(formatLocalDate(now()), rules, memory, userMemory, {
|
|
27101
27578
|
workspaceMemoryUpdatedAt: resolveUpdatedAt(rawMemory, freshness.workspaceMemory),
|
|
27102
27579
|
userMemoryUpdatedAt: resolveUpdatedAt(rawUserMemory, freshness.userMemory)
|
|
27103
|
-
}, noArtists, voiceProfile, resolveVoiceMalformed(rawVoiceProfile, freshness.userVoice), workspaceSlug, member);
|
|
27580
|
+
}, noArtists, voiceProfile, resolveVoiceMalformed(rawVoiceProfile, freshness.userVoice), workspaceSlug, member, filesPastBar);
|
|
27104
27581
|
}
|
|
27105
27582
|
var groundingCommand = defineCommand({
|
|
27106
27583
|
meta: {
|
|
@@ -28286,33 +28763,6 @@ function normaliseGeneratedAt(content) {
|
|
|
28286
28763
|
return content.replace(STAMP_SLOT_RE, GENERATED_AT_PREFIX + GENERATED_AT_PLACEHOLDER);
|
|
28287
28764
|
}
|
|
28288
28765
|
|
|
28289
|
-
// src/lib/context/markers.ts
|
|
28290
|
-
var USER_BLOCK_BEGIN = "<!-- BEGIN: user-customisable -->";
|
|
28291
|
-
var USER_BLOCK_END = "<!-- END -->";
|
|
28292
|
-
var USER_BLOCK_PLACEHOLDER = "_Add workspace-specific guidance here. This block is the only part of " + "the file Plinth preserves across regenerations._";
|
|
28293
|
-
function extractUserBlock(content) {
|
|
28294
|
-
const beginIdx = content.indexOf(USER_BLOCK_BEGIN);
|
|
28295
|
-
if (beginIdx === -1)
|
|
28296
|
-
return null;
|
|
28297
|
-
const afterBegin = beginIdx + USER_BLOCK_BEGIN.length;
|
|
28298
|
-
const endIdx = content.indexOf(USER_BLOCK_END, afterBegin);
|
|
28299
|
-
if (endIdx === -1)
|
|
28300
|
-
return null;
|
|
28301
|
-
return content.slice(afterBegin, endIdx);
|
|
28302
|
-
}
|
|
28303
|
-
function injectUserBlock(generated, preserved) {
|
|
28304
|
-
const beginIdx = generated.indexOf(USER_BLOCK_BEGIN);
|
|
28305
|
-
if (beginIdx === -1) {
|
|
28306
|
-
throw new Error("injectUserBlock: generated content has no BEGIN marker");
|
|
28307
|
-
}
|
|
28308
|
-
const afterBegin = beginIdx + USER_BLOCK_BEGIN.length;
|
|
28309
|
-
const endIdx = generated.indexOf(USER_BLOCK_END, afterBegin);
|
|
28310
|
-
if (endIdx === -1) {
|
|
28311
|
-
throw new Error("injectUserBlock: generated content has no END marker");
|
|
28312
|
-
}
|
|
28313
|
-
return generated.slice(0, afterBegin) + preserved + generated.slice(endIdx);
|
|
28314
|
-
}
|
|
28315
|
-
|
|
28316
28766
|
// src/lib/context/artist-claude-md.ts
|
|
28317
28767
|
var CONTEXT_VERSION_HEADER = "plinth-context-version";
|
|
28318
28768
|
var TASK_DISPLAY_CAP = 10;
|
|
@@ -28580,7 +29030,7 @@ function strOrNull(value) {
|
|
|
28580
29030
|
}
|
|
28581
29031
|
|
|
28582
29032
|
// src/lib/context/schema-version.ts
|
|
28583
|
-
var TEMPLATE_VERSION = "
|
|
29033
|
+
var TEMPLATE_VERSION = "5";
|
|
28584
29034
|
var HEADER_RE = /<!--\s*plinth-schema-version:\s*([a-f0-9]+)\s*-->/;
|
|
28585
29035
|
function computeSchemaVersion(inputs) {
|
|
28586
29036
|
return stableHash(inputs).slice(0, 12);
|
|
@@ -28617,7 +29067,6 @@ function decideRegen(input) {
|
|
|
28617
29067
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
28618
29068
|
import { accessSync, constants, realpathSync, statSync as statSync3 } from "node:fs";
|
|
28619
29069
|
import { basename as basename2 } from "node:path";
|
|
28620
|
-
var SETTINGS_REL = ".claude/settings.json";
|
|
28621
29070
|
var MANAGED_KEY = "_plinth";
|
|
28622
29071
|
var COMMENT_SENTINEL = 'Managed by plinth-cli. Entries tagged "_plinth" are REGENERATED on every sync:';
|
|
28623
29072
|
var MANAGED_COMMENT = [
|
|
@@ -29013,7 +29462,6 @@ function warnUnresolvedBinary(out) {
|
|
|
29013
29462
|
}
|
|
29014
29463
|
|
|
29015
29464
|
// src/lib/context/artist-context.ts
|
|
29016
|
-
var ARTIST_CLAUDE_MD = "CLAUDE.md";
|
|
29017
29465
|
var ARTIST_DAILY_LOG_MD = "daily-log.md";
|
|
29018
29466
|
function ensurePerArtistContext(deps) {
|
|
29019
29467
|
const { workspaceSlug, force, out, now } = deps;
|
|
@@ -29169,7 +29617,7 @@ function withUserBlock(lines) {
|
|
|
29169
29617
|
`) + `
|
|
29170
29618
|
`;
|
|
29171
29619
|
}
|
|
29172
|
-
function buildStartCommandMd(rosterIsEmpty2) {
|
|
29620
|
+
function buildStartCommandMd(rosterIsEmpty2, offerTrim = false) {
|
|
29173
29621
|
return withUserBlock([
|
|
29174
29622
|
"---",
|
|
29175
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.",
|
|
@@ -29189,6 +29637,14 @@ function buildStartCommandMd(rosterIsEmpty2) {
|
|
|
29189
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.",
|
|
29190
29638
|
""
|
|
29191
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
|
+
] : [],
|
|
29192
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.",
|
|
29193
29649
|
"",
|
|
29194
29650
|
"## Step 1: use the two memory indexes you were already given",
|
|
@@ -29434,6 +29890,7 @@ function buildCloseCommandMd(workspaceSlug) {
|
|
|
29434
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.",
|
|
29435
29891
|
"- **Merging two facts into one is a rewrite and shows as one**, with both original lines visible in the proposal.",
|
|
29436
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.",
|
|
29437
29894
|
"",
|
|
29438
29895
|
"**Then write only what the member confirms**, and only to the files this command can reach:",
|
|
29439
29896
|
"",
|
|
@@ -29450,6 +29907,8 @@ function buildCloseCommandMd(workspaceSlug) {
|
|
|
29450
29907
|
"",
|
|
29451
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.",
|
|
29452
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
|
+
"",
|
|
29453
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.",
|
|
29454
29913
|
"",
|
|
29455
29914
|
"## Step 7: end the session",
|
|
@@ -29460,6 +29919,8 @@ function buildCloseCommandMd(workspaceSlug) {
|
|
|
29460
29919
|
`plinth session end ${ws} --summary="<one line>"`,
|
|
29461
29920
|
"```",
|
|
29462
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
|
+
"",
|
|
29463
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.",
|
|
29464
29925
|
"",
|
|
29465
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."
|
|
@@ -29620,8 +30081,14 @@ function ensureWorkspaceCommands(deps) {
|
|
|
29620
30081
|
const created = [];
|
|
29621
30082
|
const refreshed = [];
|
|
29622
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
|
+
}
|
|
29623
30090
|
const bodies = {
|
|
29624
|
-
[START_COMMAND_REL]: buildStartCommandMd(noArtists),
|
|
30091
|
+
[START_COMMAND_REL]: buildStartCommandMd(noArtists, offerTrim),
|
|
29625
30092
|
[CLOSE_COMMAND_REL]: buildCloseCommandMd(workspaceSlug),
|
|
29626
30093
|
[ONBOARD_COMMAND_REL]: buildOnboardCommandMd(workspaceSlug)
|
|
29627
30094
|
};
|
|
@@ -29655,11 +30122,6 @@ function backupStamp2(d) {
|
|
|
29655
30122
|
var RETIRED_COMMAND_TOKENS = [
|
|
29656
30123
|
{ commandRel: CLOSE_COMMAND_REL, token: "memory-gate", version: "0.11.0" }
|
|
29657
30124
|
];
|
|
29658
|
-
var GENERATED_COMMAND_RELS = [
|
|
29659
|
-
START_COMMAND_REL,
|
|
29660
|
-
CLOSE_COMMAND_REL,
|
|
29661
|
-
ONBOARD_COMMAND_REL
|
|
29662
|
-
];
|
|
29663
30125
|
function scanStaleUserBlocks(deps) {
|
|
29664
30126
|
const retired = deps.retired ?? RETIRED_COMMAND_TOKENS;
|
|
29665
30127
|
const read = deps.read ?? readContextFile;
|
|
@@ -30079,6 +30541,108 @@ function calendarNoticeLine(kind) {
|
|
|
30079
30541
|
return `[${token} reason=${kind}]`;
|
|
30080
30542
|
}
|
|
30081
30543
|
|
|
30544
|
+
// src/lib/context/secret-scan.ts
|
|
30545
|
+
var HOLD_ON_SECRET = true;
|
|
30546
|
+
var ALLOW_SECRET_MARKER = "plinth-allow-secret";
|
|
30547
|
+
var SECRET_HELD_REASON = "possible_secret";
|
|
30548
|
+
var SECRET_RULES = [
|
|
30549
|
+
{
|
|
30550
|
+
id: "aws-access-key-id",
|
|
30551
|
+
label: "an AWS access key id",
|
|
30552
|
+
pattern: /\b(?:AKIA|ASIA|ABIA|ACCA)[0-9A-Z]{16}\b/
|
|
30553
|
+
},
|
|
30554
|
+
{
|
|
30555
|
+
id: "github-token",
|
|
30556
|
+
label: "a GitHub token",
|
|
30557
|
+
pattern: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/
|
|
30558
|
+
},
|
|
30559
|
+
{
|
|
30560
|
+
id: "slack-token",
|
|
30561
|
+
label: "a Slack token",
|
|
30562
|
+
pattern: /\bxox[baprse]-[0-9A-Za-z-]{10,}/
|
|
30563
|
+
},
|
|
30564
|
+
{
|
|
30565
|
+
id: "google-api-key",
|
|
30566
|
+
label: "a Google API key",
|
|
30567
|
+
pattern: /\bAIza[0-9A-Za-z_-]{35}\b/
|
|
30568
|
+
},
|
|
30569
|
+
{
|
|
30570
|
+
id: "anthropic-openai-key",
|
|
30571
|
+
label: "an Anthropic or OpenAI API key",
|
|
30572
|
+
pattern: /\bsk-(?:ant-|proj-)?(?=[A-Za-z0-9_-]*[A-Z])(?=[A-Za-z0-9_-]*[0-9])[A-Za-z0-9_-]{20,}/
|
|
30573
|
+
},
|
|
30574
|
+
{
|
|
30575
|
+
id: "stripe-secret-key",
|
|
30576
|
+
label: "a Stripe secret key",
|
|
30577
|
+
pattern: /\b[sr]k_live_[0-9A-Za-z]{16,}\b/
|
|
30578
|
+
},
|
|
30579
|
+
{
|
|
30580
|
+
id: "private-key-block",
|
|
30581
|
+
label: "a private key block",
|
|
30582
|
+
pattern: /-----BEGIN(?: [A-Z]+)* PRIVATE KEY-----/
|
|
30583
|
+
},
|
|
30584
|
+
{
|
|
30585
|
+
id: "jwt",
|
|
30586
|
+
label: "a JSON Web Token",
|
|
30587
|
+
pattern: /\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}/
|
|
30588
|
+
},
|
|
30589
|
+
{
|
|
30590
|
+
id: "signed-url",
|
|
30591
|
+
label: "a signed URL carrying its own credential",
|
|
30592
|
+
pattern: /[?&](?:X-Amz-Signature|X-Goog-Signature|Signature|sig)=[A-Za-z0-9%._-]{16,}/i
|
|
30593
|
+
}
|
|
30594
|
+
];
|
|
30595
|
+
function redactMatch(match) {
|
|
30596
|
+
const prefix = match.length >= 12 ? match.slice(0, 4) : "";
|
|
30597
|
+
return `${prefix}… ${match.length} characters`;
|
|
30598
|
+
}
|
|
30599
|
+
function lineIsAllowed(line) {
|
|
30600
|
+
return line.toLowerCase().includes(ALLOW_SECRET_MARKER);
|
|
30601
|
+
}
|
|
30602
|
+
function scanForSecrets(raw) {
|
|
30603
|
+
const findings = [];
|
|
30604
|
+
const lines = raw.split(`
|
|
30605
|
+
`);
|
|
30606
|
+
for (let i = 0;i < lines.length; i++) {
|
|
30607
|
+
const line = lines[i] ?? "";
|
|
30608
|
+
if (lineIsAllowed(line))
|
|
30609
|
+
continue;
|
|
30610
|
+
for (const rule of SECRET_RULES) {
|
|
30611
|
+
const m = rule.pattern.exec(line);
|
|
30612
|
+
if (!m)
|
|
30613
|
+
continue;
|
|
30614
|
+
findings.push({
|
|
30615
|
+
line: i + 1,
|
|
30616
|
+
ruleId: rule.id,
|
|
30617
|
+
label: rule.label,
|
|
30618
|
+
excerpt: redactMatch(m[0])
|
|
30619
|
+
});
|
|
30620
|
+
break;
|
|
30621
|
+
}
|
|
30622
|
+
}
|
|
30623
|
+
return findings;
|
|
30624
|
+
}
|
|
30625
|
+
var MAX_NAMED_SECRET_FINDINGS = 3;
|
|
30626
|
+
function secretHoldReason(findings, hold = HOLD_ON_SECRET) {
|
|
30627
|
+
const named = findings.slice(0, MAX_NAMED_SECRET_FINDINGS);
|
|
30628
|
+
const rest = findings.length - named.length;
|
|
30629
|
+
const detail = named.map((f) => `line ${f.line} looks like ${f.label} (${f.excerpt})`).join("; ");
|
|
30630
|
+
const more = rest > 0 ? `, and ${rest} more` : "";
|
|
30631
|
+
return `${detail.charAt(0).toUpperCase()}${detail.slice(1)}${more}. ` + `Plinth is not approved to store credentials yet, so ` + (hold ? `this file stays on this machine until you say otherwise. ` + `Remove the value, or add ${ALLOW_SECRET_MARKER} to that line to ` + `store it anyway. ` : `it has already reached Plinth on this sync. Rotate it if it is ` + `live, then remove the value — or add ${ALLOW_SECRET_MARKER} to that ` + `line to stop being told. `) + `Nothing is changed in your file either way.`;
|
|
30632
|
+
}
|
|
30633
|
+
function secretHoldSummary(findings) {
|
|
30634
|
+
const n = findings.length;
|
|
30635
|
+
return `${n} possible credential${n === 1 ? "" : "s"} found`;
|
|
30636
|
+
}
|
|
30637
|
+
function secretHoldMessage(filename, findings, hold = HOLD_ON_SECRET) {
|
|
30638
|
+
const verb = hold ? "was not synced" : "was synced";
|
|
30639
|
+
return `${filename} ${verb}. ${secretHoldReason(findings, hold)}`;
|
|
30640
|
+
}
|
|
30641
|
+
function secretHoldRecord(findings, hold = HOLD_ON_SECRET) {
|
|
30642
|
+
const verdict = hold ? "so this file was not synced" : "and it synced anyway";
|
|
30643
|
+
return `${secretHoldSummary(findings)}, ${verdict}. ${secretHoldReason(findings, hold)}`;
|
|
30644
|
+
}
|
|
30645
|
+
|
|
30082
30646
|
// src/lib/rejected.ts
|
|
30083
30647
|
var PERMANENT_REJECT_REASONS = new Set(["bad_request", "invalid_frontmatter"]);
|
|
30084
30648
|
function isPermanentReject(err) {
|
|
@@ -30228,6 +30792,45 @@ function clearReportedSafeFileRejected(state, relPath) {
|
|
|
30228
30792
|
return state;
|
|
30229
30793
|
return clearFileRejected(state, relPath);
|
|
30230
30794
|
}
|
|
30795
|
+
function recordMemorySecretHeld(args) {
|
|
30796
|
+
let path3;
|
|
30797
|
+
try {
|
|
30798
|
+
path3 = relative3(workspaceMirrorPath(args.workspaceSlug), args.absolutePath);
|
|
30799
|
+
} catch {
|
|
30800
|
+
path3 = args.absolutePath;
|
|
30801
|
+
}
|
|
30802
|
+
const withoutStale = removeUnsynced(args.state, "entity", path3).state;
|
|
30803
|
+
return persist(withoutStale, upsertRejected(withoutStale, {
|
|
30804
|
+
target: "entity",
|
|
30805
|
+
op: "upsert",
|
|
30806
|
+
path: path3,
|
|
30807
|
+
reason: SECRET_HELD_REASON,
|
|
30808
|
+
message: args.record,
|
|
30809
|
+
rejected_at: args.now().toISOString()
|
|
30810
|
+
}));
|
|
30811
|
+
}
|
|
30812
|
+
function secretHoldRecordFor(state, workspaceSlug, absolutePath) {
|
|
30813
|
+
let path3;
|
|
30814
|
+
try {
|
|
30815
|
+
path3 = relative3(workspaceMirrorPath(workspaceSlug), absolutePath);
|
|
30816
|
+
} catch {
|
|
30817
|
+
path3 = absolutePath;
|
|
30818
|
+
}
|
|
30819
|
+
return (state.rejected ?? []).find((o) => o.target === "entity" && o.path === path3 && o.reason === SECRET_HELD_REASON);
|
|
30820
|
+
}
|
|
30821
|
+
function clearMemorySecretHeld(state, workspaceSlug, absolutePath) {
|
|
30822
|
+
let path3;
|
|
30823
|
+
try {
|
|
30824
|
+
path3 = relative3(workspaceMirrorPath(workspaceSlug), absolutePath);
|
|
30825
|
+
} catch {
|
|
30826
|
+
path3 = absolutePath;
|
|
30827
|
+
}
|
|
30828
|
+
const list = state.rejected ?? [];
|
|
30829
|
+
const next = list.filter((o) => !(o.target === "entity" && o.path === path3 && o.reason === SECRET_HELD_REASON));
|
|
30830
|
+
if (next.length === list.length)
|
|
30831
|
+
return state;
|
|
30832
|
+
return commitState(state, { ...state, rejected: next });
|
|
30833
|
+
}
|
|
30231
30834
|
|
|
30232
30835
|
// src/lib/context/workspace-claude-md.ts
|
|
30233
30836
|
var EXTRACTED_TEXT_DIR = extractedTextRelPath("x").replace(/x\.md$/, "");
|
|
@@ -30283,6 +30886,8 @@ function buildWorkspaceClaudeMd(input) {
|
|
|
30283
30886
|
`- \`${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`,
|
|
30284
30887
|
`- \`.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`,
|
|
30285
30888
|
``,
|
|
30889
|
+
...renderGuidingFilesSection(),
|
|
30890
|
+
``,
|
|
30286
30891
|
`## Where key context lives: look here before hunting`,
|
|
30287
30892
|
`- 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`,
|
|
30288
30893
|
`- **Contracts, executed agreements & any PDF** → \`files/\` (search here first; \`documents/\` holds markdown bodies, not attachments)`,
|
|
@@ -30314,8 +30919,6 @@ function buildWorkspaceClaudeMd(input) {
|
|
|
30314
30919
|
}
|
|
30315
30920
|
|
|
30316
30921
|
// src/lib/context/generate.ts
|
|
30317
|
-
var CLAUDE_MD = "CLAUDE.md";
|
|
30318
|
-
var CLAUDE_LOCAL_MD = "CLAUDE.local.md";
|
|
30319
30922
|
var GITIGNORE = ".gitignore";
|
|
30320
30923
|
var GITIGNORE_ENTRIES = [
|
|
30321
30924
|
CLAUDE_LOCAL_MD,
|
|
@@ -30979,6 +31582,27 @@ async function runSessionEnd(args, deps = {}) {
|
|
|
30979
31582
|
} catch {}
|
|
30980
31583
|
}
|
|
30981
31584
|
}
|
|
31585
|
+
const anchorSlug = resolveSlug(args.workspace);
|
|
31586
|
+
if (anchorSlug.ok) {
|
|
31587
|
+
try {
|
|
31588
|
+
const before = loadState(anchorSlug.slug);
|
|
31589
|
+
let next = before;
|
|
31590
|
+
const anchored = setSizeAnchor(next, {
|
|
31591
|
+
at: now.toISOString(),
|
|
31592
|
+
kind: "session-end",
|
|
31593
|
+
sizes: measureBarredFiles(anchorSlug.slug).sizes
|
|
31594
|
+
});
|
|
31595
|
+
next = anchored.state;
|
|
31596
|
+
let changed = anchored.changed;
|
|
31597
|
+
if (args.trimmed === true) {
|
|
31598
|
+
const stamped = setLastTrimAt(next, now.toISOString());
|
|
31599
|
+
next = stamped.state;
|
|
31600
|
+
changed = changed || stamped.changed;
|
|
31601
|
+
}
|
|
31602
|
+
if (changed)
|
|
31603
|
+
commitState(before, next);
|
|
31604
|
+
} catch {}
|
|
31605
|
+
}
|
|
30982
31606
|
const dock = readDockSignalEnv(env2);
|
|
30983
31607
|
if (dock === null)
|
|
30984
31608
|
return { exitCode: 0 };
|
|
@@ -31080,6 +31704,10 @@ var sessionCommand = defineCommand({
|
|
|
31080
31704
|
},
|
|
31081
31705
|
args: {
|
|
31082
31706
|
workspace: { type: "string", description: "Workspace slug" },
|
|
31707
|
+
trimmed: {
|
|
31708
|
+
type: "boolean",
|
|
31709
|
+
description: "This close wrote a memory trim; records the date so /plinth:start stops offering one"
|
|
31710
|
+
},
|
|
31083
31711
|
summary: {
|
|
31084
31712
|
type: "string",
|
|
31085
31713
|
description: "One-line summary of the session, forwarded to the dock",
|
|
@@ -31089,7 +31717,8 @@ var sessionCommand = defineCommand({
|
|
|
31089
31717
|
async run({ args }) {
|
|
31090
31718
|
const result = await runSessionEnd({
|
|
31091
31719
|
workspace: args.workspace,
|
|
31092
|
-
summary: args.summary
|
|
31720
|
+
summary: args.summary,
|
|
31721
|
+
trimmed: args.trimmed === true
|
|
31093
31722
|
});
|
|
31094
31723
|
process.exitCode = result.exitCode;
|
|
31095
31724
|
}
|
|
@@ -35698,6 +36327,41 @@ async function reconcileFilesSubtree(args) {
|
|
|
35698
36327
|
|
|
35699
36328
|
// src/lib/artist-memory/sync.ts
|
|
35700
36329
|
import { relative as relative9 } from "node:path";
|
|
36330
|
+
|
|
36331
|
+
// src/lib/context/secret-gate.ts
|
|
36332
|
+
function applySecretGate(args) {
|
|
36333
|
+
const hold = args.hold ?? HOLD_ON_SECRET;
|
|
36334
|
+
const release = () => ({
|
|
36335
|
+
state: clearMemorySecretHeld(args.state, args.workspaceSlug, args.absolutePath),
|
|
36336
|
+
held: false,
|
|
36337
|
+
findings: []
|
|
36338
|
+
});
|
|
36339
|
+
if (args.alreadyAcked)
|
|
36340
|
+
return release();
|
|
36341
|
+
const findings = scanForSecrets(args.body);
|
|
36342
|
+
if (findings.length === 0)
|
|
36343
|
+
return release();
|
|
36344
|
+
const record2 = secretHoldRecord(findings, hold);
|
|
36345
|
+
const standing = secretHoldRecordFor(args.state, args.workspaceSlug, args.absolutePath);
|
|
36346
|
+
if (standing?.message === record2) {
|
|
36347
|
+
return { state: args.state, held: hold, findings };
|
|
36348
|
+
}
|
|
36349
|
+
args.output?.warn(secretHoldMessage(args.filename, findings, hold));
|
|
36350
|
+
args.output?.log(`[${args.logTag}_SECRET_HELD ws=${args.workspaceSlug} ` + `findings=${findings.length} held=${hold}]`);
|
|
36351
|
+
return {
|
|
36352
|
+
state: recordMemorySecretHeld({
|
|
36353
|
+
state: args.state,
|
|
36354
|
+
workspaceSlug: args.workspaceSlug,
|
|
36355
|
+
absolutePath: args.absolutePath,
|
|
36356
|
+
record: record2,
|
|
36357
|
+
now: args.now
|
|
36358
|
+
}),
|
|
36359
|
+
held: hold,
|
|
36360
|
+
findings
|
|
36361
|
+
};
|
|
36362
|
+
}
|
|
36363
|
+
|
|
36364
|
+
// src/lib/artist-memory/sync.ts
|
|
35701
36365
|
var MAX_MEMORY_PUSH_ATTEMPTS = 3;
|
|
35702
36366
|
var ENTITY_TYPE = "artists";
|
|
35703
36367
|
function conflictSlug(slug) {
|
|
@@ -35805,11 +36469,26 @@ async function pushArtistMemory(args) {
|
|
|
35805
36469
|
}
|
|
35806
36470
|
const localHash = stableHash(local);
|
|
35807
36471
|
const runPatch = async () => {
|
|
35808
|
-
|
|
36472
|
+
let base2 = args.mutex ? loadState(workspaceSlug) : state;
|
|
35809
36473
|
const entry2 = getEntryBySlug(base2, ENTITY_TYPE, slug);
|
|
35810
36474
|
if (!entry2)
|
|
35811
36475
|
return { state: base2, outcome: { kind: "ignored", slug } };
|
|
35812
|
-
|
|
36476
|
+
const alreadyAcked = entry2.memory_hash === localHash;
|
|
36477
|
+
const gate = applySecretGate({
|
|
36478
|
+
state: base2,
|
|
36479
|
+
workspaceSlug,
|
|
36480
|
+
absolutePath: artistMemoryFilePath(workspaceSlug, slug),
|
|
36481
|
+
filename: `artists/${slug}/${ARTIST_MEMORY_ANCHOR}`,
|
|
36482
|
+
body: local,
|
|
36483
|
+
alreadyAcked,
|
|
36484
|
+
output,
|
|
36485
|
+
logTag: "ARTIST_MEMORY",
|
|
36486
|
+
now
|
|
36487
|
+
});
|
|
36488
|
+
base2 = gate.state;
|
|
36489
|
+
if (gate.held)
|
|
36490
|
+
return { state: base2, outcome: { kind: "secret_held", slug } };
|
|
36491
|
+
if (alreadyAcked) {
|
|
35813
36492
|
return { state: base2, outcome: { kind: "noop", slug } };
|
|
35814
36493
|
}
|
|
35815
36494
|
const acked = entry2.memory_hash;
|
|
@@ -38093,11 +38772,26 @@ async function pushWorkspaceMemory(args) {
|
|
|
38093
38772
|
}
|
|
38094
38773
|
const localHash = stableHash(local);
|
|
38095
38774
|
const acked = getWorkspaceMemoryState(state);
|
|
38096
|
-
|
|
38097
|
-
|
|
38775
|
+
const alreadyAcked = Boolean(acked && acked.body_hash === localHash);
|
|
38776
|
+
const gate = applySecretGate({
|
|
38777
|
+
state,
|
|
38778
|
+
workspaceSlug,
|
|
38779
|
+
absolutePath: contextFilePath(workspaceSlug, WORKSPACE_MEMORY_FILENAME),
|
|
38780
|
+
filename: WORKSPACE_MEMORY_FILENAME,
|
|
38781
|
+
body: local,
|
|
38782
|
+
alreadyAcked,
|
|
38783
|
+
output,
|
|
38784
|
+
logTag: "WORKSPACE_MEMORY",
|
|
38785
|
+
now
|
|
38786
|
+
});
|
|
38787
|
+
const gated = gate.state;
|
|
38788
|
+
if (gate.held)
|
|
38789
|
+
return { state: gated, outcome: { kind: "secret_held" } };
|
|
38790
|
+
if (alreadyAcked) {
|
|
38791
|
+
return { state: gated, outcome: { kind: "noop" } };
|
|
38098
38792
|
}
|
|
38099
38793
|
if (extractWorkspaceMemory(local) === null && (acked?.updated_at ?? null) === null) {
|
|
38100
|
-
const next2 = persistMemory(
|
|
38794
|
+
const next2 = persistMemory(gated, { body_hash: localHash, updated_at: null });
|
|
38101
38795
|
return { state: next2, outcome: { kind: "noop" } };
|
|
38102
38796
|
}
|
|
38103
38797
|
const res = await putWorkspaceMemory({
|
|
@@ -38108,7 +38802,7 @@ async function pushWorkspaceMemory(args) {
|
|
|
38108
38802
|
fetchImpl
|
|
38109
38803
|
});
|
|
38110
38804
|
if (res.ok) {
|
|
38111
|
-
const next2 = persistMemory(
|
|
38805
|
+
const next2 = persistMemory(gated, {
|
|
38112
38806
|
body_hash: localHash,
|
|
38113
38807
|
updated_at: res.row.updated_at
|
|
38114
38808
|
});
|
|
@@ -38117,7 +38811,7 @@ async function pushWorkspaceMemory(args) {
|
|
|
38117
38811
|
}
|
|
38118
38812
|
if (res.code === 413) {
|
|
38119
38813
|
output?.warn(`${WORKSPACE_MEMORY_FILENAME} is over the ${Math.round(MAX_WORKSPACE_MEMORY_BYTES / 1024)}KB cap — not synced.`);
|
|
38120
|
-
return { state, outcome: { kind: "over_cap" } };
|
|
38814
|
+
return { state: gated, outcome: { kind: "over_cap" } };
|
|
38121
38815
|
}
|
|
38122
38816
|
{
|
|
38123
38817
|
const probe2 = await getWorkspaceMemory({ baseUrl, pat, fetchImpl });
|
|
@@ -38130,7 +38824,7 @@ async function pushWorkspaceMemory(args) {
|
|
|
38130
38824
|
fetchImpl
|
|
38131
38825
|
});
|
|
38132
38826
|
if (retry.ok) {
|
|
38133
|
-
const next2 = persistMemory(
|
|
38827
|
+
const next2 = persistMemory(gated, {
|
|
38134
38828
|
body_hash: localHash,
|
|
38135
38829
|
updated_at: retry.row.updated_at
|
|
38136
38830
|
});
|
|
@@ -38139,7 +38833,7 @@ async function pushWorkspaceMemory(args) {
|
|
|
38139
38833
|
}
|
|
38140
38834
|
if (retry.code === 413) {
|
|
38141
38835
|
output?.warn(`${WORKSPACE_MEMORY_FILENAME} is over the ${Math.round(MAX_WORKSPACE_MEMORY_BYTES / 1024)}KB cap — not synced.`);
|
|
38142
|
-
return { state, outcome: { kind: "over_cap" } };
|
|
38836
|
+
return { state: gated, outcome: { kind: "over_cap" } };
|
|
38143
38837
|
}
|
|
38144
38838
|
}
|
|
38145
38839
|
}
|
|
@@ -38151,7 +38845,7 @@ async function pushWorkspaceMemory(args) {
|
|
|
38151
38845
|
contents: local,
|
|
38152
38846
|
side: "local"
|
|
38153
38847
|
});
|
|
38154
|
-
const next = await restoreFromCloud2(args);
|
|
38848
|
+
const next = await restoreFromCloud2({ ...args, state: gated });
|
|
38155
38849
|
output?.warn(`Conflict on ${WORKSPACE_MEMORY_FILENAME} -> stashed local edit to ${stashPath} (cloud-wins; cloud copy restored).`);
|
|
38156
38850
|
return { state: next, outcome: { kind: "conflict", stashPath } };
|
|
38157
38851
|
}
|
|
@@ -38363,11 +39057,29 @@ async function pushSingleton(descriptor, args) {
|
|
|
38363
39057
|
}
|
|
38364
39058
|
const localHash = stableHash(local);
|
|
38365
39059
|
const acked = descriptor.getState(state);
|
|
38366
|
-
|
|
38367
|
-
|
|
39060
|
+
const alreadyAcked = Boolean(acked && acked.body_hash === localHash);
|
|
39061
|
+
let gated = state;
|
|
39062
|
+
if (descriptor.secretScanned) {
|
|
39063
|
+
const gate = applySecretGate({
|
|
39064
|
+
state,
|
|
39065
|
+
workspaceSlug,
|
|
39066
|
+
absolutePath: filePath(descriptor, workspaceSlug),
|
|
39067
|
+
filename: descriptor.filename,
|
|
39068
|
+
body: local,
|
|
39069
|
+
alreadyAcked,
|
|
39070
|
+
output,
|
|
39071
|
+
logTag: descriptor.logTag,
|
|
39072
|
+
now
|
|
39073
|
+
});
|
|
39074
|
+
gated = gate.state;
|
|
39075
|
+
if (gate.held)
|
|
39076
|
+
return { state: gated, outcome: { kind: "secret_held" } };
|
|
39077
|
+
}
|
|
39078
|
+
if (alreadyAcked) {
|
|
39079
|
+
return { state: gated, outcome: { kind: "noop" } };
|
|
38368
39080
|
}
|
|
38369
39081
|
if (descriptor.extractActive(local) === null && (acked?.updated_at ?? null) === null) {
|
|
38370
|
-
const next2 = persist2(descriptor,
|
|
39082
|
+
const next2 = persist2(descriptor, gated, { body_hash: localHash, updated_at: null });
|
|
38371
39083
|
return { state: next2, outcome: { kind: "noop" } };
|
|
38372
39084
|
}
|
|
38373
39085
|
const res = await descriptor.api.put({
|
|
@@ -38378,13 +39090,13 @@ async function pushSingleton(descriptor, args) {
|
|
|
38378
39090
|
fetchImpl
|
|
38379
39091
|
});
|
|
38380
39092
|
if (res.ok) {
|
|
38381
|
-
const next2 = persist2(descriptor,
|
|
39093
|
+
const next2 = persist2(descriptor, gated, ackedFrom(res.row, localHash));
|
|
38382
39094
|
output?.log(`[${descriptor.logTag}_PUT ws=${workspaceSlug} is_empty=${res.row.is_empty}` + `${res.row.status ? ` status=${res.row.status}` : ""}]`);
|
|
38383
39095
|
return { state: next2, outcome: { kind: "pushed" } };
|
|
38384
39096
|
}
|
|
38385
39097
|
if (res.code === 413) {
|
|
38386
39098
|
output?.warn(`${descriptor.filename} is over the ${Math.round(descriptor.maxBytes / 1024)}KB cap — not synced.`);
|
|
38387
|
-
return { state, outcome: { kind: "over_cap" } };
|
|
39099
|
+
return { state: gated, outcome: { kind: "over_cap" } };
|
|
38388
39100
|
}
|
|
38389
39101
|
{
|
|
38390
39102
|
const probe2 = await descriptor.api.get({ baseUrl, pat, fetchImpl });
|
|
@@ -38397,13 +39109,13 @@ async function pushSingleton(descriptor, args) {
|
|
|
38397
39109
|
fetchImpl
|
|
38398
39110
|
});
|
|
38399
39111
|
if (retry.ok) {
|
|
38400
|
-
const next2 = persist2(descriptor,
|
|
39112
|
+
const next2 = persist2(descriptor, gated, ackedFrom(retry.row, localHash));
|
|
38401
39113
|
output?.log(`[${descriptor.logTag}_PUT ws=${workspaceSlug} is_empty=${retry.row.is_empty}` + `${retry.row.status ? ` status=${retry.row.status}` : ""}]`);
|
|
38402
39114
|
return { state: next2, outcome: { kind: "pushed" } };
|
|
38403
39115
|
}
|
|
38404
39116
|
if (retry.code === 413) {
|
|
38405
39117
|
output?.warn(`${descriptor.filename} is over the ${Math.round(descriptor.maxBytes / 1024)}KB cap — not synced.`);
|
|
38406
|
-
return { state, outcome: { kind: "over_cap" } };
|
|
39118
|
+
return { state: gated, outcome: { kind: "over_cap" } };
|
|
38407
39119
|
}
|
|
38408
39120
|
}
|
|
38409
39121
|
}
|
|
@@ -38415,7 +39127,7 @@ async function pushSingleton(descriptor, args) {
|
|
|
38415
39127
|
contents: local,
|
|
38416
39128
|
side: "local"
|
|
38417
39129
|
});
|
|
38418
|
-
const next = await restoreFromCloud3(descriptor, args);
|
|
39130
|
+
const next = await restoreFromCloud3(descriptor, { ...args, state: gated });
|
|
38419
39131
|
output?.warn(`Conflict on ${descriptor.filename} -> stashed local edit to ${stashPath} (cloud-wins; cloud copy restored).`);
|
|
38420
39132
|
return { state: next, outcome: { kind: "conflict", stashPath } };
|
|
38421
39133
|
}
|
|
@@ -38437,6 +39149,7 @@ var USER_MEMORY_DESCRIPTOR = {
|
|
|
38437
39149
|
mutexKey: USER_MEMORY_MUTEX_KEY,
|
|
38438
39150
|
logTag: "USER_MEMORY",
|
|
38439
39151
|
maxBytes: MAX_USER_MEMORY_BYTES,
|
|
39152
|
+
secretScanned: true,
|
|
38440
39153
|
extractActive: extractUserMemory,
|
|
38441
39154
|
getState: getUserMemoryState,
|
|
38442
39155
|
setState: setUserMemoryState,
|
|
@@ -38469,6 +39182,7 @@ var VOICE_DESCRIPTOR = {
|
|
|
38469
39182
|
mutexKey: VOICE_MUTEX_KEY,
|
|
38470
39183
|
logTag: "VOICE",
|
|
38471
39184
|
maxBytes: MAX_USER_VOICE_BYTES,
|
|
39185
|
+
secretScanned: false,
|
|
38472
39186
|
extractActive: extractVoiceProfile,
|
|
38473
39187
|
getState: getUserVoiceState,
|
|
38474
39188
|
setState: setUserVoiceState,
|
|
@@ -39855,6 +40569,22 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
39855
40569
|
const msg = err instanceof Error ? err.message : "unknown error";
|
|
39856
40570
|
out.warn(`Context generation skipped: ${msg}`);
|
|
39857
40571
|
}
|
|
40572
|
+
try {
|
|
40573
|
+
const measureSizes = deps.measureSizes ?? measureBarredFiles;
|
|
40574
|
+
const beforeAnchor = loadState(slug);
|
|
40575
|
+
if (beforeAnchor.size_anchor === undefined) {
|
|
40576
|
+
const seeded = setSizeAnchor(beforeAnchor, {
|
|
40577
|
+
at: now().toISOString(),
|
|
40578
|
+
kind: "sync",
|
|
40579
|
+
sizes: measureSizes(slug).sizes
|
|
40580
|
+
});
|
|
40581
|
+
if (seeded.changed)
|
|
40582
|
+
commitState(beforeAnchor, seeded.state);
|
|
40583
|
+
}
|
|
40584
|
+
} catch (err) {
|
|
40585
|
+
const msg = err instanceof Error ? err.message : "unknown error";
|
|
40586
|
+
out.warn(`Could not record the file-size anchor: ${msg}`);
|
|
40587
|
+
}
|
|
39858
40588
|
const lastFailure = memberRefreshFailedAt.get(slug);
|
|
39859
40589
|
const failureCooling = lastFailure !== undefined && now().getTime() - lastFailure < MEMBER_REFRESH_RETRY_AFTER_MS;
|
|
39860
40590
|
if (!failureCooling && memberNeedsRefresh(entry2, now)) {
|
|
@@ -41623,6 +42353,7 @@ function reportWorkspaceMemoryOutcome(spinner, slug, state, outcome) {
|
|
|
41623
42353
|
spinner.warn(`Conflict on ${WORKSPACE_MEMORY_FILENAME} → ${outcome.stashPath} (cloud-wins; local copy stashed)`);
|
|
41624
42354
|
spinner.update(`Watching ${slug} · ${summary}`);
|
|
41625
42355
|
return;
|
|
42356
|
+
case "secret_held":
|
|
41626
42357
|
case "over_cap":
|
|
41627
42358
|
case "noop":
|
|
41628
42359
|
return;
|
|
@@ -41639,6 +42370,7 @@ function reportUserMemoryOutcome(spinner, slug, state, outcome) {
|
|
|
41639
42370
|
spinner.warn(`Conflict on ${USER_MEMORY_FILENAME} → ${outcome.stashPath} (cloud-wins; local copy stashed)`);
|
|
41640
42371
|
spinner.update(`Watching ${slug} · ${summary}`);
|
|
41641
42372
|
return;
|
|
42373
|
+
case "secret_held":
|
|
41642
42374
|
case "over_cap":
|
|
41643
42375
|
case "noop":
|
|
41644
42376
|
return;
|
|
@@ -41691,6 +42423,7 @@ function reportArtistMemoryOutcome(spinner, slug, state, outcome) {
|
|
|
41691
42423
|
spinner.warn(`Conflict on artists/${outcome.slug}/memory.md → ${outcome.stashPath} (cloud-wins; local copy stashed)`);
|
|
41692
42424
|
spinner.update(`Watching ${slug} · ${summary}`);
|
|
41693
42425
|
return;
|
|
42426
|
+
case "secret_held":
|
|
41694
42427
|
case "not_found":
|
|
41695
42428
|
case "noop":
|
|
41696
42429
|
case "ignored":
|
|
@@ -41726,96 +42459,6 @@ var startCommand = defineCommand({
|
|
|
41726
42459
|
// src/commands/status.ts
|
|
41727
42460
|
import { statSync as statSync19 } from "node:fs";
|
|
41728
42461
|
|
|
41729
|
-
// src/lib/context/memory-bars.ts
|
|
41730
|
-
var RULE_FILE_BAR_BYTES = 6000;
|
|
41731
|
-
var FACT_STORE_BAR_BYTES = 12000;
|
|
41732
|
-
var MAX_NAMED_FACT_STORES = 5;
|
|
41733
|
-
function countRuleShapedLines(body) {
|
|
41734
|
-
let count = 0;
|
|
41735
|
-
for (const line of body.split(`
|
|
41736
|
-
`)) {
|
|
41737
|
-
const trimmed = line.trim();
|
|
41738
|
-
if (/^[-*+]\s/.test(trimmed))
|
|
41739
|
-
count += 1;
|
|
41740
|
-
else if (/^\*\*[^*]+\*\*/.test(trimmed))
|
|
41741
|
-
count += 1;
|
|
41742
|
-
}
|
|
41743
|
-
return count;
|
|
41744
|
-
}
|
|
41745
|
-
function factBody(raw) {
|
|
41746
|
-
return raw.replace(/<!--[\s\S]*?-->/g, "").replace(/<!--[\s\S]*$/, "").split(`
|
|
41747
|
-
`).filter((line) => !/^\s*#{1,6}\s/.test(line)).join(`
|
|
41748
|
-
`).trim();
|
|
41749
|
-
}
|
|
41750
|
-
var RULE_FILES = [
|
|
41751
|
-
{ filename: VOICE_FILENAME, agentMayEdit: false, memberOwned: true, ridesInjection: false },
|
|
41752
|
-
{ filename: WORKSPACE_RULES_FILENAME, agentMayEdit: true, memberOwned: false, ridesInjection: true }
|
|
41753
|
-
];
|
|
41754
|
-
function assessMemoryBars(workspaceSlug, deps = {}) {
|
|
41755
|
-
const readContext = deps.readContext ?? readContextFile;
|
|
41756
|
-
const readArtistMemory = deps.readArtistMemory ?? readArtistMemoryFile;
|
|
41757
|
-
const rosterOf = deps.readRosterFn ?? readRoster;
|
|
41758
|
-
const over = [];
|
|
41759
|
-
for (const spec of RULE_FILES) {
|
|
41760
|
-
let raw = null;
|
|
41761
|
-
try {
|
|
41762
|
-
raw = readContext(workspaceSlug, spec.filename);
|
|
41763
|
-
} catch {
|
|
41764
|
-
raw = null;
|
|
41765
|
-
}
|
|
41766
|
-
if (raw === null)
|
|
41767
|
-
continue;
|
|
41768
|
-
const bytes = Buffer.byteLength(raw);
|
|
41769
|
-
if (bytes <= RULE_FILE_BAR_BYTES)
|
|
41770
|
-
continue;
|
|
41771
|
-
over.push({
|
|
41772
|
-
path: spec.filename,
|
|
41773
|
-
kind: "rule",
|
|
41774
|
-
bytes,
|
|
41775
|
-
bar: RULE_FILE_BAR_BYTES,
|
|
41776
|
-
rules: countRuleShapedLines(raw),
|
|
41777
|
-
facts: groupFacts(factBody(raw)).length,
|
|
41778
|
-
agentMayEdit: spec.agentMayEdit,
|
|
41779
|
-
memberOwned: spec.memberOwned,
|
|
41780
|
-
buys: !spec.ridesInjection ? "per-read" : Buffer.byteLength(extractWorkspaceRules(raw) ?? "") > 0 ? "injection" : "nothing"
|
|
41781
|
-
});
|
|
41782
|
-
}
|
|
41783
|
-
let roster;
|
|
41784
|
-
try {
|
|
41785
|
-
roster = rosterOf(workspaceSlug);
|
|
41786
|
-
} catch {
|
|
41787
|
-
roster = [];
|
|
41788
|
-
}
|
|
41789
|
-
const stores = [];
|
|
41790
|
-
for (const artist of roster) {
|
|
41791
|
-
let raw = null;
|
|
41792
|
-
try {
|
|
41793
|
-
raw = readArtistMemory(workspaceSlug, artist.slug);
|
|
41794
|
-
} catch {
|
|
41795
|
-
raw = null;
|
|
41796
|
-
}
|
|
41797
|
-
if (raw === null)
|
|
41798
|
-
continue;
|
|
41799
|
-
const bytes = Buffer.byteLength(raw);
|
|
41800
|
-
if (bytes <= FACT_STORE_BAR_BYTES)
|
|
41801
|
-
continue;
|
|
41802
|
-
stores.push({
|
|
41803
|
-
path: `artists/${artist.slug}/memory.md`,
|
|
41804
|
-
kind: "fact",
|
|
41805
|
-
bytes,
|
|
41806
|
-
bar: FACT_STORE_BAR_BYTES,
|
|
41807
|
-
rules: countRuleShapedLines(raw),
|
|
41808
|
-
facts: groupFacts(factBody(raw)).length,
|
|
41809
|
-
agentMayEdit: true,
|
|
41810
|
-
memberOwned: true,
|
|
41811
|
-
buys: "per-read"
|
|
41812
|
-
});
|
|
41813
|
-
}
|
|
41814
|
-
stores.sort((a, b) => b.bytes - a.bytes || a.path.localeCompare(b.path));
|
|
41815
|
-
const named = stores.slice(0, MAX_NAMED_FACT_STORES);
|
|
41816
|
-
return { over: [...over, ...named], notNamed: stores.length - named.length };
|
|
41817
|
-
}
|
|
41818
|
-
|
|
41819
42462
|
// src/lib/build-stamp.ts
|
|
41820
42463
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
41821
42464
|
import { existsSync as existsSync9, mkdirSync as mkdirSync11, readFileSync as readFileSync26, writeFileSync as writeFileSync8 } from "node:fs";
|
|
@@ -42127,6 +42770,8 @@ function scanPendingWrites(args) {
|
|
|
42127
42770
|
continue;
|
|
42128
42771
|
if (Buffer.byteLength(local) > lane.maxBytes)
|
|
42129
42772
|
continue;
|
|
42773
|
+
if (reported.entities.has(lane.filename))
|
|
42774
|
+
continue;
|
|
42130
42775
|
const localHash = stableHash(local);
|
|
42131
42776
|
if (lane.acked && lane.acked.body_hash === localHash)
|
|
42132
42777
|
continue;
|
|
@@ -42152,6 +42797,8 @@ function scanPendingWrites(args) {
|
|
|
42152
42797
|
continue;
|
|
42153
42798
|
if (isPristineMemoryStub(local))
|
|
42154
42799
|
continue;
|
|
42800
|
+
if (reported.entities.has(`artists/${entry2.slug}/memory.md`))
|
|
42801
|
+
continue;
|
|
42155
42802
|
const localHash = stableHash(local);
|
|
42156
42803
|
if (entry2.memory_hash === localHash)
|
|
42157
42804
|
continue;
|
|
@@ -42166,6 +42813,7 @@ function scanPendingWrites(args) {
|
|
|
42166
42813
|
}
|
|
42167
42814
|
|
|
42168
42815
|
// src/commands/status.ts
|
|
42816
|
+
var MAX_NAMED_GROWTH_FILES = 5;
|
|
42169
42817
|
var LABEL_WIDTH = 9;
|
|
42170
42818
|
function row(label, value) {
|
|
42171
42819
|
return `${label.padEnd(LABEL_WIDTH)} ${value}`;
|
|
@@ -42584,6 +43232,13 @@ async function runStatus(args, deps = {}) {
|
|
|
42584
43232
|
out.info(cont(` ${GROUNDING_CATEGORY_LABELS[c].padEnd(width)} ` + `${split[c].toLocaleString("en-GB").padStart(6)}` + (MEMBER_EDITABLE_CATEGORIES.includes(c) ? " *" : "")));
|
|
42585
43233
|
}
|
|
42586
43234
|
out.info(cont(`A * marks what is yours to change; the rest is Plinth's own ` + `grounding and is the same on every workspace.`));
|
|
43235
|
+
const groundingBytes = split.grounding;
|
|
43236
|
+
if (groundingBytes > 0) {
|
|
43237
|
+
const paras = report.groundingParagraphs;
|
|
43238
|
+
const shaped = report.groundingRuleShaped;
|
|
43239
|
+
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.`;
|
|
43240
|
+
out.info(cont(`Plinth's own grounding is ${groundingBytes.toLocaleString("en-GB")} bytes ` + `across ${paras} paragraphs, about one rule each. ${proxyClause}`));
|
|
43241
|
+
}
|
|
42587
43242
|
}
|
|
42588
43243
|
const contributors = [];
|
|
42589
43244
|
const rides = (name, reduce) => {
|
|
@@ -42665,6 +43320,82 @@ async function runStatus(args, deps = {}) {
|
|
|
42665
43320
|
out.info(cont(`Ending a session with ${CLOSE_COMMAND_INVOCATION} offers to propose a trim for the artist stores.`));
|
|
42666
43321
|
}
|
|
42667
43322
|
}
|
|
43323
|
+
let generated = [];
|
|
43324
|
+
try {
|
|
43325
|
+
generated = assessGeneratedFileBars(slug);
|
|
43326
|
+
} catch {
|
|
43327
|
+
generated = [];
|
|
43328
|
+
}
|
|
43329
|
+
const generatedOver = generated.filter((g) => g.over);
|
|
43330
|
+
if (generatedOver.length > 0) {
|
|
43331
|
+
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.`));
|
|
43332
|
+
const n = (v) => v.toLocaleString("en-GB");
|
|
43333
|
+
for (const g of generatedOver) {
|
|
43334
|
+
const unit = g.unit === "lines" ? "lines" : "bytes";
|
|
43335
|
+
const measure = `${g.path} is ${n(g.measured)} ${unit} against a ${n(g.bar)}-${g.unit === "lines" ? "line" : "byte"} bar`;
|
|
43336
|
+
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`;
|
|
43337
|
+
out.info(cont(`${measure}${shares}. About ${g.ruleShaped} rule-shaped lines.`));
|
|
43338
|
+
}
|
|
43339
|
+
const within = generated.filter((g) => !g.over).map((g) => g.path);
|
|
43340
|
+
if (within.length > 0) {
|
|
43341
|
+
out.info(cont(`${within.join(", ")} ${within.length === 1 ? "is" : "are"} within bar.`));
|
|
43342
|
+
}
|
|
43343
|
+
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."));
|
|
43344
|
+
}
|
|
43345
|
+
let longLines = [];
|
|
43346
|
+
try {
|
|
43347
|
+
longLines = assessIndexLines(slug);
|
|
43348
|
+
} catch {
|
|
43349
|
+
longLines = [];
|
|
43350
|
+
}
|
|
43351
|
+
if (longLines.length > 0) {
|
|
43352
|
+
const total = longLines.reduce((sum, l) => sum + l.over, 0);
|
|
43353
|
+
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.`));
|
|
43354
|
+
for (const l of longLines) {
|
|
43355
|
+
out.info(cont(`${l.filename}: ${l.over} of ${l.facts}.`));
|
|
43356
|
+
for (const line of l.longest) {
|
|
43357
|
+
out.info(cont(` ${line.bytes.toLocaleString("en-GB")} bytes ${line.opening}`));
|
|
43358
|
+
}
|
|
43359
|
+
}
|
|
43360
|
+
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."));
|
|
43361
|
+
}
|
|
43362
|
+
const anchor = localState.size_anchor;
|
|
43363
|
+
if (anchor !== undefined) {
|
|
43364
|
+
let now2 = {
|
|
43365
|
+
sizes: {},
|
|
43366
|
+
answered: []
|
|
43367
|
+
};
|
|
43368
|
+
try {
|
|
43369
|
+
now2 = measureBarredFiles(slug);
|
|
43370
|
+
} catch {
|
|
43371
|
+
now2 = { sizes: {}, answered: [] };
|
|
43372
|
+
}
|
|
43373
|
+
const answered = new Set(now2.answered);
|
|
43374
|
+
const nowSizes = now2.sizes;
|
|
43375
|
+
const deltas = Object.entries(anchor.sizes).flatMap(([path5, then]) => {
|
|
43376
|
+
const bytes = nowSizes[path5];
|
|
43377
|
+
if (bytes === undefined) {
|
|
43378
|
+
return answered.has(path5) ? [{ path: path5, bytes: 0, delta: -then, gone: true }] : [];
|
|
43379
|
+
}
|
|
43380
|
+
const delta = bytes - then;
|
|
43381
|
+
return delta === 0 ? [] : [{ path: path5, bytes, delta, gone: false }];
|
|
43382
|
+
}).sort((a, b) => Math.abs(b.delta) - Math.abs(a.delta));
|
|
43383
|
+
if (deltas.length > 0) {
|
|
43384
|
+
const when = shortDate(anchor.at);
|
|
43385
|
+
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`;
|
|
43386
|
+
const named = deltas.slice(0, MAX_NAMED_GROWTH_FILES);
|
|
43387
|
+
out.info(row("Growth", `${deltas.length} file${deltas.length === 1 ? " has" : "s have"} changed size ${since}.`));
|
|
43388
|
+
for (const d of named) {
|
|
43389
|
+
const sign = d.delta > 0 ? "+" : "-";
|
|
43390
|
+
const size = Math.abs(d.delta).toLocaleString("en-GB");
|
|
43391
|
+
out.info(cont(d.gone ? `${d.path} GONE (was ${size} bytes)` : `${d.path} ${sign}${size} bytes (now ${d.bytes.toLocaleString("en-GB")})`));
|
|
43392
|
+
}
|
|
43393
|
+
if (deltas.length > named.length) {
|
|
43394
|
+
out.info(cont(`And ${deltas.length - named.length} more, not named here.`));
|
|
43395
|
+
}
|
|
43396
|
+
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.`));
|
|
43397
|
+
}
|
|
43398
|
+
}
|
|
42668
43399
|
out.info("");
|
|
42669
43400
|
return { exitCode: 0 };
|
|
42670
43401
|
}
|