@plinth-music/cli 0.15.0 → 0.16.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 +32 -0
- package/README.md +2 -2
- package/dist/build-stamp.json +2 -2
- package/dist/cli.js +2162 -738
- 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.16.0",
|
|
7588
7588
|
description: "Plinth capstone sync daemon and CLI",
|
|
7589
7589
|
type: "module",
|
|
7590
7590
|
private: false,
|
|
@@ -22333,6 +22333,7 @@ var ArtistLiveSchema = exports_external.object({
|
|
|
22333
22333
|
name: exports_external.string(),
|
|
22334
22334
|
body: exports_external.string().nullable(),
|
|
22335
22335
|
memory: exports_external.string().nullable().default(null),
|
|
22336
|
+
memory_generated: exports_external.boolean().default(false),
|
|
22336
22337
|
status: exports_external.string(),
|
|
22337
22338
|
bio: exports_external.string().nullable(),
|
|
22338
22339
|
notes: exports_external.string().nullable(),
|
|
@@ -22373,7 +22374,8 @@ var SERVER_AUTHORITATIVE_KEYS = new Set([
|
|
|
22373
22374
|
"color",
|
|
22374
22375
|
"spotify_artist_id",
|
|
22375
22376
|
"soundcharts_uuid",
|
|
22376
|
-
"sort_order"
|
|
22377
|
+
"sort_order",
|
|
22378
|
+
"memory_generated"
|
|
22377
22379
|
]);
|
|
22378
22380
|
var WIRE_EDITABLE_TOP_LEVEL_KEYS = new Set([
|
|
22379
22381
|
"name",
|
|
@@ -23688,6 +23690,16 @@ function readArtistMemoryFile(workspaceSlug, entitySlug) {
|
|
|
23688
23690
|
throw err;
|
|
23689
23691
|
}
|
|
23690
23692
|
}
|
|
23693
|
+
function readArtistAnchorFile(workspaceSlug, entitySlug) {
|
|
23694
|
+
const target = entityFilePath(workspaceSlug, "artists", entitySlug);
|
|
23695
|
+
try {
|
|
23696
|
+
return readFileSync(target, "utf8");
|
|
23697
|
+
} catch (err) {
|
|
23698
|
+
if (err?.code === "ENOENT")
|
|
23699
|
+
return null;
|
|
23700
|
+
throw err;
|
|
23701
|
+
}
|
|
23702
|
+
}
|
|
23691
23703
|
function deleteArtistMemoryFile(workspaceSlug, entitySlug) {
|
|
23692
23704
|
const target = artistMemoryFilePath(workspaceSlug, entitySlug);
|
|
23693
23705
|
try {
|
|
@@ -23699,6 +23711,113 @@ function deleteArtistMemoryFile(workspaceSlug, entitySlug) {
|
|
|
23699
23711
|
throw err;
|
|
23700
23712
|
}
|
|
23701
23713
|
}
|
|
23714
|
+
var MEMORY_FACTS_DIR = "memory";
|
|
23715
|
+
var MEMORY_FACTS_PRIVATE_SEGMENT = "private";
|
|
23716
|
+
var MEMORY_FACT_SLUG_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
23717
|
+
var MEMORY_FACT_SLUG_MAX = 64;
|
|
23718
|
+
function assertFactSlug(slug) {
|
|
23719
|
+
if (!MEMORY_FACT_SLUG_RE.test(slug) || slug.length > MEMORY_FACT_SLUG_MAX) {
|
|
23720
|
+
throw new MirrorPathError(`Invalid memory fact slug: ${JSON.stringify(slug)}`);
|
|
23721
|
+
}
|
|
23722
|
+
}
|
|
23723
|
+
function memoryFactRelPath(pile, slug) {
|
|
23724
|
+
assertFactSlug(slug);
|
|
23725
|
+
return pile === "private" ? `${MEMORY_FACTS_DIR}/${MEMORY_FACTS_PRIVATE_SEGMENT}/${slug}.md` : `${MEMORY_FACTS_DIR}/${slug}.md`;
|
|
23726
|
+
}
|
|
23727
|
+
function parseMemoryFactRelPath(rel) {
|
|
23728
|
+
const posix = rel.split(sep).join("/");
|
|
23729
|
+
if (!posix.startsWith(`${MEMORY_FACTS_DIR}/`) || !posix.endsWith(".md")) {
|
|
23730
|
+
return null;
|
|
23731
|
+
}
|
|
23732
|
+
const tail = posix.slice(MEMORY_FACTS_DIR.length + 1, -".md".length);
|
|
23733
|
+
const privatePrefix = `${MEMORY_FACTS_PRIVATE_SEGMENT}/`;
|
|
23734
|
+
if (tail.startsWith(privatePrefix)) {
|
|
23735
|
+
const slug = tail.slice(privatePrefix.length);
|
|
23736
|
+
return MEMORY_FACT_SLUG_RE.test(slug) && slug.length <= MEMORY_FACT_SLUG_MAX ? { pile: "private", slug } : null;
|
|
23737
|
+
}
|
|
23738
|
+
return MEMORY_FACT_SLUG_RE.test(tail) && tail.length <= MEMORY_FACT_SLUG_MAX ? { pile: "shared", slug: tail } : null;
|
|
23739
|
+
}
|
|
23740
|
+
function memoryFactFilePath(workspaceSlug, pile, slug) {
|
|
23741
|
+
const ws = workspaceMirrorPath(workspaceSlug);
|
|
23742
|
+
const posix = memoryFactRelPath(pile, slug);
|
|
23743
|
+
const native = sep === "/" ? posix : posix.split("/").join(sep);
|
|
23744
|
+
const target = resolve(join2(ws, native));
|
|
23745
|
+
const prefix = join2(ws, MEMORY_FACTS_DIR) + sep;
|
|
23746
|
+
if (!target.startsWith(prefix)) {
|
|
23747
|
+
throw new MirrorPathError(`Resolved memory fact path escapes the memory subtree`);
|
|
23748
|
+
}
|
|
23749
|
+
assertNoSymlinkedComponent(ws, target);
|
|
23750
|
+
return target;
|
|
23751
|
+
}
|
|
23752
|
+
function memoryFactsSubtreeRoot(workspaceSlug) {
|
|
23753
|
+
const ws = workspaceMirrorPath(workspaceSlug);
|
|
23754
|
+
const target = join2(ws, MEMORY_FACTS_DIR);
|
|
23755
|
+
assertNoSymlinkedComponent(ws, target);
|
|
23756
|
+
return target;
|
|
23757
|
+
}
|
|
23758
|
+
function writeMemoryFactFile(workspaceSlug, pile, slug, content) {
|
|
23759
|
+
const target = memoryFactFilePath(workspaceSlug, pile, slug);
|
|
23760
|
+
const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
|
|
23761
|
+
mkdirSync(dirname(target), { recursive: true, mode: 448 });
|
|
23762
|
+
writeFileSync(tmp, content, { mode: 384 });
|
|
23763
|
+
try {
|
|
23764
|
+
renameSync(tmp, target);
|
|
23765
|
+
} catch (err) {
|
|
23766
|
+
try {
|
|
23767
|
+
unlinkSync(tmp);
|
|
23768
|
+
} catch {}
|
|
23769
|
+
throw err;
|
|
23770
|
+
}
|
|
23771
|
+
}
|
|
23772
|
+
function readMemoryFactFile(workspaceSlug, pile, slug) {
|
|
23773
|
+
try {
|
|
23774
|
+
return readFileSync(memoryFactFilePath(workspaceSlug, pile, slug), "utf8");
|
|
23775
|
+
} catch (err) {
|
|
23776
|
+
if (err?.code === "ENOENT")
|
|
23777
|
+
return null;
|
|
23778
|
+
throw err;
|
|
23779
|
+
}
|
|
23780
|
+
}
|
|
23781
|
+
function deleteMemoryFactFile(workspaceSlug, pile, slug) {
|
|
23782
|
+
const target = memoryFactFilePath(workspaceSlug, pile, slug);
|
|
23783
|
+
try {
|
|
23784
|
+
unlinkSync(target);
|
|
23785
|
+
return true;
|
|
23786
|
+
} catch (err) {
|
|
23787
|
+
if (err?.code === "ENOENT")
|
|
23788
|
+
return false;
|
|
23789
|
+
throw err;
|
|
23790
|
+
}
|
|
23791
|
+
}
|
|
23792
|
+
function listMemoryFactFiles(workspaceSlug) {
|
|
23793
|
+
let root;
|
|
23794
|
+
try {
|
|
23795
|
+
root = memoryFactsSubtreeRoot(workspaceSlug);
|
|
23796
|
+
} catch {
|
|
23797
|
+
return [];
|
|
23798
|
+
}
|
|
23799
|
+
const found = [];
|
|
23800
|
+
const read = (dir, pile) => {
|
|
23801
|
+
let entries;
|
|
23802
|
+
try {
|
|
23803
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
23804
|
+
} catch {
|
|
23805
|
+
return;
|
|
23806
|
+
}
|
|
23807
|
+
for (const e of entries) {
|
|
23808
|
+
if (!e.isFile() || !e.name.endsWith(".md"))
|
|
23809
|
+
continue;
|
|
23810
|
+
const slug = e.name.slice(0, -3);
|
|
23811
|
+
if (!MEMORY_FACT_SLUG_RE.test(slug) || slug.length > MEMORY_FACT_SLUG_MAX) {
|
|
23812
|
+
continue;
|
|
23813
|
+
}
|
|
23814
|
+
found.push({ pile, slug });
|
|
23815
|
+
}
|
|
23816
|
+
};
|
|
23817
|
+
read(root, "shared");
|
|
23818
|
+
read(join2(root, MEMORY_FACTS_PRIVATE_SEGMENT), "private");
|
|
23819
|
+
return found.sort((a, b) => a.pile === b.pile ? a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0 : a.pile === "shared" ? -1 : 1);
|
|
23820
|
+
}
|
|
23702
23821
|
var FILES_SUBTREE_DIR = "files";
|
|
23703
23822
|
var FILES_CONTROL_CHAR_RE = new RegExp("[\\x00-\\x1f\\x7f]");
|
|
23704
23823
|
function validateFilesRelPath(relPath) {
|
|
@@ -25910,6 +26029,9 @@ var COMMAND_BAR_BYTES = 24000;
|
|
|
25910
26029
|
var INDEX_LINE_MAX_BYTES = 300;
|
|
25911
26030
|
var MAX_NAMED_FACT_STORES = 5;
|
|
25912
26031
|
var MAX_NAMED_LONG_INDEX_LINES = 3;
|
|
26032
|
+
var CONTEXT_BLOCK_BAR_CHARS = 2000;
|
|
26033
|
+
var CURRENT_STATE_AS_OF_MAX_DAYS = 30;
|
|
26034
|
+
var MAX_NAMED_CONTEXT_FINDINGS = 5;
|
|
25913
26035
|
function countRuleShapedLines(body) {
|
|
25914
26036
|
let count = 0;
|
|
25915
26037
|
for (const line of body.split(`
|
|
@@ -25926,531 +26048,15 @@ function countParagraphs(body) {
|
|
|
25926
26048
|
return body.split(/\n\s*\n/).map((p) => p.trim()).filter((p) => p.length > 0).length;
|
|
25927
26049
|
}
|
|
25928
26050
|
|
|
25929
|
-
// src/lib/context/grounding.ts
|
|
25930
|
-
var WORKSPACE_RULES_FILENAME = "workspace-rules.md";
|
|
25931
|
-
var HOUSE_STYLE_LEGACY_FILENAME = "house-style.md";
|
|
25932
|
-
var WORKSPACE_MEMORY_FILENAME = "workspace-memory.md";
|
|
25933
|
-
var USER_MEMORY_FILENAME = "user-memory.md";
|
|
25934
|
-
var VOICE_FILENAME = "voice.md";
|
|
25935
|
-
var WORKSPACE_RULES_PRIVATE_HEADING = "## Workspace-specific (private, never promotes)";
|
|
25936
|
-
var WORKSPACE_RULES_GENERAL_HEADING = "## Generalisable manager-craft (may promote)";
|
|
25937
|
-
var WORKSPACE_RULES_SECTION_HEADINGS = [
|
|
25938
|
-
WORKSPACE_RULES_PRIVATE_HEADING,
|
|
25939
|
-
WORKSPACE_RULES_GENERAL_HEADING
|
|
25940
|
-
];
|
|
25941
|
-
function extractWorkspaceRules(raw) {
|
|
25942
|
-
if (raw == null)
|
|
25943
|
-
return null;
|
|
25944
|
-
const withoutComments = raw.replace(/<!--[\s\S]*?-->/g, "").replace(/<!--[\s\S]*$/, "");
|
|
25945
|
-
const stripped = withoutComments.split(`
|
|
25946
|
-
`).filter((line) => !WORKSPACE_RULES_SECTION_HEADINGS.includes(line.trim())).join(`
|
|
25947
|
-
`).trim();
|
|
25948
|
-
return stripped.length > 0 ? stripped : null;
|
|
25949
|
-
}
|
|
25950
|
-
function extractWorkspaceMemory(raw) {
|
|
25951
|
-
if (raw == null)
|
|
25952
|
-
return null;
|
|
25953
|
-
const stripped = raw.replace(/<!--[\s\S]*?-->/g, "").replace(/<!--[\s\S]*$/, "").trim();
|
|
25954
|
-
return stripped.length > 0 ? stripped : null;
|
|
25955
|
-
}
|
|
25956
|
-
function extractUserMemory(raw) {
|
|
25957
|
-
if (raw == null)
|
|
25958
|
-
return null;
|
|
25959
|
-
const stripped = raw.replace(/<!--[\s\S]*?-->/g, "").replace(/<!--[\s\S]*$/, "").trim();
|
|
25960
|
-
return stripped.length > 0 ? stripped : null;
|
|
25961
|
-
}
|
|
25962
|
-
function extractVoiceProfile(raw) {
|
|
25963
|
-
if (raw == null)
|
|
25964
|
-
return null;
|
|
25965
|
-
const stripped = raw.replace(/<!--[\s\S]*?-->/g, "").trim();
|
|
25966
|
-
return stripped.length > 0 ? stripped : null;
|
|
25967
|
-
}
|
|
25968
|
-
function daysBetween(from, to) {
|
|
25969
|
-
const parse6 = (d) => {
|
|
25970
|
-
const parts = d.split("-");
|
|
25971
|
-
return Date.UTC(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2]));
|
|
25972
|
-
};
|
|
25973
|
-
return Math.round((parse6(to) - parse6(from)) / 86400000);
|
|
25974
|
-
}
|
|
25975
|
-
function renderFreshnessStamp(today, updatedAt) {
|
|
25976
|
-
const UNKNOWN = `The age of this index is not known here, so treat its lines as undated ` + `rather than as fresh.`;
|
|
25977
|
-
if (updatedAt == null)
|
|
25978
|
-
return UNKNOWN;
|
|
25979
|
-
const banked = new Date(updatedAt);
|
|
25980
|
-
if (Number.isNaN(banked.getTime()))
|
|
25981
|
-
return UNKNOWN;
|
|
25982
|
-
const bankedDay = formatLocalDate(banked);
|
|
25983
|
-
const days = Math.max(0, daysBetween(bankedDay, today));
|
|
25984
|
-
const age = days === 0 ? "today" : days === 1 ? "1 day ago" : `${days} days ago`;
|
|
25985
|
-
return `This index was last updated on ${bankedDay}, ${age}.`;
|
|
25986
|
-
}
|
|
25987
|
-
var GROUNDING_BUDGET_BYTES = 20000;
|
|
25988
|
-
var GROUNDING_CATEGORY_ORDER = [
|
|
25989
|
-
"grounding",
|
|
25990
|
-
"identity",
|
|
25991
|
-
"budgetNote",
|
|
25992
|
-
"sizeNote",
|
|
25993
|
-
"voice",
|
|
25994
|
-
"rules",
|
|
25995
|
-
"workspaceFacts",
|
|
25996
|
-
"personalFacts"
|
|
25997
|
-
];
|
|
25998
|
-
var GROUNDING_CATEGORY_LABELS = {
|
|
25999
|
-
grounding: "Plinth's own grounding",
|
|
26000
|
-
identity: "who you are",
|
|
26001
|
-
budgetNote: "the trim notes",
|
|
26002
|
-
sizeNote: "the size warning",
|
|
26003
|
-
voice: "voice pointer",
|
|
26004
|
-
rules: "this workspace's rules",
|
|
26005
|
-
workspaceFacts: "workspace facts",
|
|
26006
|
-
personalFacts: "your own facts"
|
|
26007
|
-
};
|
|
26008
|
-
var MEMBER_EDITABLE_CATEGORIES = [
|
|
26009
|
-
"rules",
|
|
26010
|
-
"workspaceFacts",
|
|
26011
|
-
"personalFacts"
|
|
26012
|
-
];
|
|
26013
|
-
function segmentBytes(lines, cats) {
|
|
26014
|
-
const out = {
|
|
26015
|
-
grounding: 0,
|
|
26016
|
-
identity: 0,
|
|
26017
|
-
rules: 0,
|
|
26018
|
-
workspaceFacts: 0,
|
|
26019
|
-
personalFacts: 0,
|
|
26020
|
-
voice: 0,
|
|
26021
|
-
budgetNote: 0,
|
|
26022
|
-
sizeNote: 0
|
|
26023
|
-
};
|
|
26024
|
-
lines.forEach((text, i) => {
|
|
26025
|
-
const cat = cats[i];
|
|
26026
|
-
out[cat] += Buffer.byteLength(text) + (i < lines.length - 1 ? 1 : 0);
|
|
26027
|
-
});
|
|
26028
|
-
return out;
|
|
26029
|
-
}
|
|
26030
|
-
var IDENTITY_LINE_MAX_BYTES = 300;
|
|
26031
|
-
function renderMemberIdentity(member, workspaceSlug) {
|
|
26032
|
-
if (member === null)
|
|
26033
|
-
return null;
|
|
26034
|
-
const tail = `. Tasks you create are assigned to this member unless told otherwise.`;
|
|
26035
|
-
const budget = IDENTITY_LINE_MAX_BYTES - Buffer.byteLength(tail);
|
|
26036
|
-
const email3 = oneLine(member.email);
|
|
26037
|
-
const build = (withName, withRole, withZone) => {
|
|
26038
|
-
const who = withName && member.displayName ? `${oneLine(member.displayName)} (${email3})` : email3;
|
|
26039
|
-
const clauses = [`You are working for ${who}`];
|
|
26040
|
-
if (withRole && member.role)
|
|
26041
|
-
clauses.push(`role ${oneLine(member.role)}`);
|
|
26042
|
-
if (workspaceSlug !== null)
|
|
26043
|
-
clauses.push(`in workspace ${workspaceSlug}`);
|
|
26044
|
-
if (withZone && member.timezone)
|
|
26045
|
-
clauses.push(`timezone ${oneLine(member.timezone)}`);
|
|
26046
|
-
return clauses.join(", ");
|
|
26047
|
-
};
|
|
26048
|
-
const candidates = [
|
|
26049
|
-
build(true, true, true),
|
|
26050
|
-
build(true, true, false),
|
|
26051
|
-
build(true, false, false),
|
|
26052
|
-
build(false, false, false)
|
|
26053
|
-
];
|
|
26054
|
-
if (member.displayName) {
|
|
26055
|
-
const slugClause = workspaceSlug === null ? `` : `, in workspace ${workspaceSlug}`;
|
|
26056
|
-
candidates.push(`You are working for ${oneLine(member.displayName)}${slugClause}`);
|
|
26057
|
-
}
|
|
26058
|
-
const head = candidates.find((c) => Buffer.byteLength(c) <= budget);
|
|
26059
|
-
if (head === undefined)
|
|
26060
|
-
return null;
|
|
26061
|
-
return head + tail;
|
|
26062
|
-
}
|
|
26063
|
-
function oneLine(value) {
|
|
26064
|
-
return value.replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim();
|
|
26065
|
-
}
|
|
26066
|
-
function shouldOfferOnboarding(rosterIsEmpty, member) {
|
|
26067
|
-
if (member?.onboardingCompletedAt)
|
|
26068
|
-
return false;
|
|
26069
|
-
return rosterIsEmpty;
|
|
26070
|
-
}
|
|
26071
|
-
function bankedDate(line) {
|
|
26072
|
-
const matches = line.match(/\d{4}-\d{2}-\d{2}/g);
|
|
26073
|
-
if (matches === null || matches.length === 0)
|
|
26074
|
-
return null;
|
|
26075
|
-
let latest = matches[0];
|
|
26076
|
-
for (const m of matches)
|
|
26077
|
-
if (m > latest)
|
|
26078
|
-
latest = m;
|
|
26079
|
-
return latest;
|
|
26080
|
-
}
|
|
26081
|
-
var POINTER_RE = /\[\[[^\]]+\]\]/g;
|
|
26082
|
-
var TITLE_TRUNCATION_MARK = " …";
|
|
26083
|
-
function factTitle(fact) {
|
|
26084
|
-
const firstLine = fact.split(`
|
|
26085
|
-
`)[0];
|
|
26086
|
-
const match = /[.?!](\s|$)/.exec(firstLine);
|
|
26087
|
-
const sentence = match === null ? firstLine : firstLine.slice(0, match.index + 1);
|
|
26088
|
-
const missing = [...fact.matchAll(POINTER_RE)].map((m) => m[0]).filter((p) => !sentence.includes(p));
|
|
26089
|
-
const unique = [...new Set(missing)];
|
|
26090
|
-
const named = unique.length === 0 ? sentence : `${sentence} ${unique.join(" ")}`;
|
|
26091
|
-
const truncated = named !== fact;
|
|
26092
|
-
return { text: truncated ? `${named}${TITLE_TRUNCATION_MARK}` : named, truncated };
|
|
26093
|
-
}
|
|
26094
|
-
function rankFacts(facts) {
|
|
26095
|
-
return facts.map((line, index) => ({ line, index, date: bankedDate(line) })).sort((a, b) => {
|
|
26096
|
-
const ad = a.date ?? "";
|
|
26097
|
-
const bd = b.date ?? "";
|
|
26098
|
-
if (ad !== bd)
|
|
26099
|
-
return ad < bd ? 1 : -1;
|
|
26100
|
-
return b.index - a.index;
|
|
26101
|
-
}).map(({ index, line }) => ({ index, line }));
|
|
26102
|
-
}
|
|
26103
|
-
function groupFacts(body) {
|
|
26104
|
-
const facts = [];
|
|
26105
|
-
for (const line of body.split(`
|
|
26106
|
-
`)) {
|
|
26107
|
-
if (line.trim().length === 0)
|
|
26108
|
-
continue;
|
|
26109
|
-
if (/^\s/.test(line) && facts.length > 0) {
|
|
26110
|
-
facts[facts.length - 1] = `${facts[facts.length - 1]}
|
|
26111
|
-
${line}`;
|
|
26112
|
-
} else {
|
|
26113
|
-
facts.push(line);
|
|
26114
|
-
}
|
|
26115
|
-
}
|
|
26116
|
-
return facts;
|
|
26117
|
-
}
|
|
26118
|
-
function selectIndexContent(body, capBytes) {
|
|
26119
|
-
const lines = groupFacts(body);
|
|
26120
|
-
if (Buffer.byteLength(body) <= capBytes) {
|
|
26121
|
-
return { text: body, titles: "", kept: lines.length, named: 0, total: lines.length };
|
|
26122
|
-
}
|
|
26123
|
-
const ranked = rankFacts(lines);
|
|
26124
|
-
const titleOf = new Map;
|
|
26125
|
-
const info2 = new Map;
|
|
26126
|
-
for (const { index, line } of ranked) {
|
|
26127
|
-
const t = factTitle(line);
|
|
26128
|
-
info2.set(index, t);
|
|
26129
|
-
titleOf.set(index, t.text);
|
|
26130
|
-
}
|
|
26131
|
-
const carried = new Map;
|
|
26132
|
-
const used = () => {
|
|
26133
|
-
let bytes = 0;
|
|
26134
|
-
let bodies = 0;
|
|
26135
|
-
let titles = 0;
|
|
26136
|
-
for (const [index, kind] of carried) {
|
|
26137
|
-
const line = lines[index];
|
|
26138
|
-
if (kind === "body") {
|
|
26139
|
-
bytes += Buffer.byteLength(line);
|
|
26140
|
-
bodies++;
|
|
26141
|
-
} else {
|
|
26142
|
-
bytes += Buffer.byteLength(titleOf.get(index));
|
|
26143
|
-
titles++;
|
|
26144
|
-
}
|
|
26145
|
-
}
|
|
26146
|
-
return bytes + Math.max(0, bodies - 1) + Math.max(0, titles - 1);
|
|
26147
|
-
};
|
|
26148
|
-
for (const { index } of ranked) {
|
|
26149
|
-
if (info2.get(index)?.truncated !== true)
|
|
26150
|
-
continue;
|
|
26151
|
-
carried.set(index, "title");
|
|
26152
|
-
if (used() > capBytes)
|
|
26153
|
-
carried.delete(index);
|
|
26154
|
-
}
|
|
26155
|
-
for (const { index } of ranked) {
|
|
26156
|
-
const was = carried.get(index);
|
|
26157
|
-
carried.set(index, "body");
|
|
26158
|
-
if (used() > capBytes) {
|
|
26159
|
-
if (was === undefined)
|
|
26160
|
-
carried.delete(index);
|
|
26161
|
-
else
|
|
26162
|
-
carried.set(index, was);
|
|
26163
|
-
break;
|
|
26164
|
-
}
|
|
26165
|
-
}
|
|
26166
|
-
const inFileOrder = (kind) => [...carried.entries()].filter(([, k]) => k === kind).map(([i]) => i).sort((a, b) => a - b);
|
|
26167
|
-
const bodyIdx = inFileOrder("body");
|
|
26168
|
-
const titleIdx = inFileOrder("title");
|
|
26169
|
-
return {
|
|
26170
|
-
text: bodyIdx.map((i) => lines[i]).join(`
|
|
26171
|
-
`),
|
|
26172
|
-
titles: titleIdx.map((i) => titleOf.get(i)).join(`
|
|
26173
|
-
`),
|
|
26174
|
-
kept: bodyIdx.length,
|
|
26175
|
-
named: titleIdx.length,
|
|
26176
|
-
total: lines.length
|
|
26177
|
-
};
|
|
26178
|
-
}
|
|
26179
|
-
function trimmedIndexNote(kept, total, filename) {
|
|
26180
|
-
if (kept === 0) {
|
|
26181
|
-
return `None of this index is shown here: it is over the size this injection ` + `carries, so it lives in \`${filename}\` at the mirror root and nowhere ` + `else. READ THAT FILE IN FULL before you rely on what this workspace has ` + `taught you, and before you answer that no fact covers something.`;
|
|
26182
|
-
}
|
|
26183
|
-
return `Those are the ${kept} most recently banked of ${total} facts, not all of ` + `them: this injection carries a fixed budget and the rest of the index did ` + `not fit. The other ${total - kept} are in \`${filename}\` at the mirror ` + `root. READ THAT FILE IN FULL before you rely on what this workspace has ` + `taught you, and before you answer that no fact covers something. An index ` + `that is silent because it was trimmed reads exactly like one that has ` + `settled a subject.`;
|
|
26184
|
-
}
|
|
26185
|
-
function titlesLeadIn(kept, named, unnamed, total, filename) {
|
|
26186
|
-
const parts = [`${kept} above in full`];
|
|
26187
|
-
parts.push(`${named} below as ${named === 1 ? "its opening" : "their openings"} only`);
|
|
26188
|
-
if (unnamed > 0)
|
|
26189
|
-
parts.push(`${unnamed} not shown at all`);
|
|
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.`;
|
|
26191
|
-
}
|
|
26192
|
-
function composeGroundingBlock(today, workspaceRules, workspaceMemory, userMemory, freshness, rosterIsEmpty, voiceProfile, voiceMalformed, workspaceSlug, workspaceMemoryNote, userMemoryNote, member, workspaceTitles = null, userTitles = null, filesPastBar = 0) {
|
|
26193
|
-
const gateFlag = workspaceSlug === null ? `--workspace="<this workspace>"` : `--workspace ${workspaceSlug}`;
|
|
26194
|
-
const identity = renderMemberIdentity(member, workspaceSlug);
|
|
26195
|
-
const block = [];
|
|
26196
|
-
const cats = [];
|
|
26197
|
-
const add = (cat, ...lines) => {
|
|
26198
|
-
for (const line of lines) {
|
|
26199
|
-
block.push(line);
|
|
26200
|
-
cats.push(cat);
|
|
26201
|
-
}
|
|
26202
|
-
};
|
|
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.`);
|
|
26204
|
-
if (identity !== null)
|
|
26205
|
-
add("identity", ``, identity);
|
|
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.`);
|
|
26207
|
-
if (workspaceRules === null && voiceProfile === null) {
|
|
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.`);
|
|
26209
|
-
}
|
|
26210
|
-
add("grounding", ``, `Read the draft back before you say it is ready, and read what the tool actually ` + `produced rather than what you sent it. Do this every time you write one, not ` + `once a session: a later edit can undo what an earlier check confirmed. A ` + `draft surface takes your text and renders it, and it may escape what you ` + `wrote, wrap it, quote an earlier message underneath it, thread it somewhere ` + `you did not intend, or leave the first one in place and add a second. What ` + `the person opens is the rendered result, not your input, and those are not ` + `always the same thing. So fetch the staged draft, look at its body, its ` + `recipients, the thread it landed on and that there is one of it, and only ` + `then say it is ready. Separately, and once per session is enough for this ` + `part: if a tool's own description tells you how it handles your text, treat ` + `that as a claim like any other and check it against what came out. A tool ` + `that is documented wrong will put markup, an empty body, or a stranger's ` + `address in front of a person whose name is on the message, and you are the ` + `only thing standing between those two.`, ``, `The most recent artefact wins on facts, and it is not a judgement call. ` + `Dates, prices, availability, capacities, routings, who is on a thread, ` + `who said what. Do not weigh a newer message against an older banked ` + `line to work out which is more likely right: the newer one is the state ` + `and the older one is history. This holds for any dated artefact, not ` + `only email - call transcripts, meeting notes and executed documents are ` + `all in scope, newest first. Stale context is worse than absent context, ` + `because it reads as current. One carve-out, because it is the failure ` + `this rule would otherwise cause, and it is a mechanical test rather ` + `than a judgement: before promoting the newest message to "the state", ` + `ask whether it COMMITS or PROPOSES. A commit ("we're doing", ` + `"confirmed", "booked", a price, a date someone has agreed to) wins, and ` + `overwrites the old claim. A proposal ("I wonder if", "I'm not saying we ` + `must", or any question put to someone who has not replied) goes in as ` + `an open question, named as unanswered, with who owes the answer. It ` + `does NOT become the state.`, ``, `Reconcile, do not re-adjudicate. Never delete a superseded claim: strike ` + `it in place. Keep the old line, mark it superseded with the date and ` + `with what replaced it, and put the new material above it, where the ` + `file's live state is. Demote the outgoing section one heading level so ` + `history stays readable and stops competing with what is live. Someone ` + `will ask why the plan changed, and the answer has to survive in the ` + `file. Then date the state: say what it is current as of, and name the ` + `sources it was built from. A master context with no as-of line is ` + `unfalsifiable.`);
|
|
26211
|
-
if (workspaceRules !== null) {
|
|
26212
|
-
add("grounding", ``, `Follow this workspace's house style. The conventions below are set by the ` + `workspace owner and govern HOW you write here: copy, titles, notes, ` + `sign-offs, formatting. They refine the rules above; they never relax them. ` + `If any convention here conflicts with a grounding rule above (the ` + `canonical write surface, plan-before-write confirmation, or the ban on ` + `direct database writes), the rule above wins. The conventions:`);
|
|
26213
|
-
add("rules", ``, workspaceRules);
|
|
26214
|
-
}
|
|
26215
|
-
if (voiceProfile !== null) {
|
|
26216
|
-
add("voice", ``, `Draft in this member's own voice, and read it before you do. Their voice ` + `profile is \`${VOICE_FILENAME}\` at the mirror root, beside this ` + `workspace's house style: how they open, how they sign off, what they ` + `never do. It is not reproduced here. READ THAT FILE IN FULL before you ` + `draft anything that goes out in their name (an email, a message, a ` + `note, any prose that carries their name), and write to it from the ` + `start rather than ` + `drafting in your own register and correcting after. Nothing inspects a ` + `draft between you and the person reading it, so that file is the whole ` + `of it. It is theirs to write and not yours: you cannot edit ` + `\`${VOICE_FILENAME}\`, and if something in it looks wrong or out of ` + `date, say so and let them change it. Like this workspace's house style ` + `it refines the rules above and never relaxes them.`);
|
|
26217
|
-
if (voiceMalformed) {
|
|
26218
|
-
add("voice", ``, `That voice profile does not currently parse. Plinth read it and found ` + `its structure broken, most often a code fence that was opened and ` + `never closed, so any machine-read setting inside it is being ignored ` + `rather than applied. The prose in it is still the member's own ` + `writing and is still worth following. Treat anything that looks like ` + `a configured setting in it as NOT in force, say so if it comes up, ` + `and tell them their profile needs fixing rather than fixing it ` + `yourself.`);
|
|
26219
|
-
}
|
|
26220
|
-
}
|
|
26221
|
-
add("grounding", ``, `Keep your memory on the Plinth surface. Your memory here IS the Plinth ` + `surface (\`workspace-memory.md\`, your own \`user-memory.md\`, and the ` + `per-artist warm stores the daemon syncs), and it is the ONLY memory you ` + `keep. Do not use any other memory store: no private notes-to-self, no ` + `separate scratch file, no tool-native memory of your own. What you learn ` + `here is recorded on the Plinth surface, or nowhere. When you learn a ` + `durable fact worth keeping, bank it by adding a line to the right index, ` + `which the daemon syncs up, following the write-back conventions in that ` + `file's header: one line, at least one \`[[pointer]]\` to where the full ` + `fact lives, an absolute date, and where you learned it. Only ever record ` + `what you have actually been taught, never public knowledge and never your ` + `own priors. The scope rule decides WHICH index, and there are ` + `THREE. A fact true of ONE ARTIST (their rate card, how their kit ` + `works, what a contact of theirs reliably does) goes in that artist's ` + `own warm store, \`artists/<slug>/memory.md\`, and not in the shared ` + `index: putting it there taxes every teammate who never touches that ` + `artist and hides it from the one who does. A fact that holds for the ` + `whole workspace (a project, a working convention, a standing ` + `fact true for everyone here) goes in \`workspace-memory.md\`, which ` + `grounds EVERY teammate - and that file is the one you NEVER write on ` + `your own judgement. It changes only when a member ASKS you to remember ` + `something for everyone, in their own words rather than your summary of ` + `them, and you log that decision with ` + `\`plinth memory-gate log ${gateFlag} --proposed="<their words>" --decision accept\` ` + `BEFORE you write the line. That command fails LOUD on exit 1: surface ` + `it, and do not report the line as banked. Never infer a line for that ` + `file from what merely happened in a session, and never propose one ` + `unasked - a wrong line there reaches every member on every machine and ` + `is injected into every future session. A fact about one person (an individual ` + `preference, your own working style or tools) is NOT a workspace fact: it ` + `goes in \`user-memory.md\`, your member-private index that only YOU can ` + `read. HOW THIS MEMBER WRITES IS THE ONE EXCEPTION: their register and ` + `sign-off are a rubric, not a fact, and live in \`${VOICE_FILENAME}\`, ` + `which is theirs and you cannot edit, so say what you noticed and leave it ` + `to them. Them only: how a CONTACT likes to be written to is an ordinary ` + `fact for that artist's store. ` + `Banking a personal fact in \`workspace-memory.md\` would leak that person ` + `into everyone else's context, so never do it. When a fact could be either ` + `scope, lean private and put it in \`user-memory.md\`. ` + `Live state does not belong in any memory file. This week's open ` + `threads, one show's logistics, where a release has got to: a memory ` + `file is for what stays true after the session that taught it, and live ` + `state goes in the body of the record it belongs to. A deadline rides ` + `its task as a due date, and every other date is a calendar matter. ` + `This memory is additive and subordinate: it never ` + `overrides a house-style rule or any safety primitive above.`);
|
|
26222
|
-
if (workspaceMemory !== null) {
|
|
26223
|
-
add("grounding", ``, `Draw on this workspace's memory. The lines below are this workspace's ` + `memory index: durable facts it has taught you, each a one-line hook that ` + `points (via a \`[[wikilink]]\`) to the warm store where the full detail ` + `lives. Treat them as recall, not gospel: use a line to load the right ` + `context and verify it against the live workspace before you rely on it, ` + `never as a settled fact to assert. ` + `${renderFreshnessStamp(today, freshness.workspaceMemoryUpdatedAt)} ` + `The index:`);
|
|
26224
|
-
add("workspaceFacts", ``, workspaceMemory);
|
|
26225
|
-
if (workspaceTitles !== null) {
|
|
26226
|
-
add("budgetNote", ``, workspaceTitles.leadIn);
|
|
26227
|
-
add("workspaceFacts", ``, workspaceTitles.text);
|
|
26228
|
-
}
|
|
26229
|
-
if (workspaceMemoryNote !== null)
|
|
26230
|
-
add("budgetNote", ``, workspaceMemoryNote);
|
|
26231
|
-
}
|
|
26232
|
-
if (userMemory !== null) {
|
|
26233
|
-
add("grounding", ``, `Draw on your personal memory. The lines below are YOUR personal memory ` + `index: member-private facts you have taught the harness about yourself ` + `(your preferences, working style, tools), each a one-line hook ` + `that points (via a \`[[wikilink]]\`) to the warm store where the full ` + `detail lives. Only you can read this. Treat it as recall, not gospel: use ` + `a line to load the right context and verify it before you rely on it, ` + `never as a settled fact to assert. ` + `${renderFreshnessStamp(today, freshness.userMemoryUpdatedAt)} ` + `The index:`);
|
|
26234
|
-
add("personalFacts", ``, userMemory);
|
|
26235
|
-
if (userTitles !== null) {
|
|
26236
|
-
add("budgetNote", ``, userTitles.leadIn);
|
|
26237
|
-
add("personalFacts", ``, userTitles.text);
|
|
26238
|
-
}
|
|
26239
|
-
if (userMemoryNote !== null)
|
|
26240
|
-
add("budgetNote", ``, userMemoryNote);
|
|
26241
|
-
}
|
|
26242
|
-
if (workspaceMemory !== null || userMemory !== null) {
|
|
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.`);
|
|
26244
|
-
}
|
|
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
|
-
}
|
|
26249
|
-
if (shouldOfferOnboarding(rosterIsEmpty, member)) {
|
|
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.`);
|
|
26251
|
-
}
|
|
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
|
-
};
|
|
26261
|
-
}
|
|
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);
|
|
26264
|
-
const whole = compose(workspaceMemory, userMemory, null, null);
|
|
26265
|
-
const untrimmed = () => {
|
|
26266
|
-
const all = (body) => {
|
|
26267
|
-
const n = groupFacts(body).length;
|
|
26268
|
-
return { text: body, titles: "", kept: n, named: 0, total: n };
|
|
26269
|
-
};
|
|
26270
|
-
return {
|
|
26271
|
-
block: whole.text,
|
|
26272
|
-
split: whole.split,
|
|
26273
|
-
groundingParagraphs: whole.groundingParagraphs,
|
|
26274
|
-
groundingRuleShaped: whole.groundingRuleShaped,
|
|
26275
|
-
picks: {
|
|
26276
|
-
wmPick: workspaceMemory === null ? null : all(workspaceMemory),
|
|
26277
|
-
umPick: userMemory === null ? null : all(userMemory)
|
|
26278
|
-
}
|
|
26279
|
-
};
|
|
26280
|
-
};
|
|
26281
|
-
if (Buffer.byteLength(whole.text) <= GROUNDING_BUDGET_BYTES)
|
|
26282
|
-
return untrimmed();
|
|
26283
|
-
if (workspaceMemory === null && userMemory === null)
|
|
26284
|
-
return untrimmed();
|
|
26285
|
-
const factCount = (body) => body === null ? 0 : groupFacts(body).length;
|
|
26286
|
-
const wmFacts = factCount(workspaceMemory);
|
|
26287
|
-
const umFacts = factCount(userMemory);
|
|
26288
|
-
const titlesFor = (pick2, filename) => pick2 === null || pick2.named === 0 ? null : {
|
|
26289
|
-
leadIn: titlesLeadIn(pick2.kept, pick2.named, pick2.total - pick2.kept - pick2.named, pick2.total, filename),
|
|
26290
|
-
text: pick2.titles
|
|
26291
|
-
};
|
|
26292
|
-
const noteFor = (pick2, filename) => pick2 === null || pick2.kept === pick2.total ? null : trimmedIndexNote(pick2.kept, pick2.total, filename);
|
|
26293
|
-
const measureFixed = (withNotes2, withTitles) => Buffer.byteLength(compose(workspaceMemory === null ? null : "", userMemory === null ? null : "", withNotes2 && workspaceMemory !== null ? trimmedIndexNote(wmFacts, wmFacts, WORKSPACE_MEMORY_FILENAME) : null, withNotes2 && userMemory !== null ? trimmedIndexNote(umFacts, umFacts, USER_MEMORY_FILENAME) : null, withTitles && workspaceMemory !== null ? { leadIn: titlesLeadIn(wmFacts, wmFacts, wmFacts, wmFacts, WORKSPACE_MEMORY_FILENAME), text: "" } : null, withTitles && userMemory !== null ? { leadIn: titlesLeadIn(umFacts, umFacts, umFacts, umFacts, USER_MEMORY_FILENAME), text: "" } : null).text);
|
|
26294
|
-
const wmSize = workspaceMemory === null ? 0 : Buffer.byteLength(workspaceMemory);
|
|
26295
|
-
const umSize = userMemory === null ? 0 : Buffer.byteLength(userMemory);
|
|
26296
|
-
const allocate = (remaining) => {
|
|
26297
|
-
const half = Math.floor(remaining / 2);
|
|
26298
|
-
let umCap;
|
|
26299
|
-
let wmCap;
|
|
26300
|
-
if (umSize <= half) {
|
|
26301
|
-
umCap = umSize;
|
|
26302
|
-
wmCap = remaining - umSize;
|
|
26303
|
-
} else if (wmSize <= half) {
|
|
26304
|
-
wmCap = wmSize;
|
|
26305
|
-
umCap = remaining - wmSize;
|
|
26306
|
-
} else {
|
|
26307
|
-
umCap = half;
|
|
26308
|
-
wmCap = remaining - half;
|
|
26309
|
-
}
|
|
26310
|
-
const umPick = userMemory === null ? null : selectIndexContent(userMemory, umCap);
|
|
26311
|
-
const umUsed = umPick === null ? 0 : Buffer.byteLength(umPick.text) + Buffer.byteLength(umPick.titles);
|
|
26312
|
-
return {
|
|
26313
|
-
wmPick: workspaceMemory === null ? null : selectIndexContent(workspaceMemory, Math.max(wmCap, remaining - umUsed)),
|
|
26314
|
-
umPick
|
|
26315
|
-
};
|
|
26316
|
-
};
|
|
26317
|
-
const build = (picks, withNotes2) => {
|
|
26318
|
-
const keep = (pick2) => withNotes2 || pick2 !== null && pick2.named === 0;
|
|
26319
|
-
return compose(picks.wmPick === null ? null : picks.wmPick.text, picks.umPick === null ? null : picks.umPick.text, keep(picks.wmPick) ? noteFor(picks.wmPick, WORKSPACE_MEMORY_FILENAME) : null, keep(picks.umPick) ? noteFor(picks.umPick, USER_MEMORY_FILENAME) : null, titlesFor(picks.wmPick, WORKSPACE_MEMORY_FILENAME), titlesFor(picks.umPick, USER_MEMORY_FILENAME));
|
|
26320
|
-
};
|
|
26321
|
-
const refine2 = (seed, withNotes2) => {
|
|
26322
|
-
const asReserve = (section) => section === null ? null : { leadIn: section.leadIn, text: "" };
|
|
26323
|
-
const keep = (pick2) => withNotes2 || pick2 !== null && pick2.named === 0;
|
|
26324
|
-
const fixed = Buffer.byteLength(compose(workspaceMemory === null ? null : "", userMemory === null ? null : "", keep(seed.wmPick) ? noteFor(seed.wmPick, WORKSPACE_MEMORY_FILENAME) : null, keep(seed.umPick) ? noteFor(seed.umPick, USER_MEMORY_FILENAME) : null, asReserve(titlesFor(seed.wmPick, WORKSPACE_MEMORY_FILENAME)), asReserve(titlesFor(seed.umPick, USER_MEMORY_FILENAME))).text);
|
|
26325
|
-
return allocate(Math.max(0, GROUNDING_BUDGET_BYTES - fixed));
|
|
26326
|
-
};
|
|
26327
|
-
const noNotes = allocate(Math.max(0, GROUNDING_BUDGET_BYTES - measureFixed(false, true)));
|
|
26328
|
-
const withNotes = allocate(Math.max(0, GROUNDING_BUDGET_BYTES - measureFixed(true, true)));
|
|
26329
|
-
const candidates = [
|
|
26330
|
-
{ picks: withNotes, composed: build(withNotes, true) },
|
|
26331
|
-
{ picks: noNotes, composed: build(noNotes, false) },
|
|
26332
|
-
{ picks: withNotes, composed: build(withNotes, false) }
|
|
26333
|
-
].concat([
|
|
26334
|
-
{ seed: noNotes, notes: false },
|
|
26335
|
-
{ seed: withNotes, notes: true }
|
|
26336
|
-
].map(({ seed, notes }) => {
|
|
26337
|
-
const picks = refine2(seed, notes);
|
|
26338
|
-
return { picks, composed: build(picks, notes) };
|
|
26339
|
-
})).map((c) => ({
|
|
26340
|
-
...c,
|
|
26341
|
-
bytes: Buffer.byteLength(c.composed.text),
|
|
26342
|
-
named: (c.picks.wmPick?.kept ?? 0) + (c.picks.wmPick?.named ?? 0) + (c.picks.umPick?.kept ?? 0) + (c.picks.umPick?.named ?? 0),
|
|
26343
|
-
whole: (c.picks.wmPick?.kept ?? 0) + (c.picks.umPick?.kept ?? 0)
|
|
26344
|
-
}));
|
|
26345
|
-
const fitting = candidates.filter((c) => c.bytes <= GROUNDING_BUDGET_BYTES);
|
|
26346
|
-
const better = (a, b) => b.named !== a.named ? b.named > a.named : b.whole > a.whole;
|
|
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);
|
|
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
|
-
};
|
|
26355
|
-
}
|
|
26356
|
-
var buildGroundingReportFor = buildGroundingReport;
|
|
26357
|
-
function formatLocalDate(d) {
|
|
26358
|
-
const year = d.getFullYear();
|
|
26359
|
-
const month = String(d.getMonth() + 1).padStart(2, "0");
|
|
26360
|
-
const day = String(d.getDate()).padStart(2, "0");
|
|
26361
|
-
return `${year}-${month}-${day}`;
|
|
26362
|
-
}
|
|
26363
|
-
|
|
26364
|
-
// src/lib/hash.ts
|
|
26365
|
-
import { createHash } from "node:crypto";
|
|
26366
|
-
function stableHash(value) {
|
|
26367
|
-
return createHash("sha256").update(stableStringify(value)).digest("hex");
|
|
26368
|
-
}
|
|
26369
|
-
function stableStringify(value) {
|
|
26370
|
-
if (value === null || typeof value !== "object") {
|
|
26371
|
-
return JSON.stringify(value);
|
|
26372
|
-
}
|
|
26373
|
-
if (Array.isArray(value)) {
|
|
26374
|
-
return "[" + value.map((v) => stableStringify(v)).join(",") + "]";
|
|
26375
|
-
}
|
|
26376
|
-
const obj = value;
|
|
26377
|
-
const keys = Object.keys(obj).sort();
|
|
26378
|
-
return "{" + keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",") + "}";
|
|
26379
|
-
}
|
|
26380
|
-
|
|
26381
|
-
// src/lib/context/roster.ts
|
|
26382
|
-
var import_gray_matter5 = __toESM(require_gray_matter(), 1);
|
|
26383
|
-
import { readdirSync as readdirSync3, readFileSync as readFileSync7 } from "node:fs";
|
|
26384
|
-
import { join as join6 } from "node:path";
|
|
26385
|
-
function readRoster(workspaceSlug) {
|
|
26386
|
-
let artistsDir;
|
|
26387
|
-
try {
|
|
26388
|
-
artistsDir = join6(workspaceMirrorPath(workspaceSlug), "artists");
|
|
26389
|
-
} catch {
|
|
26390
|
-
return [];
|
|
26391
|
-
}
|
|
26392
|
-
let slugs;
|
|
26393
|
-
try {
|
|
26394
|
-
slugs = readdirSync3(artistsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
|
|
26395
|
-
} catch {
|
|
26396
|
-
return [];
|
|
26397
|
-
}
|
|
26398
|
-
const roster = [];
|
|
26399
|
-
for (const slug of slugs) {
|
|
26400
|
-
let raw;
|
|
26401
|
-
try {
|
|
26402
|
-
raw = readFileSync7(join6(artistsDir, slug, "_artist.md"), "utf8");
|
|
26403
|
-
} catch {
|
|
26404
|
-
continue;
|
|
26405
|
-
}
|
|
26406
|
-
try {
|
|
26407
|
-
const fm = import_gray_matter5.default(raw).data;
|
|
26408
|
-
const name = typeof fm.name === "string" && fm.name.length > 0 ? fm.name : slug;
|
|
26409
|
-
const status = typeof fm.status === "string" ? fm.status : null;
|
|
26410
|
-
roster.push({ slug, name, status });
|
|
26411
|
-
} catch {
|
|
26412
|
-
roster.push({ slug, name: slug, status: null });
|
|
26413
|
-
}
|
|
26414
|
-
}
|
|
26415
|
-
return roster;
|
|
26416
|
-
}
|
|
26417
|
-
function rosterIsEmpty(workspaceSlug) {
|
|
26418
|
-
let artistsDir;
|
|
26419
|
-
try {
|
|
26420
|
-
artistsDir = join6(workspaceMirrorPath(workspaceSlug), "artists");
|
|
26421
|
-
} catch {
|
|
26422
|
-
return false;
|
|
26423
|
-
}
|
|
26424
|
-
let entries;
|
|
26425
|
-
try {
|
|
26426
|
-
entries = readdirSync3(artistsDir, { withFileTypes: true });
|
|
26427
|
-
} catch (err) {
|
|
26428
|
-
return err?.code === "ENOENT";
|
|
26429
|
-
}
|
|
26430
|
-
for (const entry of entries) {
|
|
26431
|
-
if (!entry.isDirectory())
|
|
26432
|
-
continue;
|
|
26433
|
-
try {
|
|
26434
|
-
readFileSync7(join6(artistsDir, entry.name, "_artist.md"), "utf8");
|
|
26435
|
-
return false;
|
|
26436
|
-
} catch (err) {
|
|
26437
|
-
if (err?.code === "ENOENT")
|
|
26438
|
-
continue;
|
|
26439
|
-
return false;
|
|
26440
|
-
}
|
|
26441
|
-
}
|
|
26442
|
-
return true;
|
|
26443
|
-
}
|
|
26444
|
-
|
|
26445
26051
|
// src/lib/state.ts
|
|
26446
26052
|
import {
|
|
26447
26053
|
mkdirSync as mkdirSync6,
|
|
26448
|
-
readFileSync as
|
|
26054
|
+
readFileSync as readFileSync7,
|
|
26449
26055
|
renameSync as renameSync5,
|
|
26450
26056
|
unlinkSync as unlinkSync5,
|
|
26451
26057
|
writeFileSync as writeFileSync5
|
|
26452
26058
|
} from "node:fs";
|
|
26453
|
-
import { dirname as dirname4, join as
|
|
26059
|
+
import { dirname as dirname4, join as join6 } from "node:path";
|
|
26454
26060
|
var StateEntrySchema = exports_external.object({
|
|
26455
26061
|
id: exports_external.string().uuid(),
|
|
26456
26062
|
slug: exports_external.string().min(1),
|
|
@@ -26460,7 +26066,8 @@ var StateEntrySchema = exports_external.object({
|
|
|
26460
26066
|
frontmatter_hash: exports_external.string().min(1),
|
|
26461
26067
|
inode: exports_external.number().int().nonnegative().optional(),
|
|
26462
26068
|
body_len: exports_external.number().int().nonnegative().optional(),
|
|
26463
|
-
memory_hash: exports_external.string().min(1).optional()
|
|
26069
|
+
memory_hash: exports_external.string().min(1).optional(),
|
|
26070
|
+
memory_generated: exports_external.boolean().optional()
|
|
26464
26071
|
});
|
|
26465
26072
|
var EntityEntriesSchema = exports_external.record(exports_external.string().uuid(), StateEntrySchema);
|
|
26466
26073
|
var FileStateEntrySchema = exports_external.object({
|
|
@@ -26480,17 +26087,27 @@ var WorkspaceRulesStateSchema = exports_external.object({
|
|
|
26480
26087
|
});
|
|
26481
26088
|
var WorkspaceMemoryStateSchema = exports_external.object({
|
|
26482
26089
|
body_hash: exports_external.string().min(1),
|
|
26483
|
-
updated_at: exports_external.string().datetime({ offset: true }).nullable()
|
|
26090
|
+
updated_at: exports_external.string().datetime({ offset: true }).nullable(),
|
|
26091
|
+
generated: exports_external.boolean().optional()
|
|
26484
26092
|
});
|
|
26485
26093
|
var UserMemoryStateSchema = exports_external.object({
|
|
26486
26094
|
body_hash: exports_external.string().min(1),
|
|
26487
|
-
updated_at: exports_external.string().datetime({ offset: true }).nullable()
|
|
26095
|
+
updated_at: exports_external.string().datetime({ offset: true }).nullable(),
|
|
26096
|
+
generated: exports_external.boolean().optional()
|
|
26488
26097
|
});
|
|
26489
26098
|
var UserVoiceStateSchema = exports_external.object({
|
|
26490
26099
|
body_hash: exports_external.string().min(1),
|
|
26491
26100
|
updated_at: exports_external.string().datetime({ offset: true }).nullable(),
|
|
26492
26101
|
status: exports_external.enum(["absent", "malformed", "valid"]).optional()
|
|
26493
26102
|
});
|
|
26103
|
+
var MemoryFactEntrySchema = exports_external.object({
|
|
26104
|
+
id: exports_external.string().min(1),
|
|
26105
|
+
body_hash: exports_external.string().min(1)
|
|
26106
|
+
});
|
|
26107
|
+
var MemoryFactsStateSchema = exports_external.object({
|
|
26108
|
+
fetched_at: exports_external.string().datetime({ offset: true }),
|
|
26109
|
+
facts: exports_external.record(exports_external.string(), MemoryFactEntrySchema).default({})
|
|
26110
|
+
});
|
|
26494
26111
|
var UnsyncedOpSchema = exports_external.object({
|
|
26495
26112
|
target: exports_external.enum(["entity", "file"]),
|
|
26496
26113
|
op: exports_external.enum(["upsert", "delete"]),
|
|
@@ -26549,7 +26166,8 @@ var WorkspaceStateV2Schema = exports_external.object({
|
|
|
26549
26166
|
frontmatter_id_backfill_version: exports_external.number().int().nonnegative().optional(),
|
|
26550
26167
|
team_rejected: TeamRejectedSchema.optional(),
|
|
26551
26168
|
size_anchor: SizeAnchorSchema.optional(),
|
|
26552
|
-
last_trim_at: exports_external.string().min(1).optional()
|
|
26169
|
+
last_trim_at: exports_external.string().min(1).optional(),
|
|
26170
|
+
memory_facts: MemoryFactsStateSchema.optional()
|
|
26553
26171
|
});
|
|
26554
26172
|
var WorkspaceStateV1Schema = exports_external.object({
|
|
26555
26173
|
version: exports_external.literal(1),
|
|
@@ -26557,10 +26175,10 @@ var WorkspaceStateV1Schema = exports_external.object({
|
|
|
26557
26175
|
documents: EntityEntriesSchema
|
|
26558
26176
|
});
|
|
26559
26177
|
function stateFilePath(workspaceSlug) {
|
|
26560
|
-
return
|
|
26178
|
+
return join6(plinthHome(), workspaceSlug, "state.json");
|
|
26561
26179
|
}
|
|
26562
26180
|
function stateTmpPath(workspaceSlug) {
|
|
26563
|
-
return
|
|
26181
|
+
return join6(plinthHome(), workspaceSlug, `state.json.${process.pid}.tmp`);
|
|
26564
26182
|
}
|
|
26565
26183
|
function emptyState(workspaceSlug) {
|
|
26566
26184
|
return { version: 2, workspace_slug: workspaceSlug, entries: {}, files: {} };
|
|
@@ -26576,7 +26194,7 @@ function migrateV1(v1) {
|
|
|
26576
26194
|
function loadState(workspaceSlug) {
|
|
26577
26195
|
let raw;
|
|
26578
26196
|
try {
|
|
26579
|
-
raw =
|
|
26197
|
+
raw = readFileSync7(stateFilePath(workspaceSlug), "utf8");
|
|
26580
26198
|
} catch (err) {
|
|
26581
26199
|
if (err?.code === "ENOENT") {
|
|
26582
26200
|
return emptyState(workspaceSlug);
|
|
@@ -26741,7 +26359,8 @@ function commitState(before, after) {
|
|
|
26741
26359
|
frontmatter_id_backfill_version: mergeScalar(fresh.frontmatter_id_backfill_version, before.frontmatter_id_backfill_version, after.frontmatter_id_backfill_version),
|
|
26742
26360
|
team_rejected: mergeScalar(fresh.team_rejected, before.team_rejected, after.team_rejected),
|
|
26743
26361
|
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)
|
|
26362
|
+
last_trim_at: mergeScalar(fresh.last_trim_at, before.last_trim_at, after.last_trim_at),
|
|
26363
|
+
memory_facts: mergeScalar(fresh.memory_facts, before.memory_facts, after.memory_facts)
|
|
26745
26364
|
};
|
|
26746
26365
|
writeState(merged);
|
|
26747
26366
|
const singletonOverruled = rules.overruled || memory.overruled || userMemory.overruled || userVoice.overruled;
|
|
@@ -26970,109 +26589,693 @@ function listTeamRejected(state) {
|
|
|
26970
26589
|
function teamRejectedFetchedAt(state) {
|
|
26971
26590
|
return state.team_rejected?.fetched_at;
|
|
26972
26591
|
}
|
|
26973
|
-
function setTeamRejected(state, rows, fetchedAt) {
|
|
26974
|
-
const existing = state.team_rejected;
|
|
26975
|
-
if (existing && rowsEqual(existing.rows, rows)) {
|
|
26976
|
-
return { state, changed: false };
|
|
26592
|
+
function setTeamRejected(state, rows, fetchedAt) {
|
|
26593
|
+
const existing = state.team_rejected;
|
|
26594
|
+
if (existing && rowsEqual(existing.rows, rows)) {
|
|
26595
|
+
return { state, changed: false };
|
|
26596
|
+
}
|
|
26597
|
+
return {
|
|
26598
|
+
state: { ...state, team_rejected: { fetched_at: fetchedAt, rows } },
|
|
26599
|
+
changed: true
|
|
26600
|
+
};
|
|
26601
|
+
}
|
|
26602
|
+
function rowsEqual(a, b) {
|
|
26603
|
+
if (a.length !== b.length)
|
|
26604
|
+
return false;
|
|
26605
|
+
return a.every((x, i) => {
|
|
26606
|
+
const y = b[i];
|
|
26607
|
+
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;
|
|
26608
|
+
});
|
|
26609
|
+
}
|
|
26610
|
+
function getWorkspaceRulesState(state) {
|
|
26611
|
+
return state.workspace_rules;
|
|
26612
|
+
}
|
|
26613
|
+
function setWorkspaceRulesState(state, entry) {
|
|
26614
|
+
const existing = state.workspace_rules;
|
|
26615
|
+
if (existing && existing.body_hash === entry.body_hash && existing.updated_at === entry.updated_at) {
|
|
26616
|
+
return { state, changed: false };
|
|
26617
|
+
}
|
|
26618
|
+
return { state: { ...state, workspace_rules: entry }, changed: true };
|
|
26619
|
+
}
|
|
26620
|
+
function getWorkspaceMemoryState(state) {
|
|
26621
|
+
return state.workspace_memory;
|
|
26622
|
+
}
|
|
26623
|
+
function setWorkspaceMemoryState(state, entry) {
|
|
26624
|
+
const existing = state.workspace_memory;
|
|
26625
|
+
if (existing && existing.body_hash === entry.body_hash && existing.updated_at === entry.updated_at && existing.generated === entry.generated) {
|
|
26626
|
+
return { state, changed: false };
|
|
26627
|
+
}
|
|
26628
|
+
return { state: { ...state, workspace_memory: entry }, changed: true };
|
|
26629
|
+
}
|
|
26630
|
+
function getUserMemoryState(state) {
|
|
26631
|
+
return state.user_memory;
|
|
26632
|
+
}
|
|
26633
|
+
function setUserMemoryState(state, entry) {
|
|
26634
|
+
const existing = state.user_memory;
|
|
26635
|
+
if (existing && existing.body_hash === entry.body_hash && existing.updated_at === entry.updated_at && existing.generated === entry.generated) {
|
|
26636
|
+
return { state, changed: false };
|
|
26637
|
+
}
|
|
26638
|
+
return { state: { ...state, user_memory: entry }, changed: true };
|
|
26639
|
+
}
|
|
26640
|
+
function getMemoryFactEntries(state) {
|
|
26641
|
+
return state.memory_facts?.facts ?? {};
|
|
26642
|
+
}
|
|
26643
|
+
function getMemoryFactEntry(state, relPath) {
|
|
26644
|
+
return state.memory_facts?.facts[relPath];
|
|
26645
|
+
}
|
|
26646
|
+
function setMemoryFactsState(state, entry) {
|
|
26647
|
+
const existing = state.memory_facts;
|
|
26648
|
+
if (existing && sameFactRegistry(existing.facts, entry.facts)) {
|
|
26649
|
+
return { state, changed: false };
|
|
26650
|
+
}
|
|
26651
|
+
return { state: { ...state, memory_facts: entry }, changed: true };
|
|
26652
|
+
}
|
|
26653
|
+
function sameFactRegistry(a, b) {
|
|
26654
|
+
const aKeys = Object.keys(a);
|
|
26655
|
+
if (aKeys.length !== Object.keys(b).length)
|
|
26656
|
+
return false;
|
|
26657
|
+
for (const key of aKeys) {
|
|
26658
|
+
const left = a[key];
|
|
26659
|
+
const right = b[key];
|
|
26660
|
+
if (!right || left.id !== right.id || left.body_hash !== right.body_hash) {
|
|
26661
|
+
return false;
|
|
26662
|
+
}
|
|
26663
|
+
}
|
|
26664
|
+
return true;
|
|
26665
|
+
}
|
|
26666
|
+
function getUserVoiceState(state) {
|
|
26667
|
+
return state.user_voice;
|
|
26668
|
+
}
|
|
26669
|
+
function setUserVoiceState(state, entry) {
|
|
26670
|
+
const existing = state.user_voice;
|
|
26671
|
+
if (existing && existing.body_hash === entry.body_hash && existing.updated_at === entry.updated_at && existing.status === entry.status) {
|
|
26672
|
+
return { state, changed: false };
|
|
26673
|
+
}
|
|
26674
|
+
return { state: { ...state, user_voice: entry }, changed: true };
|
|
26675
|
+
}
|
|
26676
|
+
var ABSENT_FRESHNESS = {
|
|
26677
|
+
workspaceMemory: null,
|
|
26678
|
+
userMemory: null,
|
|
26679
|
+
userVoice: null
|
|
26680
|
+
};
|
|
26681
|
+
function readMemoryFreshness(workspaceSlug) {
|
|
26682
|
+
let json2;
|
|
26683
|
+
try {
|
|
26684
|
+
json2 = JSON.parse(readFileSync7(stateFilePath(workspaceSlug), "utf8"));
|
|
26685
|
+
} catch {
|
|
26686
|
+
return ABSENT_FRESHNESS;
|
|
26687
|
+
}
|
|
26688
|
+
if (json2 === null || typeof json2 !== "object")
|
|
26689
|
+
return ABSENT_FRESHNESS;
|
|
26690
|
+
const record2 = json2;
|
|
26691
|
+
const workspace = WorkspaceMemoryStateSchema.safeParse(record2.workspace_memory);
|
|
26692
|
+
const user = UserMemoryStateSchema.safeParse(record2.user_memory);
|
|
26693
|
+
const voice = UserVoiceStateSchema.safeParse(record2.user_voice);
|
|
26694
|
+
return {
|
|
26695
|
+
workspaceMemory: workspace.success ? workspace.data : null,
|
|
26696
|
+
userMemory: user.success ? user.data : null,
|
|
26697
|
+
userVoice: voice.success ? voice.data : null
|
|
26698
|
+
};
|
|
26699
|
+
}
|
|
26700
|
+
function setSizeAnchor(state, anchor) {
|
|
26701
|
+
if (anchor.kind === "sync" && state.size_anchor !== undefined) {
|
|
26702
|
+
return { state, changed: false };
|
|
26703
|
+
}
|
|
26704
|
+
const existing = state.size_anchor;
|
|
26705
|
+
if (existing !== undefined && existing.kind === anchor.kind && existing.at === anchor.at && sizesEqual(existing.sizes, anchor.sizes)) {
|
|
26706
|
+
return { state, changed: false };
|
|
26707
|
+
}
|
|
26708
|
+
return { state: { ...state, size_anchor: anchor }, changed: true };
|
|
26709
|
+
}
|
|
26710
|
+
function sizesEqual(a, b) {
|
|
26711
|
+
const ak = Object.keys(a);
|
|
26712
|
+
const bk = Object.keys(b);
|
|
26713
|
+
if (ak.length !== bk.length)
|
|
26714
|
+
return false;
|
|
26715
|
+
return ak.every((k) => a[k] === b[k]);
|
|
26716
|
+
}
|
|
26717
|
+
function setLastTrimAt(state, at) {
|
|
26718
|
+
if (state.last_trim_at === at)
|
|
26719
|
+
return { state, changed: false };
|
|
26720
|
+
return { state: { ...state, last_trim_at: at }, changed: true };
|
|
26721
|
+
}
|
|
26722
|
+
|
|
26723
|
+
// src/lib/memory-facts/stores.ts
|
|
26724
|
+
var BLOB_MEMORY_STORES = Object.freeze({
|
|
26725
|
+
workspace: false,
|
|
26726
|
+
user: false
|
|
26727
|
+
});
|
|
26728
|
+
function memoryStoreModesFrom(state) {
|
|
26729
|
+
return {
|
|
26730
|
+
workspace: state.workspace_memory?.generated === true,
|
|
26731
|
+
user: state.user_memory?.generated === true
|
|
26732
|
+
};
|
|
26733
|
+
}
|
|
26734
|
+
function readMemoryStoreModes(workspaceSlug) {
|
|
26735
|
+
try {
|
|
26736
|
+
return memoryStoreModesFrom(loadState(workspaceSlug));
|
|
26737
|
+
} catch {
|
|
26738
|
+
return BLOB_MEMORY_STORES;
|
|
26739
|
+
}
|
|
26740
|
+
}
|
|
26741
|
+
function anyGenerated(modes) {
|
|
26742
|
+
return modes.workspace || modes.user;
|
|
26743
|
+
}
|
|
26744
|
+
function generatedMemoryStashDepsFrom(state) {
|
|
26745
|
+
const artists = state.entries["artists"] ?? {};
|
|
26746
|
+
const generatedArtists = new Set;
|
|
26747
|
+
for (const entry of Object.values(artists)) {
|
|
26748
|
+
if (entry.memory_generated === true)
|
|
26749
|
+
generatedArtists.add(entry.slug);
|
|
26750
|
+
}
|
|
26751
|
+
return {
|
|
26752
|
+
artistMemoryGenerated: (slug) => generatedArtists.has(slug),
|
|
26753
|
+
workspaceMemoryGenerated: () => state.workspace_memory?.generated === true,
|
|
26754
|
+
userMemoryGenerated: () => state.user_memory?.generated === true
|
|
26755
|
+
};
|
|
26756
|
+
}
|
|
26757
|
+
|
|
26758
|
+
// src/lib/context/grounding.ts
|
|
26759
|
+
var WORKSPACE_RULES_FILENAME = "workspace-rules.md";
|
|
26760
|
+
var HOUSE_STYLE_LEGACY_FILENAME = "house-style.md";
|
|
26761
|
+
var WORKSPACE_MEMORY_FILENAME = "workspace-memory.md";
|
|
26762
|
+
var USER_MEMORY_FILENAME = "user-memory.md";
|
|
26763
|
+
var VOICE_FILENAME = "voice.md";
|
|
26764
|
+
var WORKSPACE_RULES_PRIVATE_HEADING = "## Workspace-specific (private, never promotes)";
|
|
26765
|
+
var WORKSPACE_RULES_GENERAL_HEADING = "## Generalisable manager-craft (may promote)";
|
|
26766
|
+
var WORKSPACE_RULES_SECTION_HEADINGS = [
|
|
26767
|
+
WORKSPACE_RULES_PRIVATE_HEADING,
|
|
26768
|
+
WORKSPACE_RULES_GENERAL_HEADING
|
|
26769
|
+
];
|
|
26770
|
+
function extractWorkspaceRules(raw) {
|
|
26771
|
+
if (raw == null)
|
|
26772
|
+
return null;
|
|
26773
|
+
const withoutComments = raw.replace(/<!--[\s\S]*?-->/g, "").replace(/<!--[\s\S]*$/, "");
|
|
26774
|
+
const stripped = withoutComments.split(`
|
|
26775
|
+
`).filter((line) => !WORKSPACE_RULES_SECTION_HEADINGS.includes(line.trim())).join(`
|
|
26776
|
+
`).trim();
|
|
26777
|
+
return stripped.length > 0 ? stripped : null;
|
|
26778
|
+
}
|
|
26779
|
+
function extractWorkspaceMemory(raw) {
|
|
26780
|
+
if (raw == null)
|
|
26781
|
+
return null;
|
|
26782
|
+
const stripped = raw.replace(/<!--[\s\S]*?-->/g, "").replace(/<!--[\s\S]*$/, "").trim();
|
|
26783
|
+
return stripped.length > 0 ? stripped : null;
|
|
26784
|
+
}
|
|
26785
|
+
function extractUserMemory(raw) {
|
|
26786
|
+
if (raw == null)
|
|
26787
|
+
return null;
|
|
26788
|
+
const stripped = raw.replace(/<!--[\s\S]*?-->/g, "").replace(/<!--[\s\S]*$/, "").trim();
|
|
26789
|
+
return stripped.length > 0 ? stripped : null;
|
|
26790
|
+
}
|
|
26791
|
+
function extractVoiceProfile(raw) {
|
|
26792
|
+
if (raw == null)
|
|
26793
|
+
return null;
|
|
26794
|
+
const stripped = raw.replace(/<!--[\s\S]*?-->/g, "").trim();
|
|
26795
|
+
return stripped.length > 0 ? stripped : null;
|
|
26796
|
+
}
|
|
26797
|
+
function daysBetween(from, to) {
|
|
26798
|
+
const parse6 = (d) => {
|
|
26799
|
+
const parts = d.split("-");
|
|
26800
|
+
return Date.UTC(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2]));
|
|
26801
|
+
};
|
|
26802
|
+
return Math.round((parse6(to) - parse6(from)) / 86400000);
|
|
26803
|
+
}
|
|
26804
|
+
function renderFreshnessStamp(today, updatedAt) {
|
|
26805
|
+
const UNKNOWN = `The age of this index is not known here, so treat its lines as undated ` + `rather than as fresh.`;
|
|
26806
|
+
if (updatedAt == null)
|
|
26807
|
+
return UNKNOWN;
|
|
26808
|
+
const banked = new Date(updatedAt);
|
|
26809
|
+
if (Number.isNaN(banked.getTime()))
|
|
26810
|
+
return UNKNOWN;
|
|
26811
|
+
const bankedDay = formatLocalDate(banked);
|
|
26812
|
+
const days = Math.max(0, daysBetween(bankedDay, today));
|
|
26813
|
+
const age = days === 0 ? "today" : days === 1 ? "1 day ago" : `${days} days ago`;
|
|
26814
|
+
return `This index was last updated on ${bankedDay}, ${age}.`;
|
|
26815
|
+
}
|
|
26816
|
+
var GROUNDING_BUDGET_BYTES = 20000;
|
|
26817
|
+
var GROUNDING_CATEGORY_ORDER = [
|
|
26818
|
+
"grounding",
|
|
26819
|
+
"identity",
|
|
26820
|
+
"budgetNote",
|
|
26821
|
+
"sizeNote",
|
|
26822
|
+
"voice",
|
|
26823
|
+
"rules",
|
|
26824
|
+
"workspaceFacts",
|
|
26825
|
+
"personalFacts"
|
|
26826
|
+
];
|
|
26827
|
+
var GROUNDING_CATEGORY_LABELS = {
|
|
26828
|
+
grounding: "Plinth's own grounding",
|
|
26829
|
+
identity: "who you are",
|
|
26830
|
+
budgetNote: "the trim notes",
|
|
26831
|
+
sizeNote: "the size warning",
|
|
26832
|
+
voice: "voice pointer",
|
|
26833
|
+
rules: "this workspace's rules",
|
|
26834
|
+
workspaceFacts: "workspace facts",
|
|
26835
|
+
personalFacts: "your own facts"
|
|
26836
|
+
};
|
|
26837
|
+
var MEMBER_EDITABLE_CATEGORIES = [
|
|
26838
|
+
"rules",
|
|
26839
|
+
"workspaceFacts",
|
|
26840
|
+
"personalFacts"
|
|
26841
|
+
];
|
|
26842
|
+
function segmentBytes(lines, cats) {
|
|
26843
|
+
const out = {
|
|
26844
|
+
grounding: 0,
|
|
26845
|
+
identity: 0,
|
|
26846
|
+
rules: 0,
|
|
26847
|
+
workspaceFacts: 0,
|
|
26848
|
+
personalFacts: 0,
|
|
26849
|
+
voice: 0,
|
|
26850
|
+
budgetNote: 0,
|
|
26851
|
+
sizeNote: 0
|
|
26852
|
+
};
|
|
26853
|
+
lines.forEach((text, i) => {
|
|
26854
|
+
const cat = cats[i];
|
|
26855
|
+
out[cat] += Buffer.byteLength(text) + (i < lines.length - 1 ? 1 : 0);
|
|
26856
|
+
});
|
|
26857
|
+
return out;
|
|
26858
|
+
}
|
|
26859
|
+
var IDENTITY_LINE_MAX_BYTES = 300;
|
|
26860
|
+
function renderMemberIdentity(member, workspaceSlug) {
|
|
26861
|
+
if (member === null)
|
|
26862
|
+
return null;
|
|
26863
|
+
const tail = `. Tasks you create are assigned to this member unless told otherwise.`;
|
|
26864
|
+
const budget = IDENTITY_LINE_MAX_BYTES - Buffer.byteLength(tail);
|
|
26865
|
+
const email3 = oneLine(member.email);
|
|
26866
|
+
const build = (withName, withRole, withZone) => {
|
|
26867
|
+
const who = withName && member.displayName ? `${oneLine(member.displayName)} (${email3})` : email3;
|
|
26868
|
+
const clauses = [`You are working for ${who}`];
|
|
26869
|
+
if (withRole && member.role)
|
|
26870
|
+
clauses.push(`role ${oneLine(member.role)}`);
|
|
26871
|
+
if (workspaceSlug !== null)
|
|
26872
|
+
clauses.push(`in workspace ${workspaceSlug}`);
|
|
26873
|
+
if (withZone && member.timezone)
|
|
26874
|
+
clauses.push(`timezone ${oneLine(member.timezone)}`);
|
|
26875
|
+
return clauses.join(", ");
|
|
26876
|
+
};
|
|
26877
|
+
const candidates = [
|
|
26878
|
+
build(true, true, true),
|
|
26879
|
+
build(true, true, false),
|
|
26880
|
+
build(true, false, false),
|
|
26881
|
+
build(false, false, false)
|
|
26882
|
+
];
|
|
26883
|
+
if (member.displayName) {
|
|
26884
|
+
const slugClause = workspaceSlug === null ? `` : `, in workspace ${workspaceSlug}`;
|
|
26885
|
+
candidates.push(`You are working for ${oneLine(member.displayName)}${slugClause}`);
|
|
26886
|
+
}
|
|
26887
|
+
const head = candidates.find((c) => Buffer.byteLength(c) <= budget);
|
|
26888
|
+
if (head === undefined)
|
|
26889
|
+
return null;
|
|
26890
|
+
return head + tail;
|
|
26891
|
+
}
|
|
26892
|
+
function oneLine(value) {
|
|
26893
|
+
return value.replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim();
|
|
26894
|
+
}
|
|
26895
|
+
function shouldOfferOnboarding(rosterIsEmpty, member) {
|
|
26896
|
+
if (member?.onboardingCompletedAt)
|
|
26897
|
+
return false;
|
|
26898
|
+
return rosterIsEmpty;
|
|
26899
|
+
}
|
|
26900
|
+
function bankedDate(line) {
|
|
26901
|
+
const matches = line.match(/\d{4}-\d{2}-\d{2}/g);
|
|
26902
|
+
if (matches === null || matches.length === 0)
|
|
26903
|
+
return null;
|
|
26904
|
+
let latest = matches[0];
|
|
26905
|
+
for (const m of matches)
|
|
26906
|
+
if (m > latest)
|
|
26907
|
+
latest = m;
|
|
26908
|
+
return latest;
|
|
26909
|
+
}
|
|
26910
|
+
var POINTER_RE = /\[\[[^\]]+\]\]/g;
|
|
26911
|
+
var TITLE_TRUNCATION_MARK = " …";
|
|
26912
|
+
function factTitle(fact) {
|
|
26913
|
+
const firstLine = fact.split(`
|
|
26914
|
+
`)[0];
|
|
26915
|
+
const match = /[.?!](\s|$)/.exec(firstLine);
|
|
26916
|
+
const sentence = match === null ? firstLine : firstLine.slice(0, match.index + 1);
|
|
26917
|
+
const missing = [...fact.matchAll(POINTER_RE)].map((m) => m[0]).filter((p) => !sentence.includes(p));
|
|
26918
|
+
const unique = [...new Set(missing)];
|
|
26919
|
+
const named = unique.length === 0 ? sentence : `${sentence} ${unique.join(" ")}`;
|
|
26920
|
+
const truncated = named !== fact;
|
|
26921
|
+
return { text: truncated ? `${named}${TITLE_TRUNCATION_MARK}` : named, truncated };
|
|
26922
|
+
}
|
|
26923
|
+
function rankFacts(facts) {
|
|
26924
|
+
return facts.map((line, index) => ({ line, index, date: bankedDate(line) })).sort((a, b) => {
|
|
26925
|
+
const ad = a.date ?? "";
|
|
26926
|
+
const bd = b.date ?? "";
|
|
26927
|
+
if (ad !== bd)
|
|
26928
|
+
return ad < bd ? 1 : -1;
|
|
26929
|
+
return b.index - a.index;
|
|
26930
|
+
}).map(({ index, line }) => ({ index, line }));
|
|
26931
|
+
}
|
|
26932
|
+
function groupFacts(body) {
|
|
26933
|
+
const facts = [];
|
|
26934
|
+
for (const line of body.split(`
|
|
26935
|
+
`)) {
|
|
26936
|
+
if (line.trim().length === 0)
|
|
26937
|
+
continue;
|
|
26938
|
+
if (/^\s/.test(line) && facts.length > 0) {
|
|
26939
|
+
facts[facts.length - 1] = `${facts[facts.length - 1]}
|
|
26940
|
+
${line}`;
|
|
26941
|
+
} else {
|
|
26942
|
+
facts.push(line);
|
|
26943
|
+
}
|
|
26944
|
+
}
|
|
26945
|
+
return facts;
|
|
26946
|
+
}
|
|
26947
|
+
function selectIndexContent(body, capBytes) {
|
|
26948
|
+
const lines = groupFacts(body);
|
|
26949
|
+
if (Buffer.byteLength(body) <= capBytes) {
|
|
26950
|
+
return { text: body, titles: "", kept: lines.length, named: 0, total: lines.length };
|
|
26951
|
+
}
|
|
26952
|
+
const ranked = rankFacts(lines);
|
|
26953
|
+
const titleOf = new Map;
|
|
26954
|
+
const info2 = new Map;
|
|
26955
|
+
for (const { index, line } of ranked) {
|
|
26956
|
+
const t = factTitle(line);
|
|
26957
|
+
info2.set(index, t);
|
|
26958
|
+
titleOf.set(index, t.text);
|
|
26959
|
+
}
|
|
26960
|
+
const carried = new Map;
|
|
26961
|
+
const used = () => {
|
|
26962
|
+
let bytes = 0;
|
|
26963
|
+
let bodies = 0;
|
|
26964
|
+
let titles = 0;
|
|
26965
|
+
for (const [index, kind] of carried) {
|
|
26966
|
+
const line = lines[index];
|
|
26967
|
+
if (kind === "body") {
|
|
26968
|
+
bytes += Buffer.byteLength(line);
|
|
26969
|
+
bodies++;
|
|
26970
|
+
} else {
|
|
26971
|
+
bytes += Buffer.byteLength(titleOf.get(index));
|
|
26972
|
+
titles++;
|
|
26973
|
+
}
|
|
26974
|
+
}
|
|
26975
|
+
return bytes + Math.max(0, bodies - 1) + Math.max(0, titles - 1);
|
|
26976
|
+
};
|
|
26977
|
+
for (const { index } of ranked) {
|
|
26978
|
+
if (info2.get(index)?.truncated !== true)
|
|
26979
|
+
continue;
|
|
26980
|
+
carried.set(index, "title");
|
|
26981
|
+
if (used() > capBytes)
|
|
26982
|
+
carried.delete(index);
|
|
26983
|
+
}
|
|
26984
|
+
for (const { index } of ranked) {
|
|
26985
|
+
const was = carried.get(index);
|
|
26986
|
+
carried.set(index, "body");
|
|
26987
|
+
if (used() > capBytes) {
|
|
26988
|
+
if (was === undefined)
|
|
26989
|
+
carried.delete(index);
|
|
26990
|
+
else
|
|
26991
|
+
carried.set(index, was);
|
|
26992
|
+
break;
|
|
26993
|
+
}
|
|
26994
|
+
}
|
|
26995
|
+
const inFileOrder = (kind) => [...carried.entries()].filter(([, k]) => k === kind).map(([i]) => i).sort((a, b) => a - b);
|
|
26996
|
+
const bodyIdx = inFileOrder("body");
|
|
26997
|
+
const titleIdx = inFileOrder("title");
|
|
26998
|
+
return {
|
|
26999
|
+
text: bodyIdx.map((i) => lines[i]).join(`
|
|
27000
|
+
`),
|
|
27001
|
+
titles: titleIdx.map((i) => titleOf.get(i)).join(`
|
|
27002
|
+
`),
|
|
27003
|
+
kept: bodyIdx.length,
|
|
27004
|
+
named: titleIdx.length,
|
|
27005
|
+
total: lines.length
|
|
27006
|
+
};
|
|
27007
|
+
}
|
|
27008
|
+
function trimmedIndexNote(kept, total, filename) {
|
|
27009
|
+
if (kept === 0) {
|
|
27010
|
+
return `None of this index is shown here: it is over the size this injection ` + `carries, so it lives in \`${filename}\` at the mirror root and nowhere ` + `else. READ THAT FILE IN FULL before you rely on what this workspace has ` + `taught you, and before you answer that no fact covers something.`;
|
|
27011
|
+
}
|
|
27012
|
+
return `Those are the ${kept} most recently banked of ${total} facts, not all of ` + `them: this injection carries a fixed budget and the rest of the index did ` + `not fit. The other ${total - kept} are in \`${filename}\` at the mirror ` + `root. READ THAT FILE IN FULL before you rely on what this workspace has ` + `taught you, and before you answer that no fact covers something. An index ` + `that is silent because it was trimmed reads exactly like one that has ` + `settled a subject.`;
|
|
27013
|
+
}
|
|
27014
|
+
function titlesLeadIn(kept, named, unnamed, total, filename) {
|
|
27015
|
+
const parts = [`${kept} above in full`];
|
|
27016
|
+
parts.push(`${named} below as ${named === 1 ? "its opening" : "their openings"} only`);
|
|
27017
|
+
if (unnamed > 0)
|
|
27018
|
+
parts.push(`${unnamed} not shown at all`);
|
|
27019
|
+
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.`;
|
|
27020
|
+
}
|
|
27021
|
+
function composeGroundingBlock(today, workspaceRules, workspaceMemory, userMemory, freshness, rosterIsEmpty, voiceProfile, voiceMalformed, workspaceSlug, workspaceMemoryNote, userMemoryNote, member, workspaceTitles = null, userTitles = null, filesPastBar = 0, memoryStores = BLOB_MEMORY_STORES) {
|
|
27022
|
+
const gateFlag = workspaceSlug === null ? `--workspace="<this workspace>"` : `--workspace ${workspaceSlug}`;
|
|
27023
|
+
const identity = renderMemberIdentity(member, workspaceSlug);
|
|
27024
|
+
const block = [];
|
|
27025
|
+
const cats = [];
|
|
27026
|
+
const add = (cat, ...lines) => {
|
|
27027
|
+
for (const line of lines) {
|
|
27028
|
+
block.push(line);
|
|
27029
|
+
cats.push(cat);
|
|
27030
|
+
}
|
|
27031
|
+
};
|
|
27032
|
+
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.`);
|
|
27033
|
+
if (identity !== null)
|
|
27034
|
+
add("identity", ``, identity);
|
|
27035
|
+
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.`);
|
|
27036
|
+
if (workspaceRules === null && voiceProfile === null) {
|
|
27037
|
+
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.`);
|
|
27038
|
+
}
|
|
27039
|
+
add("grounding", ``, `Read the draft back before you say it is ready, and read what the tool actually ` + `produced rather than what you sent it. Do this every time you write one, not ` + `once a session: a later edit can undo what an earlier check confirmed. A ` + `draft surface takes your text and renders it, and it may escape what you ` + `wrote, wrap it, quote an earlier message underneath it, thread it somewhere ` + `you did not intend, or leave the first one in place and add a second. What ` + `the person opens is the rendered result, not your input, and those are not ` + `always the same thing. So fetch the staged draft, look at its body, its ` + `recipients, the thread it landed on and that there is one of it, and only ` + `then say it is ready. Separately, and once per session is enough for this ` + `part: if a tool's own description tells you how it handles your text, treat ` + `that as a claim like any other and check it against what came out. A tool ` + `that is documented wrong will put markup, an empty body, or a stranger's ` + `address in front of a person whose name is on the message, and you are the ` + `only thing standing between those two.`, ``, `The most recent artefact wins on facts, and it is not a judgement call. ` + `Dates, prices, availability, capacities, routings, who is on a thread, ` + `who said what. Do not weigh a newer message against an older banked ` + `line to work out which is more likely right: the newer one is the state ` + `and the older one is history. This holds for any dated artefact, not ` + `only email - call transcripts, meeting notes and executed documents are ` + `all in scope, newest first. Stale context is worse than absent context, ` + `because it reads as current. One carve-out, because it is the failure ` + `this rule would otherwise cause, and it is a mechanical test rather ` + `than a judgement: before promoting the newest message to "the state", ` + `ask whether it COMMITS or PROPOSES. A commit ("we're doing", ` + `"confirmed", "booked", a price, a date someone has agreed to) wins, and ` + `overwrites the old claim. A proposal ("I wonder if", "I'm not saying we ` + `must", or any question put to someone who has not replied) goes in as ` + `an open question, named as unanswered, with who owes the answer. It ` + `does NOT become the state.`, ``, `Reconcile, do not re-adjudicate. Never delete a superseded claim: strike ` + `it in place. Keep the old line, mark it superseded with the date and ` + `with what replaced it, and put the new material above it, where the ` + `file's live state is. Demote the outgoing section one heading level so ` + `history stays readable and stops competing with what is live. Someone ` + `will ask why the plan changed, and the answer has to survive in the ` + `file. Then date the state: say what it is current as of, and name the ` + `sources it was built from. A master context with no as-of line is ` + `unfalsifiable.`);
|
|
27040
|
+
if (workspaceRules !== null) {
|
|
27041
|
+
add("grounding", ``, `Follow this workspace's house style. The conventions below are set by the ` + `workspace owner and govern HOW you write here: copy, titles, notes, ` + `sign-offs, formatting. They refine the rules above; they never relax them. ` + `If any convention here conflicts with a grounding rule above (the ` + `canonical write surface, plan-before-write confirmation, or the ban on ` + `direct database writes), the rule above wins. The conventions:`);
|
|
27042
|
+
add("rules", ``, workspaceRules);
|
|
27043
|
+
}
|
|
27044
|
+
if (voiceProfile !== null) {
|
|
27045
|
+
add("voice", ``, `Draft in this member's own voice, and read it before you do. Their voice ` + `profile is \`${VOICE_FILENAME}\` at the mirror root, beside this ` + `workspace's house style: how they open, how they sign off, what they ` + `never do. It is not reproduced here. READ THAT FILE IN FULL before you ` + `draft anything that goes out in their name (an email, a message, a ` + `note, any prose that carries their name), and write to it from the ` + `start rather than ` + `drafting in your own register and correcting after. Nothing inspects a ` + `draft between you and the person reading it, so that file is the whole ` + `of it. It is theirs to write and not yours: you cannot edit ` + `\`${VOICE_FILENAME}\`, and if something in it looks wrong or out of ` + `date, say so and let them change it. Like this workspace's house style ` + `it refines the rules above and never relaxes them.`);
|
|
27046
|
+
if (voiceMalformed) {
|
|
27047
|
+
add("voice", ``, `That voice profile does not currently parse. Plinth read it and found ` + `its structure broken, most often a code fence that was opened and ` + `never closed, so any machine-read setting inside it is being ignored ` + `rather than applied. The prose in it is still the member's own ` + `writing and is still worth following. Treat anything that looks like ` + `a configured setting in it as NOT in force, say so if it comes up, ` + `and tell them their profile needs fixing rather than fixing it ` + `yourself.`);
|
|
27048
|
+
}
|
|
27049
|
+
}
|
|
27050
|
+
add("grounding", ``, `Keep your memory on the Plinth surface. Your memory here IS the Plinth ` + `surface (\`workspace-memory.md\`, your own \`user-memory.md\`, and the ` + `per-artist warm stores the daemon syncs), and it is the ONLY memory you ` + `keep. Do not use any other memory store: no private notes-to-self, no ` + `separate scratch file, no tool-native memory of your own. What you learn ` + `here is recorded on the Plinth surface, or nowhere. When you learn a ` + `durable fact worth keeping, bank it by adding a line to the right index, ` + `which the daemon syncs up, following the write-back conventions in that ` + `file's header: one line, at least one \`[[pointer]]\` to where the full ` + `fact lives, an absolute date, and where you learned it. Only ever record ` + `what you have actually been taught, never public knowledge and never your ` + `own priors. The scope rule decides WHICH index, and there are ` + `THREE. A fact true of ONE ARTIST (their rate card, how their kit ` + `works, what a contact of theirs reliably does) goes in that artist's ` + `own warm store, \`artists/<slug>/memory.md\`, and not in the shared ` + `index: putting it there taxes every teammate who never touches that ` + `artist and hides it from the one who does. A fact that holds for the ` + `whole workspace (a project, a working convention, a standing ` + `fact true for everyone here) goes in \`workspace-memory.md\`, which ` + `grounds EVERY teammate - and that file is the one you NEVER write on ` + `your own judgement. It changes only when a member ASKS you to remember ` + `something for everyone, in their own words rather than your summary of ` + `them, and you log that decision with ` + `\`plinth memory-gate log ${gateFlag} --proposed="<their words>" --decision accept\` ` + `BEFORE you write the line. That command fails LOUD on exit 1: surface ` + `it, and do not report the line as banked. Never infer a line for that ` + `file from what merely happened in a session, and never propose one ` + `unasked - a wrong line there reaches every member on every machine and ` + `is injected into every future session. A fact about one person (an individual ` + `preference, your own working style or tools) is NOT a workspace fact: it ` + `goes in \`user-memory.md\`, your member-private index that only YOU can ` + `read. HOW THIS MEMBER WRITES IS THE ONE EXCEPTION: their register and ` + `sign-off are a rubric, not a fact, and live in \`${VOICE_FILENAME}\`, ` + `which is theirs and you cannot edit, so say what you noticed and leave it ` + `to them. Them only: how a CONTACT likes to be written to is an ordinary ` + `fact for that artist's store. ` + `Banking a personal fact in \`workspace-memory.md\` would leak that person ` + `into everyone else's context, so never do it. When a fact could be either ` + `scope, lean private and put it in \`user-memory.md\`. ` + `Live state does not belong in any memory file. This week's open ` + `threads, one show's logistics, where a release has got to: a memory ` + `file is for what stays true after the session that taught it, and live ` + `state goes in the body of the record it belongs to. A deadline rides ` + `its task as a due date, and every other date is a calendar matter. ` + `This memory is additive and subordinate: it never ` + `overrides a house-style rule or any safety primitive above.` + generatedStoresClause(memoryStores));
|
|
27051
|
+
if (workspaceMemory !== null) {
|
|
27052
|
+
add("grounding", ``, `Draw on this workspace's memory. The lines below are this workspace's ` + `memory index: durable facts it has taught you, each a one-line hook that ` + `points (via a \`[[wikilink]]\`) to the warm store where the full detail ` + `lives. Treat them as recall, not gospel: use a line to load the right ` + `context and verify it against the live workspace before you rely on it, ` + `never as a settled fact to assert. ` + `${renderFreshnessStamp(today, freshness.workspaceMemoryUpdatedAt)} ` + `The index:`);
|
|
27053
|
+
add("workspaceFacts", ``, workspaceMemory);
|
|
27054
|
+
if (workspaceTitles !== null) {
|
|
27055
|
+
add("budgetNote", ``, workspaceTitles.leadIn);
|
|
27056
|
+
add("workspaceFacts", ``, workspaceTitles.text);
|
|
27057
|
+
}
|
|
27058
|
+
if (workspaceMemoryNote !== null)
|
|
27059
|
+
add("budgetNote", ``, workspaceMemoryNote);
|
|
27060
|
+
}
|
|
27061
|
+
if (userMemory !== null) {
|
|
27062
|
+
add("grounding", ``, `Draw on your personal memory. The lines below are YOUR personal memory ` + `index: member-private facts you have taught the harness about yourself ` + `(your preferences, working style, tools), each a one-line hook ` + `that points (via a \`[[wikilink]]\`) to the warm store where the full ` + `detail lives. Only you can read this. Treat it as recall, not gospel: use ` + `a line to load the right context and verify it before you rely on it, ` + `never as a settled fact to assert. ` + `${renderFreshnessStamp(today, freshness.userMemoryUpdatedAt)} ` + `The index:`);
|
|
27063
|
+
add("personalFacts", ``, userMemory);
|
|
27064
|
+
if (userTitles !== null) {
|
|
27065
|
+
add("budgetNote", ``, userTitles.leadIn);
|
|
27066
|
+
add("personalFacts", ``, userTitles.text);
|
|
27067
|
+
}
|
|
27068
|
+
if (userMemoryNote !== null)
|
|
27069
|
+
add("budgetNote", ``, userMemoryNote);
|
|
27070
|
+
}
|
|
27071
|
+
if (workspaceMemory !== null || userMemory !== null) {
|
|
27072
|
+
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.`);
|
|
26977
27073
|
}
|
|
27074
|
+
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.`);
|
|
27075
|
+
if (filesPastBar > 0) {
|
|
27076
|
+
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.`);
|
|
27077
|
+
}
|
|
27078
|
+
if (shouldOfferOnboarding(rosterIsEmpty, member)) {
|
|
27079
|
+
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.`);
|
|
27080
|
+
}
|
|
27081
|
+
const groundingText = block.filter((_, i) => cats[i] === "grounding").join(`
|
|
27082
|
+
`);
|
|
26978
27083
|
return {
|
|
26979
|
-
|
|
26980
|
-
|
|
27084
|
+
text: block.join(`
|
|
27085
|
+
`),
|
|
27086
|
+
split: segmentBytes(block, cats),
|
|
27087
|
+
groundingParagraphs: countParagraphs(groundingText),
|
|
27088
|
+
groundingRuleShaped: countRuleShapedLines(groundingText)
|
|
26981
27089
|
};
|
|
26982
27090
|
}
|
|
26983
|
-
function
|
|
26984
|
-
if (
|
|
26985
|
-
return
|
|
26986
|
-
|
|
26987
|
-
|
|
26988
|
-
|
|
26989
|
-
});
|
|
26990
|
-
}
|
|
26991
|
-
function getWorkspaceRulesState(state) {
|
|
26992
|
-
return state.workspace_rules;
|
|
26993
|
-
}
|
|
26994
|
-
function setWorkspaceRulesState(state, entry) {
|
|
26995
|
-
const existing = state.workspace_rules;
|
|
26996
|
-
if (existing && existing.body_hash === entry.body_hash && existing.updated_at === entry.updated_at) {
|
|
26997
|
-
return { state, changed: false };
|
|
26998
|
-
}
|
|
26999
|
-
return { state: { ...state, workspace_rules: entry }, changed: true };
|
|
27091
|
+
function generatedStoresClause(modes) {
|
|
27092
|
+
if (!anyGenerated(modes))
|
|
27093
|
+
return "";
|
|
27094
|
+
const which = modes.workspace && modes.user ? `\`${WORKSPACE_MEMORY_FILENAME}\` and \`${USER_MEMORY_FILENAME}\` are` : modes.workspace ? `\`${WORKSPACE_MEMORY_FILENAME}\` is` : `\`${USER_MEMORY_FILENAME}\` is`;
|
|
27095
|
+
const tools = modes.user ? modes.workspace ? `\`create_user_memory_fact\` for a personal fact, which writes on one ` + `call, and \`create_memory_fact\` for a shared one, which shows you the ` + `fact first and needs a second call to save it` : `\`create_user_memory_fact\`, which writes on one call` : `\`create_memory_fact\`, which shows you the fact first and needs a ` + `second call to save it`;
|
|
27096
|
+
return ` HOW you bank a fact has changed for this workspace: ${which} now ` + `GENERATED from memory facts, so do not add a line to ${modes.workspace && modes.user ? "either file" : "that file"} - it is rewritten from the ` + `facts on every sync. Use ${tools}. Correct a fact with ` + `\`supersede_memory_fact\` or \`delete_memory_fact\` (\`_user_\` for your own), ` + `taking the id from the fact file's frontmatter. Each fact's full text lives ` + `in \`memory/<slug>.md\`, or \`memory/private/<slug>.md\` for your own, and ` + `the index lines above link to them - read the one you need rather than the ` + `whole folder. A hand edit to a generated file does not reach Plinth: it is ` + `set aside and \`plinth status\` says where.`;
|
|
27000
27097
|
}
|
|
27001
|
-
function
|
|
27002
|
-
|
|
27098
|
+
function buildGroundingReport(today, workspaceRules = null, workspaceMemory = null, userMemory = null, freshness = {}, rosterIsEmpty = false, voiceProfile = null, voiceMalformed = false, workspaceSlug = null, member = null, filesPastBar = 0, memoryStores = BLOB_MEMORY_STORES) {
|
|
27099
|
+
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, memoryStores);
|
|
27100
|
+
const whole = compose(workspaceMemory, userMemory, null, null);
|
|
27101
|
+
const untrimmed = () => {
|
|
27102
|
+
const all = (body) => {
|
|
27103
|
+
const n = groupFacts(body).length;
|
|
27104
|
+
return { text: body, titles: "", kept: n, named: 0, total: n };
|
|
27105
|
+
};
|
|
27106
|
+
return {
|
|
27107
|
+
block: whole.text,
|
|
27108
|
+
split: whole.split,
|
|
27109
|
+
groundingParagraphs: whole.groundingParagraphs,
|
|
27110
|
+
groundingRuleShaped: whole.groundingRuleShaped,
|
|
27111
|
+
picks: {
|
|
27112
|
+
wmPick: workspaceMemory === null ? null : all(workspaceMemory),
|
|
27113
|
+
umPick: userMemory === null ? null : all(userMemory)
|
|
27114
|
+
}
|
|
27115
|
+
};
|
|
27116
|
+
};
|
|
27117
|
+
if (Buffer.byteLength(whole.text) <= GROUNDING_BUDGET_BYTES)
|
|
27118
|
+
return untrimmed();
|
|
27119
|
+
if (workspaceMemory === null && userMemory === null)
|
|
27120
|
+
return untrimmed();
|
|
27121
|
+
const factCount = (body) => body === null ? 0 : groupFacts(body).length;
|
|
27122
|
+
const wmFacts = factCount(workspaceMemory);
|
|
27123
|
+
const umFacts = factCount(userMemory);
|
|
27124
|
+
const titlesFor = (pick2, filename) => pick2 === null || pick2.named === 0 ? null : {
|
|
27125
|
+
leadIn: titlesLeadIn(pick2.kept, pick2.named, pick2.total - pick2.kept - pick2.named, pick2.total, filename),
|
|
27126
|
+
text: pick2.titles
|
|
27127
|
+
};
|
|
27128
|
+
const noteFor = (pick2, filename) => pick2 === null || pick2.kept === pick2.total ? null : trimmedIndexNote(pick2.kept, pick2.total, filename);
|
|
27129
|
+
const measureFixed = (withNotes2, withTitles) => Buffer.byteLength(compose(workspaceMemory === null ? null : "", userMemory === null ? null : "", withNotes2 && workspaceMemory !== null ? trimmedIndexNote(wmFacts, wmFacts, WORKSPACE_MEMORY_FILENAME) : null, withNotes2 && userMemory !== null ? trimmedIndexNote(umFacts, umFacts, USER_MEMORY_FILENAME) : null, withTitles && workspaceMemory !== null ? { leadIn: titlesLeadIn(wmFacts, wmFacts, wmFacts, wmFacts, WORKSPACE_MEMORY_FILENAME), text: "" } : null, withTitles && userMemory !== null ? { leadIn: titlesLeadIn(umFacts, umFacts, umFacts, umFacts, USER_MEMORY_FILENAME), text: "" } : null).text);
|
|
27130
|
+
const wmSize = workspaceMemory === null ? 0 : Buffer.byteLength(workspaceMemory);
|
|
27131
|
+
const umSize = userMemory === null ? 0 : Buffer.byteLength(userMemory);
|
|
27132
|
+
const allocate = (remaining) => {
|
|
27133
|
+
const half = Math.floor(remaining / 2);
|
|
27134
|
+
let umCap;
|
|
27135
|
+
let wmCap;
|
|
27136
|
+
if (umSize <= half) {
|
|
27137
|
+
umCap = umSize;
|
|
27138
|
+
wmCap = remaining - umSize;
|
|
27139
|
+
} else if (wmSize <= half) {
|
|
27140
|
+
wmCap = wmSize;
|
|
27141
|
+
umCap = remaining - wmSize;
|
|
27142
|
+
} else {
|
|
27143
|
+
umCap = half;
|
|
27144
|
+
wmCap = remaining - half;
|
|
27145
|
+
}
|
|
27146
|
+
const umPick = userMemory === null ? null : selectIndexContent(userMemory, umCap);
|
|
27147
|
+
const umUsed = umPick === null ? 0 : Buffer.byteLength(umPick.text) + Buffer.byteLength(umPick.titles);
|
|
27148
|
+
return {
|
|
27149
|
+
wmPick: workspaceMemory === null ? null : selectIndexContent(workspaceMemory, Math.max(wmCap, remaining - umUsed)),
|
|
27150
|
+
umPick
|
|
27151
|
+
};
|
|
27152
|
+
};
|
|
27153
|
+
const build = (picks, withNotes2) => {
|
|
27154
|
+
const keep = (pick2) => withNotes2 || pick2 !== null && pick2.named === 0;
|
|
27155
|
+
return compose(picks.wmPick === null ? null : picks.wmPick.text, picks.umPick === null ? null : picks.umPick.text, keep(picks.wmPick) ? noteFor(picks.wmPick, WORKSPACE_MEMORY_FILENAME) : null, keep(picks.umPick) ? noteFor(picks.umPick, USER_MEMORY_FILENAME) : null, titlesFor(picks.wmPick, WORKSPACE_MEMORY_FILENAME), titlesFor(picks.umPick, USER_MEMORY_FILENAME));
|
|
27156
|
+
};
|
|
27157
|
+
const refine2 = (seed, withNotes2) => {
|
|
27158
|
+
const asReserve = (section) => section === null ? null : { leadIn: section.leadIn, text: "" };
|
|
27159
|
+
const keep = (pick2) => withNotes2 || pick2 !== null && pick2.named === 0;
|
|
27160
|
+
const fixed = Buffer.byteLength(compose(workspaceMemory === null ? null : "", userMemory === null ? null : "", keep(seed.wmPick) ? noteFor(seed.wmPick, WORKSPACE_MEMORY_FILENAME) : null, keep(seed.umPick) ? noteFor(seed.umPick, USER_MEMORY_FILENAME) : null, asReserve(titlesFor(seed.wmPick, WORKSPACE_MEMORY_FILENAME)), asReserve(titlesFor(seed.umPick, USER_MEMORY_FILENAME))).text);
|
|
27161
|
+
return allocate(Math.max(0, GROUNDING_BUDGET_BYTES - fixed));
|
|
27162
|
+
};
|
|
27163
|
+
const noNotes = allocate(Math.max(0, GROUNDING_BUDGET_BYTES - measureFixed(false, true)));
|
|
27164
|
+
const withNotes = allocate(Math.max(0, GROUNDING_BUDGET_BYTES - measureFixed(true, true)));
|
|
27165
|
+
const candidates = [
|
|
27166
|
+
{ picks: withNotes, composed: build(withNotes, true) },
|
|
27167
|
+
{ picks: noNotes, composed: build(noNotes, false) },
|
|
27168
|
+
{ picks: withNotes, composed: build(withNotes, false) }
|
|
27169
|
+
].concat([
|
|
27170
|
+
{ seed: noNotes, notes: false },
|
|
27171
|
+
{ seed: withNotes, notes: true }
|
|
27172
|
+
].map(({ seed, notes }) => {
|
|
27173
|
+
const picks = refine2(seed, notes);
|
|
27174
|
+
return { picks, composed: build(picks, notes) };
|
|
27175
|
+
})).map((c) => ({
|
|
27176
|
+
...c,
|
|
27177
|
+
bytes: Buffer.byteLength(c.composed.text),
|
|
27178
|
+
named: (c.picks.wmPick?.kept ?? 0) + (c.picks.wmPick?.named ?? 0) + (c.picks.umPick?.kept ?? 0) + (c.picks.umPick?.named ?? 0),
|
|
27179
|
+
whole: (c.picks.wmPick?.kept ?? 0) + (c.picks.umPick?.kept ?? 0)
|
|
27180
|
+
}));
|
|
27181
|
+
const fitting = candidates.filter((c) => c.bytes <= GROUNDING_BUDGET_BYTES);
|
|
27182
|
+
const better = (a, b) => b.named !== a.named ? b.named > a.named : b.whole > a.whole;
|
|
27183
|
+
const best = fitting.length > 0 ? fitting.reduce((a, b) => better(a, b) ? b : a) : candidates.reduce((a, b) => b.bytes < a.bytes ? b : a);
|
|
27184
|
+
return {
|
|
27185
|
+
block: best.composed.text,
|
|
27186
|
+
picks: best.picks,
|
|
27187
|
+
split: best.composed.split,
|
|
27188
|
+
groundingParagraphs: best.composed.groundingParagraphs,
|
|
27189
|
+
groundingRuleShaped: best.composed.groundingRuleShaped
|
|
27190
|
+
};
|
|
27003
27191
|
}
|
|
27004
|
-
|
|
27005
|
-
|
|
27006
|
-
|
|
27007
|
-
|
|
27008
|
-
|
|
27009
|
-
return {
|
|
27192
|
+
var buildGroundingReportFor = buildGroundingReport;
|
|
27193
|
+
function formatLocalDate(d) {
|
|
27194
|
+
const year = d.getFullYear();
|
|
27195
|
+
const month = String(d.getMonth() + 1).padStart(2, "0");
|
|
27196
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
27197
|
+
return `${year}-${month}-${day}`;
|
|
27010
27198
|
}
|
|
27011
|
-
|
|
27012
|
-
|
|
27199
|
+
|
|
27200
|
+
// src/lib/hash.ts
|
|
27201
|
+
import { createHash } from "node:crypto";
|
|
27202
|
+
function stableHash(value) {
|
|
27203
|
+
return createHash("sha256").update(stableStringify(value)).digest("hex");
|
|
27013
27204
|
}
|
|
27014
|
-
function
|
|
27015
|
-
|
|
27016
|
-
|
|
27017
|
-
return { state, changed: false };
|
|
27205
|
+
function stableStringify(value) {
|
|
27206
|
+
if (value === null || typeof value !== "object") {
|
|
27207
|
+
return JSON.stringify(value);
|
|
27018
27208
|
}
|
|
27019
|
-
|
|
27020
|
-
|
|
27021
|
-
function getUserVoiceState(state) {
|
|
27022
|
-
return state.user_voice;
|
|
27023
|
-
}
|
|
27024
|
-
function setUserVoiceState(state, entry) {
|
|
27025
|
-
const existing = state.user_voice;
|
|
27026
|
-
if (existing && existing.body_hash === entry.body_hash && existing.updated_at === entry.updated_at && existing.status === entry.status) {
|
|
27027
|
-
return { state, changed: false };
|
|
27209
|
+
if (Array.isArray(value)) {
|
|
27210
|
+
return "[" + value.map((v) => stableStringify(v)).join(",") + "]";
|
|
27028
27211
|
}
|
|
27029
|
-
|
|
27212
|
+
const obj = value;
|
|
27213
|
+
const keys = Object.keys(obj).sort();
|
|
27214
|
+
return "{" + keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",") + "}";
|
|
27030
27215
|
}
|
|
27031
|
-
|
|
27032
|
-
|
|
27033
|
-
|
|
27034
|
-
|
|
27035
|
-
};
|
|
27036
|
-
function
|
|
27037
|
-
let
|
|
27216
|
+
|
|
27217
|
+
// src/lib/context/roster.ts
|
|
27218
|
+
var import_gray_matter5 = __toESM(require_gray_matter(), 1);
|
|
27219
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync8 } from "node:fs";
|
|
27220
|
+
import { join as join7 } from "node:path";
|
|
27221
|
+
function readRoster(workspaceSlug) {
|
|
27222
|
+
let artistsDir;
|
|
27038
27223
|
try {
|
|
27039
|
-
|
|
27224
|
+
artistsDir = join7(workspaceMirrorPath(workspaceSlug), "artists");
|
|
27040
27225
|
} catch {
|
|
27041
|
-
return
|
|
27226
|
+
return [];
|
|
27042
27227
|
}
|
|
27043
|
-
|
|
27044
|
-
|
|
27045
|
-
|
|
27046
|
-
|
|
27047
|
-
|
|
27048
|
-
const voice = UserVoiceStateSchema.safeParse(record2.user_voice);
|
|
27049
|
-
return {
|
|
27050
|
-
workspaceMemory: workspace.success ? workspace.data : null,
|
|
27051
|
-
userMemory: user.success ? user.data : null,
|
|
27052
|
-
userVoice: voice.success ? voice.data : null
|
|
27053
|
-
};
|
|
27054
|
-
}
|
|
27055
|
-
function setSizeAnchor(state, anchor) {
|
|
27056
|
-
if (anchor.kind === "sync" && state.size_anchor !== undefined) {
|
|
27057
|
-
return { state, changed: false };
|
|
27228
|
+
let slugs;
|
|
27229
|
+
try {
|
|
27230
|
+
slugs = readdirSync3(artistsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
|
|
27231
|
+
} catch {
|
|
27232
|
+
return [];
|
|
27058
27233
|
}
|
|
27059
|
-
const
|
|
27060
|
-
|
|
27061
|
-
|
|
27234
|
+
const roster = [];
|
|
27235
|
+
for (const slug of slugs) {
|
|
27236
|
+
let raw;
|
|
27237
|
+
try {
|
|
27238
|
+
raw = readFileSync8(join7(artistsDir, slug, "_artist.md"), "utf8");
|
|
27239
|
+
} catch {
|
|
27240
|
+
continue;
|
|
27241
|
+
}
|
|
27242
|
+
try {
|
|
27243
|
+
const fm = import_gray_matter5.default(raw).data;
|
|
27244
|
+
const name = typeof fm.name === "string" && fm.name.length > 0 ? fm.name : slug;
|
|
27245
|
+
const status = typeof fm.status === "string" ? fm.status : null;
|
|
27246
|
+
roster.push({ slug, name, status });
|
|
27247
|
+
} catch {
|
|
27248
|
+
roster.push({ slug, name: slug, status: null });
|
|
27249
|
+
}
|
|
27062
27250
|
}
|
|
27063
|
-
return
|
|
27251
|
+
return roster;
|
|
27064
27252
|
}
|
|
27065
|
-
function
|
|
27066
|
-
|
|
27067
|
-
|
|
27068
|
-
|
|
27253
|
+
function rosterIsEmpty(workspaceSlug) {
|
|
27254
|
+
let artistsDir;
|
|
27255
|
+
try {
|
|
27256
|
+
artistsDir = join7(workspaceMirrorPath(workspaceSlug), "artists");
|
|
27257
|
+
} catch {
|
|
27069
27258
|
return false;
|
|
27070
|
-
|
|
27071
|
-
|
|
27072
|
-
|
|
27073
|
-
|
|
27074
|
-
|
|
27075
|
-
|
|
27259
|
+
}
|
|
27260
|
+
let entries;
|
|
27261
|
+
try {
|
|
27262
|
+
entries = readdirSync3(artistsDir, { withFileTypes: true });
|
|
27263
|
+
} catch (err) {
|
|
27264
|
+
return err?.code === "ENOENT";
|
|
27265
|
+
}
|
|
27266
|
+
for (const entry of entries) {
|
|
27267
|
+
if (!entry.isDirectory())
|
|
27268
|
+
continue;
|
|
27269
|
+
try {
|
|
27270
|
+
readFileSync8(join7(artistsDir, entry.name, "_artist.md"), "utf8");
|
|
27271
|
+
return false;
|
|
27272
|
+
} catch (err) {
|
|
27273
|
+
if (err?.code === "ENOENT")
|
|
27274
|
+
continue;
|
|
27275
|
+
return false;
|
|
27276
|
+
}
|
|
27277
|
+
}
|
|
27278
|
+
return true;
|
|
27076
27279
|
}
|
|
27077
27280
|
|
|
27078
27281
|
// src/lib/context/guiding-files.ts
|
|
@@ -27081,6 +27284,7 @@ var CLAUDE_LOCAL_MD = "CLAUDE.local.md";
|
|
|
27081
27284
|
var ARTIST_CLAUDE_MD = "CLAUDE.md";
|
|
27082
27285
|
var SETTINGS_REL = ".claude/settings.json";
|
|
27083
27286
|
var ARTIST_SLUG_PLACEHOLDER = "<slug>";
|
|
27287
|
+
var MEMORY_FACT_SLUG_PLACEHOLDER = "<slug>";
|
|
27084
27288
|
var artistFile = (name) => `artists/${ARTIST_SLUG_PLACEHOLDER}/${name}`;
|
|
27085
27289
|
function withThousands(n) {
|
|
27086
27290
|
return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
@@ -27129,12 +27333,26 @@ var GUIDING_FILES = [
|
|
|
27129
27333
|
reach: "you only",
|
|
27130
27334
|
bar: `${withThousands(RULE_FILE_BAR_BYTES)} bytes`
|
|
27131
27335
|
},
|
|
27336
|
+
{
|
|
27337
|
+
path: `${MEMORY_FACTS_DIR}/${MEMORY_FACT_SLUG_PLACEHOLDER}.md`,
|
|
27338
|
+
writtenBy: "Plinth, from a memory fact; read-only, edits are set aside",
|
|
27339
|
+
readWhen: "read on demand; only its index line is injected",
|
|
27340
|
+
reach: "everyone",
|
|
27341
|
+
bar: NO_BAR
|
|
27342
|
+
},
|
|
27343
|
+
{
|
|
27344
|
+
path: `${MEMORY_FACTS_DIR}/${MEMORY_FACTS_PRIVATE_SEGMENT}/${MEMORY_FACT_SLUG_PLACEHOLDER}.md`,
|
|
27345
|
+
writtenBy: "Plinth, from one of your memory facts; read-only",
|
|
27346
|
+
readWhen: "read on demand; only its index line is injected",
|
|
27347
|
+
reach: "you only",
|
|
27348
|
+
bar: NO_BAR
|
|
27349
|
+
},
|
|
27132
27350
|
{
|
|
27133
27351
|
path: artistFile(ARTIST_ANCHOR_FILENAME),
|
|
27134
27352
|
writtenBy: "you and the agent; syncs both ways",
|
|
27135
27353
|
readWhen: "read on demand",
|
|
27136
27354
|
reach: "everyone",
|
|
27137
|
-
bar:
|
|
27355
|
+
bar: `${withThousands(CONTEXT_BLOCK_BAR_CHARS)} characters per block`
|
|
27138
27356
|
},
|
|
27139
27357
|
{
|
|
27140
27358
|
path: artistFile(ARTIST_MEMORY_ANCHOR),
|
|
@@ -27335,17 +27553,20 @@ function assessGeneratedFileBars(workspaceSlug, deps = {}) {
|
|
|
27335
27553
|
return out;
|
|
27336
27554
|
}
|
|
27337
27555
|
var OPENING_MAX_BYTES = 90;
|
|
27338
|
-
function
|
|
27339
|
-
const stripped =
|
|
27556
|
+
function terminalSafeText(text, maxBytes) {
|
|
27557
|
+
const stripped = text.replace(/[\x00-\x1F\x7F-\x9F]+/g, " ");
|
|
27340
27558
|
const folded = stripped.replace(/\s+/g, " ").trim();
|
|
27341
27559
|
let out = "";
|
|
27342
27560
|
for (const ch of folded) {
|
|
27343
|
-
if (Buffer.byteLength(out + ch) >
|
|
27561
|
+
if (Buffer.byteLength(out + ch) > maxBytes)
|
|
27344
27562
|
return `${out.trimEnd()}…`;
|
|
27345
27563
|
out += ch;
|
|
27346
27564
|
}
|
|
27347
27565
|
return out;
|
|
27348
27566
|
}
|
|
27567
|
+
function terminalSafeOpening(fact) {
|
|
27568
|
+
return terminalSafeText(fact, OPENING_MAX_BYTES);
|
|
27569
|
+
}
|
|
27349
27570
|
function assessIndexLines(workspaceSlug, deps = {}) {
|
|
27350
27571
|
const readContext = deps.readContext ?? readContextFile;
|
|
27351
27572
|
const specs = [
|
|
@@ -27577,7 +27798,7 @@ function runGroundingReport(deps = {}) {
|
|
|
27577
27798
|
return buildGroundingReportFor(formatLocalDate(now()), rules, memory, userMemory, {
|
|
27578
27799
|
workspaceMemoryUpdatedAt: resolveUpdatedAt(rawMemory, freshness.workspaceMemory),
|
|
27579
27800
|
userMemoryUpdatedAt: resolveUpdatedAt(rawUserMemory, freshness.userMemory)
|
|
27580
|
-
}, noArtists, voiceProfile, resolveVoiceMalformed(rawVoiceProfile, freshness.userVoice), workspaceSlug, member, filesPastBar);
|
|
27801
|
+
}, noArtists, voiceProfile, resolveVoiceMalformed(rawVoiceProfile, freshness.userVoice), workspaceSlug, member, filesPastBar, workspaceSlug ? readMemoryStoreModes(workspaceSlug) : BLOB_MEMORY_STORES);
|
|
27581
27802
|
}
|
|
27582
27803
|
var groundingCommand = defineCommand({
|
|
27583
27804
|
meta: {
|
|
@@ -28911,6 +29132,15 @@ function artistUuidBySlug(state) {
|
|
|
28911
29132
|
}
|
|
28912
29133
|
return map2;
|
|
28913
29134
|
}
|
|
29135
|
+
function isOpenTask(fm) {
|
|
29136
|
+
if (fm.done === true)
|
|
29137
|
+
return false;
|
|
29138
|
+
const status = strOrNull(fm.status);
|
|
29139
|
+
return status !== "done" && status !== "archived";
|
|
29140
|
+
}
|
|
29141
|
+
function isActiveProject(fm) {
|
|
29142
|
+
return strOrNull(fm.status) !== "done";
|
|
29143
|
+
}
|
|
28914
29144
|
function readArtistProfile(workspaceSlug, slug) {
|
|
28915
29145
|
const fm = readEntityFrontmatter(workspaceSlug, "artists", slug) ?? {};
|
|
28916
29146
|
return {
|
|
@@ -28940,7 +29170,7 @@ function activeProjectCountsBySlug(workspaceSlug, state) {
|
|
|
28940
29170
|
const artistSlug = slugByArtistId.get(artistId);
|
|
28941
29171
|
if (!artistSlug)
|
|
28942
29172
|
continue;
|
|
28943
|
-
if (
|
|
29173
|
+
if (!isActiveProject(fm))
|
|
28944
29174
|
continue;
|
|
28945
29175
|
counts.set(artistSlug, (counts.get(artistSlug) ?? 0) + 1);
|
|
28946
29176
|
}
|
|
@@ -28972,9 +29202,9 @@ function buildArtistEntityIndex(workspaceSlug, state) {
|
|
|
28972
29202
|
const artistSlug = slugByArtistId.get(artistId);
|
|
28973
29203
|
if (!artistSlug)
|
|
28974
29204
|
continue;
|
|
28975
|
-
|
|
28976
|
-
if (status === "done")
|
|
29205
|
+
if (!isActiveProject(fm))
|
|
28977
29206
|
continue;
|
|
29207
|
+
const status = strOrNull(fm.status);
|
|
28978
29208
|
bucketFor(artistSlug).projects.push({
|
|
28979
29209
|
id: entry2.id,
|
|
28980
29210
|
slug: entry2.slug,
|
|
@@ -28994,7 +29224,7 @@ function buildArtistEntityIndex(workspaceSlug, state) {
|
|
|
28994
29224
|
const artistSlug = slugByArtistId.get(artistId);
|
|
28995
29225
|
if (!artistSlug)
|
|
28996
29226
|
continue;
|
|
28997
|
-
if (fm
|
|
29227
|
+
if (!isOpenTask(fm))
|
|
28998
29228
|
continue;
|
|
28999
29229
|
bucketFor(artistSlug).tasks.push({
|
|
29000
29230
|
id: entry2.id,
|
|
@@ -29617,7 +29847,7 @@ function withUserBlock(lines) {
|
|
|
29617
29847
|
`) + `
|
|
29618
29848
|
`;
|
|
29619
29849
|
}
|
|
29620
|
-
function buildStartCommandMd(rosterIsEmpty2, offerTrim = false) {
|
|
29850
|
+
function buildStartCommandMd(rosterIsEmpty2, offerTrim = false, stores = BLOB_MEMORY_STORES) {
|
|
29621
29851
|
return withUserBlock([
|
|
29622
29852
|
"---",
|
|
29623
29853
|
"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.",
|
|
@@ -29651,6 +29881,10 @@ function buildStartCommandMd(rosterIsEmpty2, offerTrim = false) {
|
|
|
29651
29881
|
"",
|
|
29652
29882
|
"The SessionStart hook injects **both memory indexes**, above: the workspace index (durable facts and conventions that hold for anyone here) and your personal index (facts true of **this member**: how they work, what they prefer). They may arrive trimmed to their newest lines, and the injection says so where it happens. **Do not re-read `workspace-memory.md` or `user-memory.md` wholesale** - they are already in this context window and re-reading them is the single most expensive thing this command could do for nothing - but if the injection says an index was trimmed and you need what is missing, read that file.",
|
|
29653
29883
|
"",
|
|
29884
|
+
...anyGenerated(stores) ? [
|
|
29885
|
+
"**Each index line links to the fact's full text; open only the one you need.** A line reads `- [[memory/<slug>]] - <description>`, and the description is a hook rather than the fact. The body is at `memory/<slug>.md`, or `memory/private/<slug>.md` for one of your own. **Do not read the folder** - that is the whole corpus, which is what the index exists to spare you.",
|
|
29886
|
+
""
|
|
29887
|
+
] : [],
|
|
29654
29888
|
"**Who you are working for is in the grounding above, not in an index.** The hook renders the member, their email and the workspace from the credentials this machine logged in with, so do not go looking for identity in either memory file and do not ask the user for it. **Use the personal index for what it is actually for**, which is the half that gets skipped: how this member works and what they prefer.",
|
|
29655
29889
|
"",
|
|
29656
29890
|
"If neither index appears above, the hook did not run - then, and only then, read the two files at the **mirror root**, the workspace folder holding `CLAUDE.md` (if this session started inside `artists/<slug>/`, that is the parent). Read them silently; do not dump either file into the conversation.",
|
|
@@ -29710,7 +29944,7 @@ function buildStartCommandMd(rosterIsEmpty2, offerTrim = false) {
|
|
|
29710
29944
|
`When the session is finished, \`${CLOSE_COMMAND_INVOCATION}\` is the other half of this one: it banks what was learned before the context is gone.`
|
|
29711
29945
|
]);
|
|
29712
29946
|
}
|
|
29713
|
-
function buildCloseCommandMd(workspaceSlug) {
|
|
29947
|
+
function buildCloseCommandMd(workspaceSlug, stores = BLOB_MEMORY_STORES) {
|
|
29714
29948
|
const ws = `--workspace ${workspaceSlug}`;
|
|
29715
29949
|
return withUserBlock([
|
|
29716
29950
|
"---",
|
|
@@ -29754,7 +29988,7 @@ function buildCloseCommandMd(workspaceSlug) {
|
|
|
29754
29988
|
"Each loose end goes to the surface that holds it, and nowhere else:",
|
|
29755
29989
|
"",
|
|
29756
29990
|
"- **Something to do** goes to a Plinth task, scoped to its artist or project, with an owner and a real due date where one exists. Fold it into the existing umbrella task rather than adding a second.",
|
|
29757
|
-
"- **State or context that changed** goes to the artist's master context, the body of `artists/<slug>/_artist.md`, or to the project's summary. Prose goes through the mirror file.",
|
|
29991
|
+
"- **State or context that changed** goes to the artist's master context, the body of `artists/<slug>/_artist.md`, or to the project's summary. Prose goes through the mirror file. **If the `plinth status` from Step 2 named that artist's `_artist.md` on a `Walls`, `Played` or `As of` row, act on it while you are in the file**: split a wall into one short block per workstream, move what has played out from under its upcoming label, and re-date the current-state heading only once you have reconciled it.",
|
|
29758
29992
|
"- **What happened this session** goes to the artist's own record: a dated section in the body of `artists/<slug>/_artist.md`, one per artist touched. Prose, the way a hub carries it - what was discussed, what was decided, what was done, what is still open. **This is the half a teammate can open.** That body syncs to the artist's Plinth record and the app renders it in the Knowledge Base context pane, so a session banked here is one somebody else can pick up cold. **It is a record of a session, not a statement of where things stand**: current state belongs in the prose above it, edited in place, which is the bullet before this one. A log that accumulates live state rots, and that is the failure this step was moved to escape.",
|
|
29759
29993
|
"",
|
|
29760
29994
|
"**Where the record goes in the file, and it is not the top.** One `## Session log` heading at the **bottom** of the body, newest section first beneath it. The pane renders the whole body, so the master context has to read first and the log sits under it. If the file carries a `<!-- BEGIN: contacts -->` block, the log goes **above** that marker: everything between the contacts markers is rendered from Plinth and is overwritten on the next sync.",
|
|
@@ -29812,6 +30046,10 @@ function buildCloseCommandMd(workspaceSlug) {
|
|
|
29812
30046
|
"",
|
|
29813
30047
|
"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.**",
|
|
29814
30048
|
"",
|
|
30049
|
+
...anyGenerated(stores) ? [
|
|
30050
|
+
"**Some artist stores are generated and some are not, so check before you write.** If `artists/<slug>/memory.md` opens with a `- [[memory/<slug>]] - <description>` list, it is generated from that artist's memory facts and a hand edit will not reach Plinth. Use `create_memory_fact` naming the artist; it shows you the fact first and needs a second call to save it, which is the same ask-first gate as the shared pile because an artist fact is a shared fact. If it reads as ordinary prose, write it directly as above.",
|
|
30051
|
+
""
|
|
30052
|
+
] : [],
|
|
29815
30053
|
"**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.",
|
|
29816
30054
|
"",
|
|
29817
30055
|
"**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.",
|
|
@@ -29834,10 +30072,17 @@ function buildCloseCommandMd(workspaceSlug) {
|
|
|
29834
30072
|
"",
|
|
29835
30073
|
"**Live state does not go here.** This week's open threads, one show's logistics, where a release has got to: that is Step 3's business, and it goes in the body of `artists/<slug>/_artist.md`. This file is for what stays true after the session that taught it.",
|
|
29836
30074
|
"",
|
|
29837
|
-
|
|
29838
|
-
|
|
29839
|
-
|
|
29840
|
-
|
|
30075
|
+
...stores.user ? [
|
|
30076
|
+
"#### True of this member: `create_user_memory_fact`. The file is output.",
|
|
30077
|
+
"",
|
|
30078
|
+
"Facts true of this member: what they prefer, how they work, their own identifiers. **`user-memory.md` is GENERATED from this member's memory facts, so do not write it** - a hand edit does not reach Plinth, is set aside, and `plinth status` says where. Create the fact with `create_user_memory_fact`, which writes on one call because this pile is theirs alone; the index and the body file are rewritten from the facts on the next sync. Correct one with `supersede_user_memory_fact` or `delete_user_memory_fact`, taking the id from the fact file's frontmatter. **List what you wrote in the closing summary**, so it is visible without opening the file.",
|
|
30079
|
+
""
|
|
30080
|
+
] : [
|
|
30081
|
+
"#### True of this member: `user-memory.md` at the mirror root. Write it directly.",
|
|
30082
|
+
"",
|
|
30083
|
+
"Facts true of this member: what they prefer, how they work, their own identifiers. **List what you wrote in the closing summary**, so it is visible without opening the file.",
|
|
30084
|
+
""
|
|
30085
|
+
],
|
|
29841
30086
|
"**How they WRITE does not go here, and it is not a fact.** Their register, greetings, sign-off and the things they never do are a rubric, and they live in `voice.md` at the mirror root - a file the member owns and you are denied from editing, because the profile you are judged against is not yours to rewrite. So do not bank a voice rule in this index or any other. Say what you noticed in the closing summary and leave it for them to put in their own profile.",
|
|
29842
30087
|
"",
|
|
29843
30088
|
"If the file is not there yet, create it the same way as the artist store: a one-line title, the same conventions in a comment, and a real fact in the same edit. **Check what the workspace already says before you write a personal line** - a convention that is already in `workspace-rules.md` or the workspace's own house-style file is in force for everyone, and copying it here duplicates a live rule into a second injected surface instead of adding anything.",
|
|
@@ -30087,9 +30332,10 @@ function ensureWorkspaceCommands(deps) {
|
|
|
30087
30332
|
} catch {
|
|
30088
30333
|
offerTrim = false;
|
|
30089
30334
|
}
|
|
30335
|
+
const stores = (deps.readMemoryStores ?? readMemoryStoreModes)(workspaceSlug);
|
|
30090
30336
|
const bodies = {
|
|
30091
|
-
[START_COMMAND_REL]: buildStartCommandMd(noArtists, offerTrim),
|
|
30092
|
-
[CLOSE_COMMAND_REL]: buildCloseCommandMd(workspaceSlug),
|
|
30337
|
+
[START_COMMAND_REL]: buildStartCommandMd(noArtists, offerTrim, stores),
|
|
30338
|
+
[CLOSE_COMMAND_REL]: buildCloseCommandMd(workspaceSlug, stores),
|
|
30093
30339
|
[ONBOARD_COMMAND_REL]: buildOnboardCommandMd(workspaceSlug)
|
|
30094
30340
|
};
|
|
30095
30341
|
for (const rel of GENERATED_COMMAND_RELS) {
|
|
@@ -30831,6 +31077,37 @@ function clearMemorySecretHeld(state, workspaceSlug, absolutePath) {
|
|
|
30831
31077
|
return state;
|
|
30832
31078
|
return commitState(state, { ...state, rejected: next });
|
|
30833
31079
|
}
|
|
31080
|
+
var MEMORY_GENERATED_REJECT_REASON = "memory_generated";
|
|
31081
|
+
function recordMemoryGeneratedRefusal(args) {
|
|
31082
|
+
let path3;
|
|
31083
|
+
try {
|
|
31084
|
+
path3 = relative3(workspaceMirrorPath(args.workspaceSlug), args.absolutePath);
|
|
31085
|
+
} catch {
|
|
31086
|
+
path3 = args.absolutePath;
|
|
31087
|
+
}
|
|
31088
|
+
const withoutStale = removeUnsynced(args.state, "entity", path3).state;
|
|
31089
|
+
return persist(withoutStale, upsertRejected(withoutStale, {
|
|
31090
|
+
target: "entity",
|
|
31091
|
+
op: "upsert",
|
|
31092
|
+
path: path3,
|
|
31093
|
+
reason: MEMORY_GENERATED_REJECT_REASON,
|
|
31094
|
+
message: args.record,
|
|
31095
|
+
rejected_at: args.now().toISOString()
|
|
31096
|
+
}));
|
|
31097
|
+
}
|
|
31098
|
+
function clearMemoryGeneratedRefusal(state, workspaceSlug, absolutePath) {
|
|
31099
|
+
let path3;
|
|
31100
|
+
try {
|
|
31101
|
+
path3 = relative3(workspaceMirrorPath(workspaceSlug), absolutePath);
|
|
31102
|
+
} catch {
|
|
31103
|
+
path3 = absolutePath;
|
|
31104
|
+
}
|
|
31105
|
+
const list = state.rejected ?? [];
|
|
31106
|
+
const next = list.filter((o) => !(o.target === "entity" && o.path === path3 && o.reason === MEMORY_GENERATED_REJECT_REASON));
|
|
31107
|
+
if (next.length === list.length)
|
|
31108
|
+
return state;
|
|
31109
|
+
return commitState(state, { ...state, rejected: next });
|
|
31110
|
+
}
|
|
30834
31111
|
|
|
30835
31112
|
// src/lib/context/workspace-claude-md.ts
|
|
30836
31113
|
var EXTRACTED_TEXT_DIR = extractedTextRelPath("x").replace(/x\.md$/, "");
|
|
@@ -33957,6 +34234,349 @@ function classifyFilesPath(args) {
|
|
|
33957
34234
|
return validateFilesRelPath(posix) ? { relPath: posix } : null;
|
|
33958
34235
|
}
|
|
33959
34236
|
|
|
34237
|
+
// src/lib/memory-facts/sync.ts
|
|
34238
|
+
import { isAbsolute as isAbsolute2, relative as relative6, sep as sep4 } from "node:path";
|
|
34239
|
+
|
|
34240
|
+
// src/lib/memory-facts/api.ts
|
|
34241
|
+
var FactCommonSchema = exports_external.object({
|
|
34242
|
+
id: exports_external.string(),
|
|
34243
|
+
slug: exports_external.string(),
|
|
34244
|
+
description: exports_external.string(),
|
|
34245
|
+
body_md: exports_external.string(),
|
|
34246
|
+
fact_type: exports_external.string(),
|
|
34247
|
+
status: exports_external.string(),
|
|
34248
|
+
superseded_by: exports_external.string().nullish(),
|
|
34249
|
+
generality: exports_external.string().nullish(),
|
|
34250
|
+
source: exports_external.string().nullish(),
|
|
34251
|
+
created_at: exports_external.string().nullish(),
|
|
34252
|
+
updated_at: exports_external.string().nullish()
|
|
34253
|
+
});
|
|
34254
|
+
var WorkspaceFactWireSchema = FactCommonSchema.extend({
|
|
34255
|
+
artist_id: exports_external.string().nullish(),
|
|
34256
|
+
revision: exports_external.number().nullish()
|
|
34257
|
+
});
|
|
34258
|
+
var UserFactWireSchema = FactCommonSchema;
|
|
34259
|
+
var MemoryFactsFeedSchema = exports_external.object({
|
|
34260
|
+
workspace_facts: exports_external.array(WorkspaceFactWireSchema),
|
|
34261
|
+
user_facts: exports_external.array(UserFactWireSchema)
|
|
34262
|
+
});
|
|
34263
|
+
var ROUTE = "/api/sync/memory-facts";
|
|
34264
|
+
function baseHeaders2(pat) {
|
|
34265
|
+
return { Authorization: `Bearer ${pat}`, Accept: "application/json" };
|
|
34266
|
+
}
|
|
34267
|
+
async function getMemoryFacts(args) {
|
|
34268
|
+
const { baseUrl, pat, fetchImpl = fetch } = args;
|
|
34269
|
+
const url2 = `${baseUrl.replace(/\/$/, "")}${ROUTE}`;
|
|
34270
|
+
let res;
|
|
34271
|
+
try {
|
|
34272
|
+
res = await fetchImpl(url2, { method: "GET", headers: baseHeaders2(pat) });
|
|
34273
|
+
} catch (err) {
|
|
34274
|
+
const msg = err instanceof Error ? err.message : "network error";
|
|
34275
|
+
throw new ApiError(0, `Could not reach ${url2}: ${msg}`);
|
|
34276
|
+
}
|
|
34277
|
+
if (res.status === 401) {
|
|
34278
|
+
throw new ApiError(401, "Token was rejected by the server. Run `plinth login --workspace <slug>` to refresh.");
|
|
34279
|
+
}
|
|
34280
|
+
if (res.status === 429) {
|
|
34281
|
+
throw new ApiError(429, "Rate limited by the server. Wait a few seconds and retry.", parseRetryAfterMs(res.headers.get("retry-after")));
|
|
34282
|
+
}
|
|
34283
|
+
if (!res.ok) {
|
|
34284
|
+
throw new ApiError(res.status, `sync memory-facts failed with HTTP ${res.status}`);
|
|
34285
|
+
}
|
|
34286
|
+
const body = await res.json();
|
|
34287
|
+
const parsed = MemoryFactsFeedSchema.safeParse(body);
|
|
34288
|
+
if (!parsed.success) {
|
|
34289
|
+
throw new ApiError(res.status, `sync memory-facts response did not match the expected shape: ${parsed.error.message}`);
|
|
34290
|
+
}
|
|
34291
|
+
return parsed.data;
|
|
34292
|
+
}
|
|
34293
|
+
|
|
34294
|
+
// src/lib/memory-facts/refusal.ts
|
|
34295
|
+
var MEMORY_IS_GENERATED_CODE = "memory_is_generated";
|
|
34296
|
+
var GeneratedRefusalSchema = exports_external.object({
|
|
34297
|
+
error: exports_external.string(),
|
|
34298
|
+
code: exports_external.literal(MEMORY_IS_GENERATED_CODE)
|
|
34299
|
+
});
|
|
34300
|
+
function factToolsFor(pile) {
|
|
34301
|
+
return pile === "private" ? "`create_user_memory_fact`, `supersede_user_memory_fact` and `delete_user_memory_fact`" : "`create_memory_fact`, `supersede_memory_fact` and `delete_memory_fact`";
|
|
34302
|
+
}
|
|
34303
|
+
function generatedMemoryRefusalRecord(args) {
|
|
34304
|
+
const which = args.factId ? ` (fact \`${args.factId}\`)` : "";
|
|
34305
|
+
return `\`${args.relPath}\`${which} is generated from memory facts, so your edit was ` + `not sent to Plinth. It is kept at ${args.stashPath}. To make the change, ` + `use ${factToolsFor(args.pile)} - the file is rewritten from the facts on ` + `the next sync.`;
|
|
34306
|
+
}
|
|
34307
|
+
var MEMORY_FACT_CONFLICT_ENTITY = "memory";
|
|
34308
|
+
function isGeneratedMemoryStash(label, deps) {
|
|
34309
|
+
if (label.startsWith(`${MEMORY_FACT_CONFLICT_ENTITY}/`)) {
|
|
34310
|
+
const tail = label.slice(MEMORY_FACT_CONFLICT_ENTITY.length + 1);
|
|
34311
|
+
if (tail.startsWith("private."))
|
|
34312
|
+
return "private";
|
|
34313
|
+
if (tail.startsWith("shared."))
|
|
34314
|
+
return "shared";
|
|
34315
|
+
return null;
|
|
34316
|
+
}
|
|
34317
|
+
if (label === "workspace-memory/workspace-memory") {
|
|
34318
|
+
return deps.workspaceMemoryGenerated() ? "shared" : null;
|
|
34319
|
+
}
|
|
34320
|
+
if (label === "user-memory/user-memory") {
|
|
34321
|
+
return deps.userMemoryGenerated() ? "private" : null;
|
|
34322
|
+
}
|
|
34323
|
+
const artist = label.match(/^artists\/(.+)\.memory$/);
|
|
34324
|
+
if (artist) {
|
|
34325
|
+
return deps.artistMemoryGenerated(artist[1]) ? "shared" : null;
|
|
34326
|
+
}
|
|
34327
|
+
return null;
|
|
34328
|
+
}
|
|
34329
|
+
|
|
34330
|
+
// src/lib/memory-facts/refuse.ts
|
|
34331
|
+
function refuseGeneratedMemoryEdit(args) {
|
|
34332
|
+
const stashPath = stashConflict({
|
|
34333
|
+
workspaceSlug: args.workspaceSlug,
|
|
34334
|
+
entityType: args.conflictEntity,
|
|
34335
|
+
entitySlug: args.conflictSlug,
|
|
34336
|
+
timestamp: args.now(),
|
|
34337
|
+
contents: args.localContent,
|
|
34338
|
+
side: "local"
|
|
34339
|
+
});
|
|
34340
|
+
args.writeCanonical(args.canonicalBody);
|
|
34341
|
+
const record2 = generatedMemoryRefusalRecord({
|
|
34342
|
+
relPath: args.relPath,
|
|
34343
|
+
pile: args.pile,
|
|
34344
|
+
stashPath,
|
|
34345
|
+
factId: args.factId
|
|
34346
|
+
});
|
|
34347
|
+
args.output?.warn(record2);
|
|
34348
|
+
args.output?.log(`[MEMORY_GENERATED_REFUSED ws=${args.workspaceSlug} path=${args.relPath} stashed]`);
|
|
34349
|
+
const state = recordMemoryGeneratedRefusal({
|
|
34350
|
+
state: args.state,
|
|
34351
|
+
workspaceSlug: args.workspaceSlug,
|
|
34352
|
+
absolutePath: args.absolutePath,
|
|
34353
|
+
record: record2,
|
|
34354
|
+
now: args.now
|
|
34355
|
+
});
|
|
34356
|
+
return { state, stashPath };
|
|
34357
|
+
}
|
|
34358
|
+
|
|
34359
|
+
// src/lib/memory-facts/render.ts
|
|
34360
|
+
function yamlString(value) {
|
|
34361
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t").replace(/[\u0000-\u001f\u007f]/g, (c) => `\\x${c.charCodeAt(0).toString(16).padStart(2, "0")}`);
|
|
34362
|
+
return `"${escaped}"`;
|
|
34363
|
+
}
|
|
34364
|
+
var SOURCE_UNKNOWN = "unknown";
|
|
34365
|
+
function toRenderableWorkspaceFact(row, artistSlugById) {
|
|
34366
|
+
const artistId = row.artist_id ?? null;
|
|
34367
|
+
return {
|
|
34368
|
+
pile: "shared",
|
|
34369
|
+
id: row.id,
|
|
34370
|
+
slug: row.slug,
|
|
34371
|
+
description: row.description,
|
|
34372
|
+
fact_type: row.fact_type,
|
|
34373
|
+
status: row.status,
|
|
34374
|
+
source: row.source,
|
|
34375
|
+
superseded_by: row.superseded_by,
|
|
34376
|
+
generality: row.generality,
|
|
34377
|
+
body_md: row.body_md,
|
|
34378
|
+
artist_id: artistId,
|
|
34379
|
+
artist_slug: artistId ? artistSlugById(artistId) : undefined
|
|
34380
|
+
};
|
|
34381
|
+
}
|
|
34382
|
+
function toRenderableUserFact(row) {
|
|
34383
|
+
return {
|
|
34384
|
+
pile: "private",
|
|
34385
|
+
id: row.id,
|
|
34386
|
+
slug: row.slug,
|
|
34387
|
+
description: row.description,
|
|
34388
|
+
fact_type: row.fact_type,
|
|
34389
|
+
status: row.status,
|
|
34390
|
+
source: row.source,
|
|
34391
|
+
superseded_by: row.superseded_by,
|
|
34392
|
+
generality: row.generality,
|
|
34393
|
+
body_md: row.body_md
|
|
34394
|
+
};
|
|
34395
|
+
}
|
|
34396
|
+
function renderFactFile(fact) {
|
|
34397
|
+
const lines = ["---"];
|
|
34398
|
+
lines.push(`id: ${yamlString(fact.id)}`);
|
|
34399
|
+
lines.push(`name: ${yamlString(fact.slug)}`);
|
|
34400
|
+
lines.push(`description: ${yamlString(fact.description)}`);
|
|
34401
|
+
lines.push(`type: ${yamlString(fact.fact_type)}`);
|
|
34402
|
+
lines.push(`status: ${yamlString(fact.status)}`);
|
|
34403
|
+
lines.push(`source: ${yamlString(fact.source ?? SOURCE_UNKNOWN)}`);
|
|
34404
|
+
if (fact.generality)
|
|
34405
|
+
lines.push(`generality: ${yamlString(fact.generality)}`);
|
|
34406
|
+
if (fact.artist_id)
|
|
34407
|
+
lines.push(`artist_id: ${yamlString(fact.artist_id)}`);
|
|
34408
|
+
if (fact.artist_slug)
|
|
34409
|
+
lines.push(`artist: ${yamlString(fact.artist_slug)}`);
|
|
34410
|
+
if (fact.superseded_by) {
|
|
34411
|
+
lines.push(`superseded_by: ${yamlString(fact.superseded_by)}`);
|
|
34412
|
+
}
|
|
34413
|
+
lines.push("---");
|
|
34414
|
+
lines.push("");
|
|
34415
|
+
lines.push(...headerComment(fact));
|
|
34416
|
+
lines.push("");
|
|
34417
|
+
const body = fact.body_md.replace(/\s+$/, "");
|
|
34418
|
+
if (body.length > 0)
|
|
34419
|
+
lines.push(body);
|
|
34420
|
+
return lines.join(`
|
|
34421
|
+
`).replace(/\n*$/, "") + `
|
|
34422
|
+
`;
|
|
34423
|
+
}
|
|
34424
|
+
function headerComment(fact) {
|
|
34425
|
+
const rel = memoryFactRelPath(fact.pile, fact.slug);
|
|
34426
|
+
const out = [
|
|
34427
|
+
"<!-- GENERATED by Plinth from a memory fact. Edits here are not saved:",
|
|
34428
|
+
` the next sync rewrites this file. To change this fact, use`,
|
|
34429
|
+
` ${factToolsFor(fact.pile)}`,
|
|
34430
|
+
` with id ${fact.id}. This file is ${rel}.`
|
|
34431
|
+
];
|
|
34432
|
+
if (fact.status !== "active") {
|
|
34433
|
+
out.push(` STATUS ${fact.status.toUpperCase()}: this fact is no longer current and is not in any index.`);
|
|
34434
|
+
}
|
|
34435
|
+
out.push("-->");
|
|
34436
|
+
return out;
|
|
34437
|
+
}
|
|
34438
|
+
|
|
34439
|
+
// src/lib/memory-facts/sync.ts
|
|
34440
|
+
function artistSlugLookup(state) {
|
|
34441
|
+
const entries = state.entries["artists"] ?? {};
|
|
34442
|
+
const byId = new Map;
|
|
34443
|
+
for (const entry2 of Object.values(entries))
|
|
34444
|
+
byId.set(entry2.id, entry2.slug);
|
|
34445
|
+
return (id) => byId.get(id);
|
|
34446
|
+
}
|
|
34447
|
+
async function syncMemoryFacts(args) {
|
|
34448
|
+
const { workspaceSlug, baseUrl, pat, fetchImpl, output, suppressor } = args;
|
|
34449
|
+
const now = args.now ?? (() => new Date);
|
|
34450
|
+
let state = args.state;
|
|
34451
|
+
let feed;
|
|
34452
|
+
try {
|
|
34453
|
+
feed = await getMemoryFacts({ baseUrl, pat, fetchImpl });
|
|
34454
|
+
} catch (err) {
|
|
34455
|
+
if (err instanceof ApiError && err.status === 404) {
|
|
34456
|
+
output?.log(`[MEMORY_FACTS ws=${workspaceSlug} skipped reason=route_absent]`);
|
|
34457
|
+
return { state, written: 0, deleted: 0, refused: 0, facts: 0 };
|
|
34458
|
+
}
|
|
34459
|
+
throw err;
|
|
34460
|
+
}
|
|
34461
|
+
const slugFor = artistSlugLookup(state);
|
|
34462
|
+
const facts = [
|
|
34463
|
+
...feed.workspace_facts.map((row) => toRenderableWorkspaceFact(row, slugFor)),
|
|
34464
|
+
...feed.user_facts.map(toRenderableUserFact)
|
|
34465
|
+
];
|
|
34466
|
+
const acked = getMemoryFactEntries(state);
|
|
34467
|
+
const registry3 = {};
|
|
34468
|
+
const live = new Set;
|
|
34469
|
+
let written = 0;
|
|
34470
|
+
let refused = 0;
|
|
34471
|
+
for (const fact of facts) {
|
|
34472
|
+
let relPath;
|
|
34473
|
+
try {
|
|
34474
|
+
relPath = memoryFactRelPath(fact.pile, fact.slug);
|
|
34475
|
+
} catch {
|
|
34476
|
+
output?.warn(`Memory fact "${fact.slug}" has a slug this mirror cannot write - skipped.`);
|
|
34477
|
+
continue;
|
|
34478
|
+
}
|
|
34479
|
+
live.add(relPath);
|
|
34480
|
+
const rendered = renderFactFile(fact);
|
|
34481
|
+
const renderedHash = stableHash(rendered);
|
|
34482
|
+
const onDisk = readMemoryFactFile(workspaceSlug, fact.pile, fact.slug);
|
|
34483
|
+
const ackedHash = acked[relPath]?.body_hash;
|
|
34484
|
+
if (onDisk !== null && stableHash(onDisk) === renderedHash) {
|
|
34485
|
+
state = clearMemoryGeneratedRefusal(state, workspaceSlug, memoryFactFilePath(workspaceSlug, fact.pile, fact.slug));
|
|
34486
|
+
registry3[relPath] = { id: fact.id, body_hash: renderedHash };
|
|
34487
|
+
continue;
|
|
34488
|
+
}
|
|
34489
|
+
if (onDisk !== null && stableHash(onDisk) !== ackedHash) {
|
|
34490
|
+
const refusal = refuseGeneratedMemoryEdit({
|
|
34491
|
+
state,
|
|
34492
|
+
workspaceSlug,
|
|
34493
|
+
absolutePath: memoryFactFilePath(workspaceSlug, fact.pile, fact.slug),
|
|
34494
|
+
relPath,
|
|
34495
|
+
pile: fact.pile,
|
|
34496
|
+
conflictEntity: "memory",
|
|
34497
|
+
conflictSlug: `${fact.pile}.${fact.slug}`,
|
|
34498
|
+
localContent: onDisk,
|
|
34499
|
+
canonicalBody: rendered,
|
|
34500
|
+
writeCanonical: (body) => writeFact(args, fact.pile, fact.slug, body),
|
|
34501
|
+
factId: fact.id,
|
|
34502
|
+
now,
|
|
34503
|
+
output
|
|
34504
|
+
});
|
|
34505
|
+
state = refusal.state;
|
|
34506
|
+
registry3[relPath] = { id: fact.id, body_hash: renderedHash };
|
|
34507
|
+
refused++;
|
|
34508
|
+
continue;
|
|
34509
|
+
}
|
|
34510
|
+
writeFact(args, fact.pile, fact.slug, rendered);
|
|
34511
|
+
registry3[relPath] = { id: fact.id, body_hash: renderedHash };
|
|
34512
|
+
written++;
|
|
34513
|
+
}
|
|
34514
|
+
let deleted = 0;
|
|
34515
|
+
for (const { pile, slug } of listMemoryFactFiles(workspaceSlug)) {
|
|
34516
|
+
const relPath = memoryFactRelPath(pile, slug);
|
|
34517
|
+
if (live.has(relPath))
|
|
34518
|
+
continue;
|
|
34519
|
+
if (acked[relPath] === undefined)
|
|
34520
|
+
continue;
|
|
34521
|
+
const abs = memoryFactFilePath(workspaceSlug, pile, slug);
|
|
34522
|
+
suppressor?.registerDelete(abs);
|
|
34523
|
+
if (deleteMemoryFactFile(workspaceSlug, pile, slug))
|
|
34524
|
+
deleted++;
|
|
34525
|
+
state = clearMemoryGeneratedRefusal(state, workspaceSlug, abs);
|
|
34526
|
+
}
|
|
34527
|
+
const { state: next, changed } = setMemoryFactsState(state, {
|
|
34528
|
+
fetched_at: now().toISOString(),
|
|
34529
|
+
facts: registry3
|
|
34530
|
+
});
|
|
34531
|
+
state = changed ? commitState(state, next) : state;
|
|
34532
|
+
if (written > 0 || deleted > 0 || refused > 0) {
|
|
34533
|
+
output?.log(`[MEMORY_FACTS ws=${workspaceSlug} facts=${facts.length} written=${written} deleted=${deleted} refused=${refused}]`);
|
|
34534
|
+
}
|
|
34535
|
+
return { state, written, deleted, refused, facts: facts.length };
|
|
34536
|
+
}
|
|
34537
|
+
function writeFact(args, pile, slug, content) {
|
|
34538
|
+
const abs = memoryFactFilePath(args.workspaceSlug, pile, slug);
|
|
34539
|
+
args.suppressor?.registerWrite(abs, {
|
|
34540
|
+
bodyHash: stableHash(content),
|
|
34541
|
+
frontmatterHash: ""
|
|
34542
|
+
});
|
|
34543
|
+
writeMemoryFactFile(args.workspaceSlug, pile, slug, content);
|
|
34544
|
+
}
|
|
34545
|
+
function classifyMemoryFactPath(args) {
|
|
34546
|
+
let root;
|
|
34547
|
+
try {
|
|
34548
|
+
root = memoryFactsSubtreeRoot(args.workspaceSlug);
|
|
34549
|
+
} catch {
|
|
34550
|
+
return null;
|
|
34551
|
+
}
|
|
34552
|
+
const rel = relative6(root, args.absolutePath);
|
|
34553
|
+
if (rel === "" || rel.startsWith("..") || isAbsolute2(rel))
|
|
34554
|
+
return null;
|
|
34555
|
+
const mirrorRel = `${MEMORY_FACTS_DIR}/${rel.split(sep4).join("/")}`;
|
|
34556
|
+
return { relPath: mirrorRel, fact: parseMemoryFactRelPath(mirrorRel) };
|
|
34557
|
+
}
|
|
34558
|
+
function classifyMemoryFactEvent(args) {
|
|
34559
|
+
if (args.path === null)
|
|
34560
|
+
return { kind: "ignore" };
|
|
34561
|
+
if (args.ownEcho)
|
|
34562
|
+
return { kind: "echo" };
|
|
34563
|
+
if (args.kind !== "upsert")
|
|
34564
|
+
return { kind: "ignore" };
|
|
34565
|
+
if (args.path.fact !== null && args.known) {
|
|
34566
|
+
return { kind: "generated", relPath: args.path.relPath, pile: args.path.fact.pile };
|
|
34567
|
+
}
|
|
34568
|
+
return { kind: "unowned", relPath: args.path.relPath };
|
|
34569
|
+
}
|
|
34570
|
+
function memoryFactEventWarning(v) {
|
|
34571
|
+
if (v.kind === "generated") {
|
|
34572
|
+
return `${v.relPath} is generated from a memory fact - your edit will be replaced ` + `on the next sync and kept in your conflicts folder. Change the fact with ` + `${factToolsFor(v.pile)}.`;
|
|
34573
|
+
}
|
|
34574
|
+
if (v.kind === "unowned") {
|
|
34575
|
+
return `${v.relPath} is not synced: \`memory/\` holds files Plinth generates from ` + `memory facts. Put your own notes in \`files/\` instead.`;
|
|
34576
|
+
}
|
|
34577
|
+
return null;
|
|
34578
|
+
}
|
|
34579
|
+
|
|
33960
34580
|
// src/lib/files/destructive-gate.ts
|
|
33961
34581
|
function isFileGutting(priorSize, newSize) {
|
|
33962
34582
|
if (priorSize === undefined)
|
|
@@ -34091,7 +34711,7 @@ class FileDestructiveGate {
|
|
|
34091
34711
|
import { readFileSync as readFileSync18, statSync as statSync10 } from "node:fs";
|
|
34092
34712
|
|
|
34093
34713
|
// src/lib/unsynced.ts
|
|
34094
|
-
import { join as join12, relative as
|
|
34714
|
+
import { join as join12, relative as relative7 } from "node:path";
|
|
34095
34715
|
var RECORDABLE_REASONS = new Set([
|
|
34096
34716
|
"rate_limit",
|
|
34097
34717
|
"server",
|
|
@@ -34111,7 +34731,7 @@ function recordEntityUnsynced(args) {
|
|
|
34111
34731
|
const cat = categorizeError(args.err);
|
|
34112
34732
|
let path5;
|
|
34113
34733
|
try {
|
|
34114
|
-
path5 =
|
|
34734
|
+
path5 = relative7(workspaceMirrorPath(args.workspaceSlug), args.absolutePath);
|
|
34115
34735
|
} catch {
|
|
34116
34736
|
path5 = args.absolutePath;
|
|
34117
34737
|
}
|
|
@@ -35210,11 +35830,11 @@ function createPattern(matcher) {
|
|
|
35210
35830
|
if (matcher.path === string4)
|
|
35211
35831
|
return true;
|
|
35212
35832
|
if (matcher.recursive) {
|
|
35213
|
-
const
|
|
35214
|
-
if (!
|
|
35833
|
+
const relative10 = sp2.relative(matcher.path, string4);
|
|
35834
|
+
if (!relative10) {
|
|
35215
35835
|
return false;
|
|
35216
35836
|
}
|
|
35217
|
-
return !
|
|
35837
|
+
return !relative10.startsWith("..") && !sp2.isAbsolute(relative10);
|
|
35218
35838
|
}
|
|
35219
35839
|
return false;
|
|
35220
35840
|
};
|
|
@@ -35844,7 +36464,7 @@ var chokidar_default = { watch, FSWatcher };
|
|
|
35844
36464
|
|
|
35845
36465
|
// src/lib/watcher.ts
|
|
35846
36466
|
import { statSync as statSync9 } from "node:fs";
|
|
35847
|
-
import { sep as
|
|
36467
|
+
import { sep as sep5 } from "node:path";
|
|
35848
36468
|
|
|
35849
36469
|
// src/lib/rename-classifier.ts
|
|
35850
36470
|
class RenameClassifier {
|
|
@@ -35975,7 +36595,7 @@ function watcherIgnoreMatcher(workspaceRoot) {
|
|
|
35975
36595
|
return (absolutePath) => patterns.some((re) => re.test(absolutePath));
|
|
35976
36596
|
}
|
|
35977
36597
|
function escSep() {
|
|
35978
|
-
return
|
|
36598
|
+
return sep5 === "/" ? "/" : "\\\\";
|
|
35979
36599
|
}
|
|
35980
36600
|
|
|
35981
36601
|
class DebounceMap {
|
|
@@ -36326,7 +36946,7 @@ async function reconcileFilesSubtree(args) {
|
|
|
36326
36946
|
}
|
|
36327
36947
|
|
|
36328
36948
|
// src/lib/artist-memory/sync.ts
|
|
36329
|
-
import { relative as
|
|
36949
|
+
import { relative as relative10 } from "node:path";
|
|
36330
36950
|
|
|
36331
36951
|
// src/lib/context/secret-gate.ts
|
|
36332
36952
|
function applySecretGate(args) {
|
|
@@ -36367,6 +36987,10 @@ var ENTITY_TYPE = "artists";
|
|
|
36367
36987
|
function conflictSlug(slug) {
|
|
36368
36988
|
return `${slug}.memory`;
|
|
36369
36989
|
}
|
|
36990
|
+
var GENERATED_PILE = "shared";
|
|
36991
|
+
function memoryRelPath(slug) {
|
|
36992
|
+
return `artists/${slug}/${ARTIST_MEMORY_ANCHOR}`;
|
|
36993
|
+
}
|
|
36370
36994
|
function persistEntry2(state, entry2) {
|
|
36371
36995
|
const { state: next, changed } = upsertEntry(state, ENTITY_TYPE, entry2);
|
|
36372
36996
|
if (!changed)
|
|
@@ -36393,7 +37017,7 @@ function classifyArtistMemoryPath(args) {
|
|
|
36393
37017
|
} catch {
|
|
36394
37018
|
return null;
|
|
36395
37019
|
}
|
|
36396
|
-
const rel =
|
|
37020
|
+
const rel = relative10(root, args.absolutePath);
|
|
36397
37021
|
const parts = rel.split("/");
|
|
36398
37022
|
if (parts.length !== 3)
|
|
36399
37023
|
return null;
|
|
@@ -36415,25 +37039,55 @@ function classifyArtistMemoryPath(args) {
|
|
|
36415
37039
|
}
|
|
36416
37040
|
function applyArtistMemoryOnPull(args) {
|
|
36417
37041
|
const { workspaceSlug, live, ackedMemoryHash, suppressor, now, out } = args;
|
|
37042
|
+
let state = args.state;
|
|
36418
37043
|
const slug = live.slug;
|
|
36419
37044
|
const cloudMemory = live.memory;
|
|
37045
|
+
const generated = live.memory_generated === true;
|
|
36420
37046
|
const local = readArtistMemoryFile(workspaceSlug, slug);
|
|
37047
|
+
const refuse = (localContent, canonical) => {
|
|
37048
|
+
const refused = refuseGeneratedMemoryEdit({
|
|
37049
|
+
state,
|
|
37050
|
+
workspaceSlug,
|
|
37051
|
+
absolutePath: artistMemoryFilePath(workspaceSlug, slug),
|
|
37052
|
+
relPath: memoryRelPath(slug),
|
|
37053
|
+
pile: GENERATED_PILE,
|
|
37054
|
+
conflictEntity: ENTITY_TYPE,
|
|
37055
|
+
conflictSlug: conflictSlug(slug),
|
|
37056
|
+
localContent,
|
|
37057
|
+
canonicalBody: canonical,
|
|
37058
|
+
writeCanonical: (body) => writeMemory(workspaceSlug, slug, body, suppressor),
|
|
37059
|
+
now,
|
|
37060
|
+
output: out
|
|
37061
|
+
});
|
|
37062
|
+
state = refused.state;
|
|
37063
|
+
};
|
|
36421
37064
|
if (cloudMemory === null) {
|
|
36422
37065
|
if (local !== null && isPristineMemoryStub(local)) {
|
|
36423
37066
|
const abs = artistMemoryFilePath(workspaceSlug, slug);
|
|
36424
37067
|
suppressor?.registerDelete(abs);
|
|
36425
37068
|
deleteArtistMemoryFile(workspaceSlug, slug);
|
|
36426
|
-
return { memoryHash: undefined };
|
|
37069
|
+
return { memoryHash: undefined, state };
|
|
36427
37070
|
}
|
|
36428
|
-
return { memoryHash: ackedMemoryHash };
|
|
37071
|
+
return { memoryHash: ackedMemoryHash, state };
|
|
36429
37072
|
}
|
|
36430
37073
|
const cloudHash = stableHash(cloudMemory);
|
|
36431
37074
|
if (cloudHash === ackedMemoryHash && local !== null) {
|
|
36432
|
-
|
|
37075
|
+
if (generated && stableHash(local) !== ackedMemoryHash) {
|
|
37076
|
+
refuse(local, cloudMemory);
|
|
37077
|
+
return { memoryHash: cloudHash, state };
|
|
37078
|
+
}
|
|
37079
|
+
if (generated) {
|
|
37080
|
+
state = clearMemoryGeneratedRefusal(state, workspaceSlug, artistMemoryFilePath(workspaceSlug, slug));
|
|
37081
|
+
}
|
|
37082
|
+
return { memoryHash: ackedMemoryHash, state };
|
|
36433
37083
|
}
|
|
36434
37084
|
if (local !== null) {
|
|
36435
37085
|
const localHash = stableHash(local);
|
|
36436
37086
|
if (localHash !== ackedMemoryHash && localHash !== cloudHash) {
|
|
37087
|
+
if (generated) {
|
|
37088
|
+
refuse(local, cloudMemory);
|
|
37089
|
+
return { memoryHash: cloudHash, state };
|
|
37090
|
+
}
|
|
36437
37091
|
const stashPath = stashConflict({
|
|
36438
37092
|
workspaceSlug,
|
|
36439
37093
|
entityType: ENTITY_TYPE,
|
|
@@ -36446,7 +37100,7 @@ function applyArtistMemoryOnPull(args) {
|
|
|
36446
37100
|
}
|
|
36447
37101
|
}
|
|
36448
37102
|
writeMemory(workspaceSlug, slug, cloudMemory, suppressor);
|
|
36449
|
-
return { memoryHash: cloudHash };
|
|
37103
|
+
return { memoryHash: cloudHash, state };
|
|
36450
37104
|
}
|
|
36451
37105
|
function writeMemory(workspaceSlug, slug, content, suppressor) {
|
|
36452
37106
|
const abs = artistMemoryFilePath(workspaceSlug, slug);
|
|
@@ -36489,8 +37143,46 @@ async function pushArtistMemory(args) {
|
|
|
36489
37143
|
if (gate.held)
|
|
36490
37144
|
return { state: base2, outcome: { kind: "secret_held", slug } };
|
|
36491
37145
|
if (alreadyAcked) {
|
|
37146
|
+
if (entry2.memory_generated === true) {
|
|
37147
|
+
base2 = clearMemoryGeneratedRefusal(base2, workspaceSlug, artistMemoryFilePath(workspaceSlug, slug));
|
|
37148
|
+
}
|
|
36492
37149
|
return { state: base2, outcome: { kind: "noop", slug } };
|
|
36493
37150
|
}
|
|
37151
|
+
if (entry2.memory_generated === true) {
|
|
37152
|
+
const row = await getEntity({
|
|
37153
|
+
strategy: artistsStrategy,
|
|
37154
|
+
baseUrl,
|
|
37155
|
+
pat,
|
|
37156
|
+
id: entry2.id,
|
|
37157
|
+
fetchImpl
|
|
37158
|
+
});
|
|
37159
|
+
const canonical = row.memory ?? "";
|
|
37160
|
+
const refused = refuseGeneratedMemoryEdit({
|
|
37161
|
+
state: base2,
|
|
37162
|
+
workspaceSlug,
|
|
37163
|
+
absolutePath: artistMemoryFilePath(workspaceSlug, slug),
|
|
37164
|
+
relPath: memoryRelPath(slug),
|
|
37165
|
+
pile: GENERATED_PILE,
|
|
37166
|
+
conflictEntity: ENTITY_TYPE,
|
|
37167
|
+
conflictSlug: conflictSlug(slug),
|
|
37168
|
+
localContent: local,
|
|
37169
|
+
canonicalBody: canonical,
|
|
37170
|
+
writeCanonical: (body) => writeMemory(workspaceSlug, slug, body, suppressor),
|
|
37171
|
+
now,
|
|
37172
|
+
output
|
|
37173
|
+
});
|
|
37174
|
+
const stamped = {
|
|
37175
|
+
...entry2,
|
|
37176
|
+
updated_at: row.updated_at,
|
|
37177
|
+
revision: row.revision,
|
|
37178
|
+
memory_hash: stableHash(canonical),
|
|
37179
|
+
memory_generated: row.memory_generated
|
|
37180
|
+
};
|
|
37181
|
+
return {
|
|
37182
|
+
state: persistEntry2(refused.state, stamped),
|
|
37183
|
+
outcome: { kind: "generated_refused", slug, stashPath: refused.stashPath }
|
|
37184
|
+
};
|
|
37185
|
+
}
|
|
36494
37186
|
const acked = entry2.memory_hash;
|
|
36495
37187
|
let cas = entry2.updated_at;
|
|
36496
37188
|
const landed = (doc2, note = "") => {
|
|
@@ -36998,16 +37690,7 @@ async function fetchCalendar(args) {
|
|
|
36998
37690
|
return parsed.data;
|
|
36999
37691
|
}
|
|
37000
37692
|
|
|
37001
|
-
// src/lib/calendar/
|
|
37002
|
-
var CALENDAR_BODY_MARKER = "<!-- PLINTH:CALENDAR-BODY -->";
|
|
37003
|
-
var OWNERSHIP_NOTE2 = [
|
|
37004
|
-
"<!-- Generated by Plinth from the calendars mapped in this workspace. This file",
|
|
37005
|
-
" is DERIVED: one-way, overwritten on every refresh, and never uploaded.",
|
|
37006
|
-
" Your edits here are lost. Dates are changed in Plinth's calendar view, by a",
|
|
37007
|
-
" person - not from this file and not by an agent. -->"
|
|
37008
|
-
].join(`
|
|
37009
|
-
`);
|
|
37010
|
-
var DEFAULT_USER_LABEL = "the Plinth account this mirror syncs with";
|
|
37693
|
+
// src/lib/calendar/day.ts
|
|
37011
37694
|
var WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
37012
37695
|
var MONTHS = [
|
|
37013
37696
|
"Jan",
|
|
@@ -37024,32 +37707,12 @@ var MONTHS = [
|
|
|
37024
37707
|
"Dec"
|
|
37025
37708
|
];
|
|
37026
37709
|
var DAY_MS = 86400000;
|
|
37027
|
-
var FULL_BAND_DAYS = 14;
|
|
37028
|
-
function yamlString(value) {
|
|
37029
|
-
return JSON.stringify(value);
|
|
37030
|
-
}
|
|
37031
|
-
function oneLine2(value, maxLength = 200) {
|
|
37032
|
-
const flattened = value.replace(/[\u0000-\u001F\u007F]+/g, " ").replace(/<!--/g, "<! --").replace(/\s+/g, " ").trim();
|
|
37033
|
-
if (flattened.length === 0)
|
|
37034
|
-
return "(untitled)";
|
|
37035
|
-
if (flattened.length <= maxLength)
|
|
37036
|
-
return flattened;
|
|
37037
|
-
return `${flattened.slice(0, maxLength - 3)}...`;
|
|
37038
|
-
}
|
|
37039
37710
|
function dateParts(value) {
|
|
37040
37711
|
const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(value);
|
|
37041
37712
|
if (!m)
|
|
37042
37713
|
return null;
|
|
37043
37714
|
return { y: Number(m[1]), m: Number(m[2]), d: Number(m[3]) };
|
|
37044
37715
|
}
|
|
37045
|
-
function timeParts(value) {
|
|
37046
|
-
const m = /T(\d{2}):(\d{2})(?::\d{2}(?:\.\d+)?)?(Z|[+-]\d{2}:\d{2})?$/.exec(value);
|
|
37047
|
-
if (!m)
|
|
37048
|
-
return null;
|
|
37049
|
-
const rawOffset = m[3];
|
|
37050
|
-
const offset = rawOffset === undefined ? "" : rawOffset === "Z" ? "UTC" : rawOffset;
|
|
37051
|
-
return { hh: m[1], mi: m[2], offset };
|
|
37052
|
-
}
|
|
37053
37716
|
function utcMs(parts) {
|
|
37054
37717
|
return Date.UTC(parts.y, parts.m - 1, parts.d);
|
|
37055
37718
|
}
|
|
@@ -37063,6 +37726,116 @@ function formatDay(parts, referenceYear) {
|
|
|
37063
37726
|
const year = parts.y === referenceYear ? "" : ` ${parts.y}`;
|
|
37064
37727
|
return `${weekday} ${parts.d} ${month}${year}`;
|
|
37065
37728
|
}
|
|
37729
|
+
function localDayOf(now) {
|
|
37730
|
+
return { y: now.getFullYear(), m: now.getMonth() + 1, d: now.getDate() };
|
|
37731
|
+
}
|
|
37732
|
+
function daysBetween2(from, to) {
|
|
37733
|
+
return Math.round((utcMs(to) - utcMs(from)) / DAY_MS);
|
|
37734
|
+
}
|
|
37735
|
+
function isRealDay(parts) {
|
|
37736
|
+
if (parts.m < 1 || parts.m > 12 || parts.d < 1 || parts.d > 31)
|
|
37737
|
+
return false;
|
|
37738
|
+
const d = new Date(utcMs(parts));
|
|
37739
|
+
return d.getUTCFullYear() === parts.y && d.getUTCMonth() + 1 === parts.m && d.getUTCDate() === parts.d;
|
|
37740
|
+
}
|
|
37741
|
+
var WEEKDAY_WORD = "(Sun(?:day)?|Mon(?:day)?|Tue(?:s|sday)?|Wed(?:nesday)?|Thu(?:r|rs|rsday)?|Fri(?:day)?|Sat(?:urday)?)";
|
|
37742
|
+
var MONTH_WORD = "(Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|June?|July?|Aug(?:ust)?|Sep(?:t(?:ember)?)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)";
|
|
37743
|
+
var MENTION_RE = new RegExp(`\\b(\\d{4})-(\\d{2})-(\\d{2})\\b` + `|` + `(?:\\b${WEEKDAY_WORD},?\\s+)?\\b(\\d{1,2})(?:st|nd|rd|th)?\\s+${MONTH_WORD}\\b\\.?(?:,?\\s+(\\d{4})\\b)?`, "gi");
|
|
37744
|
+
var YEAR_WINDOW = 10;
|
|
37745
|
+
function weekdayIndex(word) {
|
|
37746
|
+
const stem = word.slice(0, 3).toLowerCase();
|
|
37747
|
+
return WEEKDAYS.findIndex((w) => w.toLowerCase() === stem);
|
|
37748
|
+
}
|
|
37749
|
+
function monthIndex(word) {
|
|
37750
|
+
const stem = word.slice(0, 3).toLowerCase();
|
|
37751
|
+
return MONTHS.findIndex((m) => m.toLowerCase() === stem);
|
|
37752
|
+
}
|
|
37753
|
+
function inferYear(day, month, weekday, today) {
|
|
37754
|
+
const candidates = [today.y, today.y - 1, today.y + 1].map((y) => ({ y, parts: { y, m: month, d: day } })).filter((c) => isRealDay(c.parts)).sort((a, b) => Math.abs(utcMs(a.parts) - utcMs(today)) - Math.abs(utcMs(b.parts) - utcMs(today)));
|
|
37755
|
+
if (weekday !== null) {
|
|
37756
|
+
const match = candidates.find((c) => new Date(utcMs(c.parts)).getUTCDay() === weekday);
|
|
37757
|
+
if (match)
|
|
37758
|
+
return match.y;
|
|
37759
|
+
}
|
|
37760
|
+
return candidates[0]?.y ?? today.y;
|
|
37761
|
+
}
|
|
37762
|
+
function readDayMentions(text, today) {
|
|
37763
|
+
const out = [];
|
|
37764
|
+
for (const m of text.matchAll(MENTION_RE)) {
|
|
37765
|
+
let parts;
|
|
37766
|
+
let yearInferred = false;
|
|
37767
|
+
let length = m[0].length;
|
|
37768
|
+
if (m[1] !== undefined) {
|
|
37769
|
+
parts = { y: Number(m[1]), m: Number(m[2]), d: Number(m[3]) };
|
|
37770
|
+
} else {
|
|
37771
|
+
if (m[6] === "may")
|
|
37772
|
+
continue;
|
|
37773
|
+
const weekday = m[4] === undefined ? null : weekdayIndex(m[4]);
|
|
37774
|
+
const day = Number(m[5]);
|
|
37775
|
+
const month = monthIndex(m[6]) + 1;
|
|
37776
|
+
if (month < 1)
|
|
37777
|
+
continue;
|
|
37778
|
+
const written = m[7] === undefined ? null : Number(m[7]);
|
|
37779
|
+
if (written !== null && Math.abs(written - today.y) <= YEAR_WINDOW) {
|
|
37780
|
+
parts = { y: written, m: month, d: day };
|
|
37781
|
+
} else {
|
|
37782
|
+
if (written !== null)
|
|
37783
|
+
length -= /,?\s+\d{4}$/.exec(m[0])?.[0].length ?? 0;
|
|
37784
|
+
parts = { y: inferYear(day, month, weekday, today), m: month, d: day };
|
|
37785
|
+
yearInferred = true;
|
|
37786
|
+
}
|
|
37787
|
+
}
|
|
37788
|
+
if (!isRealDay(parts))
|
|
37789
|
+
continue;
|
|
37790
|
+
out.push({ parts, index: m.index, length, yearInferred });
|
|
37791
|
+
}
|
|
37792
|
+
return out;
|
|
37793
|
+
}
|
|
37794
|
+
function readAsOf(text, today) {
|
|
37795
|
+
const at = /\bas\s+of\b/i.exec(text);
|
|
37796
|
+
if (!at)
|
|
37797
|
+
return null;
|
|
37798
|
+
const after = text.slice(at.index + at[0].length);
|
|
37799
|
+
const first = readDayMentions(after, today)[0];
|
|
37800
|
+
if (!first)
|
|
37801
|
+
return null;
|
|
37802
|
+
if (!/^[\s:,\-(]*$/.test(after.slice(0, first.index)))
|
|
37803
|
+
return null;
|
|
37804
|
+
const ahead = utcMs(first.parts) > utcMs(today);
|
|
37805
|
+
const parts = first.yearInferred && ahead ? { ...first.parts, y: first.parts.y - 1 } : first.parts;
|
|
37806
|
+
return { parts, yearInferred: first.yearInferred };
|
|
37807
|
+
}
|
|
37808
|
+
|
|
37809
|
+
// src/lib/calendar/render.ts
|
|
37810
|
+
var CALENDAR_BODY_MARKER = "<!-- PLINTH:CALENDAR-BODY -->";
|
|
37811
|
+
var OWNERSHIP_NOTE2 = [
|
|
37812
|
+
"<!-- Generated by Plinth from the calendars mapped in this workspace. This file",
|
|
37813
|
+
" is DERIVED: one-way, overwritten on every refresh, and never uploaded.",
|
|
37814
|
+
" Your edits here are lost. Dates are changed in Plinth's calendar view, by a",
|
|
37815
|
+
" person - not from this file and not by an agent. -->"
|
|
37816
|
+
].join(`
|
|
37817
|
+
`);
|
|
37818
|
+
var DEFAULT_USER_LABEL = "the Plinth account this mirror syncs with";
|
|
37819
|
+
var FULL_BAND_DAYS = 14;
|
|
37820
|
+
function yamlString2(value) {
|
|
37821
|
+
return JSON.stringify(value);
|
|
37822
|
+
}
|
|
37823
|
+
function oneLine2(value, maxLength = 200) {
|
|
37824
|
+
const flattened = value.replace(/[\u0000-\u001F\u007F]+/g, " ").replace(/<!--/g, "<! --").replace(/\s+/g, " ").trim();
|
|
37825
|
+
if (flattened.length === 0)
|
|
37826
|
+
return "(untitled)";
|
|
37827
|
+
if (flattened.length <= maxLength)
|
|
37828
|
+
return flattened;
|
|
37829
|
+
return `${flattened.slice(0, maxLength - 3)}...`;
|
|
37830
|
+
}
|
|
37831
|
+
function timeParts(value) {
|
|
37832
|
+
const m = /T(\d{2}):(\d{2})(?::\d{2}(?:\.\d+)?)?(Z|[+-]\d{2}:\d{2})?$/.exec(value);
|
|
37833
|
+
if (!m)
|
|
37834
|
+
return null;
|
|
37835
|
+
const rawOffset = m[3];
|
|
37836
|
+
const offset = rawOffset === undefined ? "" : rawOffset === "Z" ? "UTC" : rawOffset;
|
|
37837
|
+
return { hh: m[1], mi: m[2], offset };
|
|
37838
|
+
}
|
|
37066
37839
|
function sortInstant(event) {
|
|
37067
37840
|
if (!event.all_day) {
|
|
37068
37841
|
const ms = Date.parse(event.start);
|
|
@@ -37199,12 +37972,12 @@ function renderArtistCalendar(args) {
|
|
|
37199
37972
|
"---",
|
|
37200
37973
|
"plinth_generated: true",
|
|
37201
37974
|
"view: calendar",
|
|
37202
|
-
`artist_slug: ${row.artist_slug === null ? "null" :
|
|
37203
|
-
`artist_name: ${row.artist_name === null ? "null" :
|
|
37204
|
-
`calendar_name: ${
|
|
37205
|
-
`generated_at: ${
|
|
37206
|
-
`window_start: ${
|
|
37207
|
-
`window_end: ${
|
|
37975
|
+
`artist_slug: ${row.artist_slug === null ? "null" : yamlString2(row.artist_slug)}`,
|
|
37976
|
+
`artist_name: ${row.artist_name === null ? "null" : yamlString2(row.artist_name)}`,
|
|
37977
|
+
`calendar_name: ${yamlString2(row.calendar_name)}`,
|
|
37978
|
+
`generated_at: ${yamlString2(generatedAt)}`,
|
|
37979
|
+
`window_start: ${yamlString2(window2.time_min)}`,
|
|
37980
|
+
`window_end: ${yamlString2(window2.time_max)}`,
|
|
37208
37981
|
`status: ${row.status}`,
|
|
37209
37982
|
"fetch_state: fresh",
|
|
37210
37983
|
`entry_count: ${events.length}`,
|
|
@@ -37285,9 +38058,9 @@ function renderUpcoming(args) {
|
|
|
37285
38058
|
"plinth_generated: true",
|
|
37286
38059
|
"view: calendar",
|
|
37287
38060
|
"scope: workspace",
|
|
37288
|
-
`generated_at: ${
|
|
37289
|
-
`window_start: ${
|
|
37290
|
-
`window_end: ${
|
|
38061
|
+
`generated_at: ${yamlString2(generatedAt)}`,
|
|
38062
|
+
`window_start: ${yamlString2(window2.time_min)}`,
|
|
38063
|
+
`window_end: ${yamlString2(window2.time_max)}`,
|
|
37291
38064
|
`artist_count: ${artistCount}`,
|
|
37292
38065
|
`mapped_artist_count: ${mappedArtistCount}`,
|
|
37293
38066
|
`calendars_read: ${fetched.length}`,
|
|
@@ -37383,8 +38156,8 @@ function renderStaleFile(existing, args) {
|
|
|
37383
38156
|
"---",
|
|
37384
38157
|
...carried,
|
|
37385
38158
|
"fetch_state: stale",
|
|
37386
|
-
`last_refresh_attempt: ${
|
|
37387
|
-
`last_refresh_error: ${
|
|
38159
|
+
`last_refresh_attempt: ${yamlString2(args.attemptedAt)}`,
|
|
38160
|
+
`last_refresh_error: ${yamlString2(oneLine2(args.reason, 300))}`,
|
|
37388
38161
|
"---",
|
|
37389
38162
|
"",
|
|
37390
38163
|
OWNERSHIP_NOTE2,
|
|
@@ -38287,7 +39060,7 @@ async function reconcileDivergedRows(args) {
|
|
|
38287
39060
|
|
|
38288
39061
|
// src/lib/workspace-rules/sync.ts
|
|
38289
39062
|
import { existsSync as existsSync4 } from "node:fs";
|
|
38290
|
-
import { relative as
|
|
39063
|
+
import { relative as relative11 } from "node:path";
|
|
38291
39064
|
|
|
38292
39065
|
// src/lib/workspace-rules/api.ts
|
|
38293
39066
|
var MAX_WORKSPACE_RULES_BYTES = 256 * 1024;
|
|
@@ -38302,16 +39075,16 @@ var PreconditionSchema2 = exports_external.object({
|
|
|
38302
39075
|
code: exports_external.literal("precondition_failed"),
|
|
38303
39076
|
current_updated_at: exports_external.string()
|
|
38304
39077
|
});
|
|
38305
|
-
function
|
|
39078
|
+
function baseHeaders3(pat) {
|
|
38306
39079
|
return { Authorization: `Bearer ${pat}`, Accept: "application/json" };
|
|
38307
39080
|
}
|
|
38308
|
-
var
|
|
39081
|
+
var ROUTE2 = "/api/sync/workspace-rules";
|
|
38309
39082
|
async function getWorkspaceRules(args) {
|
|
38310
39083
|
const { baseUrl, pat, fetchImpl = fetch } = args;
|
|
38311
|
-
const url2 = `${baseUrl.replace(/\/$/, "")}${
|
|
39084
|
+
const url2 = `${baseUrl.replace(/\/$/, "")}${ROUTE2}`;
|
|
38312
39085
|
let res;
|
|
38313
39086
|
try {
|
|
38314
|
-
res = await fetchImpl(url2, { method: "GET", headers:
|
|
39087
|
+
res = await fetchImpl(url2, { method: "GET", headers: baseHeaders3(pat) });
|
|
38315
39088
|
} catch (err) {
|
|
38316
39089
|
const msg = err instanceof Error ? err.message : "network error";
|
|
38317
39090
|
throw new ApiError(0, `Could not reach ${url2}: ${msg}`);
|
|
@@ -38334,9 +39107,9 @@ async function getWorkspaceRules(args) {
|
|
|
38334
39107
|
}
|
|
38335
39108
|
async function putWorkspaceRules(args) {
|
|
38336
39109
|
const { baseUrl, pat, bodyMd, expectedUpdatedAt, fetchImpl = fetch } = args;
|
|
38337
|
-
const url2 = `${baseUrl.replace(/\/$/, "")}${
|
|
39110
|
+
const url2 = `${baseUrl.replace(/\/$/, "")}${ROUTE2}`;
|
|
38338
39111
|
const headers = {
|
|
38339
|
-
...
|
|
39112
|
+
...baseHeaders3(pat),
|
|
38340
39113
|
"Content-Type": "application/json"
|
|
38341
39114
|
};
|
|
38342
39115
|
if (expectedUpdatedAt)
|
|
@@ -38422,7 +39195,7 @@ function classifyWorkspaceRulesPath(args) {
|
|
|
38422
39195
|
} catch {
|
|
38423
39196
|
return false;
|
|
38424
39197
|
}
|
|
38425
|
-
return
|
|
39198
|
+
return relative11(root, args.absolutePath) === WORKSPACE_RULES_FILENAME;
|
|
38426
39199
|
}
|
|
38427
39200
|
function migrateHouseStyleFile(workspaceSlug) {
|
|
38428
39201
|
let target;
|
|
@@ -38573,7 +39346,7 @@ async function reconcileWorkspaceRules(args) {
|
|
|
38573
39346
|
var WORKSPACE_RULES_MUTEX_KEY = "workspace-rules";
|
|
38574
39347
|
|
|
38575
39348
|
// src/lib/workspace-memory/sync.ts
|
|
38576
|
-
import { relative as
|
|
39349
|
+
import { relative as relative12 } from "node:path";
|
|
38577
39350
|
|
|
38578
39351
|
// src/lib/workspace-memory/api.ts
|
|
38579
39352
|
var MAX_WORKSPACE_MEMORY_BYTES = 256 * 1024;
|
|
@@ -38582,22 +39355,23 @@ var WorkspaceMemoryProjectionSchema = exports_external.object({
|
|
|
38582
39355
|
is_empty: exports_external.boolean(),
|
|
38583
39356
|
updated_at: exports_external.string().datetime({ offset: true }).nullable(),
|
|
38584
39357
|
created_at: exports_external.string().datetime({ offset: true }).nullable(),
|
|
38585
|
-
last_edited_by: exports_external.string().uuid().nullable()
|
|
39358
|
+
last_edited_by: exports_external.string().uuid().nullable(),
|
|
39359
|
+
generated: exports_external.boolean().optional().catch(undefined)
|
|
38586
39360
|
});
|
|
38587
39361
|
var PreconditionSchema3 = exports_external.object({
|
|
38588
39362
|
code: exports_external.literal("precondition_failed"),
|
|
38589
39363
|
current_updated_at: exports_external.string()
|
|
38590
39364
|
});
|
|
38591
|
-
function
|
|
39365
|
+
function baseHeaders4(pat) {
|
|
38592
39366
|
return { Authorization: `Bearer ${pat}`, Accept: "application/json" };
|
|
38593
39367
|
}
|
|
38594
|
-
var
|
|
39368
|
+
var ROUTE3 = "/api/sync/workspace-memory";
|
|
38595
39369
|
async function getWorkspaceMemory(args) {
|
|
38596
39370
|
const { baseUrl, pat, fetchImpl = fetch } = args;
|
|
38597
|
-
const url2 = `${baseUrl.replace(/\/$/, "")}${
|
|
39371
|
+
const url2 = `${baseUrl.replace(/\/$/, "")}${ROUTE3}`;
|
|
38598
39372
|
let res;
|
|
38599
39373
|
try {
|
|
38600
|
-
res = await fetchImpl(url2, { method: "GET", headers:
|
|
39374
|
+
res = await fetchImpl(url2, { method: "GET", headers: baseHeaders4(pat) });
|
|
38601
39375
|
} catch (err) {
|
|
38602
39376
|
const msg = err instanceof Error ? err.message : "network error";
|
|
38603
39377
|
throw new ApiError(0, `Could not reach ${url2}: ${msg}`);
|
|
@@ -38620,9 +39394,9 @@ async function getWorkspaceMemory(args) {
|
|
|
38620
39394
|
}
|
|
38621
39395
|
async function putWorkspaceMemory(args) {
|
|
38622
39396
|
const { baseUrl, pat, bodyMd, expectedUpdatedAt, fetchImpl = fetch } = args;
|
|
38623
|
-
const url2 = `${baseUrl.replace(/\/$/, "")}${
|
|
39397
|
+
const url2 = `${baseUrl.replace(/\/$/, "")}${ROUTE3}`;
|
|
38624
39398
|
const headers = {
|
|
38625
|
-
...
|
|
39399
|
+
...baseHeaders4(pat),
|
|
38626
39400
|
"Content-Type": "application/json"
|
|
38627
39401
|
};
|
|
38628
39402
|
if (expectedUpdatedAt)
|
|
@@ -38657,6 +39431,14 @@ async function putWorkspaceMemory(args) {
|
|
|
38657
39431
|
}
|
|
38658
39432
|
if (res.status === 409) {
|
|
38659
39433
|
const json2 = await res.json();
|
|
39434
|
+
const generated = GeneratedRefusalSchema.safeParse(json2);
|
|
39435
|
+
if (generated.success) {
|
|
39436
|
+
return {
|
|
39437
|
+
ok: false,
|
|
39438
|
+
code: "memory_is_generated",
|
|
39439
|
+
serverMessage: generated.data.error
|
|
39440
|
+
};
|
|
39441
|
+
}
|
|
38660
39442
|
const parsed = PreconditionSchema3.safeParse(json2);
|
|
38661
39443
|
if (!parsed.success) {
|
|
38662
39444
|
throw new ApiError(409, `put workspace-memory 409 body did not match precondition_failed shape`);
|
|
@@ -38684,6 +39466,7 @@ async function putWorkspaceMemory(args) {
|
|
|
38684
39466
|
// src/lib/workspace-memory/sync.ts
|
|
38685
39467
|
var CONFLICT_ENTITY2 = "workspace-memory";
|
|
38686
39468
|
var CONFLICT_SLUG2 = "workspace-memory";
|
|
39469
|
+
var GENERATED_PILE2 = "shared";
|
|
38687
39470
|
function memoryFilePath(workspaceSlug) {
|
|
38688
39471
|
return contextFilePath(workspaceSlug, WORKSPACE_MEMORY_FILENAME);
|
|
38689
39472
|
}
|
|
@@ -38708,7 +39491,7 @@ function classifyWorkspaceMemoryPath(args) {
|
|
|
38708
39491
|
} catch {
|
|
38709
39492
|
return false;
|
|
38710
39493
|
}
|
|
38711
|
-
return
|
|
39494
|
+
return relative12(root, args.absolutePath) === WORKSPACE_MEMORY_FILENAME;
|
|
38712
39495
|
}
|
|
38713
39496
|
async function pullWorkspaceMemory(args) {
|
|
38714
39497
|
const { workspaceSlug, baseUrl, pat, state, fetchImpl, output, suppressor } = args;
|
|
@@ -38722,12 +39505,37 @@ async function pullWorkspaceMemory(args) {
|
|
|
38722
39505
|
const cloudHash = stableHash(cloud.body_md);
|
|
38723
39506
|
const ackedHash = getWorkspaceMemoryState(state)?.body_hash;
|
|
38724
39507
|
if (local !== null && stableHash(local) === cloudHash) {
|
|
38725
|
-
const { state:
|
|
39508
|
+
const { state: stamped } = setWorkspaceMemoryState(state, {
|
|
38726
39509
|
body_hash: cloudHash,
|
|
38727
|
-
updated_at: cloud.updated_at
|
|
39510
|
+
updated_at: cloud.updated_at,
|
|
39511
|
+
generated: cloud.generated
|
|
38728
39512
|
});
|
|
39513
|
+
const next2 = clearMemoryGeneratedRefusal(stamped, workspaceSlug, memoryFilePath(workspaceSlug));
|
|
38729
39514
|
return { state: next2, action: "noop" };
|
|
38730
39515
|
}
|
|
39516
|
+
if (cloud.generated === true && local !== null && !localEmpty && stableHash(local) !== ackedHash) {
|
|
39517
|
+
const refused = refuseGeneratedMemoryEdit({
|
|
39518
|
+
state,
|
|
39519
|
+
workspaceSlug,
|
|
39520
|
+
absolutePath: memoryFilePath(workspaceSlug),
|
|
39521
|
+
relPath: WORKSPACE_MEMORY_FILENAME,
|
|
39522
|
+
pile: GENERATED_PILE2,
|
|
39523
|
+
conflictEntity: CONFLICT_ENTITY2,
|
|
39524
|
+
conflictSlug: CONFLICT_SLUG2,
|
|
39525
|
+
localContent: local,
|
|
39526
|
+
canonicalBody: cloud.body_md,
|
|
39527
|
+
writeCanonical: (body) => writeLocal2(workspaceSlug, body, suppressor),
|
|
39528
|
+
now,
|
|
39529
|
+
output
|
|
39530
|
+
});
|
|
39531
|
+
const { state: next2 } = setWorkspaceMemoryState(refused.state, {
|
|
39532
|
+
body_hash: cloudHash,
|
|
39533
|
+
updated_at: cloud.updated_at,
|
|
39534
|
+
generated: true
|
|
39535
|
+
});
|
|
39536
|
+
output?.log(`[WORKSPACE_MEMORY_PULL_GENERATED_REFUSED ws=${workspaceSlug} stashed]`);
|
|
39537
|
+
return { state: next2, action: "written" };
|
|
39538
|
+
}
|
|
38731
39539
|
if (local !== null && ackedHash !== undefined && cloudHash === ackedHash) {
|
|
38732
39540
|
output?.log(`[WORKSPACE_MEMORY_PULL_PRESERVED ws=${workspaceSlug} reason=pending_push]`);
|
|
38733
39541
|
return { state, action: "preserved" };
|
|
@@ -38750,7 +39558,8 @@ async function pullWorkspaceMemory(args) {
|
|
|
38750
39558
|
writeLocal2(workspaceSlug, cloud.body_md, suppressor);
|
|
38751
39559
|
const { state: next } = setWorkspaceMemoryState(state, {
|
|
38752
39560
|
body_hash: cloudHash,
|
|
38753
|
-
updated_at: cloud.updated_at
|
|
39561
|
+
updated_at: cloud.updated_at,
|
|
39562
|
+
generated: cloud.generated
|
|
38754
39563
|
});
|
|
38755
39564
|
return { state: next, action: cloud.is_empty ? "materialised" : "written" };
|
|
38756
39565
|
}
|
|
@@ -38760,7 +39569,8 @@ async function restoreFromCloud2(args) {
|
|
|
38760
39569
|
writeLocal2(workspaceSlug, cloud.body_md, suppressor);
|
|
38761
39570
|
return persistMemory(state, {
|
|
38762
39571
|
body_hash: stableHash(cloud.body_md),
|
|
38763
|
-
updated_at: cloud.updated_at
|
|
39572
|
+
updated_at: cloud.updated_at,
|
|
39573
|
+
generated: cloud.generated
|
|
38764
39574
|
});
|
|
38765
39575
|
}
|
|
38766
39576
|
async function pushWorkspaceMemory(args) {
|
|
@@ -38773,6 +39583,32 @@ async function pushWorkspaceMemory(args) {
|
|
|
38773
39583
|
const localHash = stableHash(local);
|
|
38774
39584
|
const acked = getWorkspaceMemoryState(state);
|
|
38775
39585
|
const alreadyAcked = Boolean(acked && acked.body_hash === localHash);
|
|
39586
|
+
if (acked?.generated === true && !alreadyAcked) {
|
|
39587
|
+
const cloud = await getWorkspaceMemory({ baseUrl, pat, fetchImpl });
|
|
39588
|
+
const refused = refuseGeneratedMemoryEdit({
|
|
39589
|
+
state,
|
|
39590
|
+
workspaceSlug,
|
|
39591
|
+
absolutePath: memoryFilePath(workspaceSlug),
|
|
39592
|
+
relPath: WORKSPACE_MEMORY_FILENAME,
|
|
39593
|
+
pile: GENERATED_PILE2,
|
|
39594
|
+
conflictEntity: CONFLICT_ENTITY2,
|
|
39595
|
+
conflictSlug: CONFLICT_SLUG2,
|
|
39596
|
+
localContent: local,
|
|
39597
|
+
canonicalBody: cloud.body_md,
|
|
39598
|
+
writeCanonical: (body) => writeLocal2(workspaceSlug, body, args.suppressor),
|
|
39599
|
+
now,
|
|
39600
|
+
output
|
|
39601
|
+
});
|
|
39602
|
+
const next2 = persistMemory(refused.state, {
|
|
39603
|
+
body_hash: stableHash(cloud.body_md),
|
|
39604
|
+
updated_at: cloud.updated_at,
|
|
39605
|
+
generated: cloud.generated
|
|
39606
|
+
});
|
|
39607
|
+
return {
|
|
39608
|
+
state: next2,
|
|
39609
|
+
outcome: { kind: "generated_refused", stashPath: refused.stashPath }
|
|
39610
|
+
};
|
|
39611
|
+
}
|
|
38776
39612
|
const gate = applySecretGate({
|
|
38777
39613
|
state,
|
|
38778
39614
|
workspaceSlug,
|
|
@@ -38804,7 +39640,8 @@ async function pushWorkspaceMemory(args) {
|
|
|
38804
39640
|
if (res.ok) {
|
|
38805
39641
|
const next2 = persistMemory(gated, {
|
|
38806
39642
|
body_hash: localHash,
|
|
38807
|
-
updated_at: res.row.updated_at
|
|
39643
|
+
updated_at: res.row.updated_at,
|
|
39644
|
+
generated: res.row.generated
|
|
38808
39645
|
});
|
|
38809
39646
|
output?.log(`[WORKSPACE_MEMORY_PUT ws=${workspaceSlug} is_empty=${res.row.is_empty}]`);
|
|
38810
39647
|
return { state: next2, outcome: { kind: "pushed" } };
|
|
@@ -38813,6 +39650,33 @@ async function pushWorkspaceMemory(args) {
|
|
|
38813
39650
|
output?.warn(`${WORKSPACE_MEMORY_FILENAME} is over the ${Math.round(MAX_WORKSPACE_MEMORY_BYTES / 1024)}KB cap — not synced.`);
|
|
38814
39651
|
return { state: gated, outcome: { kind: "over_cap" } };
|
|
38815
39652
|
}
|
|
39653
|
+
if (res.code === "memory_is_generated") {
|
|
39654
|
+
const cloud = await getWorkspaceMemory({ baseUrl, pat, fetchImpl });
|
|
39655
|
+
const refused = refuseGeneratedMemoryEdit({
|
|
39656
|
+
state: gated,
|
|
39657
|
+
workspaceSlug,
|
|
39658
|
+
absolutePath: memoryFilePath(workspaceSlug),
|
|
39659
|
+
relPath: WORKSPACE_MEMORY_FILENAME,
|
|
39660
|
+
pile: GENERATED_PILE2,
|
|
39661
|
+
conflictEntity: CONFLICT_ENTITY2,
|
|
39662
|
+
conflictSlug: CONFLICT_SLUG2,
|
|
39663
|
+
localContent: local,
|
|
39664
|
+
canonicalBody: cloud.body_md,
|
|
39665
|
+
writeCanonical: (body) => writeLocal2(workspaceSlug, body, args.suppressor),
|
|
39666
|
+
now,
|
|
39667
|
+
output
|
|
39668
|
+
});
|
|
39669
|
+
output?.log(`[WORKSPACE_MEMORY_PUT_REFUSED ws=${workspaceSlug} code=memory_is_generated]`);
|
|
39670
|
+
const next2 = persistMemory(refused.state, {
|
|
39671
|
+
body_hash: stableHash(cloud.body_md),
|
|
39672
|
+
updated_at: cloud.updated_at,
|
|
39673
|
+
generated: cloud.generated
|
|
39674
|
+
});
|
|
39675
|
+
return {
|
|
39676
|
+
state: next2,
|
|
39677
|
+
outcome: { kind: "generated_refused", stashPath: refused.stashPath }
|
|
39678
|
+
};
|
|
39679
|
+
}
|
|
38816
39680
|
{
|
|
38817
39681
|
const probe2 = await getWorkspaceMemory({ baseUrl, pat, fetchImpl });
|
|
38818
39682
|
if (acked && stableHash(probe2.body_md) === acked.body_hash) {
|
|
@@ -38826,7 +39690,8 @@ async function pushWorkspaceMemory(args) {
|
|
|
38826
39690
|
if (retry.ok) {
|
|
38827
39691
|
const next2 = persistMemory(gated, {
|
|
38828
39692
|
body_hash: localHash,
|
|
38829
|
-
updated_at: retry.row.updated_at
|
|
39693
|
+
updated_at: retry.row.updated_at,
|
|
39694
|
+
generated: retry.row.generated
|
|
38830
39695
|
});
|
|
38831
39696
|
output?.log(`[WORKSPACE_MEMORY_PUT ws=${workspaceSlug} is_empty=${retry.row.is_empty}]`);
|
|
38832
39697
|
return { state: next2, outcome: { kind: "pushed" } };
|
|
@@ -38835,6 +39700,32 @@ async function pushWorkspaceMemory(args) {
|
|
|
38835
39700
|
output?.warn(`${WORKSPACE_MEMORY_FILENAME} is over the ${Math.round(MAX_WORKSPACE_MEMORY_BYTES / 1024)}KB cap — not synced.`);
|
|
38836
39701
|
return { state: gated, outcome: { kind: "over_cap" } };
|
|
38837
39702
|
}
|
|
39703
|
+
if (retry.code === "memory_is_generated") {
|
|
39704
|
+
const cloud = await getWorkspaceMemory({ baseUrl, pat, fetchImpl });
|
|
39705
|
+
const refused = refuseGeneratedMemoryEdit({
|
|
39706
|
+
state: gated,
|
|
39707
|
+
workspaceSlug,
|
|
39708
|
+
absolutePath: memoryFilePath(workspaceSlug),
|
|
39709
|
+
relPath: WORKSPACE_MEMORY_FILENAME,
|
|
39710
|
+
pile: GENERATED_PILE2,
|
|
39711
|
+
conflictEntity: CONFLICT_ENTITY2,
|
|
39712
|
+
conflictSlug: CONFLICT_SLUG2,
|
|
39713
|
+
localContent: local,
|
|
39714
|
+
canonicalBody: cloud.body_md,
|
|
39715
|
+
writeCanonical: (body) => writeLocal2(workspaceSlug, body, args.suppressor),
|
|
39716
|
+
now,
|
|
39717
|
+
output
|
|
39718
|
+
});
|
|
39719
|
+
const next2 = persistMemory(refused.state, {
|
|
39720
|
+
body_hash: stableHash(cloud.body_md),
|
|
39721
|
+
updated_at: cloud.updated_at,
|
|
39722
|
+
generated: cloud.generated
|
|
39723
|
+
});
|
|
39724
|
+
return {
|
|
39725
|
+
state: next2,
|
|
39726
|
+
outcome: { kind: "generated_refused", stashPath: refused.stashPath }
|
|
39727
|
+
};
|
|
39728
|
+
}
|
|
38838
39729
|
}
|
|
38839
39730
|
}
|
|
38840
39731
|
const stashPath = stashConflict({
|
|
@@ -38860,6 +39751,7 @@ var SingletonProjectionSchema = exports_external.object({
|
|
|
38860
39751
|
body_md: exports_external.string(),
|
|
38861
39752
|
is_empty: exports_external.boolean(),
|
|
38862
39753
|
status: SingletonStatusSchema.optional().catch(undefined),
|
|
39754
|
+
generated: exports_external.boolean().optional().catch(undefined),
|
|
38863
39755
|
updated_at: exports_external.string().datetime({ offset: true }).nullable(),
|
|
38864
39756
|
created_at: exports_external.string().datetime({ offset: true }).nullable(),
|
|
38865
39757
|
last_edited_by: exports_external.string().uuid().nullable()
|
|
@@ -38870,7 +39762,7 @@ var PreconditionSchema4 = exports_external.object({
|
|
|
38870
39762
|
code: exports_external.literal("precondition_failed"),
|
|
38871
39763
|
current_updated_at: exports_external.string()
|
|
38872
39764
|
});
|
|
38873
|
-
function
|
|
39765
|
+
function baseHeaders5(pat) {
|
|
38874
39766
|
return { Authorization: `Bearer ${pat}`, Accept: "application/json" };
|
|
38875
39767
|
}
|
|
38876
39768
|
async function serverErrorMessage(res, fallback) {
|
|
@@ -38888,7 +39780,7 @@ function createSingletonApi(config2) {
|
|
|
38888
39780
|
const url2 = `${baseUrl.replace(/\/$/, "")}${route}`;
|
|
38889
39781
|
let res;
|
|
38890
39782
|
try {
|
|
38891
|
-
res = await fetchImpl(url2, { method: "GET", headers:
|
|
39783
|
+
res = await fetchImpl(url2, { method: "GET", headers: baseHeaders5(pat) });
|
|
38892
39784
|
} catch (err) {
|
|
38893
39785
|
const msg = err instanceof Error ? err.message : "network error";
|
|
38894
39786
|
throw new ApiError(0, `Could not reach ${url2}: ${msg}`);
|
|
@@ -38913,7 +39805,7 @@ function createSingletonApi(config2) {
|
|
|
38913
39805
|
const { baseUrl, pat, bodyMd, expectedUpdatedAt, fetchImpl = fetch } = args;
|
|
38914
39806
|
const url2 = `${baseUrl.replace(/\/$/, "")}${route}`;
|
|
38915
39807
|
const headers = {
|
|
38916
|
-
...
|
|
39808
|
+
...baseHeaders5(pat),
|
|
38917
39809
|
"Content-Type": "application/json"
|
|
38918
39810
|
};
|
|
38919
39811
|
if (expectedUpdatedAt)
|
|
@@ -38948,6 +39840,14 @@ function createSingletonApi(config2) {
|
|
|
38948
39840
|
}
|
|
38949
39841
|
if (res.status === 409) {
|
|
38950
39842
|
const json2 = await res.json();
|
|
39843
|
+
const generated = GeneratedRefusalSchema.safeParse(json2);
|
|
39844
|
+
if (generated.success) {
|
|
39845
|
+
return {
|
|
39846
|
+
ok: false,
|
|
39847
|
+
code: "memory_is_generated",
|
|
39848
|
+
serverMessage: generated.data.error
|
|
39849
|
+
};
|
|
39850
|
+
}
|
|
38951
39851
|
const parsed = PreconditionSchema4.safeParse(json2);
|
|
38952
39852
|
if (!parsed.success) {
|
|
38953
39853
|
throw new ApiError(409, `put ${surfaceName} 409 body did not match precondition_failed shape`);
|
|
@@ -38970,7 +39870,7 @@ function createSingletonApi(config2) {
|
|
|
38970
39870
|
}
|
|
38971
39871
|
|
|
38972
39872
|
// src/lib/singleton/engine.ts
|
|
38973
|
-
import { relative as
|
|
39873
|
+
import { relative as relative13 } from "node:path";
|
|
38974
39874
|
function filePath(descriptor, workspaceSlug) {
|
|
38975
39875
|
return contextFilePath(workspaceSlug, descriptor.filename);
|
|
38976
39876
|
}
|
|
@@ -38986,7 +39886,8 @@ function ackedFrom(projection, bodyHash) {
|
|
|
38986
39886
|
return {
|
|
38987
39887
|
body_hash: bodyHash,
|
|
38988
39888
|
updated_at: projection.updated_at,
|
|
38989
|
-
status: projection.status
|
|
39889
|
+
status: projection.status,
|
|
39890
|
+
generated: projection.generated
|
|
38990
39891
|
};
|
|
38991
39892
|
}
|
|
38992
39893
|
function persist2(descriptor, state, entry2) {
|
|
@@ -39002,7 +39903,7 @@ function classifySingletonPath(descriptor, args) {
|
|
|
39002
39903
|
} catch {
|
|
39003
39904
|
return false;
|
|
39004
39905
|
}
|
|
39005
|
-
return
|
|
39906
|
+
return relative13(root, args.absolutePath) === descriptor.filename;
|
|
39006
39907
|
}
|
|
39007
39908
|
async function pullSingleton(descriptor, args) {
|
|
39008
39909
|
const { workspaceSlug, baseUrl, pat, state, fetchImpl, output, suppressor } = args;
|
|
@@ -39017,9 +39918,29 @@ async function pullSingleton(descriptor, args) {
|
|
|
39017
39918
|
const ackedHash = descriptor.getState(state)?.body_hash;
|
|
39018
39919
|
const localHash = local !== null ? stableHash(local) : null;
|
|
39019
39920
|
if (localHash !== null && localHash === cloudHash) {
|
|
39020
|
-
const { state:
|
|
39921
|
+
const { state: stamped } = descriptor.setState(state, ackedFrom(cloud, cloudHash));
|
|
39922
|
+
const next2 = descriptor.generatedPile === null ? stamped : clearMemoryGeneratedRefusal(stamped, workspaceSlug, filePath(descriptor, workspaceSlug));
|
|
39021
39923
|
return { state: next2, action: "noop" };
|
|
39022
39924
|
}
|
|
39925
|
+
if (descriptor.generatedPile !== null && cloud.generated === true && local !== null && !localEmpty && localHash !== ackedHash) {
|
|
39926
|
+
const refused = refuseGeneratedMemoryEdit({
|
|
39927
|
+
state,
|
|
39928
|
+
workspaceSlug,
|
|
39929
|
+
absolutePath: filePath(descriptor, workspaceSlug),
|
|
39930
|
+
relPath: descriptor.filename,
|
|
39931
|
+
pile: descriptor.generatedPile,
|
|
39932
|
+
conflictEntity: descriptor.conflictEntity,
|
|
39933
|
+
conflictSlug: descriptor.conflictSlug,
|
|
39934
|
+
localContent: local,
|
|
39935
|
+
canonicalBody: cloud.body_md,
|
|
39936
|
+
writeCanonical: (body) => writeLocal3(descriptor, workspaceSlug, body, suppressor),
|
|
39937
|
+
now,
|
|
39938
|
+
output
|
|
39939
|
+
});
|
|
39940
|
+
const { state: next2 } = descriptor.setState(refused.state, ackedFrom(cloud, cloudHash));
|
|
39941
|
+
output?.log(`[${descriptor.logTag}_PULL_GENERATED_REFUSED ws=${workspaceSlug} stashed]`);
|
|
39942
|
+
return { state: next2, action: "written" };
|
|
39943
|
+
}
|
|
39023
39944
|
if (local !== null && ackedHash !== undefined && cloudHash === ackedHash) {
|
|
39024
39945
|
output?.log(`[${descriptor.logTag}_PULL_PRESERVED ws=${workspaceSlug} reason=pending_push]`);
|
|
39025
39946
|
return { state, action: "preserved" };
|
|
@@ -39058,6 +39979,28 @@ async function pushSingleton(descriptor, args) {
|
|
|
39058
39979
|
const localHash = stableHash(local);
|
|
39059
39980
|
const acked = descriptor.getState(state);
|
|
39060
39981
|
const alreadyAcked = Boolean(acked && acked.body_hash === localHash);
|
|
39982
|
+
if (descriptor.generatedPile !== null && acked?.generated === true && !alreadyAcked) {
|
|
39983
|
+
const cloud = await descriptor.api.get({ baseUrl, pat, fetchImpl });
|
|
39984
|
+
const refused = refuseGeneratedMemoryEdit({
|
|
39985
|
+
state,
|
|
39986
|
+
workspaceSlug,
|
|
39987
|
+
absolutePath: filePath(descriptor, workspaceSlug),
|
|
39988
|
+
relPath: descriptor.filename,
|
|
39989
|
+
pile: descriptor.generatedPile,
|
|
39990
|
+
conflictEntity: descriptor.conflictEntity,
|
|
39991
|
+
conflictSlug: descriptor.conflictSlug,
|
|
39992
|
+
localContent: local,
|
|
39993
|
+
canonicalBody: cloud.body_md,
|
|
39994
|
+
writeCanonical: (body) => writeLocal3(descriptor, workspaceSlug, body, args.suppressor),
|
|
39995
|
+
now,
|
|
39996
|
+
output
|
|
39997
|
+
});
|
|
39998
|
+
const next2 = persist2(descriptor, refused.state, ackedFrom(cloud, stableHash(cloud.body_md)));
|
|
39999
|
+
return {
|
|
40000
|
+
state: next2,
|
|
40001
|
+
outcome: { kind: "generated_refused", stashPath: refused.stashPath }
|
|
40002
|
+
};
|
|
40003
|
+
}
|
|
39061
40004
|
let gated = state;
|
|
39062
40005
|
if (descriptor.secretScanned) {
|
|
39063
40006
|
const gate = applySecretGate({
|
|
@@ -39098,6 +40041,29 @@ async function pushSingleton(descriptor, args) {
|
|
|
39098
40041
|
output?.warn(`${descriptor.filename} is over the ${Math.round(descriptor.maxBytes / 1024)}KB cap — not synced.`);
|
|
39099
40042
|
return { state: gated, outcome: { kind: "over_cap" } };
|
|
39100
40043
|
}
|
|
40044
|
+
if (res.code === "memory_is_generated") {
|
|
40045
|
+
const cloud = await descriptor.api.get({ baseUrl, pat, fetchImpl });
|
|
40046
|
+
const refused = refuseGeneratedMemoryEdit({
|
|
40047
|
+
state: gated,
|
|
40048
|
+
workspaceSlug,
|
|
40049
|
+
absolutePath: filePath(descriptor, workspaceSlug),
|
|
40050
|
+
relPath: descriptor.filename,
|
|
40051
|
+
pile: descriptor.generatedPile ?? "shared",
|
|
40052
|
+
conflictEntity: descriptor.conflictEntity,
|
|
40053
|
+
conflictSlug: descriptor.conflictSlug,
|
|
40054
|
+
localContent: local,
|
|
40055
|
+
canonicalBody: cloud.body_md,
|
|
40056
|
+
writeCanonical: (body) => writeLocal3(descriptor, workspaceSlug, body, args.suppressor),
|
|
40057
|
+
now,
|
|
40058
|
+
output
|
|
40059
|
+
});
|
|
40060
|
+
output?.log(`[${descriptor.logTag}_PUT_REFUSED ws=${workspaceSlug} code=memory_is_generated]`);
|
|
40061
|
+
const next2 = persist2(descriptor, refused.state, ackedFrom(cloud, stableHash(cloud.body_md)));
|
|
40062
|
+
return {
|
|
40063
|
+
state: next2,
|
|
40064
|
+
outcome: { kind: "generated_refused", stashPath: refused.stashPath }
|
|
40065
|
+
};
|
|
40066
|
+
}
|
|
39101
40067
|
{
|
|
39102
40068
|
const probe2 = await descriptor.api.get({ baseUrl, pat, fetchImpl });
|
|
39103
40069
|
if (acked && stableHash(probe2.body_md) === acked.body_hash) {
|
|
@@ -39117,6 +40083,28 @@ async function pushSingleton(descriptor, args) {
|
|
|
39117
40083
|
output?.warn(`${descriptor.filename} is over the ${Math.round(descriptor.maxBytes / 1024)}KB cap — not synced.`);
|
|
39118
40084
|
return { state: gated, outcome: { kind: "over_cap" } };
|
|
39119
40085
|
}
|
|
40086
|
+
if (retry.code === "memory_is_generated") {
|
|
40087
|
+
const cloud = await descriptor.api.get({ baseUrl, pat, fetchImpl });
|
|
40088
|
+
const refused = refuseGeneratedMemoryEdit({
|
|
40089
|
+
state: gated,
|
|
40090
|
+
workspaceSlug,
|
|
40091
|
+
absolutePath: filePath(descriptor, workspaceSlug),
|
|
40092
|
+
relPath: descriptor.filename,
|
|
40093
|
+
pile: descriptor.generatedPile ?? "shared",
|
|
40094
|
+
conflictEntity: descriptor.conflictEntity,
|
|
40095
|
+
conflictSlug: descriptor.conflictSlug,
|
|
40096
|
+
localContent: local,
|
|
40097
|
+
canonicalBody: cloud.body_md,
|
|
40098
|
+
writeCanonical: (body) => writeLocal3(descriptor, workspaceSlug, body, args.suppressor),
|
|
40099
|
+
now,
|
|
40100
|
+
output
|
|
40101
|
+
});
|
|
40102
|
+
const next2 = persist2(descriptor, refused.state, ackedFrom(cloud, stableHash(cloud.body_md)));
|
|
40103
|
+
return {
|
|
40104
|
+
state: next2,
|
|
40105
|
+
outcome: { kind: "generated_refused", stashPath: refused.stashPath }
|
|
40106
|
+
};
|
|
40107
|
+
}
|
|
39120
40108
|
}
|
|
39121
40109
|
}
|
|
39122
40110
|
const stashPath = stashConflict({
|
|
@@ -39150,6 +40138,7 @@ var USER_MEMORY_DESCRIPTOR = {
|
|
|
39150
40138
|
logTag: "USER_MEMORY",
|
|
39151
40139
|
maxBytes: MAX_USER_MEMORY_BYTES,
|
|
39152
40140
|
secretScanned: true,
|
|
40141
|
+
generatedPile: "private",
|
|
39153
40142
|
extractActive: extractUserMemory,
|
|
39154
40143
|
getState: getUserMemoryState,
|
|
39155
40144
|
setState: setUserMemoryState,
|
|
@@ -39183,6 +40172,7 @@ var VOICE_DESCRIPTOR = {
|
|
|
39183
40172
|
logTag: "VOICE",
|
|
39184
40173
|
maxBytes: MAX_USER_VOICE_BYTES,
|
|
39185
40174
|
secretScanned: false,
|
|
40175
|
+
generatedPile: null,
|
|
39186
40176
|
extractActive: extractVoiceProfile,
|
|
39187
40177
|
getState: getUserVoiceState,
|
|
39188
40178
|
setState: setUserVoiceState,
|
|
@@ -39329,6 +40319,11 @@ import { join as join16 } from "node:path";
|
|
|
39329
40319
|
// src/lib/conflicts-view.ts
|
|
39330
40320
|
import { existsSync as existsSync5 } from "node:fs";
|
|
39331
40321
|
import { join as join15 } from "node:path";
|
|
40322
|
+
var NO_GENERATED_MEMORY = {
|
|
40323
|
+
artistMemoryGenerated: () => false,
|
|
40324
|
+
workspaceMemoryGenerated: () => false,
|
|
40325
|
+
userMemoryGenerated: () => false
|
|
40326
|
+
};
|
|
39332
40327
|
var FOOTER = [
|
|
39333
40328
|
"---",
|
|
39334
40329
|
"",
|
|
@@ -39351,7 +40346,7 @@ function absolute(date5) {
|
|
|
39351
40346
|
function shellQuote2(s) {
|
|
39352
40347
|
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
39353
40348
|
}
|
|
39354
|
-
function resolveConflictRows(workspaceSlug, stashes) {
|
|
40349
|
+
function resolveConflictRows(workspaceSlug, stashes, generatedMemory = NO_GENERATED_MEMORY) {
|
|
39355
40350
|
let root;
|
|
39356
40351
|
try {
|
|
39357
40352
|
root = workspaceMirrorPath(workspaceSlug);
|
|
@@ -39359,6 +40354,9 @@ function resolveConflictRows(workspaceSlug, stashes) {
|
|
|
39359
40354
|
root = null;
|
|
39360
40355
|
}
|
|
39361
40356
|
return stashes.map((stash) => {
|
|
40357
|
+
const pile = isGeneratedMemoryStash(stash.label, generatedMemory);
|
|
40358
|
+
if (pile !== null)
|
|
40359
|
+
return { kind: "generated-memory", stash, pile };
|
|
39362
40360
|
if (root === null || !stash.label.startsWith("files/")) {
|
|
39363
40361
|
return { kind: "entity", stash };
|
|
39364
40362
|
}
|
|
@@ -39424,6 +40422,8 @@ function renderRow(row) {
|
|
|
39424
40422
|
out.push("Your version is at:", "", "```", stash.absPath, "```", "");
|
|
39425
40423
|
if (row.kind === "unsafe-path") {
|
|
39426
40424
|
out.push("**Plinth has not written a copy command for this one, on purpose.** The", "path it would have to write to is not one Plinth can vouch for: either the", "stash filename does not describe a path in this workspace, or a folder on", "the way to it is a link pointing somewhere else. Running a copy built from", "that would write your file somewhere you did not choose.", "", "Open the stash, see what is in it, and put it back by hand.");
|
|
40425
|
+
} else if (row.kind === "generated-memory") {
|
|
40426
|
+
out.push("**This file is generated from memory facts, so putting the text back", "will not keep it.** The next sync rewrites the file and sets your copy", "aside again.", "", `Make the change with ${factToolsFor(row.pile)},`, "then delete the stash. The file is rewritten from the facts.");
|
|
39427
40427
|
} else {
|
|
39428
40428
|
out.push("Compare it with the file in your mirror and re-apply anything you still", "want by editing that file. Then delete the stash.");
|
|
39429
40429
|
}
|
|
@@ -39476,7 +40476,7 @@ ${FOOTER}
|
|
|
39476
40476
|
function refreshConflictsView(workspaceSlug) {
|
|
39477
40477
|
try {
|
|
39478
40478
|
const stashes = listConflictStashes(workspaceSlug);
|
|
39479
|
-
const rows = resolveConflictRows(workspaceSlug, stashes);
|
|
40479
|
+
const rows = resolveConflictRows(workspaceSlug, stashes, generatedMemoryStashDepsFrom(loadState(workspaceSlug)));
|
|
39480
40480
|
const body = renderConflictsView(rows);
|
|
39481
40481
|
if (readConflictsView(workspaceSlug) === body) {
|
|
39482
40482
|
return { stashes: rows.length, written: false };
|
|
@@ -39499,7 +40499,7 @@ import { existsSync as existsSync6, readFileSync as readFileSync22, statSync as
|
|
|
39499
40499
|
// src/lib/files/extracted-text.ts
|
|
39500
40500
|
import { createHash as createHash3 } from "node:crypto";
|
|
39501
40501
|
var DERIVED_TEXT_VERSION = 2;
|
|
39502
|
-
function
|
|
40502
|
+
function yamlString3(value) {
|
|
39503
40503
|
return JSON.stringify(value);
|
|
39504
40504
|
}
|
|
39505
40505
|
var PARTIAL_WARNING = [
|
|
@@ -39538,8 +40538,8 @@ function renderExtractedText(doc2) {
|
|
|
39538
40538
|
const frontmatter = [
|
|
39539
40539
|
"---",
|
|
39540
40540
|
"plinth_generated: true",
|
|
39541
|
-
`source_file: ${doc2.bytesInMirror ?
|
|
39542
|
-
`source_uri: ${doc2.sourceUri === undefined ? "unknown" : doc2.sourceUri === null ? "null" :
|
|
40541
|
+
`source_file: ${doc2.bytesInMirror ? yamlString3(`files/${doc2.relPath}`) : "null"}`,
|
|
40542
|
+
`source_uri: ${doc2.sourceUri === undefined ? "unknown" : doc2.sourceUri === null ? "null" : yamlString3(doc2.sourceUri)}`,
|
|
39543
40543
|
`content_quality: ${doc2.quality ?? "unchecked"}`,
|
|
39544
40544
|
`body_withheld: ${withheld}`,
|
|
39545
40545
|
`extracted_chars: ${withheld ? 0 : doc2.text.length}`,
|
|
@@ -40526,6 +41526,27 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
40526
41526
|
const voiceWarning = voiceMalformedWarning(state, slug);
|
|
40527
41527
|
if (voiceWarning)
|
|
40528
41528
|
out.warn(voiceWarning);
|
|
41529
|
+
const factsResult = await syncMemoryFacts({
|
|
41530
|
+
workspaceSlug: slug,
|
|
41531
|
+
baseUrl: entry2.api_base_url,
|
|
41532
|
+
pat,
|
|
41533
|
+
state,
|
|
41534
|
+
fetchImpl,
|
|
41535
|
+
now,
|
|
41536
|
+
output: out,
|
|
41537
|
+
suppressor: deps.suppressor
|
|
41538
|
+
});
|
|
41539
|
+
if (factsResult.state !== state) {
|
|
41540
|
+
state = factsResult.state;
|
|
41541
|
+
}
|
|
41542
|
+
commitPending();
|
|
41543
|
+
if (factsResult.written > 0 || factsResult.deleted > 0) {
|
|
41544
|
+
counters["memory-facts"] = {
|
|
41545
|
+
fetched: factsResult.facts,
|
|
41546
|
+
written: factsResult.written,
|
|
41547
|
+
deleted: factsResult.deleted
|
|
41548
|
+
};
|
|
41549
|
+
}
|
|
40529
41550
|
} catch (err) {
|
|
40530
41551
|
out.log(pullFailedLine(categorizeError(err)));
|
|
40531
41552
|
if (err instanceof ApiError) {
|
|
@@ -40687,14 +41708,24 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
|
|
|
40687
41708
|
const path5 = entityFilePath(workspaceSlug, strategy.type, live.slug);
|
|
40688
41709
|
if (existsSync7(path5)) {
|
|
40689
41710
|
if (strategy.type === "artists") {
|
|
40690
|
-
|
|
41711
|
+
const artistLive = live;
|
|
41712
|
+
state = applyArtistMemoryOnPull({
|
|
40691
41713
|
workspaceSlug,
|
|
40692
|
-
live,
|
|
41714
|
+
live: artistLive,
|
|
40693
41715
|
ackedMemoryHash: existingEntry.memory_hash,
|
|
41716
|
+
state,
|
|
40694
41717
|
suppressor,
|
|
40695
41718
|
now,
|
|
40696
41719
|
out
|
|
40697
|
-
});
|
|
41720
|
+
}).state;
|
|
41721
|
+
if (existingEntry.memory_generated !== artistLive.memory_generated) {
|
|
41722
|
+
const bumped = upsertEntry(state, strategy.type, {
|
|
41723
|
+
...existingEntry,
|
|
41724
|
+
memory_generated: artistLive.memory_generated
|
|
41725
|
+
});
|
|
41726
|
+
if (bumped.changed)
|
|
41727
|
+
state = bumped.state;
|
|
41728
|
+
}
|
|
40698
41729
|
}
|
|
40699
41730
|
return { state, action: "noop_overwrite" };
|
|
40700
41731
|
}
|
|
@@ -40731,15 +41762,21 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
|
|
|
40731
41762
|
inode = undefined;
|
|
40732
41763
|
}
|
|
40733
41764
|
let memoryHash;
|
|
41765
|
+
let memoryGenerated;
|
|
40734
41766
|
if (strategy.type === "artists") {
|
|
40735
|
-
|
|
41767
|
+
const artistLive = live;
|
|
41768
|
+
const applied = applyArtistMemoryOnPull({
|
|
40736
41769
|
workspaceSlug,
|
|
40737
|
-
live,
|
|
41770
|
+
live: artistLive,
|
|
40738
41771
|
ackedMemoryHash: existingEntry?.memory_hash,
|
|
41772
|
+
state,
|
|
40739
41773
|
suppressor,
|
|
40740
41774
|
now,
|
|
40741
41775
|
out
|
|
40742
|
-
})
|
|
41776
|
+
});
|
|
41777
|
+
memoryHash = applied.memoryHash;
|
|
41778
|
+
state = applied.state;
|
|
41779
|
+
memoryGenerated = artistLive.memory_generated;
|
|
40743
41780
|
}
|
|
40744
41781
|
const { state: nextState } = upsertEntry(state, strategy.type, {
|
|
40745
41782
|
id: live.id,
|
|
@@ -40750,7 +41787,8 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
|
|
|
40750
41787
|
frontmatter_hash: hashes.frontmatterHash,
|
|
40751
41788
|
inode,
|
|
40752
41789
|
body_len: hashes.bodyLen,
|
|
40753
|
-
memory_hash: memoryHash
|
|
41790
|
+
memory_hash: memoryHash,
|
|
41791
|
+
memory_generated: memoryGenerated
|
|
40754
41792
|
});
|
|
40755
41793
|
return { state: nextState, action: "written" };
|
|
40756
41794
|
}
|
|
@@ -41272,6 +42310,30 @@ function makeWatchHandler(deps) {
|
|
|
41272
42310
|
await handleArtistMemoryEvent(memoryMatch.slug, absolutePath, event.kind === "upsert");
|
|
41273
42311
|
return;
|
|
41274
42312
|
}
|
|
42313
|
+
const factPath = classifyMemoryFactPath({ workspaceSlug, absolutePath });
|
|
42314
|
+
if (factPath) {
|
|
42315
|
+
let ownEcho = false;
|
|
42316
|
+
if (event.kind === "upsert") {
|
|
42317
|
+
try {
|
|
42318
|
+
ownEcho = suppressor?.shouldSuppressUpsert(absolutePath, {
|
|
42319
|
+
bodyHash: stableHash(readFileSync25(absolutePath, "utf8")),
|
|
42320
|
+
frontmatterHash: ""
|
|
42321
|
+
}) === true;
|
|
42322
|
+
} catch {
|
|
42323
|
+
ownEcho = true;
|
|
42324
|
+
}
|
|
42325
|
+
}
|
|
42326
|
+
const warning2 = memoryFactEventWarning(classifyMemoryFactEvent({
|
|
42327
|
+
path: factPath,
|
|
42328
|
+
kind: event.kind === "upsert" ? "upsert" : "unlink",
|
|
42329
|
+
ownEcho,
|
|
42330
|
+
known: getMemoryFactEntry(getState(), factPath.relPath) !== undefined
|
|
42331
|
+
}));
|
|
42332
|
+
if (warning2)
|
|
42333
|
+
spinner.warn(warning2);
|
|
42334
|
+
spinner.update(`Watching ${workspaceSlug} · ${watchingSummary(getState())}`);
|
|
42335
|
+
return;
|
|
42336
|
+
}
|
|
41275
42337
|
const classified = classifyMirrorPath({ workspaceSlug, absolutePath });
|
|
41276
42338
|
if (!classified) {
|
|
41277
42339
|
const key = entitySymlinkRefusalKey({ workspaceSlug, absolutePath });
|
|
@@ -42459,6 +43521,305 @@ var startCommand = defineCommand({
|
|
|
42459
43521
|
// src/commands/status.ts
|
|
42460
43522
|
import { statSync as statSync19 } from "node:fs";
|
|
42461
43523
|
|
|
43524
|
+
// src/lib/context/context-walls.ts
|
|
43525
|
+
function prepare(raw) {
|
|
43526
|
+
const text = stripContactsBlock(raw.replace(/^---\r?\n(?:[\s\S]*?\r?\n)?---\r?\n/, ""));
|
|
43527
|
+
return text.replace(/<!--[\s\S]*?-->/g, "").replace(/<!--[\s\S]*$/, "");
|
|
43528
|
+
}
|
|
43529
|
+
function indentOf(line) {
|
|
43530
|
+
let cols = 0;
|
|
43531
|
+
for (const ch of line) {
|
|
43532
|
+
if (ch === " ")
|
|
43533
|
+
cols += 1;
|
|
43534
|
+
else if (ch === "\t")
|
|
43535
|
+
cols += 4;
|
|
43536
|
+
else
|
|
43537
|
+
break;
|
|
43538
|
+
}
|
|
43539
|
+
return cols;
|
|
43540
|
+
}
|
|
43541
|
+
var HEADING_RE = /^(#{1,6})\s+(.*?)\s*#*\s*$/;
|
|
43542
|
+
var ITEM_RE = /^(\s*)(?:[-*+]|\d{1,3}[.)])\s+(.*)$/;
|
|
43543
|
+
var FENCE_RE = /^\s*(?:`{3,}|~{3,})/;
|
|
43544
|
+
var TABLE_RE = /^\s*\|/;
|
|
43545
|
+
var LABEL_RE = /^[^\p{L}\p{N}*_`]*\*\*(.+?)\*\*/u;
|
|
43546
|
+
var UPCOMING_RE = /upcoming/i;
|
|
43547
|
+
var CURRENT_STATE_RE = /^current[\s-]+state\b/i;
|
|
43548
|
+
var SESSION_LOG_RE = /^session[\s-]+log$/i;
|
|
43549
|
+
function latestOf(dates) {
|
|
43550
|
+
let latest = null;
|
|
43551
|
+
for (const d of dates) {
|
|
43552
|
+
if (latest === null || utcMs(d) > utcMs(latest))
|
|
43553
|
+
latest = d;
|
|
43554
|
+
}
|
|
43555
|
+
return latest;
|
|
43556
|
+
}
|
|
43557
|
+
function openingOf(text) {
|
|
43558
|
+
const safe = terminalSafeText(text, 400);
|
|
43559
|
+
const chars = [...safe];
|
|
43560
|
+
if (chars.length <= 60)
|
|
43561
|
+
return safe;
|
|
43562
|
+
return `${chars.slice(0, 60).join("").trimEnd()}…`;
|
|
43563
|
+
}
|
|
43564
|
+
function safeLabel(text) {
|
|
43565
|
+
return terminalSafeText(text, 120);
|
|
43566
|
+
}
|
|
43567
|
+
function scanContextText(raw, today, opts = {}) {
|
|
43568
|
+
const scanner = new ContextScanner(today, opts.initialHeading ?? null);
|
|
43569
|
+
for (const line of prepare(raw).split(/\r?\n/))
|
|
43570
|
+
scanner.line(line);
|
|
43571
|
+
return scanner.finish();
|
|
43572
|
+
}
|
|
43573
|
+
|
|
43574
|
+
class ContextScanner {
|
|
43575
|
+
today;
|
|
43576
|
+
todayMs;
|
|
43577
|
+
heading;
|
|
43578
|
+
block = null;
|
|
43579
|
+
blockDates = [];
|
|
43580
|
+
blockRegion = null;
|
|
43581
|
+
blockIsLabel = false;
|
|
43582
|
+
open = [];
|
|
43583
|
+
fence = false;
|
|
43584
|
+
lastBlank = true;
|
|
43585
|
+
sessionLogLevel = null;
|
|
43586
|
+
walls = [];
|
|
43587
|
+
played = [];
|
|
43588
|
+
asOf = [];
|
|
43589
|
+
constructor(today, initialHeading) {
|
|
43590
|
+
this.today = today;
|
|
43591
|
+
this.todayMs = utcMs(today);
|
|
43592
|
+
this.heading = initialHeading;
|
|
43593
|
+
}
|
|
43594
|
+
line(line) {
|
|
43595
|
+
if (FENCE_RE.test(line)) {
|
|
43596
|
+
this.fence = !this.fence;
|
|
43597
|
+
this.flushBlock();
|
|
43598
|
+
this.lastBlank = false;
|
|
43599
|
+
return;
|
|
43600
|
+
}
|
|
43601
|
+
if (this.fence)
|
|
43602
|
+
return;
|
|
43603
|
+
if (/^\s*$/.test(line)) {
|
|
43604
|
+
this.flushBlock();
|
|
43605
|
+
this.lastBlank = true;
|
|
43606
|
+
return;
|
|
43607
|
+
}
|
|
43608
|
+
const h = HEADING_RE.exec(line);
|
|
43609
|
+
if (h) {
|
|
43610
|
+
this.onHeading(h[1].length, h[2]);
|
|
43611
|
+
return;
|
|
43612
|
+
}
|
|
43613
|
+
if (this.sessionLogLevel !== null)
|
|
43614
|
+
return;
|
|
43615
|
+
if (TABLE_RE.test(line)) {
|
|
43616
|
+
this.onTableRow(line);
|
|
43617
|
+
return;
|
|
43618
|
+
}
|
|
43619
|
+
const item = ITEM_RE.exec(line);
|
|
43620
|
+
if (item) {
|
|
43621
|
+
this.onItem(line, item[2]);
|
|
43622
|
+
return;
|
|
43623
|
+
}
|
|
43624
|
+
this.onText(line);
|
|
43625
|
+
}
|
|
43626
|
+
finish() {
|
|
43627
|
+
this.flushBlock();
|
|
43628
|
+
this.closeWhere(() => true);
|
|
43629
|
+
return { walls: this.walls, played: this.played, asOf: this.asOf };
|
|
43630
|
+
}
|
|
43631
|
+
onHeading(level, label) {
|
|
43632
|
+
this.flushBlock();
|
|
43633
|
+
this.closeWhere((r) => r.kind === "item" || r.depth >= level);
|
|
43634
|
+
this.lastBlank = false;
|
|
43635
|
+
if (this.sessionLogLevel !== null && level > this.sessionLogLevel)
|
|
43636
|
+
return;
|
|
43637
|
+
this.sessionLogLevel = SESSION_LOG_RE.test(label.trim()) ? level : null;
|
|
43638
|
+
if (this.sessionLogLevel !== null)
|
|
43639
|
+
return;
|
|
43640
|
+
this.open.push({
|
|
43641
|
+
kind: "heading",
|
|
43642
|
+
depth: level,
|
|
43643
|
+
label,
|
|
43644
|
+
upcoming: UPCOMING_RE.test(label),
|
|
43645
|
+
played: [],
|
|
43646
|
+
childBlocks: 0,
|
|
43647
|
+
ownLatest: null
|
|
43648
|
+
});
|
|
43649
|
+
this.heading = label;
|
|
43650
|
+
if (CURRENT_STATE_RE.test(label))
|
|
43651
|
+
this.noteAsOf(label);
|
|
43652
|
+
}
|
|
43653
|
+
noteAsOf(label) {
|
|
43654
|
+
const dated = readAsOf(label, this.today);
|
|
43655
|
+
if (dated === null) {
|
|
43656
|
+
this.asOf.push({ heading: safeLabel(label), daysOld: null });
|
|
43657
|
+
return;
|
|
43658
|
+
}
|
|
43659
|
+
const age = daysBetween2(dated.parts, this.today);
|
|
43660
|
+
if (age > CURRENT_STATE_AS_OF_MAX_DAYS || age < 0) {
|
|
43661
|
+
this.asOf.push({ heading: safeLabel(label), daysOld: age });
|
|
43662
|
+
}
|
|
43663
|
+
}
|
|
43664
|
+
onTableRow(line) {
|
|
43665
|
+
this.flushBlock();
|
|
43666
|
+
if (this.lastBlank) {
|
|
43667
|
+
const indent = indentOf(line);
|
|
43668
|
+
this.closeWhere((r) => r.kind === "item" && r.depth >= indent);
|
|
43669
|
+
}
|
|
43670
|
+
this.judgeEntry(this.upcomingRegion(), readDayMentions(line, this.today).map((m) => m.parts), false);
|
|
43671
|
+
this.lastBlank = false;
|
|
43672
|
+
}
|
|
43673
|
+
onItem(line, body) {
|
|
43674
|
+
this.flushBlock();
|
|
43675
|
+
const indent = indentOf(line);
|
|
43676
|
+
this.closeWhere((r) => r.kind === "item" && r.depth >= indent);
|
|
43677
|
+
const labelMatch = LABEL_RE.exec(body);
|
|
43678
|
+
const label = labelMatch ? labelMatch[1] : null;
|
|
43679
|
+
const upcoming = label !== null && UPCOMING_RE.test(label);
|
|
43680
|
+
const region = { kind: "item", depth: indent, label, upcoming, played: [], childBlocks: 0, ownLatest: null };
|
|
43681
|
+
this.open.push(region);
|
|
43682
|
+
this.block = { heading: this.heading, text: line.trim() };
|
|
43683
|
+
this.blockRegion = this.upcomingRegion();
|
|
43684
|
+
this.blockIsLabel = this.blockRegion === region;
|
|
43685
|
+
this.collectDates(labelMatch && upcoming ? body.slice(labelMatch[0].length) : body);
|
|
43686
|
+
this.lastBlank = false;
|
|
43687
|
+
}
|
|
43688
|
+
onText(line) {
|
|
43689
|
+
const indent = indentOf(line);
|
|
43690
|
+
if (this.lastBlank) {
|
|
43691
|
+
this.closeWhere((r) => r.kind === "item" && r.depth >= indent);
|
|
43692
|
+
}
|
|
43693
|
+
if (this.block === null) {
|
|
43694
|
+
this.block = { heading: this.heading, text: line.trim() };
|
|
43695
|
+
this.blockRegion = this.upcomingRegion();
|
|
43696
|
+
this.blockIsLabel = false;
|
|
43697
|
+
} else {
|
|
43698
|
+
this.block.text += `
|
|
43699
|
+
${line.trim()}`;
|
|
43700
|
+
}
|
|
43701
|
+
this.collectDates(line);
|
|
43702
|
+
this.lastBlank = false;
|
|
43703
|
+
}
|
|
43704
|
+
flushBlock() {
|
|
43705
|
+
if (this.block === null)
|
|
43706
|
+
return;
|
|
43707
|
+
const chars = [...this.block.text].length;
|
|
43708
|
+
if (chars > CONTEXT_BLOCK_BAR_CHARS) {
|
|
43709
|
+
this.walls.push({
|
|
43710
|
+
heading: this.block.heading === null ? null : safeLabel(this.block.heading),
|
|
43711
|
+
opening: openingOf(this.block.text),
|
|
43712
|
+
chars
|
|
43713
|
+
});
|
|
43714
|
+
}
|
|
43715
|
+
this.judgeEntry(this.blockRegion, this.blockDates, this.blockIsLabel);
|
|
43716
|
+
this.block = null;
|
|
43717
|
+
this.blockDates = [];
|
|
43718
|
+
this.blockRegion = null;
|
|
43719
|
+
this.blockIsLabel = false;
|
|
43720
|
+
}
|
|
43721
|
+
judgeEntry(region, dates, isLabel) {
|
|
43722
|
+
if (region === null)
|
|
43723
|
+
return;
|
|
43724
|
+
const latest = latestOf(dates);
|
|
43725
|
+
const played = latest !== null && utcMs(latest) < this.todayMs;
|
|
43726
|
+
if (isLabel) {
|
|
43727
|
+
region.ownLatest = played ? latest : null;
|
|
43728
|
+
return;
|
|
43729
|
+
}
|
|
43730
|
+
region.childBlocks += 1;
|
|
43731
|
+
if (played)
|
|
43732
|
+
region.played.push(latest);
|
|
43733
|
+
}
|
|
43734
|
+
closeRegion(r) {
|
|
43735
|
+
if (!r.upcoming)
|
|
43736
|
+
return;
|
|
43737
|
+
const entries = r.childBlocks === 0 && r.ownLatest !== null ? [r.ownLatest] : r.played;
|
|
43738
|
+
const latest = latestOf(entries);
|
|
43739
|
+
if (latest === null)
|
|
43740
|
+
return;
|
|
43741
|
+
this.played.push({ label: safeLabel(r.label ?? ""), playedEntries: entries.length, latest });
|
|
43742
|
+
}
|
|
43743
|
+
closeWhere(reject) {
|
|
43744
|
+
for (let i = this.open.length - 1;i >= 0; i -= 1) {
|
|
43745
|
+
const r = this.open[i];
|
|
43746
|
+
if (reject(r)) {
|
|
43747
|
+
this.closeRegion(r);
|
|
43748
|
+
this.open.splice(i, 1);
|
|
43749
|
+
}
|
|
43750
|
+
}
|
|
43751
|
+
}
|
|
43752
|
+
upcomingRegion() {
|
|
43753
|
+
for (let i = this.open.length - 1;i >= 0; i -= 1) {
|
|
43754
|
+
if (this.open[i].upcoming)
|
|
43755
|
+
return this.open[i];
|
|
43756
|
+
}
|
|
43757
|
+
return null;
|
|
43758
|
+
}
|
|
43759
|
+
collectDates(segment) {
|
|
43760
|
+
if (this.blockRegion === null)
|
|
43761
|
+
return;
|
|
43762
|
+
for (const mention of readDayMentions(segment, this.today))
|
|
43763
|
+
this.blockDates.push(mention.parts);
|
|
43764
|
+
}
|
|
43765
|
+
}
|
|
43766
|
+
function assessContextWalls(workspaceSlug, deps = {}) {
|
|
43767
|
+
const readAnchor = deps.readArtistAnchor ?? readArtistAnchorFile;
|
|
43768
|
+
const readContext = deps.readContext ?? readContextFile;
|
|
43769
|
+
const rosterOf = deps.readRosterFn ?? readRoster;
|
|
43770
|
+
const today = localDayOf((deps.now ?? (() => new Date))());
|
|
43771
|
+
const walls = [];
|
|
43772
|
+
const played = [];
|
|
43773
|
+
const asOf = [];
|
|
43774
|
+
let unreadable = 0;
|
|
43775
|
+
const take = (path5, result) => {
|
|
43776
|
+
for (const w of result.walls)
|
|
43777
|
+
walls.push({ path: path5, ...w });
|
|
43778
|
+
for (const p of result.played)
|
|
43779
|
+
played.push({ path: path5, ...p });
|
|
43780
|
+
for (const a of result.asOf)
|
|
43781
|
+
asOf.push({ path: path5, ...a });
|
|
43782
|
+
};
|
|
43783
|
+
let roster;
|
|
43784
|
+
try {
|
|
43785
|
+
roster = rosterOf(workspaceSlug);
|
|
43786
|
+
} catch {
|
|
43787
|
+
roster = [];
|
|
43788
|
+
}
|
|
43789
|
+
for (const artist of roster) {
|
|
43790
|
+
let raw = null;
|
|
43791
|
+
try {
|
|
43792
|
+
raw = readAnchor(workspaceSlug, artist.slug);
|
|
43793
|
+
} catch {
|
|
43794
|
+
unreadable += 1;
|
|
43795
|
+
continue;
|
|
43796
|
+
}
|
|
43797
|
+
if (raw === null)
|
|
43798
|
+
continue;
|
|
43799
|
+
take(`artists/${artist.slug}/${ARTIST_ANCHOR_FILENAME}`, scanContextText(raw, today));
|
|
43800
|
+
}
|
|
43801
|
+
let root = null;
|
|
43802
|
+
try {
|
|
43803
|
+
root = readContext(workspaceSlug, CLAUDE_MD);
|
|
43804
|
+
} catch {
|
|
43805
|
+
root = null;
|
|
43806
|
+
}
|
|
43807
|
+
if (root !== null) {
|
|
43808
|
+
const block = extractUserBlock(root);
|
|
43809
|
+
if (block !== null) {
|
|
43810
|
+
take(CLAUDE_MD, scanContextText(block, today, { initialHeading: "the customisable block" }));
|
|
43811
|
+
}
|
|
43812
|
+
}
|
|
43813
|
+
walls.sort((a, b) => b.chars - a.chars || a.path.localeCompare(b.path));
|
|
43814
|
+
played.sort((a, b) => b.playedEntries - a.playedEntries || a.path.localeCompare(b.path));
|
|
43815
|
+
asOf.sort((a, b) => {
|
|
43816
|
+
if (a.daysOld === null !== (b.daysOld === null))
|
|
43817
|
+
return a.daysOld === null ? -1 : 1;
|
|
43818
|
+
return (b.daysOld ?? 0) - (a.daysOld ?? 0) || a.path.localeCompare(b.path);
|
|
43819
|
+
});
|
|
43820
|
+
return { walls, played, asOf, unreadable };
|
|
43821
|
+
}
|
|
43822
|
+
|
|
42462
43823
|
// src/lib/build-stamp.ts
|
|
42463
43824
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
42464
43825
|
import { existsSync as existsSync9, mkdirSync as mkdirSync11, readFileSync as readFileSync26, writeFileSync as writeFileSync8 } from "node:fs";
|
|
@@ -42547,8 +43908,8 @@ function checkBuildStaleness(deps = {}) {
|
|
|
42547
43908
|
|
|
42548
43909
|
// src/lib/memory-links.ts
|
|
42549
43910
|
import { readFileSync as readFileSync27 } from "node:fs";
|
|
42550
|
-
import { relative as
|
|
42551
|
-
var MEMORY_FILE_RE = /^(workspace-memory\.md|artists\/[^/]+\/memory\.md)$/i;
|
|
43911
|
+
import { relative as relative14, sep as sep6 } from "node:path";
|
|
43912
|
+
var MEMORY_FILE_RE = /^(workspace-memory\.md|user-memory\.md|artists\/[^/]+\/memory\.md|memory\/[^/]+\.md|memory\/private\/[^/]+\.md)$/i;
|
|
42552
43913
|
function scanMemoryLinks(deps) {
|
|
42553
43914
|
const listFiles = deps.listFiles ?? ((r) => walkSubtreeFiles(r, isVcsInternal));
|
|
42554
43915
|
const readFile = deps.readFile ?? ((p) => readFileSync27(p, "utf8"));
|
|
@@ -42560,7 +43921,7 @@ function scanMemoryLinks(deps) {
|
|
|
42560
43921
|
} catch {
|
|
42561
43922
|
return { scanned: 0, unreadable: [], dead: [] };
|
|
42562
43923
|
}
|
|
42563
|
-
const relPaths = absPaths.map((p) =>
|
|
43924
|
+
const relPaths = absPaths.map((p) => relative14(root, p).split(sep6).join("/"));
|
|
42564
43925
|
const artists = buildArtistIndex(relPaths, root, (abs) => readFile(abs));
|
|
42565
43926
|
const dead = [];
|
|
42566
43927
|
const unreadable = [];
|
|
@@ -42596,7 +43957,7 @@ init_output();
|
|
|
42596
43957
|
|
|
42597
43958
|
// src/lib/pending-writes.ts
|
|
42598
43959
|
import { readFileSync as readFileSync28, statSync as statSync18 } from "node:fs";
|
|
42599
|
-
import { relative as
|
|
43960
|
+
import { relative as relative15 } from "node:path";
|
|
42600
43961
|
var import_gray_matter14 = __toESM(require_gray_matter(), 1);
|
|
42601
43962
|
var EMPTY2 = { writes: [], count: 0 };
|
|
42602
43963
|
function reportedElsewhere(state) {
|
|
@@ -42625,7 +43986,7 @@ function scanPendingWrites(args) {
|
|
|
42625
43986
|
const writes = [];
|
|
42626
43987
|
const discovered = [];
|
|
42627
43988
|
for (const absolutePath of walkSubtreeFiles(mirrorRootPath, ignored)) {
|
|
42628
|
-
const relPath =
|
|
43989
|
+
const relPath = relative15(mirrorRootPath, absolutePath);
|
|
42629
43990
|
const fileMatch = classifyFilesPath({ workspaceSlug, absolutePath });
|
|
42630
43991
|
if (fileMatch) {
|
|
42631
43992
|
if (frozenFiles.has(fileMatch.relPath))
|
|
@@ -43102,8 +44463,11 @@ async function runStatus(args, deps = {}) {
|
|
|
43102
44463
|
if (stashes.length === 0) {
|
|
43103
44464
|
out.info(row("Conflicts", "No conflict stashes."));
|
|
43104
44465
|
} else {
|
|
43105
|
-
const
|
|
43106
|
-
const
|
|
44466
|
+
const generatedDeps = generatedMemoryStashDepsFrom(localState);
|
|
44467
|
+
const generatedSide = stashes.filter((s) => isGeneratedMemoryStash(s.label, generatedDeps) !== null);
|
|
44468
|
+
const generatedLabels = new Set(generatedSide.map((s) => s.label));
|
|
44469
|
+
const cloudSide = stashes.filter((s) => s.side === "cloud" && !generatedLabels.has(s.label));
|
|
44470
|
+
const localSide = stashes.filter((s) => s.side !== "cloud" && !generatedLabels.has(s.label));
|
|
43107
44471
|
out.info(row("Conflicts", `${stashes.length} stashed cop${stashes.length === 1 ? "y" : "ies"} in ${conflictsRoot(slug)}`));
|
|
43108
44472
|
const newest = stashes[0];
|
|
43109
44473
|
const when = newest.stashedAt ? `${formatAbsolute(newest.stashedAt)} (${formatRelative(now.getTime() - newest.stashedAt.getTime())})` : "time unknown";
|
|
@@ -43112,6 +44476,9 @@ async function runStatus(args, deps = {}) {
|
|
|
43112
44476
|
if (localSide.length > 0) {
|
|
43113
44477
|
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.`));
|
|
43114
44478
|
}
|
|
44479
|
+
if (generatedSide.length > 0) {
|
|
44480
|
+
out.info(cont(`${lead(generatedSide.length)} an edit to a file Plinth generates from memory facts. Putting the text back will not keep it - change the fact instead, and the file is rewritten. The Rejected rows above name the tool for each.`));
|
|
44481
|
+
}
|
|
43115
44482
|
if (cloudSide.length > 0) {
|
|
43116
44483
|
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.`));
|
|
43117
44484
|
}
|
|
@@ -43359,6 +44726,16 @@ async function runStatus(args, deps = {}) {
|
|
|
43359
44726
|
}
|
|
43360
44727
|
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
44728
|
}
|
|
44729
|
+
let context = { walls: [], played: [], asOf: [], unreadable: 0 };
|
|
44730
|
+
try {
|
|
44731
|
+
context = assessContextWalls(slug, { now: () => now });
|
|
44732
|
+
} catch {
|
|
44733
|
+
context = { walls: [], played: [], asOf: [], unreadable: 0 };
|
|
44734
|
+
}
|
|
44735
|
+
renderWalls(out, context);
|
|
44736
|
+
renderPlayed(out, context);
|
|
44737
|
+
renderAsOf(out, context);
|
|
44738
|
+
renderUnchecked(out, context);
|
|
43362
44739
|
const anchor = localState.size_anchor;
|
|
43363
44740
|
if (anchor !== undefined) {
|
|
43364
44741
|
let now2 = {
|
|
@@ -43399,6 +44776,53 @@ async function runStatus(args, deps = {}) {
|
|
|
43399
44776
|
out.info("");
|
|
43400
44777
|
return { exitCode: 0 };
|
|
43401
44778
|
}
|
|
44779
|
+
var num = (v) => v.toLocaleString("en-GB");
|
|
44780
|
+
function renderWalls(out, context) {
|
|
44781
|
+
const total = context.walls.length;
|
|
44782
|
+
if (total === 0)
|
|
44783
|
+
return;
|
|
44784
|
+
const named = context.walls.slice(0, MAX_NAMED_CONTEXT_FINDINGS);
|
|
44785
|
+
out.info(row("Walls", `${total} block${total === 1 ? "" : "s"} of master context ${total === 1 ? "has" : "have"} ` + `grown past ${num(CONTEXT_BLOCK_BAR_CHARS)} characters.`));
|
|
44786
|
+
for (const w of named) {
|
|
44787
|
+
const where = w.heading === null ? "before its first heading" : `under "${w.heading}"`;
|
|
44788
|
+
out.info(cont(`${w.path}, ${where}: ${num(w.chars)} characters, opening "${w.opening}"`));
|
|
44789
|
+
}
|
|
44790
|
+
if (total > named.length)
|
|
44791
|
+
out.info(cont(`And ${total - named.length} more, not named here.`));
|
|
44792
|
+
out.info(cont("A block this long is one nobody reads whole. Split it into short blocks, " + "one per thing it covers - in master context, one per workstream, each " + "pointing at the record that holds its detail."));
|
|
44793
|
+
}
|
|
44794
|
+
function renderPlayed(out, context) {
|
|
44795
|
+
const total = context.played.length;
|
|
44796
|
+
if (total === 0)
|
|
44797
|
+
return;
|
|
44798
|
+
const named = context.played.slice(0, MAX_NAMED_CONTEXT_FINDINGS);
|
|
44799
|
+
out.info(row("Played", `${total} label${total === 1 ? "" : "s"} that ${total === 1 ? "says" : "say"} upcoming still ` + `${total === 1 ? "holds" : "hold"} entries that have played.`));
|
|
44800
|
+
for (const p of named) {
|
|
44801
|
+
out.info(cont(`${p.path}, under "${p.label}": ${num(p.playedEntries)} ` + `${p.playedEntries === 1 ? "entry has" : "entries have"} played, the latest ${formatDay(p.latest, -1)}.`));
|
|
44802
|
+
}
|
|
44803
|
+
if (total > named.length)
|
|
44804
|
+
out.info(cont(`And ${total - named.length} more, not named here.`));
|
|
44805
|
+
out.info(cont("An entry has played when every date in it is behind us. Move it out from " + "under the upcoming label, and put what it taught into the record it concerns."));
|
|
44806
|
+
}
|
|
44807
|
+
function renderAsOf(out, context) {
|
|
44808
|
+
const total = context.asOf.length;
|
|
44809
|
+
if (total === 0)
|
|
44810
|
+
return;
|
|
44811
|
+
const named = context.asOf.slice(0, MAX_NAMED_CONTEXT_FINDINGS);
|
|
44812
|
+
out.info(row("As of", `${total} current-state section${total === 1 ? "" : "s"} cannot be checked against a date.`));
|
|
44813
|
+
for (const a of named) {
|
|
44814
|
+
out.info(cont(a.daysOld === null ? `${a.path}: "${a.heading}" carries no as-of date.` : a.daysOld < 0 ? `${a.path}: "${a.heading}" is dated ${num(-a.daysOld)} days ahead of today, which an as-of cannot be.` : `${a.path}: "${a.heading}" is ${num(a.daysOld)} days old, past the ` + `${CURRENT_STATE_AS_OF_MAX_DAYS}-day bar.`));
|
|
44815
|
+
}
|
|
44816
|
+
if (total > named.length)
|
|
44817
|
+
out.info(cont(`And ${total - named.length} more, not named here.`));
|
|
44818
|
+
out.info(cont("Without a date a current-state section reads as current whatever has " + "happened since. Re-date it when you reconcile it against what has arrived, " + "and not before."));
|
|
44819
|
+
}
|
|
44820
|
+
function renderUnchecked(out, context) {
|
|
44821
|
+
const n = context.unreadable;
|
|
44822
|
+
if (n === 0)
|
|
44823
|
+
return;
|
|
44824
|
+
out.info(row("Unchecked", `${n} artist ${n === 1 ? "file" : "files"} could not be read, so the walls, played ` + `and as-of checks did not see ${n === 1 ? "it" : "them"}.`));
|
|
44825
|
+
}
|
|
43402
44826
|
var statusCommand = defineCommand({
|
|
43403
44827
|
meta: {
|
|
43404
44828
|
name: "status",
|
|
@@ -43737,7 +45161,7 @@ function denyUngated(detail) {
|
|
|
43737
45161
|
// src/lib/voice-gate/payload.ts
|
|
43738
45162
|
import { readFileSync as readFileSync30, statSync as statSync20 } from "node:fs";
|
|
43739
45163
|
import { homedir as homedir2 } from "node:os";
|
|
43740
|
-
import { isAbsolute as
|
|
45164
|
+
import { isAbsolute as isAbsolute4, resolve as resolve4 } from "node:path";
|
|
43741
45165
|
var MAX_BODY_BYTES = 256 * 1024;
|
|
43742
45166
|
var HELPER_BASENAME = "push_gmail_draft.py";
|
|
43743
45167
|
var DRAFT_FLAGS = ["--to", "--body-file", "--body-stdin"];
|
|
@@ -43927,7 +45351,7 @@ function applyCd(tokens, state) {
|
|
|
43927
45351
|
return { dir: state.dir, known: false };
|
|
43928
45352
|
}
|
|
43929
45353
|
const expanded = expandHome(target);
|
|
43930
|
-
if (
|
|
45354
|
+
if (isAbsolute4(expanded))
|
|
43931
45355
|
return { dir: expanded, known: true };
|
|
43932
45356
|
if (!state.known)
|
|
43933
45357
|
return state;
|
|
@@ -43958,12 +45382,12 @@ function classifyHelperSegment(tokens, dirState, readBodyFile) {
|
|
|
43958
45382
|
};
|
|
43959
45383
|
}
|
|
43960
45384
|
const expanded = expandHome(bodyFile);
|
|
43961
|
-
if (!
|
|
45385
|
+
if (!isAbsolute4(expanded) && !dirState.known) {
|
|
43962
45386
|
return {
|
|
43963
45387
|
unreadable: `the draft push uses a relative \`--body-file\` (${bodyFile}) after a \`cd\` ` + `the voice gate cannot resolve, so it cannot tell which file ships. Pass an ` + `absolute \`--body-file\` path.`
|
|
43964
45388
|
};
|
|
43965
45389
|
}
|
|
43966
|
-
const resolved =
|
|
45390
|
+
const resolved = isAbsolute4(expanded) ? expanded : resolve4(dirState.dir, expanded);
|
|
43967
45391
|
let body;
|
|
43968
45392
|
try {
|
|
43969
45393
|
body = readBodyFile(resolved);
|