@plinth-music/cli 0.15.0 → 0.17.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 +67 -0
- package/README.md +3 -3
- package/dist/build-stamp.json +2 -2
- package/dist/cli.js +2617 -806
- 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.17.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",
|
|
@@ -23652,6 +23654,46 @@ function writeMirrorFile(workspaceSlug, entityType, entitySlug, content) {
|
|
|
23652
23654
|
throw err;
|
|
23653
23655
|
}
|
|
23654
23656
|
}
|
|
23657
|
+
|
|
23658
|
+
class MirrorDestinationOccupiedError extends MirrorPathError {
|
|
23659
|
+
fromSlug;
|
|
23660
|
+
toSlug;
|
|
23661
|
+
constructor(fromSlug, toSlug) {
|
|
23662
|
+
super(`Cannot move ${JSON.stringify(fromSlug)} to ${JSON.stringify(toSlug)}: ` + `a file already exists there`);
|
|
23663
|
+
this.name = "MirrorDestinationOccupiedError";
|
|
23664
|
+
this.fromSlug = fromSlug;
|
|
23665
|
+
this.toSlug = toSlug;
|
|
23666
|
+
}
|
|
23667
|
+
}
|
|
23668
|
+
function moveEntityFile(workspaceSlug, entityType, fromSlug, toSlug, beforeMove) {
|
|
23669
|
+
const strategy = getStrategy(entityType);
|
|
23670
|
+
const rel = strategy ? strategy.mirrorRelativePath(fromSlug) : flatMirrorRelativePath(entityType)(fromSlug);
|
|
23671
|
+
if (rel.split("/").length !== 2) {
|
|
23672
|
+
throw new MirrorPathError(`Entity type ${JSON.stringify(entityType)} keeps its rows in a folder; ` + `moving one is not supported`);
|
|
23673
|
+
}
|
|
23674
|
+
const from = entityFilePath(workspaceSlug, entityType, fromSlug);
|
|
23675
|
+
const to = entityFilePath(workspaceSlug, entityType, toSlug);
|
|
23676
|
+
if (from === to)
|
|
23677
|
+
return "absent";
|
|
23678
|
+
try {
|
|
23679
|
+
lstatSync(to);
|
|
23680
|
+
throw new MirrorDestinationOccupiedError(fromSlug, toSlug);
|
|
23681
|
+
} catch (err) {
|
|
23682
|
+
if (err instanceof MirrorDestinationOccupiedError)
|
|
23683
|
+
throw err;
|
|
23684
|
+
if (err?.code !== "ENOENT")
|
|
23685
|
+
throw err;
|
|
23686
|
+
}
|
|
23687
|
+
beforeMove?.();
|
|
23688
|
+
try {
|
|
23689
|
+
renameSync(from, to);
|
|
23690
|
+
return "moved";
|
|
23691
|
+
} catch (err) {
|
|
23692
|
+
if (err?.code === "ENOENT")
|
|
23693
|
+
return "absent";
|
|
23694
|
+
throw err;
|
|
23695
|
+
}
|
|
23696
|
+
}
|
|
23655
23697
|
var ARTIST_MEMORY_ANCHOR = "memory.md";
|
|
23656
23698
|
function artistMemoryFilePath(workspaceSlug, entitySlug) {
|
|
23657
23699
|
assertSlug("entity slug", entitySlug);
|
|
@@ -23688,6 +23730,16 @@ function readArtistMemoryFile(workspaceSlug, entitySlug) {
|
|
|
23688
23730
|
throw err;
|
|
23689
23731
|
}
|
|
23690
23732
|
}
|
|
23733
|
+
function readArtistAnchorFile(workspaceSlug, entitySlug) {
|
|
23734
|
+
const target = entityFilePath(workspaceSlug, "artists", entitySlug);
|
|
23735
|
+
try {
|
|
23736
|
+
return readFileSync(target, "utf8");
|
|
23737
|
+
} catch (err) {
|
|
23738
|
+
if (err?.code === "ENOENT")
|
|
23739
|
+
return null;
|
|
23740
|
+
throw err;
|
|
23741
|
+
}
|
|
23742
|
+
}
|
|
23691
23743
|
function deleteArtistMemoryFile(workspaceSlug, entitySlug) {
|
|
23692
23744
|
const target = artistMemoryFilePath(workspaceSlug, entitySlug);
|
|
23693
23745
|
try {
|
|
@@ -23699,6 +23751,113 @@ function deleteArtistMemoryFile(workspaceSlug, entitySlug) {
|
|
|
23699
23751
|
throw err;
|
|
23700
23752
|
}
|
|
23701
23753
|
}
|
|
23754
|
+
var MEMORY_FACTS_DIR = "memory";
|
|
23755
|
+
var MEMORY_FACTS_PRIVATE_SEGMENT = "private";
|
|
23756
|
+
var MEMORY_FACT_SLUG_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
23757
|
+
var MEMORY_FACT_SLUG_MAX = 64;
|
|
23758
|
+
function assertFactSlug(slug) {
|
|
23759
|
+
if (!MEMORY_FACT_SLUG_RE.test(slug) || slug.length > MEMORY_FACT_SLUG_MAX) {
|
|
23760
|
+
throw new MirrorPathError(`Invalid memory fact slug: ${JSON.stringify(slug)}`);
|
|
23761
|
+
}
|
|
23762
|
+
}
|
|
23763
|
+
function memoryFactRelPath(pile, slug) {
|
|
23764
|
+
assertFactSlug(slug);
|
|
23765
|
+
return pile === "private" ? `${MEMORY_FACTS_DIR}/${MEMORY_FACTS_PRIVATE_SEGMENT}/${slug}.md` : `${MEMORY_FACTS_DIR}/${slug}.md`;
|
|
23766
|
+
}
|
|
23767
|
+
function parseMemoryFactRelPath(rel) {
|
|
23768
|
+
const posix = rel.split(sep).join("/");
|
|
23769
|
+
if (!posix.startsWith(`${MEMORY_FACTS_DIR}/`) || !posix.endsWith(".md")) {
|
|
23770
|
+
return null;
|
|
23771
|
+
}
|
|
23772
|
+
const tail = posix.slice(MEMORY_FACTS_DIR.length + 1, -".md".length);
|
|
23773
|
+
const privatePrefix = `${MEMORY_FACTS_PRIVATE_SEGMENT}/`;
|
|
23774
|
+
if (tail.startsWith(privatePrefix)) {
|
|
23775
|
+
const slug = tail.slice(privatePrefix.length);
|
|
23776
|
+
return MEMORY_FACT_SLUG_RE.test(slug) && slug.length <= MEMORY_FACT_SLUG_MAX ? { pile: "private", slug } : null;
|
|
23777
|
+
}
|
|
23778
|
+
return MEMORY_FACT_SLUG_RE.test(tail) && tail.length <= MEMORY_FACT_SLUG_MAX ? { pile: "shared", slug: tail } : null;
|
|
23779
|
+
}
|
|
23780
|
+
function memoryFactFilePath(workspaceSlug, pile, slug) {
|
|
23781
|
+
const ws = workspaceMirrorPath(workspaceSlug);
|
|
23782
|
+
const posix = memoryFactRelPath(pile, slug);
|
|
23783
|
+
const native = sep === "/" ? posix : posix.split("/").join(sep);
|
|
23784
|
+
const target = resolve(join2(ws, native));
|
|
23785
|
+
const prefix = join2(ws, MEMORY_FACTS_DIR) + sep;
|
|
23786
|
+
if (!target.startsWith(prefix)) {
|
|
23787
|
+
throw new MirrorPathError(`Resolved memory fact path escapes the memory subtree`);
|
|
23788
|
+
}
|
|
23789
|
+
assertNoSymlinkedComponent(ws, target);
|
|
23790
|
+
return target;
|
|
23791
|
+
}
|
|
23792
|
+
function memoryFactsSubtreeRoot(workspaceSlug) {
|
|
23793
|
+
const ws = workspaceMirrorPath(workspaceSlug);
|
|
23794
|
+
const target = join2(ws, MEMORY_FACTS_DIR);
|
|
23795
|
+
assertNoSymlinkedComponent(ws, target);
|
|
23796
|
+
return target;
|
|
23797
|
+
}
|
|
23798
|
+
function writeMemoryFactFile(workspaceSlug, pile, slug, content) {
|
|
23799
|
+
const target = memoryFactFilePath(workspaceSlug, pile, slug);
|
|
23800
|
+
const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
|
|
23801
|
+
mkdirSync(dirname(target), { recursive: true, mode: 448 });
|
|
23802
|
+
writeFileSync(tmp, content, { mode: 384 });
|
|
23803
|
+
try {
|
|
23804
|
+
renameSync(tmp, target);
|
|
23805
|
+
} catch (err) {
|
|
23806
|
+
try {
|
|
23807
|
+
unlinkSync(tmp);
|
|
23808
|
+
} catch {}
|
|
23809
|
+
throw err;
|
|
23810
|
+
}
|
|
23811
|
+
}
|
|
23812
|
+
function readMemoryFactFile(workspaceSlug, pile, slug) {
|
|
23813
|
+
try {
|
|
23814
|
+
return readFileSync(memoryFactFilePath(workspaceSlug, pile, slug), "utf8");
|
|
23815
|
+
} catch (err) {
|
|
23816
|
+
if (err?.code === "ENOENT")
|
|
23817
|
+
return null;
|
|
23818
|
+
throw err;
|
|
23819
|
+
}
|
|
23820
|
+
}
|
|
23821
|
+
function deleteMemoryFactFile(workspaceSlug, pile, slug) {
|
|
23822
|
+
const target = memoryFactFilePath(workspaceSlug, pile, slug);
|
|
23823
|
+
try {
|
|
23824
|
+
unlinkSync(target);
|
|
23825
|
+
return true;
|
|
23826
|
+
} catch (err) {
|
|
23827
|
+
if (err?.code === "ENOENT")
|
|
23828
|
+
return false;
|
|
23829
|
+
throw err;
|
|
23830
|
+
}
|
|
23831
|
+
}
|
|
23832
|
+
function listMemoryFactFiles(workspaceSlug) {
|
|
23833
|
+
let root;
|
|
23834
|
+
try {
|
|
23835
|
+
root = memoryFactsSubtreeRoot(workspaceSlug);
|
|
23836
|
+
} catch {
|
|
23837
|
+
return [];
|
|
23838
|
+
}
|
|
23839
|
+
const found = [];
|
|
23840
|
+
const read = (dir, pile) => {
|
|
23841
|
+
let entries;
|
|
23842
|
+
try {
|
|
23843
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
23844
|
+
} catch {
|
|
23845
|
+
return;
|
|
23846
|
+
}
|
|
23847
|
+
for (const e of entries) {
|
|
23848
|
+
if (!e.isFile() || !e.name.endsWith(".md"))
|
|
23849
|
+
continue;
|
|
23850
|
+
const slug = e.name.slice(0, -3);
|
|
23851
|
+
if (!MEMORY_FACT_SLUG_RE.test(slug) || slug.length > MEMORY_FACT_SLUG_MAX) {
|
|
23852
|
+
continue;
|
|
23853
|
+
}
|
|
23854
|
+
found.push({ pile, slug });
|
|
23855
|
+
}
|
|
23856
|
+
};
|
|
23857
|
+
read(root, "shared");
|
|
23858
|
+
read(join2(root, MEMORY_FACTS_PRIVATE_SEGMENT), "private");
|
|
23859
|
+
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);
|
|
23860
|
+
}
|
|
23702
23861
|
var FILES_SUBTREE_DIR = "files";
|
|
23703
23862
|
var FILES_CONTROL_CHAR_RE = new RegExp("[\\x00-\\x1f\\x7f]");
|
|
23704
23863
|
function validateFilesRelPath(relPath) {
|
|
@@ -25910,6 +26069,9 @@ var COMMAND_BAR_BYTES = 24000;
|
|
|
25910
26069
|
var INDEX_LINE_MAX_BYTES = 300;
|
|
25911
26070
|
var MAX_NAMED_FACT_STORES = 5;
|
|
25912
26071
|
var MAX_NAMED_LONG_INDEX_LINES = 3;
|
|
26072
|
+
var CONTEXT_BLOCK_BAR_CHARS = 2000;
|
|
26073
|
+
var CURRENT_STATE_AS_OF_MAX_DAYS = 30;
|
|
26074
|
+
var MAX_NAMED_CONTEXT_FINDINGS = 5;
|
|
25913
26075
|
function countRuleShapedLines(body) {
|
|
25914
26076
|
let count = 0;
|
|
25915
26077
|
for (const line of body.split(`
|
|
@@ -25926,531 +26088,15 @@ function countParagraphs(body) {
|
|
|
25926
26088
|
return body.split(/\n\s*\n/).map((p) => p.trim()).filter((p) => p.length > 0).length;
|
|
25927
26089
|
}
|
|
25928
26090
|
|
|
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
26091
|
// src/lib/state.ts
|
|
26446
26092
|
import {
|
|
26447
26093
|
mkdirSync as mkdirSync6,
|
|
26448
|
-
readFileSync as
|
|
26094
|
+
readFileSync as readFileSync7,
|
|
26449
26095
|
renameSync as renameSync5,
|
|
26450
26096
|
unlinkSync as unlinkSync5,
|
|
26451
26097
|
writeFileSync as writeFileSync5
|
|
26452
26098
|
} from "node:fs";
|
|
26453
|
-
import { dirname as dirname4, join as
|
|
26099
|
+
import { dirname as dirname4, join as join6 } from "node:path";
|
|
26454
26100
|
var StateEntrySchema = exports_external.object({
|
|
26455
26101
|
id: exports_external.string().uuid(),
|
|
26456
26102
|
slug: exports_external.string().min(1),
|
|
@@ -26460,7 +26106,8 @@ var StateEntrySchema = exports_external.object({
|
|
|
26460
26106
|
frontmatter_hash: exports_external.string().min(1),
|
|
26461
26107
|
inode: exports_external.number().int().nonnegative().optional(),
|
|
26462
26108
|
body_len: exports_external.number().int().nonnegative().optional(),
|
|
26463
|
-
memory_hash: exports_external.string().min(1).optional()
|
|
26109
|
+
memory_hash: exports_external.string().min(1).optional(),
|
|
26110
|
+
memory_generated: exports_external.boolean().optional()
|
|
26464
26111
|
});
|
|
26465
26112
|
var EntityEntriesSchema = exports_external.record(exports_external.string().uuid(), StateEntrySchema);
|
|
26466
26113
|
var FileStateEntrySchema = exports_external.object({
|
|
@@ -26480,17 +26127,27 @@ var WorkspaceRulesStateSchema = exports_external.object({
|
|
|
26480
26127
|
});
|
|
26481
26128
|
var WorkspaceMemoryStateSchema = exports_external.object({
|
|
26482
26129
|
body_hash: exports_external.string().min(1),
|
|
26483
|
-
updated_at: exports_external.string().datetime({ offset: true }).nullable()
|
|
26130
|
+
updated_at: exports_external.string().datetime({ offset: true }).nullable(),
|
|
26131
|
+
generated: exports_external.boolean().optional()
|
|
26484
26132
|
});
|
|
26485
26133
|
var UserMemoryStateSchema = exports_external.object({
|
|
26486
26134
|
body_hash: exports_external.string().min(1),
|
|
26487
|
-
updated_at: exports_external.string().datetime({ offset: true }).nullable()
|
|
26135
|
+
updated_at: exports_external.string().datetime({ offset: true }).nullable(),
|
|
26136
|
+
generated: exports_external.boolean().optional()
|
|
26488
26137
|
});
|
|
26489
26138
|
var UserVoiceStateSchema = exports_external.object({
|
|
26490
26139
|
body_hash: exports_external.string().min(1),
|
|
26491
26140
|
updated_at: exports_external.string().datetime({ offset: true }).nullable(),
|
|
26492
26141
|
status: exports_external.enum(["absent", "malformed", "valid"]).optional()
|
|
26493
26142
|
});
|
|
26143
|
+
var MemoryFactEntrySchema = exports_external.object({
|
|
26144
|
+
id: exports_external.string().min(1),
|
|
26145
|
+
body_hash: exports_external.string().min(1)
|
|
26146
|
+
});
|
|
26147
|
+
var MemoryFactsStateSchema = exports_external.object({
|
|
26148
|
+
fetched_at: exports_external.string().datetime({ offset: true }),
|
|
26149
|
+
facts: exports_external.record(exports_external.string(), MemoryFactEntrySchema).default({})
|
|
26150
|
+
});
|
|
26494
26151
|
var UnsyncedOpSchema = exports_external.object({
|
|
26495
26152
|
target: exports_external.enum(["entity", "file"]),
|
|
26496
26153
|
op: exports_external.enum(["upsert", "delete"]),
|
|
@@ -26549,7 +26206,8 @@ var WorkspaceStateV2Schema = exports_external.object({
|
|
|
26549
26206
|
frontmatter_id_backfill_version: exports_external.number().int().nonnegative().optional(),
|
|
26550
26207
|
team_rejected: TeamRejectedSchema.optional(),
|
|
26551
26208
|
size_anchor: SizeAnchorSchema.optional(),
|
|
26552
|
-
last_trim_at: exports_external.string().min(1).optional()
|
|
26209
|
+
last_trim_at: exports_external.string().min(1).optional(),
|
|
26210
|
+
memory_facts: MemoryFactsStateSchema.optional()
|
|
26553
26211
|
});
|
|
26554
26212
|
var WorkspaceStateV1Schema = exports_external.object({
|
|
26555
26213
|
version: exports_external.literal(1),
|
|
@@ -26557,10 +26215,10 @@ var WorkspaceStateV1Schema = exports_external.object({
|
|
|
26557
26215
|
documents: EntityEntriesSchema
|
|
26558
26216
|
});
|
|
26559
26217
|
function stateFilePath(workspaceSlug) {
|
|
26560
|
-
return
|
|
26218
|
+
return join6(plinthHome(), workspaceSlug, "state.json");
|
|
26561
26219
|
}
|
|
26562
26220
|
function stateTmpPath(workspaceSlug) {
|
|
26563
|
-
return
|
|
26221
|
+
return join6(plinthHome(), workspaceSlug, `state.json.${process.pid}.tmp`);
|
|
26564
26222
|
}
|
|
26565
26223
|
function emptyState(workspaceSlug) {
|
|
26566
26224
|
return { version: 2, workspace_slug: workspaceSlug, entries: {}, files: {} };
|
|
@@ -26576,7 +26234,7 @@ function migrateV1(v1) {
|
|
|
26576
26234
|
function loadState(workspaceSlug) {
|
|
26577
26235
|
let raw;
|
|
26578
26236
|
try {
|
|
26579
|
-
raw =
|
|
26237
|
+
raw = readFileSync7(stateFilePath(workspaceSlug), "utf8");
|
|
26580
26238
|
} catch (err) {
|
|
26581
26239
|
if (err?.code === "ENOENT") {
|
|
26582
26240
|
return emptyState(workspaceSlug);
|
|
@@ -26741,7 +26399,8 @@ function commitState(before, after) {
|
|
|
26741
26399
|
frontmatter_id_backfill_version: mergeScalar(fresh.frontmatter_id_backfill_version, before.frontmatter_id_backfill_version, after.frontmatter_id_backfill_version),
|
|
26742
26400
|
team_rejected: mergeScalar(fresh.team_rejected, before.team_rejected, after.team_rejected),
|
|
26743
26401
|
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)
|
|
26402
|
+
last_trim_at: mergeScalar(fresh.last_trim_at, before.last_trim_at, after.last_trim_at),
|
|
26403
|
+
memory_facts: mergeScalar(fresh.memory_facts, before.memory_facts, after.memory_facts)
|
|
26745
26404
|
};
|
|
26746
26405
|
writeState(merged);
|
|
26747
26406
|
const singletonOverruled = rules.overruled || memory.overruled || userMemory.overruled || userVoice.overruled;
|
|
@@ -26934,145 +26593,729 @@ function upsertRejected(state, candidate) {
|
|
|
26934
26593
|
next.push(merged);
|
|
26935
26594
|
return { state: { ...state, rejected: next }, changed: true };
|
|
26936
26595
|
}
|
|
26937
|
-
function removeRejected(state, target, path3) {
|
|
26938
|
-
const list = state.rejected ?? [];
|
|
26939
|
-
const next = list.filter((o) => !(o.target === target && o.path === path3));
|
|
26940
|
-
if (next.length === list.length)
|
|
26941
|
-
return { state, changed: false };
|
|
26942
|
-
return { state: { ...state, rejected: next }, changed: true };
|
|
26596
|
+
function removeRejected(state, target, path3) {
|
|
26597
|
+
const list = state.rejected ?? [];
|
|
26598
|
+
const next = list.filter((o) => !(o.target === target && o.path === path3));
|
|
26599
|
+
if (next.length === list.length)
|
|
26600
|
+
return { state, changed: false };
|
|
26601
|
+
return { state: { ...state, rejected: next }, changed: true };
|
|
26602
|
+
}
|
|
26603
|
+
function markRejectedReported(state, path3, reportedAt) {
|
|
26604
|
+
const list = state.rejected ?? [];
|
|
26605
|
+
const idx = list.findIndex((o) => o.target === "file" && o.path === path3);
|
|
26606
|
+
if (idx < 0)
|
|
26607
|
+
return { state, changed: false };
|
|
26608
|
+
if (list[idx].reported_at === reportedAt)
|
|
26609
|
+
return { state, changed: false };
|
|
26610
|
+
const next = [...list];
|
|
26611
|
+
next[idx] = { ...list[idx], reported_at: reportedAt };
|
|
26612
|
+
return { state: { ...state, rejected: next }, changed: true };
|
|
26613
|
+
}
|
|
26614
|
+
function clearRejectedReported(state, path3) {
|
|
26615
|
+
const list = state.rejected ?? [];
|
|
26616
|
+
const idx = list.findIndex((o) => o.target === "file" && o.path === path3);
|
|
26617
|
+
if (idx < 0)
|
|
26618
|
+
return { state, changed: false };
|
|
26619
|
+
if (list[idx].reported_at === undefined)
|
|
26620
|
+
return { state, changed: false };
|
|
26621
|
+
const next = [...list];
|
|
26622
|
+
const { reported_at: _dropped, ...rest } = list[idx];
|
|
26623
|
+
next[idx] = rest;
|
|
26624
|
+
return { state: { ...state, rejected: next }, changed: true };
|
|
26625
|
+
}
|
|
26626
|
+
function listTeamRejected(state) {
|
|
26627
|
+
return state.team_rejected?.rows ?? [];
|
|
26628
|
+
}
|
|
26629
|
+
function teamRejectedFetchedAt(state) {
|
|
26630
|
+
return state.team_rejected?.fetched_at;
|
|
26631
|
+
}
|
|
26632
|
+
function setTeamRejected(state, rows, fetchedAt) {
|
|
26633
|
+
const existing = state.team_rejected;
|
|
26634
|
+
if (existing && rowsEqual(existing.rows, rows)) {
|
|
26635
|
+
return { state, changed: false };
|
|
26636
|
+
}
|
|
26637
|
+
return {
|
|
26638
|
+
state: { ...state, team_rejected: { fetched_at: fetchedAt, rows } },
|
|
26639
|
+
changed: true
|
|
26640
|
+
};
|
|
26641
|
+
}
|
|
26642
|
+
function rowsEqual(a, b) {
|
|
26643
|
+
if (a.length !== b.length)
|
|
26644
|
+
return false;
|
|
26645
|
+
return a.every((x, i) => {
|
|
26646
|
+
const y = b[i];
|
|
26647
|
+
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;
|
|
26648
|
+
});
|
|
26649
|
+
}
|
|
26650
|
+
function getWorkspaceRulesState(state) {
|
|
26651
|
+
return state.workspace_rules;
|
|
26652
|
+
}
|
|
26653
|
+
function setWorkspaceRulesState(state, entry) {
|
|
26654
|
+
const existing = state.workspace_rules;
|
|
26655
|
+
if (existing && existing.body_hash === entry.body_hash && existing.updated_at === entry.updated_at) {
|
|
26656
|
+
return { state, changed: false };
|
|
26657
|
+
}
|
|
26658
|
+
return { state: { ...state, workspace_rules: entry }, changed: true };
|
|
26659
|
+
}
|
|
26660
|
+
function getWorkspaceMemoryState(state) {
|
|
26661
|
+
return state.workspace_memory;
|
|
26662
|
+
}
|
|
26663
|
+
function setWorkspaceMemoryState(state, entry) {
|
|
26664
|
+
const existing = state.workspace_memory;
|
|
26665
|
+
if (existing && existing.body_hash === entry.body_hash && existing.updated_at === entry.updated_at && existing.generated === entry.generated) {
|
|
26666
|
+
return { state, changed: false };
|
|
26667
|
+
}
|
|
26668
|
+
return { state: { ...state, workspace_memory: entry }, changed: true };
|
|
26669
|
+
}
|
|
26670
|
+
function getUserMemoryState(state) {
|
|
26671
|
+
return state.user_memory;
|
|
26672
|
+
}
|
|
26673
|
+
function setUserMemoryState(state, entry) {
|
|
26674
|
+
const existing = state.user_memory;
|
|
26675
|
+
if (existing && existing.body_hash === entry.body_hash && existing.updated_at === entry.updated_at && existing.generated === entry.generated) {
|
|
26676
|
+
return { state, changed: false };
|
|
26677
|
+
}
|
|
26678
|
+
return { state: { ...state, user_memory: entry }, changed: true };
|
|
26679
|
+
}
|
|
26680
|
+
function getMemoryFactEntries(state) {
|
|
26681
|
+
return state.memory_facts?.facts ?? {};
|
|
26682
|
+
}
|
|
26683
|
+
function getMemoryFactEntry(state, relPath) {
|
|
26684
|
+
return state.memory_facts?.facts[relPath];
|
|
26685
|
+
}
|
|
26686
|
+
function setMemoryFactsState(state, entry) {
|
|
26687
|
+
const existing = state.memory_facts;
|
|
26688
|
+
if (existing && sameFactRegistry(existing.facts, entry.facts)) {
|
|
26689
|
+
return { state, changed: false };
|
|
26690
|
+
}
|
|
26691
|
+
return { state: { ...state, memory_facts: entry }, changed: true };
|
|
26692
|
+
}
|
|
26693
|
+
function sameFactRegistry(a, b) {
|
|
26694
|
+
const aKeys = Object.keys(a);
|
|
26695
|
+
if (aKeys.length !== Object.keys(b).length)
|
|
26696
|
+
return false;
|
|
26697
|
+
for (const key of aKeys) {
|
|
26698
|
+
const left = a[key];
|
|
26699
|
+
const right = b[key];
|
|
26700
|
+
if (!right || left.id !== right.id || left.body_hash !== right.body_hash) {
|
|
26701
|
+
return false;
|
|
26702
|
+
}
|
|
26703
|
+
}
|
|
26704
|
+
return true;
|
|
26705
|
+
}
|
|
26706
|
+
function getUserVoiceState(state) {
|
|
26707
|
+
return state.user_voice;
|
|
26708
|
+
}
|
|
26709
|
+
function setUserVoiceState(state, entry) {
|
|
26710
|
+
const existing = state.user_voice;
|
|
26711
|
+
if (existing && existing.body_hash === entry.body_hash && existing.updated_at === entry.updated_at && existing.status === entry.status) {
|
|
26712
|
+
return { state, changed: false };
|
|
26713
|
+
}
|
|
26714
|
+
return { state: { ...state, user_voice: entry }, changed: true };
|
|
26715
|
+
}
|
|
26716
|
+
var ABSENT_FRESHNESS = {
|
|
26717
|
+
workspaceMemory: null,
|
|
26718
|
+
userMemory: null,
|
|
26719
|
+
userVoice: null
|
|
26720
|
+
};
|
|
26721
|
+
function readMemoryFreshness(workspaceSlug) {
|
|
26722
|
+
let json2;
|
|
26723
|
+
try {
|
|
26724
|
+
json2 = JSON.parse(readFileSync7(stateFilePath(workspaceSlug), "utf8"));
|
|
26725
|
+
} catch {
|
|
26726
|
+
return ABSENT_FRESHNESS;
|
|
26727
|
+
}
|
|
26728
|
+
if (json2 === null || typeof json2 !== "object")
|
|
26729
|
+
return ABSENT_FRESHNESS;
|
|
26730
|
+
const record2 = json2;
|
|
26731
|
+
const workspace = WorkspaceMemoryStateSchema.safeParse(record2.workspace_memory);
|
|
26732
|
+
const user = UserMemoryStateSchema.safeParse(record2.user_memory);
|
|
26733
|
+
const voice = UserVoiceStateSchema.safeParse(record2.user_voice);
|
|
26734
|
+
return {
|
|
26735
|
+
workspaceMemory: workspace.success ? workspace.data : null,
|
|
26736
|
+
userMemory: user.success ? user.data : null,
|
|
26737
|
+
userVoice: voice.success ? voice.data : null
|
|
26738
|
+
};
|
|
26739
|
+
}
|
|
26740
|
+
function setSizeAnchor(state, anchor) {
|
|
26741
|
+
if (anchor.kind === "sync" && state.size_anchor !== undefined) {
|
|
26742
|
+
return { state, changed: false };
|
|
26743
|
+
}
|
|
26744
|
+
const existing = state.size_anchor;
|
|
26745
|
+
if (existing !== undefined && existing.kind === anchor.kind && existing.at === anchor.at && sizesEqual(existing.sizes, anchor.sizes)) {
|
|
26746
|
+
return { state, changed: false };
|
|
26747
|
+
}
|
|
26748
|
+
return { state: { ...state, size_anchor: anchor }, changed: true };
|
|
26749
|
+
}
|
|
26750
|
+
function sizesEqual(a, b) {
|
|
26751
|
+
const ak = Object.keys(a);
|
|
26752
|
+
const bk = Object.keys(b);
|
|
26753
|
+
if (ak.length !== bk.length)
|
|
26754
|
+
return false;
|
|
26755
|
+
return ak.every((k) => a[k] === b[k]);
|
|
26756
|
+
}
|
|
26757
|
+
function setLastTrimAt(state, at) {
|
|
26758
|
+
if (state.last_trim_at === at)
|
|
26759
|
+
return { state, changed: false };
|
|
26760
|
+
return { state: { ...state, last_trim_at: at }, changed: true };
|
|
26761
|
+
}
|
|
26762
|
+
|
|
26763
|
+
// src/lib/memory-facts/stores.ts
|
|
26764
|
+
var BLOB_MEMORY_STORES = Object.freeze({
|
|
26765
|
+
workspace: false,
|
|
26766
|
+
user: false
|
|
26767
|
+
});
|
|
26768
|
+
function memoryStoreModesFrom(state) {
|
|
26769
|
+
return {
|
|
26770
|
+
workspace: state.workspace_memory?.generated === true,
|
|
26771
|
+
user: state.user_memory?.generated === true
|
|
26772
|
+
};
|
|
26773
|
+
}
|
|
26774
|
+
function readMemoryStoreModes(workspaceSlug) {
|
|
26775
|
+
try {
|
|
26776
|
+
return memoryStoreModesFrom(loadState(workspaceSlug));
|
|
26777
|
+
} catch {
|
|
26778
|
+
return BLOB_MEMORY_STORES;
|
|
26779
|
+
}
|
|
26780
|
+
}
|
|
26781
|
+
function anyGenerated(modes) {
|
|
26782
|
+
return modes.workspace || modes.user;
|
|
26783
|
+
}
|
|
26784
|
+
function generatedMemoryStashDepsFrom(state) {
|
|
26785
|
+
const artists = state.entries["artists"] ?? {};
|
|
26786
|
+
const generatedArtists = new Set;
|
|
26787
|
+
for (const entry of Object.values(artists)) {
|
|
26788
|
+
if (entry.memory_generated === true)
|
|
26789
|
+
generatedArtists.add(entry.slug);
|
|
26790
|
+
}
|
|
26791
|
+
return {
|
|
26792
|
+
artistMemoryGenerated: (slug) => generatedArtists.has(slug),
|
|
26793
|
+
workspaceMemoryGenerated: () => state.workspace_memory?.generated === true,
|
|
26794
|
+
userMemoryGenerated: () => state.user_memory?.generated === true
|
|
26795
|
+
};
|
|
26796
|
+
}
|
|
26797
|
+
|
|
26798
|
+
// src/lib/context/grounding.ts
|
|
26799
|
+
var WORKSPACE_RULES_FILENAME = "workspace-rules.md";
|
|
26800
|
+
var HOUSE_STYLE_LEGACY_FILENAME = "house-style.md";
|
|
26801
|
+
var WORKSPACE_MEMORY_FILENAME = "workspace-memory.md";
|
|
26802
|
+
var USER_MEMORY_FILENAME = "user-memory.md";
|
|
26803
|
+
var VOICE_FILENAME = "voice.md";
|
|
26804
|
+
var WORKSPACE_RULES_PRIVATE_HEADING = "## Workspace-specific (private, never promotes)";
|
|
26805
|
+
var WORKSPACE_RULES_GENERAL_HEADING = "## Generalisable manager-craft (may promote)";
|
|
26806
|
+
var WORKSPACE_RULES_SECTION_HEADINGS = [
|
|
26807
|
+
WORKSPACE_RULES_PRIVATE_HEADING,
|
|
26808
|
+
WORKSPACE_RULES_GENERAL_HEADING
|
|
26809
|
+
];
|
|
26810
|
+
function extractWorkspaceRules(raw) {
|
|
26811
|
+
if (raw == null)
|
|
26812
|
+
return null;
|
|
26813
|
+
const withoutComments = raw.replace(/<!--[\s\S]*?-->/g, "").replace(/<!--[\s\S]*$/, "");
|
|
26814
|
+
const stripped = withoutComments.split(`
|
|
26815
|
+
`).filter((line) => !WORKSPACE_RULES_SECTION_HEADINGS.includes(line.trim())).join(`
|
|
26816
|
+
`).trim();
|
|
26817
|
+
return stripped.length > 0 ? stripped : null;
|
|
26818
|
+
}
|
|
26819
|
+
function extractWorkspaceMemory(raw) {
|
|
26820
|
+
if (raw == null)
|
|
26821
|
+
return null;
|
|
26822
|
+
const stripped = raw.replace(/<!--[\s\S]*?-->/g, "").replace(/<!--[\s\S]*$/, "").trim();
|
|
26823
|
+
return stripped.length > 0 ? stripped : null;
|
|
26824
|
+
}
|
|
26825
|
+
function extractUserMemory(raw) {
|
|
26826
|
+
if (raw == null)
|
|
26827
|
+
return null;
|
|
26828
|
+
const stripped = raw.replace(/<!--[\s\S]*?-->/g, "").replace(/<!--[\s\S]*$/, "").trim();
|
|
26829
|
+
return stripped.length > 0 ? stripped : null;
|
|
26830
|
+
}
|
|
26831
|
+
function extractVoiceProfile(raw) {
|
|
26832
|
+
if (raw == null)
|
|
26833
|
+
return null;
|
|
26834
|
+
const stripped = raw.replace(/<!--[\s\S]*?-->/g, "").trim();
|
|
26835
|
+
return stripped.length > 0 ? stripped : null;
|
|
26836
|
+
}
|
|
26837
|
+
function daysBetween(from, to) {
|
|
26838
|
+
const parse6 = (d) => {
|
|
26839
|
+
const parts = d.split("-");
|
|
26840
|
+
return Date.UTC(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2]));
|
|
26841
|
+
};
|
|
26842
|
+
return Math.round((parse6(to) - parse6(from)) / 86400000);
|
|
26843
|
+
}
|
|
26844
|
+
function renderFreshnessStamp(today, updatedAt) {
|
|
26845
|
+
const UNKNOWN = `The age of this index is not known here, so treat its lines as undated ` + `rather than as fresh.`;
|
|
26846
|
+
if (updatedAt == null)
|
|
26847
|
+
return UNKNOWN;
|
|
26848
|
+
const banked = new Date(updatedAt);
|
|
26849
|
+
if (Number.isNaN(banked.getTime()))
|
|
26850
|
+
return UNKNOWN;
|
|
26851
|
+
const bankedDay = formatLocalDate(banked);
|
|
26852
|
+
const days = Math.max(0, daysBetween(bankedDay, today));
|
|
26853
|
+
const age = days === 0 ? "today" : days === 1 ? "1 day ago" : `${days} days ago`;
|
|
26854
|
+
return `This index was last updated on ${bankedDay}, ${age}.`;
|
|
26855
|
+
}
|
|
26856
|
+
var GROUNDING_BUDGET_BYTES = 20000;
|
|
26857
|
+
var GROUNDING_CATEGORY_ORDER = [
|
|
26858
|
+
"grounding",
|
|
26859
|
+
"identity",
|
|
26860
|
+
"budgetNote",
|
|
26861
|
+
"sizeNote",
|
|
26862
|
+
"voice",
|
|
26863
|
+
"rules",
|
|
26864
|
+
"workspaceFacts",
|
|
26865
|
+
"personalFacts"
|
|
26866
|
+
];
|
|
26867
|
+
var GROUNDING_CATEGORY_LABELS = {
|
|
26868
|
+
grounding: "Plinth's own grounding",
|
|
26869
|
+
identity: "who you are",
|
|
26870
|
+
budgetNote: "the trim notes",
|
|
26871
|
+
sizeNote: "the size warning",
|
|
26872
|
+
voice: "voice pointer",
|
|
26873
|
+
rules: "this workspace's rules",
|
|
26874
|
+
workspaceFacts: "workspace facts",
|
|
26875
|
+
personalFacts: "your own facts"
|
|
26876
|
+
};
|
|
26877
|
+
var MEMBER_EDITABLE_CATEGORIES = [
|
|
26878
|
+
"rules",
|
|
26879
|
+
"workspaceFacts",
|
|
26880
|
+
"personalFacts"
|
|
26881
|
+
];
|
|
26882
|
+
function segmentBytes(lines, cats) {
|
|
26883
|
+
const out = {
|
|
26884
|
+
grounding: 0,
|
|
26885
|
+
identity: 0,
|
|
26886
|
+
rules: 0,
|
|
26887
|
+
workspaceFacts: 0,
|
|
26888
|
+
personalFacts: 0,
|
|
26889
|
+
voice: 0,
|
|
26890
|
+
budgetNote: 0,
|
|
26891
|
+
sizeNote: 0
|
|
26892
|
+
};
|
|
26893
|
+
lines.forEach((text, i) => {
|
|
26894
|
+
const cat = cats[i];
|
|
26895
|
+
out[cat] += Buffer.byteLength(text) + (i < lines.length - 1 ? 1 : 0);
|
|
26896
|
+
});
|
|
26897
|
+
return out;
|
|
26898
|
+
}
|
|
26899
|
+
var IDENTITY_LINE_MAX_BYTES = 300;
|
|
26900
|
+
function renderMemberIdentity(member, workspaceSlug) {
|
|
26901
|
+
if (member === null)
|
|
26902
|
+
return null;
|
|
26903
|
+
const tail = `. Tasks you create are assigned to this member unless told otherwise.`;
|
|
26904
|
+
const budget = IDENTITY_LINE_MAX_BYTES - Buffer.byteLength(tail);
|
|
26905
|
+
const email3 = oneLine(member.email);
|
|
26906
|
+
const build = (withName, withRole, withZone) => {
|
|
26907
|
+
const who = withName && member.displayName ? `${oneLine(member.displayName)} (${email3})` : email3;
|
|
26908
|
+
const clauses = [`You are working for ${who}`];
|
|
26909
|
+
if (withRole && member.role)
|
|
26910
|
+
clauses.push(`role ${oneLine(member.role)}`);
|
|
26911
|
+
if (workspaceSlug !== null)
|
|
26912
|
+
clauses.push(`in workspace ${workspaceSlug}`);
|
|
26913
|
+
if (withZone && member.timezone)
|
|
26914
|
+
clauses.push(`timezone ${oneLine(member.timezone)}`);
|
|
26915
|
+
return clauses.join(", ");
|
|
26916
|
+
};
|
|
26917
|
+
const candidates = [
|
|
26918
|
+
build(true, true, true),
|
|
26919
|
+
build(true, true, false),
|
|
26920
|
+
build(true, false, false),
|
|
26921
|
+
build(false, false, false)
|
|
26922
|
+
];
|
|
26923
|
+
if (member.displayName) {
|
|
26924
|
+
const slugClause = workspaceSlug === null ? `` : `, in workspace ${workspaceSlug}`;
|
|
26925
|
+
candidates.push(`You are working for ${oneLine(member.displayName)}${slugClause}`);
|
|
26926
|
+
}
|
|
26927
|
+
const head = candidates.find((c) => Buffer.byteLength(c) <= budget);
|
|
26928
|
+
if (head === undefined)
|
|
26929
|
+
return null;
|
|
26930
|
+
return head + tail;
|
|
26931
|
+
}
|
|
26932
|
+
function oneLine(value) {
|
|
26933
|
+
return value.replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim();
|
|
26943
26934
|
}
|
|
26944
|
-
function
|
|
26945
|
-
|
|
26946
|
-
|
|
26947
|
-
|
|
26948
|
-
return { state, changed: false };
|
|
26949
|
-
if (list[idx].reported_at === reportedAt)
|
|
26950
|
-
return { state, changed: false };
|
|
26951
|
-
const next = [...list];
|
|
26952
|
-
next[idx] = { ...list[idx], reported_at: reportedAt };
|
|
26953
|
-
return { state: { ...state, rejected: next }, changed: true };
|
|
26935
|
+
function shouldOfferOnboarding(rosterIsEmpty, member) {
|
|
26936
|
+
if (member?.onboardingCompletedAt)
|
|
26937
|
+
return false;
|
|
26938
|
+
return rosterIsEmpty;
|
|
26954
26939
|
}
|
|
26955
|
-
function
|
|
26956
|
-
const
|
|
26957
|
-
|
|
26958
|
-
|
|
26959
|
-
|
|
26960
|
-
|
|
26961
|
-
|
|
26962
|
-
|
|
26963
|
-
|
|
26964
|
-
next[idx] = rest;
|
|
26965
|
-
return { state: { ...state, rejected: next }, changed: true };
|
|
26940
|
+
function bankedDate(line) {
|
|
26941
|
+
const matches = line.match(/\d{4}-\d{2}-\d{2}/g);
|
|
26942
|
+
if (matches === null || matches.length === 0)
|
|
26943
|
+
return null;
|
|
26944
|
+
let latest = matches[0];
|
|
26945
|
+
for (const m of matches)
|
|
26946
|
+
if (m > latest)
|
|
26947
|
+
latest = m;
|
|
26948
|
+
return latest;
|
|
26966
26949
|
}
|
|
26967
|
-
|
|
26968
|
-
|
|
26950
|
+
var POINTER_RE = /\[\[[^\]]+\]\]/g;
|
|
26951
|
+
var TITLE_TRUNCATION_MARK = " …";
|
|
26952
|
+
function factTitle(fact) {
|
|
26953
|
+
const firstLine = fact.split(`
|
|
26954
|
+
`)[0];
|
|
26955
|
+
const match = /[.?!](\s|$)/.exec(firstLine);
|
|
26956
|
+
const sentence = match === null ? firstLine : firstLine.slice(0, match.index + 1);
|
|
26957
|
+
const missing = [...fact.matchAll(POINTER_RE)].map((m) => m[0]).filter((p) => !sentence.includes(p));
|
|
26958
|
+
const unique = [...new Set(missing)];
|
|
26959
|
+
const named = unique.length === 0 ? sentence : `${sentence} ${unique.join(" ")}`;
|
|
26960
|
+
const truncated = named !== fact;
|
|
26961
|
+
return { text: truncated ? `${named}${TITLE_TRUNCATION_MARK}` : named, truncated };
|
|
26969
26962
|
}
|
|
26970
|
-
function
|
|
26971
|
-
return
|
|
26963
|
+
function rankFacts(facts) {
|
|
26964
|
+
return facts.map((line, index) => ({ line, index, date: bankedDate(line) })).sort((a, b) => {
|
|
26965
|
+
const ad = a.date ?? "";
|
|
26966
|
+
const bd = b.date ?? "";
|
|
26967
|
+
if (ad !== bd)
|
|
26968
|
+
return ad < bd ? 1 : -1;
|
|
26969
|
+
return b.index - a.index;
|
|
26970
|
+
}).map(({ index, line }) => ({ index, line }));
|
|
26972
26971
|
}
|
|
26973
|
-
function
|
|
26974
|
-
const
|
|
26975
|
-
|
|
26976
|
-
|
|
26972
|
+
function groupFacts(body) {
|
|
26973
|
+
const facts = [];
|
|
26974
|
+
for (const line of body.split(`
|
|
26975
|
+
`)) {
|
|
26976
|
+
if (line.trim().length === 0)
|
|
26977
|
+
continue;
|
|
26978
|
+
if (/^\s/.test(line) && facts.length > 0) {
|
|
26979
|
+
facts[facts.length - 1] = `${facts[facts.length - 1]}
|
|
26980
|
+
${line}`;
|
|
26981
|
+
} else {
|
|
26982
|
+
facts.push(line);
|
|
26983
|
+
}
|
|
26984
|
+
}
|
|
26985
|
+
return facts;
|
|
26986
|
+
}
|
|
26987
|
+
function selectIndexContent(body, capBytes) {
|
|
26988
|
+
const lines = groupFacts(body);
|
|
26989
|
+
if (Buffer.byteLength(body) <= capBytes) {
|
|
26990
|
+
return { text: body, titles: "", kept: lines.length, named: 0, total: lines.length };
|
|
26991
|
+
}
|
|
26992
|
+
const ranked = rankFacts(lines);
|
|
26993
|
+
const titleOf = new Map;
|
|
26994
|
+
const info2 = new Map;
|
|
26995
|
+
for (const { index, line } of ranked) {
|
|
26996
|
+
const t = factTitle(line);
|
|
26997
|
+
info2.set(index, t);
|
|
26998
|
+
titleOf.set(index, t.text);
|
|
26999
|
+
}
|
|
27000
|
+
const carried = new Map;
|
|
27001
|
+
const used = () => {
|
|
27002
|
+
let bytes = 0;
|
|
27003
|
+
let bodies = 0;
|
|
27004
|
+
let titles = 0;
|
|
27005
|
+
for (const [index, kind] of carried) {
|
|
27006
|
+
const line = lines[index];
|
|
27007
|
+
if (kind === "body") {
|
|
27008
|
+
bytes += Buffer.byteLength(line);
|
|
27009
|
+
bodies++;
|
|
27010
|
+
} else {
|
|
27011
|
+
bytes += Buffer.byteLength(titleOf.get(index));
|
|
27012
|
+
titles++;
|
|
27013
|
+
}
|
|
27014
|
+
}
|
|
27015
|
+
return bytes + Math.max(0, bodies - 1) + Math.max(0, titles - 1);
|
|
27016
|
+
};
|
|
27017
|
+
for (const { index } of ranked) {
|
|
27018
|
+
if (info2.get(index)?.truncated !== true)
|
|
27019
|
+
continue;
|
|
27020
|
+
carried.set(index, "title");
|
|
27021
|
+
if (used() > capBytes)
|
|
27022
|
+
carried.delete(index);
|
|
27023
|
+
}
|
|
27024
|
+
for (const { index } of ranked) {
|
|
27025
|
+
const was = carried.get(index);
|
|
27026
|
+
carried.set(index, "body");
|
|
27027
|
+
if (used() > capBytes) {
|
|
27028
|
+
if (was === undefined)
|
|
27029
|
+
carried.delete(index);
|
|
27030
|
+
else
|
|
27031
|
+
carried.set(index, was);
|
|
27032
|
+
break;
|
|
27033
|
+
}
|
|
26977
27034
|
}
|
|
27035
|
+
const inFileOrder = (kind) => [...carried.entries()].filter(([, k]) => k === kind).map(([i]) => i).sort((a, b) => a - b);
|
|
27036
|
+
const bodyIdx = inFileOrder("body");
|
|
27037
|
+
const titleIdx = inFileOrder("title");
|
|
26978
27038
|
return {
|
|
26979
|
-
|
|
26980
|
-
|
|
27039
|
+
text: bodyIdx.map((i) => lines[i]).join(`
|
|
27040
|
+
`),
|
|
27041
|
+
titles: titleIdx.map((i) => titleOf.get(i)).join(`
|
|
27042
|
+
`),
|
|
27043
|
+
kept: bodyIdx.length,
|
|
27044
|
+
named: titleIdx.length,
|
|
27045
|
+
total: lines.length
|
|
26981
27046
|
};
|
|
26982
27047
|
}
|
|
26983
|
-
function
|
|
26984
|
-
if (
|
|
26985
|
-
return
|
|
26986
|
-
return a.every((x, i) => {
|
|
26987
|
-
const y = b[i];
|
|
26988
|
-
return x.path === y.path && x.reason === y.reason && x.detail === y.detail && x.size_bytes === y.size_bytes && x.max_bytes === y.max_bytes && x.reported_by === y.reported_by && x.reported_by_name === y.reported_by_name && x.first_seen_at === y.first_seen_at && x.last_seen_at === y.last_seen_at;
|
|
26989
|
-
});
|
|
26990
|
-
}
|
|
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 };
|
|
27048
|
+
function trimmedIndexNote(kept, total, filename) {
|
|
27049
|
+
if (kept === 0) {
|
|
27050
|
+
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.`;
|
|
26998
27051
|
}
|
|
26999
|
-
return {
|
|
27052
|
+
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.`;
|
|
27000
27053
|
}
|
|
27001
|
-
function
|
|
27002
|
-
|
|
27054
|
+
function titlesLeadIn(kept, named, unnamed, total, filename) {
|
|
27055
|
+
const parts = [`${kept} above in full`];
|
|
27056
|
+
parts.push(`${named} below as ${named === 1 ? "its opening" : "their openings"} only`);
|
|
27057
|
+
if (unnamed > 0)
|
|
27058
|
+
parts.push(`${unnamed} not shown at all`);
|
|
27059
|
+
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.`;
|
|
27003
27060
|
}
|
|
27004
|
-
function
|
|
27005
|
-
const
|
|
27006
|
-
|
|
27007
|
-
|
|
27061
|
+
function composeGroundingBlock(today, workspaceRules, workspaceMemory, userMemory, freshness, rosterIsEmpty, voiceProfile, voiceMalformed, workspaceSlug, workspaceMemoryNote, userMemoryNote, member, workspaceTitles = null, userTitles = null, filesPastBar = 0, memoryStores = BLOB_MEMORY_STORES) {
|
|
27062
|
+
const gateFlag = workspaceSlug === null ? `--workspace="<this workspace>"` : `--workspace ${workspaceSlug}`;
|
|
27063
|
+
const identity = renderMemberIdentity(member, workspaceSlug);
|
|
27064
|
+
const block = [];
|
|
27065
|
+
const cats = [];
|
|
27066
|
+
const add = (cat, ...lines) => {
|
|
27067
|
+
for (const line of lines) {
|
|
27068
|
+
block.push(line);
|
|
27069
|
+
cats.push(cat);
|
|
27070
|
+
}
|
|
27071
|
+
};
|
|
27072
|
+
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.`);
|
|
27073
|
+
if (identity !== null)
|
|
27074
|
+
add("identity", ``, identity);
|
|
27075
|
+
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.`);
|
|
27076
|
+
if (workspaceRules === null && voiceProfile === null) {
|
|
27077
|
+
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.`);
|
|
27008
27078
|
}
|
|
27009
|
-
|
|
27079
|
+
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.`);
|
|
27080
|
+
if (workspaceRules !== null) {
|
|
27081
|
+
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:`);
|
|
27082
|
+
add("rules", ``, workspaceRules);
|
|
27083
|
+
}
|
|
27084
|
+
if (voiceProfile !== null) {
|
|
27085
|
+
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.`);
|
|
27086
|
+
if (voiceMalformed) {
|
|
27087
|
+
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.`);
|
|
27088
|
+
}
|
|
27089
|
+
}
|
|
27090
|
+
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));
|
|
27091
|
+
if (workspaceMemory !== null) {
|
|
27092
|
+
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:`);
|
|
27093
|
+
add("workspaceFacts", ``, workspaceMemory);
|
|
27094
|
+
if (workspaceTitles !== null) {
|
|
27095
|
+
add("budgetNote", ``, workspaceTitles.leadIn);
|
|
27096
|
+
add("workspaceFacts", ``, workspaceTitles.text);
|
|
27097
|
+
}
|
|
27098
|
+
if (workspaceMemoryNote !== null)
|
|
27099
|
+
add("budgetNote", ``, workspaceMemoryNote);
|
|
27100
|
+
}
|
|
27101
|
+
if (userMemory !== null) {
|
|
27102
|
+
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:`);
|
|
27103
|
+
add("personalFacts", ``, userMemory);
|
|
27104
|
+
if (userTitles !== null) {
|
|
27105
|
+
add("budgetNote", ``, userTitles.leadIn);
|
|
27106
|
+
add("personalFacts", ``, userTitles.text);
|
|
27107
|
+
}
|
|
27108
|
+
if (userMemoryNote !== null)
|
|
27109
|
+
add("budgetNote", ``, userMemoryNote);
|
|
27110
|
+
}
|
|
27111
|
+
if (workspaceMemory !== null || userMemory !== null) {
|
|
27112
|
+
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.`);
|
|
27113
|
+
}
|
|
27114
|
+
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.`);
|
|
27115
|
+
if (filesPastBar > 0) {
|
|
27116
|
+
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.`);
|
|
27117
|
+
}
|
|
27118
|
+
if (shouldOfferOnboarding(rosterIsEmpty, member)) {
|
|
27119
|
+
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.`);
|
|
27120
|
+
}
|
|
27121
|
+
const groundingText = block.filter((_, i) => cats[i] === "grounding").join(`
|
|
27122
|
+
`);
|
|
27123
|
+
return {
|
|
27124
|
+
text: block.join(`
|
|
27125
|
+
`),
|
|
27126
|
+
split: segmentBytes(block, cats),
|
|
27127
|
+
groundingParagraphs: countParagraphs(groundingText),
|
|
27128
|
+
groundingRuleShaped: countRuleShapedLines(groundingText)
|
|
27129
|
+
};
|
|
27010
27130
|
}
|
|
27011
|
-
function
|
|
27012
|
-
|
|
27131
|
+
function generatedStoresClause(modes) {
|
|
27132
|
+
if (!anyGenerated(modes))
|
|
27133
|
+
return "";
|
|
27134
|
+
const which = modes.workspace && modes.user ? `\`${WORKSPACE_MEMORY_FILENAME}\` and \`${USER_MEMORY_FILENAME}\` are` : modes.workspace ? `\`${WORKSPACE_MEMORY_FILENAME}\` is` : `\`${USER_MEMORY_FILENAME}\` is`;
|
|
27135
|
+
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`;
|
|
27136
|
+
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.`;
|
|
27013
27137
|
}
|
|
27014
|
-
function
|
|
27015
|
-
const
|
|
27016
|
-
|
|
27017
|
-
|
|
27018
|
-
|
|
27019
|
-
|
|
27138
|
+
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) {
|
|
27139
|
+
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);
|
|
27140
|
+
const whole = compose(workspaceMemory, userMemory, null, null);
|
|
27141
|
+
const untrimmed = () => {
|
|
27142
|
+
const all = (body) => {
|
|
27143
|
+
const n = groupFacts(body).length;
|
|
27144
|
+
return { text: body, titles: "", kept: n, named: 0, total: n };
|
|
27145
|
+
};
|
|
27146
|
+
return {
|
|
27147
|
+
block: whole.text,
|
|
27148
|
+
split: whole.split,
|
|
27149
|
+
groundingParagraphs: whole.groundingParagraphs,
|
|
27150
|
+
groundingRuleShaped: whole.groundingRuleShaped,
|
|
27151
|
+
picks: {
|
|
27152
|
+
wmPick: workspaceMemory === null ? null : all(workspaceMemory),
|
|
27153
|
+
umPick: userMemory === null ? null : all(userMemory)
|
|
27154
|
+
}
|
|
27155
|
+
};
|
|
27156
|
+
};
|
|
27157
|
+
if (Buffer.byteLength(whole.text) <= GROUNDING_BUDGET_BYTES)
|
|
27158
|
+
return untrimmed();
|
|
27159
|
+
if (workspaceMemory === null && userMemory === null)
|
|
27160
|
+
return untrimmed();
|
|
27161
|
+
const factCount = (body) => body === null ? 0 : groupFacts(body).length;
|
|
27162
|
+
const wmFacts = factCount(workspaceMemory);
|
|
27163
|
+
const umFacts = factCount(userMemory);
|
|
27164
|
+
const titlesFor = (pick2, filename) => pick2 === null || pick2.named === 0 ? null : {
|
|
27165
|
+
leadIn: titlesLeadIn(pick2.kept, pick2.named, pick2.total - pick2.kept - pick2.named, pick2.total, filename),
|
|
27166
|
+
text: pick2.titles
|
|
27167
|
+
};
|
|
27168
|
+
const noteFor = (pick2, filename) => pick2 === null || pick2.kept === pick2.total ? null : trimmedIndexNote(pick2.kept, pick2.total, filename);
|
|
27169
|
+
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);
|
|
27170
|
+
const wmSize = workspaceMemory === null ? 0 : Buffer.byteLength(workspaceMemory);
|
|
27171
|
+
const umSize = userMemory === null ? 0 : Buffer.byteLength(userMemory);
|
|
27172
|
+
const allocate = (remaining) => {
|
|
27173
|
+
const half = Math.floor(remaining / 2);
|
|
27174
|
+
let umCap;
|
|
27175
|
+
let wmCap;
|
|
27176
|
+
if (umSize <= half) {
|
|
27177
|
+
umCap = umSize;
|
|
27178
|
+
wmCap = remaining - umSize;
|
|
27179
|
+
} else if (wmSize <= half) {
|
|
27180
|
+
wmCap = wmSize;
|
|
27181
|
+
umCap = remaining - wmSize;
|
|
27182
|
+
} else {
|
|
27183
|
+
umCap = half;
|
|
27184
|
+
wmCap = remaining - half;
|
|
27185
|
+
}
|
|
27186
|
+
const umPick = userMemory === null ? null : selectIndexContent(userMemory, umCap);
|
|
27187
|
+
const umUsed = umPick === null ? 0 : Buffer.byteLength(umPick.text) + Buffer.byteLength(umPick.titles);
|
|
27188
|
+
return {
|
|
27189
|
+
wmPick: workspaceMemory === null ? null : selectIndexContent(workspaceMemory, Math.max(wmCap, remaining - umUsed)),
|
|
27190
|
+
umPick
|
|
27191
|
+
};
|
|
27192
|
+
};
|
|
27193
|
+
const build = (picks, withNotes2) => {
|
|
27194
|
+
const keep = (pick2) => withNotes2 || pick2 !== null && pick2.named === 0;
|
|
27195
|
+
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));
|
|
27196
|
+
};
|
|
27197
|
+
const refine2 = (seed, withNotes2) => {
|
|
27198
|
+
const asReserve = (section) => section === null ? null : { leadIn: section.leadIn, text: "" };
|
|
27199
|
+
const keep = (pick2) => withNotes2 || pick2 !== null && pick2.named === 0;
|
|
27200
|
+
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);
|
|
27201
|
+
return allocate(Math.max(0, GROUNDING_BUDGET_BYTES - fixed));
|
|
27202
|
+
};
|
|
27203
|
+
const noNotes = allocate(Math.max(0, GROUNDING_BUDGET_BYTES - measureFixed(false, true)));
|
|
27204
|
+
const withNotes = allocate(Math.max(0, GROUNDING_BUDGET_BYTES - measureFixed(true, true)));
|
|
27205
|
+
const candidates = [
|
|
27206
|
+
{ picks: withNotes, composed: build(withNotes, true) },
|
|
27207
|
+
{ picks: noNotes, composed: build(noNotes, false) },
|
|
27208
|
+
{ picks: withNotes, composed: build(withNotes, false) }
|
|
27209
|
+
].concat([
|
|
27210
|
+
{ seed: noNotes, notes: false },
|
|
27211
|
+
{ seed: withNotes, notes: true }
|
|
27212
|
+
].map(({ seed, notes }) => {
|
|
27213
|
+
const picks = refine2(seed, notes);
|
|
27214
|
+
return { picks, composed: build(picks, notes) };
|
|
27215
|
+
})).map((c) => ({
|
|
27216
|
+
...c,
|
|
27217
|
+
bytes: Buffer.byteLength(c.composed.text),
|
|
27218
|
+
named: (c.picks.wmPick?.kept ?? 0) + (c.picks.wmPick?.named ?? 0) + (c.picks.umPick?.kept ?? 0) + (c.picks.umPick?.named ?? 0),
|
|
27219
|
+
whole: (c.picks.wmPick?.kept ?? 0) + (c.picks.umPick?.kept ?? 0)
|
|
27220
|
+
}));
|
|
27221
|
+
const fitting = candidates.filter((c) => c.bytes <= GROUNDING_BUDGET_BYTES);
|
|
27222
|
+
const better = (a, b) => b.named !== a.named ? b.named > a.named : b.whole > a.whole;
|
|
27223
|
+
const best = fitting.length > 0 ? fitting.reduce((a, b) => better(a, b) ? b : a) : candidates.reduce((a, b) => b.bytes < a.bytes ? b : a);
|
|
27224
|
+
return {
|
|
27225
|
+
block: best.composed.text,
|
|
27226
|
+
picks: best.picks,
|
|
27227
|
+
split: best.composed.split,
|
|
27228
|
+
groundingParagraphs: best.composed.groundingParagraphs,
|
|
27229
|
+
groundingRuleShaped: best.composed.groundingRuleShaped
|
|
27230
|
+
};
|
|
27020
27231
|
}
|
|
27021
|
-
|
|
27022
|
-
|
|
27232
|
+
var buildGroundingReportFor = buildGroundingReport;
|
|
27233
|
+
function formatLocalDate(d) {
|
|
27234
|
+
const year = d.getFullYear();
|
|
27235
|
+
const month = String(d.getMonth() + 1).padStart(2, "0");
|
|
27236
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
27237
|
+
return `${year}-${month}-${day}`;
|
|
27023
27238
|
}
|
|
27024
|
-
|
|
27025
|
-
|
|
27026
|
-
|
|
27027
|
-
|
|
27239
|
+
|
|
27240
|
+
// src/lib/hash.ts
|
|
27241
|
+
import { createHash } from "node:crypto";
|
|
27242
|
+
function stableHash(value) {
|
|
27243
|
+
return createHash("sha256").update(stableStringify(value)).digest("hex");
|
|
27244
|
+
}
|
|
27245
|
+
function stableStringify(value) {
|
|
27246
|
+
if (value === null || typeof value !== "object") {
|
|
27247
|
+
return JSON.stringify(value);
|
|
27028
27248
|
}
|
|
27029
|
-
|
|
27249
|
+
if (Array.isArray(value)) {
|
|
27250
|
+
return "[" + value.map((v) => stableStringify(v)).join(",") + "]";
|
|
27251
|
+
}
|
|
27252
|
+
const obj = value;
|
|
27253
|
+
const keys = Object.keys(obj).sort();
|
|
27254
|
+
return "{" + keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",") + "}";
|
|
27030
27255
|
}
|
|
27031
|
-
|
|
27032
|
-
|
|
27033
|
-
|
|
27034
|
-
|
|
27035
|
-
};
|
|
27036
|
-
function
|
|
27037
|
-
let
|
|
27256
|
+
|
|
27257
|
+
// src/lib/context/roster.ts
|
|
27258
|
+
var import_gray_matter5 = __toESM(require_gray_matter(), 1);
|
|
27259
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync8 } from "node:fs";
|
|
27260
|
+
import { join as join7 } from "node:path";
|
|
27261
|
+
function readRoster(workspaceSlug) {
|
|
27262
|
+
let artistsDir;
|
|
27038
27263
|
try {
|
|
27039
|
-
|
|
27264
|
+
artistsDir = join7(workspaceMirrorPath(workspaceSlug), "artists");
|
|
27040
27265
|
} catch {
|
|
27041
|
-
return
|
|
27266
|
+
return [];
|
|
27042
27267
|
}
|
|
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 };
|
|
27268
|
+
let slugs;
|
|
27269
|
+
try {
|
|
27270
|
+
slugs = readdirSync3(artistsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
|
|
27271
|
+
} catch {
|
|
27272
|
+
return [];
|
|
27058
27273
|
}
|
|
27059
|
-
const
|
|
27060
|
-
|
|
27061
|
-
|
|
27274
|
+
const roster = [];
|
|
27275
|
+
for (const slug of slugs) {
|
|
27276
|
+
let raw;
|
|
27277
|
+
try {
|
|
27278
|
+
raw = readFileSync8(join7(artistsDir, slug, "_artist.md"), "utf8");
|
|
27279
|
+
} catch {
|
|
27280
|
+
continue;
|
|
27281
|
+
}
|
|
27282
|
+
try {
|
|
27283
|
+
const fm = import_gray_matter5.default(raw).data;
|
|
27284
|
+
const name = typeof fm.name === "string" && fm.name.length > 0 ? fm.name : slug;
|
|
27285
|
+
const status = typeof fm.status === "string" ? fm.status : null;
|
|
27286
|
+
roster.push({ slug, name, status });
|
|
27287
|
+
} catch {
|
|
27288
|
+
roster.push({ slug, name: slug, status: null });
|
|
27289
|
+
}
|
|
27062
27290
|
}
|
|
27063
|
-
return
|
|
27291
|
+
return roster;
|
|
27064
27292
|
}
|
|
27065
|
-
function
|
|
27066
|
-
|
|
27067
|
-
|
|
27068
|
-
|
|
27293
|
+
function rosterIsEmpty(workspaceSlug) {
|
|
27294
|
+
let artistsDir;
|
|
27295
|
+
try {
|
|
27296
|
+
artistsDir = join7(workspaceMirrorPath(workspaceSlug), "artists");
|
|
27297
|
+
} catch {
|
|
27069
27298
|
return false;
|
|
27070
|
-
|
|
27071
|
-
|
|
27072
|
-
|
|
27073
|
-
|
|
27074
|
-
|
|
27075
|
-
|
|
27299
|
+
}
|
|
27300
|
+
let entries;
|
|
27301
|
+
try {
|
|
27302
|
+
entries = readdirSync3(artistsDir, { withFileTypes: true });
|
|
27303
|
+
} catch (err) {
|
|
27304
|
+
return err?.code === "ENOENT";
|
|
27305
|
+
}
|
|
27306
|
+
for (const entry of entries) {
|
|
27307
|
+
if (!entry.isDirectory())
|
|
27308
|
+
continue;
|
|
27309
|
+
try {
|
|
27310
|
+
readFileSync8(join7(artistsDir, entry.name, "_artist.md"), "utf8");
|
|
27311
|
+
return false;
|
|
27312
|
+
} catch (err) {
|
|
27313
|
+
if (err?.code === "ENOENT")
|
|
27314
|
+
continue;
|
|
27315
|
+
return false;
|
|
27316
|
+
}
|
|
27317
|
+
}
|
|
27318
|
+
return true;
|
|
27076
27319
|
}
|
|
27077
27320
|
|
|
27078
27321
|
// src/lib/context/guiding-files.ts
|
|
@@ -27081,6 +27324,7 @@ var CLAUDE_LOCAL_MD = "CLAUDE.local.md";
|
|
|
27081
27324
|
var ARTIST_CLAUDE_MD = "CLAUDE.md";
|
|
27082
27325
|
var SETTINGS_REL = ".claude/settings.json";
|
|
27083
27326
|
var ARTIST_SLUG_PLACEHOLDER = "<slug>";
|
|
27327
|
+
var MEMORY_FACT_SLUG_PLACEHOLDER = "<slug>";
|
|
27084
27328
|
var artistFile = (name) => `artists/${ARTIST_SLUG_PLACEHOLDER}/${name}`;
|
|
27085
27329
|
function withThousands(n) {
|
|
27086
27330
|
return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
@@ -27129,12 +27373,26 @@ var GUIDING_FILES = [
|
|
|
27129
27373
|
reach: "you only",
|
|
27130
27374
|
bar: `${withThousands(RULE_FILE_BAR_BYTES)} bytes`
|
|
27131
27375
|
},
|
|
27376
|
+
{
|
|
27377
|
+
path: `${MEMORY_FACTS_DIR}/${MEMORY_FACT_SLUG_PLACEHOLDER}.md`,
|
|
27378
|
+
writtenBy: "Plinth, from a memory fact; read-only, edits are set aside",
|
|
27379
|
+
readWhen: "read on demand; only its index line is injected",
|
|
27380
|
+
reach: "everyone",
|
|
27381
|
+
bar: NO_BAR
|
|
27382
|
+
},
|
|
27383
|
+
{
|
|
27384
|
+
path: `${MEMORY_FACTS_DIR}/${MEMORY_FACTS_PRIVATE_SEGMENT}/${MEMORY_FACT_SLUG_PLACEHOLDER}.md`,
|
|
27385
|
+
writtenBy: "Plinth, from one of your memory facts; read-only",
|
|
27386
|
+
readWhen: "read on demand; only its index line is injected",
|
|
27387
|
+
reach: "you only",
|
|
27388
|
+
bar: NO_BAR
|
|
27389
|
+
},
|
|
27132
27390
|
{
|
|
27133
27391
|
path: artistFile(ARTIST_ANCHOR_FILENAME),
|
|
27134
27392
|
writtenBy: "you and the agent; syncs both ways",
|
|
27135
27393
|
readWhen: "read on demand",
|
|
27136
27394
|
reach: "everyone",
|
|
27137
|
-
bar:
|
|
27395
|
+
bar: `${withThousands(CONTEXT_BLOCK_BAR_CHARS)} characters per block`
|
|
27138
27396
|
},
|
|
27139
27397
|
{
|
|
27140
27398
|
path: artistFile(ARTIST_MEMORY_ANCHOR),
|
|
@@ -27335,17 +27593,20 @@ function assessGeneratedFileBars(workspaceSlug, deps = {}) {
|
|
|
27335
27593
|
return out;
|
|
27336
27594
|
}
|
|
27337
27595
|
var OPENING_MAX_BYTES = 90;
|
|
27338
|
-
function
|
|
27339
|
-
const stripped =
|
|
27596
|
+
function terminalSafeText(text, maxBytes) {
|
|
27597
|
+
const stripped = text.replace(/[\x00-\x1F\x7F-\x9F]+/g, " ");
|
|
27340
27598
|
const folded = stripped.replace(/\s+/g, " ").trim();
|
|
27341
27599
|
let out = "";
|
|
27342
27600
|
for (const ch of folded) {
|
|
27343
|
-
if (Buffer.byteLength(out + ch) >
|
|
27601
|
+
if (Buffer.byteLength(out + ch) > maxBytes)
|
|
27344
27602
|
return `${out.trimEnd()}…`;
|
|
27345
27603
|
out += ch;
|
|
27346
27604
|
}
|
|
27347
27605
|
return out;
|
|
27348
27606
|
}
|
|
27607
|
+
function terminalSafeOpening(fact) {
|
|
27608
|
+
return terminalSafeText(fact, OPENING_MAX_BYTES);
|
|
27609
|
+
}
|
|
27349
27610
|
function assessIndexLines(workspaceSlug, deps = {}) {
|
|
27350
27611
|
const readContext = deps.readContext ?? readContextFile;
|
|
27351
27612
|
const specs = [
|
|
@@ -27577,7 +27838,7 @@ function runGroundingReport(deps = {}) {
|
|
|
27577
27838
|
return buildGroundingReportFor(formatLocalDate(now()), rules, memory, userMemory, {
|
|
27578
27839
|
workspaceMemoryUpdatedAt: resolveUpdatedAt(rawMemory, freshness.workspaceMemory),
|
|
27579
27840
|
userMemoryUpdatedAt: resolveUpdatedAt(rawUserMemory, freshness.userMemory)
|
|
27580
|
-
}, noArtists, voiceProfile, resolveVoiceMalformed(rawVoiceProfile, freshness.userVoice), workspaceSlug, member, filesPastBar);
|
|
27841
|
+
}, noArtists, voiceProfile, resolveVoiceMalformed(rawVoiceProfile, freshness.userVoice), workspaceSlug, member, filesPastBar, workspaceSlug ? readMemoryStoreModes(workspaceSlug) : BLOB_MEMORY_STORES);
|
|
27581
27842
|
}
|
|
27582
27843
|
var groundingCommand = defineCommand({
|
|
27583
27844
|
meta: {
|
|
@@ -28911,6 +29172,15 @@ function artistUuidBySlug(state) {
|
|
|
28911
29172
|
}
|
|
28912
29173
|
return map2;
|
|
28913
29174
|
}
|
|
29175
|
+
function isOpenTask(fm) {
|
|
29176
|
+
if (fm.done === true)
|
|
29177
|
+
return false;
|
|
29178
|
+
const status = strOrNull(fm.status);
|
|
29179
|
+
return status !== "done" && status !== "archived";
|
|
29180
|
+
}
|
|
29181
|
+
function isActiveProject(fm) {
|
|
29182
|
+
return strOrNull(fm.status) !== "done";
|
|
29183
|
+
}
|
|
28914
29184
|
function readArtistProfile(workspaceSlug, slug) {
|
|
28915
29185
|
const fm = readEntityFrontmatter(workspaceSlug, "artists", slug) ?? {};
|
|
28916
29186
|
return {
|
|
@@ -28940,7 +29210,7 @@ function activeProjectCountsBySlug(workspaceSlug, state) {
|
|
|
28940
29210
|
const artistSlug = slugByArtistId.get(artistId);
|
|
28941
29211
|
if (!artistSlug)
|
|
28942
29212
|
continue;
|
|
28943
|
-
if (
|
|
29213
|
+
if (!isActiveProject(fm))
|
|
28944
29214
|
continue;
|
|
28945
29215
|
counts.set(artistSlug, (counts.get(artistSlug) ?? 0) + 1);
|
|
28946
29216
|
}
|
|
@@ -28972,9 +29242,9 @@ function buildArtistEntityIndex(workspaceSlug, state) {
|
|
|
28972
29242
|
const artistSlug = slugByArtistId.get(artistId);
|
|
28973
29243
|
if (!artistSlug)
|
|
28974
29244
|
continue;
|
|
28975
|
-
|
|
28976
|
-
if (status === "done")
|
|
29245
|
+
if (!isActiveProject(fm))
|
|
28977
29246
|
continue;
|
|
29247
|
+
const status = strOrNull(fm.status);
|
|
28978
29248
|
bucketFor(artistSlug).projects.push({
|
|
28979
29249
|
id: entry2.id,
|
|
28980
29250
|
slug: entry2.slug,
|
|
@@ -28994,7 +29264,7 @@ function buildArtistEntityIndex(workspaceSlug, state) {
|
|
|
28994
29264
|
const artistSlug = slugByArtistId.get(artistId);
|
|
28995
29265
|
if (!artistSlug)
|
|
28996
29266
|
continue;
|
|
28997
|
-
if (fm
|
|
29267
|
+
if (!isOpenTask(fm))
|
|
28998
29268
|
continue;
|
|
28999
29269
|
bucketFor(artistSlug).tasks.push({
|
|
29000
29270
|
id: entry2.id,
|
|
@@ -29617,7 +29887,7 @@ function withUserBlock(lines) {
|
|
|
29617
29887
|
`) + `
|
|
29618
29888
|
`;
|
|
29619
29889
|
}
|
|
29620
|
-
function buildStartCommandMd(rosterIsEmpty2, offerTrim = false) {
|
|
29890
|
+
function buildStartCommandMd(rosterIsEmpty2, offerTrim = false, stores = BLOB_MEMORY_STORES) {
|
|
29621
29891
|
return withUserBlock([
|
|
29622
29892
|
"---",
|
|
29623
29893
|
"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 +29921,10 @@ function buildStartCommandMd(rosterIsEmpty2, offerTrim = false) {
|
|
|
29651
29921
|
"",
|
|
29652
29922
|
"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
29923
|
"",
|
|
29924
|
+
...anyGenerated(stores) ? [
|
|
29925
|
+
"**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.",
|
|
29926
|
+
""
|
|
29927
|
+
] : [],
|
|
29654
29928
|
"**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
29929
|
"",
|
|
29656
29930
|
"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 +29984,7 @@ function buildStartCommandMd(rosterIsEmpty2, offerTrim = false) {
|
|
|
29710
29984
|
`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
29985
|
]);
|
|
29712
29986
|
}
|
|
29713
|
-
function buildCloseCommandMd(workspaceSlug) {
|
|
29987
|
+
function buildCloseCommandMd(workspaceSlug, stores = BLOB_MEMORY_STORES) {
|
|
29714
29988
|
const ws = `--workspace ${workspaceSlug}`;
|
|
29715
29989
|
return withUserBlock([
|
|
29716
29990
|
"---",
|
|
@@ -29754,7 +30028,7 @@ function buildCloseCommandMd(workspaceSlug) {
|
|
|
29754
30028
|
"Each loose end goes to the surface that holds it, and nowhere else:",
|
|
29755
30029
|
"",
|
|
29756
30030
|
"- **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.",
|
|
30031
|
+
"- **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
30032
|
"- **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
30033
|
"",
|
|
29760
30034
|
"**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 +30086,10 @@ function buildCloseCommandMd(workspaceSlug) {
|
|
|
29812
30086
|
"",
|
|
29813
30087
|
"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
30088
|
"",
|
|
30089
|
+
...anyGenerated(stores) ? [
|
|
30090
|
+
"**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.",
|
|
30091
|
+
""
|
|
30092
|
+
] : [],
|
|
29815
30093
|
"**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
30094
|
"",
|
|
29817
30095
|
"**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 +30112,17 @@ function buildCloseCommandMd(workspaceSlug) {
|
|
|
29834
30112
|
"",
|
|
29835
30113
|
"**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
30114
|
"",
|
|
29837
|
-
|
|
29838
|
-
|
|
29839
|
-
|
|
29840
|
-
|
|
30115
|
+
...stores.user ? [
|
|
30116
|
+
"#### True of this member: `create_user_memory_fact`. The file is output.",
|
|
30117
|
+
"",
|
|
30118
|
+
"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.",
|
|
30119
|
+
""
|
|
30120
|
+
] : [
|
|
30121
|
+
"#### True of this member: `user-memory.md` at the mirror root. Write it directly.",
|
|
30122
|
+
"",
|
|
30123
|
+
"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.",
|
|
30124
|
+
""
|
|
30125
|
+
],
|
|
29841
30126
|
"**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
30127
|
"",
|
|
29843
30128
|
"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 +30372,10 @@ function ensureWorkspaceCommands(deps) {
|
|
|
30087
30372
|
} catch {
|
|
30088
30373
|
offerTrim = false;
|
|
30089
30374
|
}
|
|
30375
|
+
const stores = (deps.readMemoryStores ?? readMemoryStoreModes)(workspaceSlug);
|
|
30090
30376
|
const bodies = {
|
|
30091
|
-
[START_COMMAND_REL]: buildStartCommandMd(noArtists, offerTrim),
|
|
30092
|
-
[CLOSE_COMMAND_REL]: buildCloseCommandMd(workspaceSlug),
|
|
30377
|
+
[START_COMMAND_REL]: buildStartCommandMd(noArtists, offerTrim, stores),
|
|
30378
|
+
[CLOSE_COMMAND_REL]: buildCloseCommandMd(workspaceSlug, stores),
|
|
30093
30379
|
[ONBOARD_COMMAND_REL]: buildOnboardCommandMd(workspaceSlug)
|
|
30094
30380
|
};
|
|
30095
30381
|
for (const rel of GENERATED_COMMAND_RELS) {
|
|
@@ -30761,6 +31047,31 @@ function recordEntitySymlinkRefused(args) {
|
|
|
30761
31047
|
rejected_at: args.now().toISOString()
|
|
30762
31048
|
}));
|
|
30763
31049
|
}
|
|
31050
|
+
function recordEntitySlugClash(args) {
|
|
31051
|
+
const path3 = args.mirrorRelativePath;
|
|
31052
|
+
const withoutStale = removeUnsynced(args.state, "entity", path3).state;
|
|
31053
|
+
const held = `Until then new ${args.entityType} changes are held on this machine.`;
|
|
31054
|
+
const message = args.reason === "source_unreadable" ? `${args.entityType}/${args.fromSlug} was renamed in Plinth to ` + `${args.toSlug}, but ${path3} could not be read, so it was left exactly ` + `where it is. Check the file's permissions; Plinth will move it once ` + `it can read it. ${held}` : args.occupantTracked ? `${args.entityType}/${args.fromSlug} was renamed in Plinth to ` + `${args.toSlug}, but ${args.entityType}/${args.toSlug}.md belongs to ` + `another ${args.entityType}, so nothing was moved or overwritten. ` + `This usually clears itself once that record's own rename arrives. ` + `Do not delete that file — it would delete that record in Plinth. ` + `${held}` : `${args.entityType}/${args.fromSlug} was renamed in Plinth to ` + `${args.toSlug}, but ${args.entityType}/${args.toSlug}.md already ` + `exists here, so nothing was moved or overwritten. Rename or remove ` + `that file and Plinth will move this one into place. ${held}`;
|
|
31055
|
+
const existing = (withoutStale.rejected ?? []).find((r) => r.target === "entity" && r.path === path3 && r.reason === "slug_clash");
|
|
31056
|
+
if (existing && existing.message === message)
|
|
31057
|
+
return args.state;
|
|
31058
|
+
return persist(withoutStale, upsertRejected(withoutStale, {
|
|
31059
|
+
target: "entity",
|
|
31060
|
+
op: "upsert",
|
|
31061
|
+
path: path3,
|
|
31062
|
+
status: 0,
|
|
31063
|
+
reason: "slug_clash",
|
|
31064
|
+
message,
|
|
31065
|
+
rejected_at: args.now().toISOString()
|
|
31066
|
+
}));
|
|
31067
|
+
}
|
|
31068
|
+
function clearSlugClash(state, mirrorRelativePaths) {
|
|
31069
|
+
const list = state.rejected ?? [];
|
|
31070
|
+
const next = list.filter((o) => !(o.target === "entity" && o.reason === "slug_clash" && mirrorRelativePaths.includes(o.path)));
|
|
31071
|
+
if (next.length === list.length)
|
|
31072
|
+
return state;
|
|
31073
|
+
return commitState(state, { ...state, rejected: next });
|
|
31074
|
+
}
|
|
30764
31075
|
function clearSymlinkRefused(state, target, path3) {
|
|
30765
31076
|
const list = state.rejected ?? [];
|
|
30766
31077
|
const next = list.filter((o) => !(o.target === target && o.path === path3 && o.reason === "symlink"));
|
|
@@ -30831,6 +31142,37 @@ function clearMemorySecretHeld(state, workspaceSlug, absolutePath) {
|
|
|
30831
31142
|
return state;
|
|
30832
31143
|
return commitState(state, { ...state, rejected: next });
|
|
30833
31144
|
}
|
|
31145
|
+
var MEMORY_GENERATED_REJECT_REASON = "memory_generated";
|
|
31146
|
+
function recordMemoryGeneratedRefusal(args) {
|
|
31147
|
+
let path3;
|
|
31148
|
+
try {
|
|
31149
|
+
path3 = relative3(workspaceMirrorPath(args.workspaceSlug), args.absolutePath);
|
|
31150
|
+
} catch {
|
|
31151
|
+
path3 = args.absolutePath;
|
|
31152
|
+
}
|
|
31153
|
+
const withoutStale = removeUnsynced(args.state, "entity", path3).state;
|
|
31154
|
+
return persist(withoutStale, upsertRejected(withoutStale, {
|
|
31155
|
+
target: "entity",
|
|
31156
|
+
op: "upsert",
|
|
31157
|
+
path: path3,
|
|
31158
|
+
reason: MEMORY_GENERATED_REJECT_REASON,
|
|
31159
|
+
message: args.record,
|
|
31160
|
+
rejected_at: args.now().toISOString()
|
|
31161
|
+
}));
|
|
31162
|
+
}
|
|
31163
|
+
function clearMemoryGeneratedRefusal(state, workspaceSlug, absolutePath) {
|
|
31164
|
+
let path3;
|
|
31165
|
+
try {
|
|
31166
|
+
path3 = relative3(workspaceMirrorPath(workspaceSlug), absolutePath);
|
|
31167
|
+
} catch {
|
|
31168
|
+
path3 = absolutePath;
|
|
31169
|
+
}
|
|
31170
|
+
const list = state.rejected ?? [];
|
|
31171
|
+
const next = list.filter((o) => !(o.target === "entity" && o.path === path3 && o.reason === MEMORY_GENERATED_REJECT_REASON));
|
|
31172
|
+
if (next.length === list.length)
|
|
31173
|
+
return state;
|
|
31174
|
+
return commitState(state, { ...state, rejected: next });
|
|
31175
|
+
}
|
|
30834
31176
|
|
|
30835
31177
|
// src/lib/context/workspace-claude-md.ts
|
|
30836
31178
|
var EXTRACTED_TEXT_DIR = extractedTextRelPath("x").replace(/x\.md$/, "");
|
|
@@ -32450,10 +32792,102 @@ function computeMirrorHashes(strategy, content) {
|
|
|
32450
32792
|
function hashableBody(strategy, body) {
|
|
32451
32793
|
return strategy.stripLocalBlocks?.(body) ?? body;
|
|
32452
32794
|
}
|
|
32453
|
-
function
|
|
32795
|
+
function relocateForSlugChange(args) {
|
|
32796
|
+
const { workspaceSlug, strategy, fromSlug, toSlug, acked, echo, now } = args;
|
|
32797
|
+
if (fromSlug === toSlug)
|
|
32798
|
+
return { kind: "not_needed", writeSlug: toSlug };
|
|
32799
|
+
if (!echo) {
|
|
32800
|
+
return { kind: "skipped", writeSlug: toSlug, fromSlug, reason: "unguarded" };
|
|
32801
|
+
}
|
|
32802
|
+
if (strategy.mirrorRelativePath(fromSlug).split("/").length !== 2) {
|
|
32803
|
+
return { kind: "skipped", writeSlug: toSlug, fromSlug, reason: "foldered" };
|
|
32804
|
+
}
|
|
32805
|
+
let fromPath;
|
|
32806
|
+
try {
|
|
32807
|
+
fromPath = entityFilePath(workspaceSlug, strategy.type, fromSlug);
|
|
32808
|
+
entityFilePath(workspaceSlug, strategy.type, toSlug);
|
|
32809
|
+
} catch (err) {
|
|
32810
|
+
if (err instanceof MirrorPathError) {
|
|
32811
|
+
return {
|
|
32812
|
+
kind: "skipped",
|
|
32813
|
+
writeSlug: toSlug,
|
|
32814
|
+
fromSlug,
|
|
32815
|
+
reason: "unresolvable"
|
|
32816
|
+
};
|
|
32817
|
+
}
|
|
32818
|
+
throw err;
|
|
32819
|
+
}
|
|
32820
|
+
let raw = null;
|
|
32821
|
+
try {
|
|
32822
|
+
raw = readFileSync15(fromPath, "utf8");
|
|
32823
|
+
} catch (err) {
|
|
32824
|
+
const code = err?.code;
|
|
32825
|
+
if (code === "ENOENT") {
|
|
32826
|
+
return { kind: "absent", writeSlug: toSlug, fromSlug };
|
|
32827
|
+
}
|
|
32828
|
+
return {
|
|
32829
|
+
kind: "refused",
|
|
32830
|
+
writeSlug: fromSlug,
|
|
32831
|
+
fromSlug,
|
|
32832
|
+
reason: "source_unreadable"
|
|
32833
|
+
};
|
|
32834
|
+
}
|
|
32835
|
+
let diverged = true;
|
|
32836
|
+
try {
|
|
32837
|
+
const hashes = computeMirrorHashes(strategy, raw);
|
|
32838
|
+
diverged = hashes.bodyHash !== acked.bodyHash || hashes.frontmatterHash !== acked.frontmatterHash;
|
|
32839
|
+
} catch {
|
|
32840
|
+
diverged = true;
|
|
32841
|
+
}
|
|
32842
|
+
const stashPath = diverged ? stashConflict({
|
|
32843
|
+
workspaceSlug,
|
|
32844
|
+
entityType: strategy.type,
|
|
32845
|
+
entitySlug: fromSlug,
|
|
32846
|
+
timestamp: now(),
|
|
32847
|
+
contents: raw,
|
|
32848
|
+
side: "local"
|
|
32849
|
+
}) : undefined;
|
|
32850
|
+
try {
|
|
32851
|
+
let registered = false;
|
|
32852
|
+
const result = moveEntityFile(workspaceSlug, strategy.type, fromSlug, toSlug, () => {
|
|
32853
|
+
if (echo.watcher === "live") {
|
|
32854
|
+
echo.suppressor.registerDelete(fromPath);
|
|
32855
|
+
registered = true;
|
|
32856
|
+
}
|
|
32857
|
+
});
|
|
32858
|
+
if (result === "moved") {
|
|
32859
|
+
return { kind: "moved", writeSlug: toSlug, fromSlug, stashPath };
|
|
32860
|
+
}
|
|
32861
|
+
if (registered && echo.watcher === "live") {
|
|
32862
|
+
echo.suppressor.cancelDelete(fromPath);
|
|
32863
|
+
}
|
|
32864
|
+
return { kind: "absent", writeSlug: toSlug, fromSlug };
|
|
32865
|
+
} catch (err) {
|
|
32866
|
+
if (err instanceof MirrorDestinationOccupiedError) {
|
|
32867
|
+
return {
|
|
32868
|
+
kind: "refused",
|
|
32869
|
+
writeSlug: fromSlug,
|
|
32870
|
+
fromSlug,
|
|
32871
|
+
reason: "destination_occupied",
|
|
32872
|
+
stashPath
|
|
32873
|
+
};
|
|
32874
|
+
}
|
|
32875
|
+
if (err instanceof MirrorPathError) {
|
|
32876
|
+
return {
|
|
32877
|
+
kind: "skipped",
|
|
32878
|
+
writeSlug: toSlug,
|
|
32879
|
+
fromSlug,
|
|
32880
|
+
reason: "unresolvable"
|
|
32881
|
+
};
|
|
32882
|
+
}
|
|
32883
|
+
throw err;
|
|
32884
|
+
}
|
|
32885
|
+
}
|
|
32886
|
+
function writeMirrorWithBodyPreserve(workspaceSlug, strategy, doc2, suppressor, pathSlug) {
|
|
32887
|
+
const targetSlug = pathSlug ?? doc2.slug;
|
|
32454
32888
|
let content = strategy.serialise(doc2);
|
|
32455
32889
|
if (strategy.preserveLocalBodyOnPull) {
|
|
32456
|
-
const target = entityFilePath(workspaceSlug, strategy.type,
|
|
32890
|
+
const target = entityFilePath(workspaceSlug, strategy.type, targetSlug);
|
|
32457
32891
|
let existing = null;
|
|
32458
32892
|
try {
|
|
32459
32893
|
existing = readFileSync15(target, "utf8");
|
|
@@ -32471,10 +32905,10 @@ function writeMirrorWithBodyPreserve(workspaceSlug, strategy, doc2, suppressor)
|
|
|
32471
32905
|
}
|
|
32472
32906
|
}
|
|
32473
32907
|
if (suppressor) {
|
|
32474
|
-
const target = entityFilePath(workspaceSlug, strategy.type,
|
|
32908
|
+
const target = entityFilePath(workspaceSlug, strategy.type, targetSlug);
|
|
32475
32909
|
suppressor.registerWrite(target, computeMirrorHashes(strategy, content));
|
|
32476
32910
|
}
|
|
32477
|
-
writeMirrorFile(workspaceSlug, strategy.type,
|
|
32911
|
+
writeMirrorFile(workspaceSlug, strategy.type, targetSlug, content);
|
|
32478
32912
|
return content;
|
|
32479
32913
|
}
|
|
32480
32914
|
|
|
@@ -32582,7 +33016,8 @@ function cloudMatchesAcked(strategy, cloud, acked) {
|
|
|
32582
33016
|
}
|
|
32583
33017
|
function stampStateFromMirror(args) {
|
|
32584
33018
|
const { strategy, workspaceSlug, doc: doc2, state, priorMemoryHash } = args;
|
|
32585
|
-
const
|
|
33019
|
+
const targetSlug = args.pathSlug ?? doc2.slug;
|
|
33020
|
+
const mirrorPath = entityFilePath(workspaceSlug, strategy.type, targetSlug);
|
|
32586
33021
|
const mirrorRaw = readFileSync16(mirrorPath, "utf8");
|
|
32587
33022
|
const mirrorParsed = import_gray_matter10.default(mirrorRaw);
|
|
32588
33023
|
const mirrorFm = mirrorParsed.data ?? {};
|
|
@@ -32591,7 +33026,7 @@ function stampStateFromMirror(args) {
|
|
|
32591
33026
|
const mirrorHashBody = hashableBody(strategy, mirrorParsed.content);
|
|
32592
33027
|
const refreshed = {
|
|
32593
33028
|
id: doc2.id,
|
|
32594
|
-
slug:
|
|
33029
|
+
slug: targetSlug,
|
|
32595
33030
|
updated_at: doc2.updated_at,
|
|
32596
33031
|
revision: doc2.revision,
|
|
32597
33032
|
body_hash: stableHash(mirrorHashBody),
|
|
@@ -32602,6 +33037,73 @@ function stampStateFromMirror(args) {
|
|
|
32602
33037
|
};
|
|
32603
33038
|
return persistEntry(state, strategy.type, refreshed);
|
|
32604
33039
|
}
|
|
33040
|
+
function adoptCloudCopy(args) {
|
|
33041
|
+
const { strategy, workspaceSlug, doc: doc2, output } = args;
|
|
33042
|
+
const now = args.now ?? (() => new Date);
|
|
33043
|
+
let state = args.state;
|
|
33044
|
+
const prior = getEntry(state, strategy.type, doc2.id);
|
|
33045
|
+
let writeSlug = doc2.slug;
|
|
33046
|
+
let refused = false;
|
|
33047
|
+
if (prior && prior.slug !== doc2.slug) {
|
|
33048
|
+
const relocation = relocateForSlugChange({
|
|
33049
|
+
workspaceSlug,
|
|
33050
|
+
strategy,
|
|
33051
|
+
fromSlug: prior.slug,
|
|
33052
|
+
toSlug: doc2.slug,
|
|
33053
|
+
acked: {
|
|
33054
|
+
bodyHash: prior.body_hash,
|
|
33055
|
+
frontmatterHash: prior.frontmatter_hash
|
|
33056
|
+
},
|
|
33057
|
+
echo: args.echo,
|
|
33058
|
+
now
|
|
33059
|
+
});
|
|
33060
|
+
writeSlug = relocation.writeSlug;
|
|
33061
|
+
const move = `entityType=${strategy.type} from=${prior.slug} to=${doc2.slug} id=${doc2.id}`;
|
|
33062
|
+
if (relocation.kind === "moved") {
|
|
33063
|
+
output?.log(`[SLUG_MOVE ${move} source=push]`);
|
|
33064
|
+
state = clearSlugClash(state, [
|
|
33065
|
+
strategy.mirrorRelativePath(prior.slug),
|
|
33066
|
+
strategy.mirrorRelativePath(doc2.slug)
|
|
33067
|
+
]);
|
|
33068
|
+
} else if (relocation.kind === "refused") {
|
|
33069
|
+
output?.log(`[SLUG_MOVE_REFUSED ${move} reason=${relocation.reason} source=push]`);
|
|
33070
|
+
refused = true;
|
|
33071
|
+
state = recordEntitySlugClash({
|
|
33072
|
+
state,
|
|
33073
|
+
mirrorRelativePath: strategy.mirrorRelativePath(prior.slug),
|
|
33074
|
+
entityType: strategy.type,
|
|
33075
|
+
fromSlug: prior.slug,
|
|
33076
|
+
toSlug: doc2.slug,
|
|
33077
|
+
reason: relocation.reason,
|
|
33078
|
+
occupantTracked: getEntryBySlug(state, strategy.type, doc2.slug) !== undefined,
|
|
33079
|
+
now
|
|
33080
|
+
});
|
|
33081
|
+
} else if (relocation.kind === "skipped") {
|
|
33082
|
+
output?.log(`[SLUG_MOVE_SKIP ${move} reason=${relocation.reason} source=push]`);
|
|
33083
|
+
}
|
|
33084
|
+
if (relocation.kind === "moved" || relocation.kind === "refused") {
|
|
33085
|
+
if (relocation.stashPath) {
|
|
33086
|
+
output?.warn(`Unsynced local edit on ${strategy.type}/${prior.slug} → stashed to ` + `${relocation.stashPath} (the row was renamed in Plinth; the cloud ` + `copy is authoritative).`);
|
|
33087
|
+
}
|
|
33088
|
+
}
|
|
33089
|
+
}
|
|
33090
|
+
if (refused) {
|
|
33091
|
+
return prior ? persistEntry(state, strategy.type, {
|
|
33092
|
+
...prior,
|
|
33093
|
+
updated_at: doc2.updated_at,
|
|
33094
|
+
revision: doc2.revision
|
|
33095
|
+
}) : state;
|
|
33096
|
+
}
|
|
33097
|
+
writeMirrorWithBodyPreserve(workspaceSlug, strategy, doc2, undefined, writeSlug);
|
|
33098
|
+
return stampStateFromMirror({
|
|
33099
|
+
strategy,
|
|
33100
|
+
workspaceSlug,
|
|
33101
|
+
doc: doc2,
|
|
33102
|
+
state,
|
|
33103
|
+
priorMemoryHash: args.priorMemoryHash,
|
|
33104
|
+
pathSlug: writeSlug
|
|
33105
|
+
});
|
|
33106
|
+
}
|
|
32605
33107
|
async function pullAndWriteMirror(args) {
|
|
32606
33108
|
const cloud = await getEntity({
|
|
32607
33109
|
strategy: args.strategy,
|
|
@@ -32610,13 +33112,15 @@ async function pullAndWriteMirror(args) {
|
|
|
32610
33112
|
id: args.id,
|
|
32611
33113
|
fetchImpl: args.fetchImpl
|
|
32612
33114
|
});
|
|
32613
|
-
|
|
32614
|
-
return stampStateFromMirror({
|
|
33115
|
+
return adoptCloudCopy({
|
|
32615
33116
|
strategy: args.strategy,
|
|
32616
33117
|
workspaceSlug: args.workspaceSlug,
|
|
32617
33118
|
doc: cloud,
|
|
32618
33119
|
state: args.state,
|
|
32619
|
-
priorMemoryHash: getEntry(args.state, args.strategy.type, args.id)?.memory_hash
|
|
33120
|
+
priorMemoryHash: getEntry(args.state, args.strategy.type, args.id)?.memory_hash,
|
|
33121
|
+
echo: args.echo,
|
|
33122
|
+
output: args.output,
|
|
33123
|
+
now: args.now
|
|
32620
33124
|
});
|
|
32621
33125
|
}
|
|
32622
33126
|
async function pushFile(args) {
|
|
@@ -32637,6 +33141,7 @@ async function pushFile(args) {
|
|
|
32637
33141
|
const mutex = deps?.mutex ?? new PerIdMutex;
|
|
32638
33142
|
const output = deps?.output;
|
|
32639
33143
|
const suppressor = deps?.suppressor;
|
|
33144
|
+
const echo = deps?.echo;
|
|
32640
33145
|
const strategy = getStrategy(entityType);
|
|
32641
33146
|
if (!strategy) {
|
|
32642
33147
|
return {
|
|
@@ -32650,6 +33155,8 @@ async function pushFile(args) {
|
|
|
32650
33155
|
}
|
|
32651
33156
|
if (!exists) {
|
|
32652
33157
|
return runPushDelete({
|
|
33158
|
+
echo,
|
|
33159
|
+
now,
|
|
32653
33160
|
strategy,
|
|
32654
33161
|
workspaceSlug,
|
|
32655
33162
|
baseUrl,
|
|
@@ -32668,6 +33175,8 @@ async function pushFile(args) {
|
|
|
32668
33175
|
} catch (err) {
|
|
32669
33176
|
if (err?.code === "ENOENT") {
|
|
32670
33177
|
return runPushDelete({
|
|
33178
|
+
echo,
|
|
33179
|
+
now,
|
|
32671
33180
|
strategy,
|
|
32672
33181
|
workspaceSlug,
|
|
32673
33182
|
baseUrl,
|
|
@@ -32727,6 +33236,7 @@ async function pushFile(args) {
|
|
|
32727
33236
|
const stateEntry = identity === "path" ? byInode : byInode ?? getEntryBySlug(state, entityType, slug);
|
|
32728
33237
|
if (!stateEntry) {
|
|
32729
33238
|
return runPushCreate({
|
|
33239
|
+
echo,
|
|
32730
33240
|
strategy,
|
|
32731
33241
|
workspaceSlug,
|
|
32732
33242
|
baseUrl,
|
|
@@ -32747,6 +33257,7 @@ async function pushFile(args) {
|
|
|
32747
33257
|
});
|
|
32748
33258
|
}
|
|
32749
33259
|
return runPushUpsert({
|
|
33260
|
+
echo,
|
|
32750
33261
|
strategy,
|
|
32751
33262
|
workspaceSlug,
|
|
32752
33263
|
baseUrl,
|
|
@@ -32805,7 +33316,10 @@ async function reconcileContextDocConflict(args) {
|
|
|
32805
33316
|
pat,
|
|
32806
33317
|
id: existingId,
|
|
32807
33318
|
state,
|
|
32808
|
-
fetchImpl
|
|
33319
|
+
fetchImpl,
|
|
33320
|
+
echo: args.echo,
|
|
33321
|
+
output,
|
|
33322
|
+
now
|
|
32809
33323
|
});
|
|
32810
33324
|
output?.log(contextDocConflictLine({
|
|
32811
33325
|
entityType: strategy.type,
|
|
@@ -32922,7 +33436,10 @@ async function reconcileContextDocConflict(args) {
|
|
|
32922
33436
|
pat,
|
|
32923
33437
|
id: existingId,
|
|
32924
33438
|
state,
|
|
32925
|
-
fetchImpl
|
|
33439
|
+
fetchImpl,
|
|
33440
|
+
echo: args.echo,
|
|
33441
|
+
output,
|
|
33442
|
+
now
|
|
32926
33443
|
});
|
|
32927
33444
|
output?.log(contextDocConflictLine({
|
|
32928
33445
|
entityType: strategy.type,
|
|
@@ -33144,6 +33661,7 @@ async function runPushUpsert(args) {
|
|
|
33144
33661
|
}
|
|
33145
33662
|
if (patchResult.reason === "context_doc_conflict") {
|
|
33146
33663
|
return reconcileContextDocConflict({
|
|
33664
|
+
echo: args.echo,
|
|
33147
33665
|
strategy,
|
|
33148
33666
|
workspaceSlug: args.workspaceSlug,
|
|
33149
33667
|
baseUrl: args.baseUrl,
|
|
@@ -33162,6 +33680,7 @@ async function runPushUpsert(args) {
|
|
|
33162
33680
|
}
|
|
33163
33681
|
if (strategy.fieldTypedConflict) {
|
|
33164
33682
|
return reconcileTaskConflict({
|
|
33683
|
+
echo: args.echo,
|
|
33165
33684
|
strategy,
|
|
33166
33685
|
workspaceSlug: args.workspaceSlug,
|
|
33167
33686
|
baseUrl: args.baseUrl,
|
|
@@ -33192,7 +33711,10 @@ async function runPushUpsert(args) {
|
|
|
33192
33711
|
pat: args.pat,
|
|
33193
33712
|
id: liveEntry.id,
|
|
33194
33713
|
state: workingState,
|
|
33195
|
-
fetchImpl: args.fetchImpl
|
|
33714
|
+
fetchImpl: args.fetchImpl,
|
|
33715
|
+
echo: args.echo,
|
|
33716
|
+
output: args.output,
|
|
33717
|
+
now: args.now
|
|
33196
33718
|
});
|
|
33197
33719
|
return {
|
|
33198
33720
|
outcome: {
|
|
@@ -33240,7 +33762,10 @@ async function reconcileTaskConflict(args) {
|
|
|
33240
33762
|
pat,
|
|
33241
33763
|
id: liveEntry.id,
|
|
33242
33764
|
state,
|
|
33243
|
-
fetchImpl
|
|
33765
|
+
fetchImpl,
|
|
33766
|
+
echo: args.echo,
|
|
33767
|
+
output,
|
|
33768
|
+
now
|
|
33244
33769
|
});
|
|
33245
33770
|
return {
|
|
33246
33771
|
outcome: {
|
|
@@ -33289,13 +33814,15 @@ async function reconcileTaskConflict(args) {
|
|
|
33289
33814
|
});
|
|
33290
33815
|
if (patchResult.ok) {
|
|
33291
33816
|
const stashPath2 = stashCloudSide();
|
|
33292
|
-
|
|
33293
|
-
state = stampStateFromMirror({
|
|
33817
|
+
state = adoptCloudCopy({
|
|
33294
33818
|
strategy,
|
|
33295
33819
|
workspaceSlug,
|
|
33296
33820
|
doc: patchResult.doc,
|
|
33297
33821
|
state,
|
|
33298
|
-
priorMemoryHash: liveEntry.memory_hash
|
|
33822
|
+
priorMemoryHash: liveEntry.memory_hash,
|
|
33823
|
+
echo: args.echo,
|
|
33824
|
+
output,
|
|
33825
|
+
now
|
|
33299
33826
|
});
|
|
33300
33827
|
output?.log(`[PATCH entityType=${strategy.type} slug=${patchResult.doc.slug} id=${patchResult.doc.id} revision=${patchResult.doc.revision} note=field_typed_conflict]`);
|
|
33301
33828
|
return {
|
|
@@ -33327,7 +33854,10 @@ async function reconcileTaskConflict(args) {
|
|
|
33327
33854
|
pat,
|
|
33328
33855
|
id: liveEntry.id,
|
|
33329
33856
|
state,
|
|
33330
|
-
fetchImpl
|
|
33857
|
+
fetchImpl,
|
|
33858
|
+
echo: args.echo,
|
|
33859
|
+
output,
|
|
33860
|
+
now
|
|
33331
33861
|
});
|
|
33332
33862
|
return {
|
|
33333
33863
|
outcome: {
|
|
@@ -33364,6 +33894,7 @@ async function runPushCreate(args) {
|
|
|
33364
33894
|
const inLock = args.identity === "path" ? undefined : getEntryBySlug(args.state, strategy.type, args.slug);
|
|
33365
33895
|
if (inLock) {
|
|
33366
33896
|
return runPushUpsert({
|
|
33897
|
+
echo: args.echo,
|
|
33367
33898
|
strategy,
|
|
33368
33899
|
workspaceSlug: args.workspaceSlug,
|
|
33369
33900
|
baseUrl: args.baseUrl,
|
|
@@ -33460,6 +33991,7 @@ async function runPushCreate(args) {
|
|
|
33460
33991
|
}
|
|
33461
33992
|
if (createResult.reason === "context_doc_conflict") {
|
|
33462
33993
|
return reconcileContextDocConflict({
|
|
33994
|
+
echo: args.echo,
|
|
33463
33995
|
strategy,
|
|
33464
33996
|
workspaceSlug: args.workspaceSlug,
|
|
33465
33997
|
baseUrl: args.baseUrl,
|
|
@@ -33491,7 +34023,10 @@ async function runPushCreate(args) {
|
|
|
33491
34023
|
pat: args.pat,
|
|
33492
34024
|
id: createResult.existing.id,
|
|
33493
34025
|
state: args.state,
|
|
33494
|
-
fetchImpl: args.fetchImpl
|
|
34026
|
+
fetchImpl: args.fetchImpl,
|
|
34027
|
+
echo: args.echo,
|
|
34028
|
+
output: args.output,
|
|
34029
|
+
now: args.now
|
|
33495
34030
|
});
|
|
33496
34031
|
return {
|
|
33497
34032
|
outcome: {
|
|
@@ -33579,7 +34114,10 @@ async function runPushDelete(args) {
|
|
|
33579
34114
|
pat: args.pat,
|
|
33580
34115
|
id,
|
|
33581
34116
|
state: args.state,
|
|
33582
|
-
fetchImpl: args.fetchImpl
|
|
34117
|
+
fetchImpl: args.fetchImpl,
|
|
34118
|
+
echo: args.echo,
|
|
34119
|
+
output: args.output,
|
|
34120
|
+
now: args.now
|
|
33583
34121
|
});
|
|
33584
34122
|
return {
|
|
33585
34123
|
outcome: {
|
|
@@ -33610,6 +34148,7 @@ async function pushRename(args) {
|
|
|
33610
34148
|
const now = deps?.now ?? (() => new Date);
|
|
33611
34149
|
const mutex = deps?.mutex ?? new PerIdMutex;
|
|
33612
34150
|
const output = deps?.output;
|
|
34151
|
+
const echo = deps?.echo;
|
|
33613
34152
|
const strategy = requireStrategy(entityType);
|
|
33614
34153
|
let toSlug;
|
|
33615
34154
|
try {
|
|
@@ -33718,7 +34257,10 @@ async function pushRename(args) {
|
|
|
33718
34257
|
pat,
|
|
33719
34258
|
id: patchResult.existing.id,
|
|
33720
34259
|
state,
|
|
33721
|
-
fetchImpl
|
|
34260
|
+
fetchImpl,
|
|
34261
|
+
echo,
|
|
34262
|
+
output,
|
|
34263
|
+
now
|
|
33722
34264
|
});
|
|
33723
34265
|
const finalState = await pullAndWriteMirror({
|
|
33724
34266
|
strategy,
|
|
@@ -33727,7 +34269,10 @@ async function pushRename(args) {
|
|
|
33727
34269
|
pat,
|
|
33728
34270
|
id,
|
|
33729
34271
|
state: afterColliding,
|
|
33730
|
-
fetchImpl
|
|
34272
|
+
fetchImpl,
|
|
34273
|
+
echo,
|
|
34274
|
+
output,
|
|
34275
|
+
now
|
|
33731
34276
|
});
|
|
33732
34277
|
return {
|
|
33733
34278
|
outcome: {
|
|
@@ -33759,7 +34304,10 @@ async function pushRename(args) {
|
|
|
33759
34304
|
pat,
|
|
33760
34305
|
id,
|
|
33761
34306
|
state,
|
|
33762
|
-
fetchImpl
|
|
34307
|
+
fetchImpl,
|
|
34308
|
+
echo,
|
|
34309
|
+
output,
|
|
34310
|
+
now
|
|
33763
34311
|
});
|
|
33764
34312
|
return {
|
|
33765
34313
|
outcome: {
|
|
@@ -33817,7 +34365,8 @@ async function applyBatch(batch, deps, mutex) {
|
|
|
33817
34365
|
fetchImpl: deps.fetchImpl,
|
|
33818
34366
|
now: deps.now,
|
|
33819
34367
|
mutex,
|
|
33820
|
-
output: deps.output
|
|
34368
|
+
output: deps.output,
|
|
34369
|
+
echo: deps.echo
|
|
33821
34370
|
}
|
|
33822
34371
|
});
|
|
33823
34372
|
deps.setState(result.state);
|
|
@@ -33886,7 +34435,10 @@ async function discardBatchOps(batch, deps) {
|
|
|
33886
34435
|
pat: deps.pat,
|
|
33887
34436
|
id: op.id,
|
|
33888
34437
|
state: deps.getState(),
|
|
33889
|
-
fetchImpl: deps.fetchImpl
|
|
34438
|
+
fetchImpl: deps.fetchImpl,
|
|
34439
|
+
echo: deps.echo,
|
|
34440
|
+
output: deps.output,
|
|
34441
|
+
now: deps.now
|
|
33890
34442
|
});
|
|
33891
34443
|
deps.setState(next);
|
|
33892
34444
|
} catch (err) {
|
|
@@ -33957,6 +34509,349 @@ function classifyFilesPath(args) {
|
|
|
33957
34509
|
return validateFilesRelPath(posix) ? { relPath: posix } : null;
|
|
33958
34510
|
}
|
|
33959
34511
|
|
|
34512
|
+
// src/lib/memory-facts/sync.ts
|
|
34513
|
+
import { isAbsolute as isAbsolute2, relative as relative6, sep as sep4 } from "node:path";
|
|
34514
|
+
|
|
34515
|
+
// src/lib/memory-facts/api.ts
|
|
34516
|
+
var FactCommonSchema = exports_external.object({
|
|
34517
|
+
id: exports_external.string(),
|
|
34518
|
+
slug: exports_external.string(),
|
|
34519
|
+
description: exports_external.string(),
|
|
34520
|
+
body_md: exports_external.string(),
|
|
34521
|
+
fact_type: exports_external.string(),
|
|
34522
|
+
status: exports_external.string(),
|
|
34523
|
+
superseded_by: exports_external.string().nullish(),
|
|
34524
|
+
generality: exports_external.string().nullish(),
|
|
34525
|
+
source: exports_external.string().nullish(),
|
|
34526
|
+
created_at: exports_external.string().nullish(),
|
|
34527
|
+
updated_at: exports_external.string().nullish()
|
|
34528
|
+
});
|
|
34529
|
+
var WorkspaceFactWireSchema = FactCommonSchema.extend({
|
|
34530
|
+
artist_id: exports_external.string().nullish(),
|
|
34531
|
+
revision: exports_external.number().nullish()
|
|
34532
|
+
});
|
|
34533
|
+
var UserFactWireSchema = FactCommonSchema;
|
|
34534
|
+
var MemoryFactsFeedSchema = exports_external.object({
|
|
34535
|
+
workspace_facts: exports_external.array(WorkspaceFactWireSchema),
|
|
34536
|
+
user_facts: exports_external.array(UserFactWireSchema)
|
|
34537
|
+
});
|
|
34538
|
+
var ROUTE = "/api/sync/memory-facts";
|
|
34539
|
+
function baseHeaders2(pat) {
|
|
34540
|
+
return { Authorization: `Bearer ${pat}`, Accept: "application/json" };
|
|
34541
|
+
}
|
|
34542
|
+
async function getMemoryFacts(args) {
|
|
34543
|
+
const { baseUrl, pat, fetchImpl = fetch } = args;
|
|
34544
|
+
const url2 = `${baseUrl.replace(/\/$/, "")}${ROUTE}`;
|
|
34545
|
+
let res;
|
|
34546
|
+
try {
|
|
34547
|
+
res = await fetchImpl(url2, { method: "GET", headers: baseHeaders2(pat) });
|
|
34548
|
+
} catch (err) {
|
|
34549
|
+
const msg = err instanceof Error ? err.message : "network error";
|
|
34550
|
+
throw new ApiError(0, `Could not reach ${url2}: ${msg}`);
|
|
34551
|
+
}
|
|
34552
|
+
if (res.status === 401) {
|
|
34553
|
+
throw new ApiError(401, "Token was rejected by the server. Run `plinth login --workspace <slug>` to refresh.");
|
|
34554
|
+
}
|
|
34555
|
+
if (res.status === 429) {
|
|
34556
|
+
throw new ApiError(429, "Rate limited by the server. Wait a few seconds and retry.", parseRetryAfterMs(res.headers.get("retry-after")));
|
|
34557
|
+
}
|
|
34558
|
+
if (!res.ok) {
|
|
34559
|
+
throw new ApiError(res.status, `sync memory-facts failed with HTTP ${res.status}`);
|
|
34560
|
+
}
|
|
34561
|
+
const body = await res.json();
|
|
34562
|
+
const parsed = MemoryFactsFeedSchema.safeParse(body);
|
|
34563
|
+
if (!parsed.success) {
|
|
34564
|
+
throw new ApiError(res.status, `sync memory-facts response did not match the expected shape: ${parsed.error.message}`);
|
|
34565
|
+
}
|
|
34566
|
+
return parsed.data;
|
|
34567
|
+
}
|
|
34568
|
+
|
|
34569
|
+
// src/lib/memory-facts/refusal.ts
|
|
34570
|
+
var MEMORY_IS_GENERATED_CODE = "memory_is_generated";
|
|
34571
|
+
var GeneratedRefusalSchema = exports_external.object({
|
|
34572
|
+
error: exports_external.string(),
|
|
34573
|
+
code: exports_external.literal(MEMORY_IS_GENERATED_CODE)
|
|
34574
|
+
});
|
|
34575
|
+
function factToolsFor(pile) {
|
|
34576
|
+
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`";
|
|
34577
|
+
}
|
|
34578
|
+
function generatedMemoryRefusalRecord(args) {
|
|
34579
|
+
const which = args.factId ? ` (fact \`${args.factId}\`)` : "";
|
|
34580
|
+
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.`;
|
|
34581
|
+
}
|
|
34582
|
+
var MEMORY_FACT_CONFLICT_ENTITY = "memory";
|
|
34583
|
+
function isGeneratedMemoryStash(label, deps) {
|
|
34584
|
+
if (label.startsWith(`${MEMORY_FACT_CONFLICT_ENTITY}/`)) {
|
|
34585
|
+
const tail = label.slice(MEMORY_FACT_CONFLICT_ENTITY.length + 1);
|
|
34586
|
+
if (tail.startsWith("private."))
|
|
34587
|
+
return "private";
|
|
34588
|
+
if (tail.startsWith("shared."))
|
|
34589
|
+
return "shared";
|
|
34590
|
+
return null;
|
|
34591
|
+
}
|
|
34592
|
+
if (label === "workspace-memory/workspace-memory") {
|
|
34593
|
+
return deps.workspaceMemoryGenerated() ? "shared" : null;
|
|
34594
|
+
}
|
|
34595
|
+
if (label === "user-memory/user-memory") {
|
|
34596
|
+
return deps.userMemoryGenerated() ? "private" : null;
|
|
34597
|
+
}
|
|
34598
|
+
const artist = label.match(/^artists\/(.+)\.memory$/);
|
|
34599
|
+
if (artist) {
|
|
34600
|
+
return deps.artistMemoryGenerated(artist[1]) ? "shared" : null;
|
|
34601
|
+
}
|
|
34602
|
+
return null;
|
|
34603
|
+
}
|
|
34604
|
+
|
|
34605
|
+
// src/lib/memory-facts/refuse.ts
|
|
34606
|
+
function refuseGeneratedMemoryEdit(args) {
|
|
34607
|
+
const stashPath = stashConflict({
|
|
34608
|
+
workspaceSlug: args.workspaceSlug,
|
|
34609
|
+
entityType: args.conflictEntity,
|
|
34610
|
+
entitySlug: args.conflictSlug,
|
|
34611
|
+
timestamp: args.now(),
|
|
34612
|
+
contents: args.localContent,
|
|
34613
|
+
side: "local"
|
|
34614
|
+
});
|
|
34615
|
+
args.writeCanonical(args.canonicalBody);
|
|
34616
|
+
const record2 = generatedMemoryRefusalRecord({
|
|
34617
|
+
relPath: args.relPath,
|
|
34618
|
+
pile: args.pile,
|
|
34619
|
+
stashPath,
|
|
34620
|
+
factId: args.factId
|
|
34621
|
+
});
|
|
34622
|
+
args.output?.warn(record2);
|
|
34623
|
+
args.output?.log(`[MEMORY_GENERATED_REFUSED ws=${args.workspaceSlug} path=${args.relPath} stashed]`);
|
|
34624
|
+
const state = recordMemoryGeneratedRefusal({
|
|
34625
|
+
state: args.state,
|
|
34626
|
+
workspaceSlug: args.workspaceSlug,
|
|
34627
|
+
absolutePath: args.absolutePath,
|
|
34628
|
+
record: record2,
|
|
34629
|
+
now: args.now
|
|
34630
|
+
});
|
|
34631
|
+
return { state, stashPath };
|
|
34632
|
+
}
|
|
34633
|
+
|
|
34634
|
+
// src/lib/memory-facts/render.ts
|
|
34635
|
+
function yamlString(value) {
|
|
34636
|
+
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")}`);
|
|
34637
|
+
return `"${escaped}"`;
|
|
34638
|
+
}
|
|
34639
|
+
var SOURCE_UNKNOWN = "unknown";
|
|
34640
|
+
function toRenderableWorkspaceFact(row, artistSlugById) {
|
|
34641
|
+
const artistId = row.artist_id ?? null;
|
|
34642
|
+
return {
|
|
34643
|
+
pile: "shared",
|
|
34644
|
+
id: row.id,
|
|
34645
|
+
slug: row.slug,
|
|
34646
|
+
description: row.description,
|
|
34647
|
+
fact_type: row.fact_type,
|
|
34648
|
+
status: row.status,
|
|
34649
|
+
source: row.source,
|
|
34650
|
+
superseded_by: row.superseded_by,
|
|
34651
|
+
generality: row.generality,
|
|
34652
|
+
body_md: row.body_md,
|
|
34653
|
+
artist_id: artistId,
|
|
34654
|
+
artist_slug: artistId ? artistSlugById(artistId) : undefined
|
|
34655
|
+
};
|
|
34656
|
+
}
|
|
34657
|
+
function toRenderableUserFact(row) {
|
|
34658
|
+
return {
|
|
34659
|
+
pile: "private",
|
|
34660
|
+
id: row.id,
|
|
34661
|
+
slug: row.slug,
|
|
34662
|
+
description: row.description,
|
|
34663
|
+
fact_type: row.fact_type,
|
|
34664
|
+
status: row.status,
|
|
34665
|
+
source: row.source,
|
|
34666
|
+
superseded_by: row.superseded_by,
|
|
34667
|
+
generality: row.generality,
|
|
34668
|
+
body_md: row.body_md
|
|
34669
|
+
};
|
|
34670
|
+
}
|
|
34671
|
+
function renderFactFile(fact) {
|
|
34672
|
+
const lines = ["---"];
|
|
34673
|
+
lines.push(`id: ${yamlString(fact.id)}`);
|
|
34674
|
+
lines.push(`name: ${yamlString(fact.slug)}`);
|
|
34675
|
+
lines.push(`description: ${yamlString(fact.description)}`);
|
|
34676
|
+
lines.push(`type: ${yamlString(fact.fact_type)}`);
|
|
34677
|
+
lines.push(`status: ${yamlString(fact.status)}`);
|
|
34678
|
+
lines.push(`source: ${yamlString(fact.source ?? SOURCE_UNKNOWN)}`);
|
|
34679
|
+
if (fact.generality)
|
|
34680
|
+
lines.push(`generality: ${yamlString(fact.generality)}`);
|
|
34681
|
+
if (fact.artist_id)
|
|
34682
|
+
lines.push(`artist_id: ${yamlString(fact.artist_id)}`);
|
|
34683
|
+
if (fact.artist_slug)
|
|
34684
|
+
lines.push(`artist: ${yamlString(fact.artist_slug)}`);
|
|
34685
|
+
if (fact.superseded_by) {
|
|
34686
|
+
lines.push(`superseded_by: ${yamlString(fact.superseded_by)}`);
|
|
34687
|
+
}
|
|
34688
|
+
lines.push("---");
|
|
34689
|
+
lines.push("");
|
|
34690
|
+
lines.push(...headerComment(fact));
|
|
34691
|
+
lines.push("");
|
|
34692
|
+
const body = fact.body_md.replace(/\s+$/, "");
|
|
34693
|
+
if (body.length > 0)
|
|
34694
|
+
lines.push(body);
|
|
34695
|
+
return lines.join(`
|
|
34696
|
+
`).replace(/\n*$/, "") + `
|
|
34697
|
+
`;
|
|
34698
|
+
}
|
|
34699
|
+
function headerComment(fact) {
|
|
34700
|
+
const rel = memoryFactRelPath(fact.pile, fact.slug);
|
|
34701
|
+
const out = [
|
|
34702
|
+
"<!-- GENERATED by Plinth from a memory fact. Edits here are not saved:",
|
|
34703
|
+
` the next sync rewrites this file. To change this fact, use`,
|
|
34704
|
+
` ${factToolsFor(fact.pile)}`,
|
|
34705
|
+
` with id ${fact.id}. This file is ${rel}.`
|
|
34706
|
+
];
|
|
34707
|
+
if (fact.status !== "active") {
|
|
34708
|
+
out.push(` STATUS ${fact.status.toUpperCase()}: this fact is no longer current and is not in any index.`);
|
|
34709
|
+
}
|
|
34710
|
+
out.push("-->");
|
|
34711
|
+
return out;
|
|
34712
|
+
}
|
|
34713
|
+
|
|
34714
|
+
// src/lib/memory-facts/sync.ts
|
|
34715
|
+
function artistSlugLookup(state) {
|
|
34716
|
+
const entries = state.entries["artists"] ?? {};
|
|
34717
|
+
const byId = new Map;
|
|
34718
|
+
for (const entry2 of Object.values(entries))
|
|
34719
|
+
byId.set(entry2.id, entry2.slug);
|
|
34720
|
+
return (id) => byId.get(id);
|
|
34721
|
+
}
|
|
34722
|
+
async function syncMemoryFacts(args) {
|
|
34723
|
+
const { workspaceSlug, baseUrl, pat, fetchImpl, output, suppressor } = args;
|
|
34724
|
+
const now = args.now ?? (() => new Date);
|
|
34725
|
+
let state = args.state;
|
|
34726
|
+
let feed;
|
|
34727
|
+
try {
|
|
34728
|
+
feed = await getMemoryFacts({ baseUrl, pat, fetchImpl });
|
|
34729
|
+
} catch (err) {
|
|
34730
|
+
if (err instanceof ApiError && err.status === 404) {
|
|
34731
|
+
output?.log(`[MEMORY_FACTS ws=${workspaceSlug} skipped reason=route_absent]`);
|
|
34732
|
+
return { state, written: 0, deleted: 0, refused: 0, facts: 0 };
|
|
34733
|
+
}
|
|
34734
|
+
throw err;
|
|
34735
|
+
}
|
|
34736
|
+
const slugFor = artistSlugLookup(state);
|
|
34737
|
+
const facts = [
|
|
34738
|
+
...feed.workspace_facts.map((row) => toRenderableWorkspaceFact(row, slugFor)),
|
|
34739
|
+
...feed.user_facts.map(toRenderableUserFact)
|
|
34740
|
+
];
|
|
34741
|
+
const acked = getMemoryFactEntries(state);
|
|
34742
|
+
const registry3 = {};
|
|
34743
|
+
const live = new Set;
|
|
34744
|
+
let written = 0;
|
|
34745
|
+
let refused = 0;
|
|
34746
|
+
for (const fact of facts) {
|
|
34747
|
+
let relPath;
|
|
34748
|
+
try {
|
|
34749
|
+
relPath = memoryFactRelPath(fact.pile, fact.slug);
|
|
34750
|
+
} catch {
|
|
34751
|
+
output?.warn(`Memory fact "${fact.slug}" has a slug this mirror cannot write - skipped.`);
|
|
34752
|
+
continue;
|
|
34753
|
+
}
|
|
34754
|
+
live.add(relPath);
|
|
34755
|
+
const rendered = renderFactFile(fact);
|
|
34756
|
+
const renderedHash = stableHash(rendered);
|
|
34757
|
+
const onDisk = readMemoryFactFile(workspaceSlug, fact.pile, fact.slug);
|
|
34758
|
+
const ackedHash = acked[relPath]?.body_hash;
|
|
34759
|
+
if (onDisk !== null && stableHash(onDisk) === renderedHash) {
|
|
34760
|
+
state = clearMemoryGeneratedRefusal(state, workspaceSlug, memoryFactFilePath(workspaceSlug, fact.pile, fact.slug));
|
|
34761
|
+
registry3[relPath] = { id: fact.id, body_hash: renderedHash };
|
|
34762
|
+
continue;
|
|
34763
|
+
}
|
|
34764
|
+
if (onDisk !== null && stableHash(onDisk) !== ackedHash) {
|
|
34765
|
+
const refusal = refuseGeneratedMemoryEdit({
|
|
34766
|
+
state,
|
|
34767
|
+
workspaceSlug,
|
|
34768
|
+
absolutePath: memoryFactFilePath(workspaceSlug, fact.pile, fact.slug),
|
|
34769
|
+
relPath,
|
|
34770
|
+
pile: fact.pile,
|
|
34771
|
+
conflictEntity: "memory",
|
|
34772
|
+
conflictSlug: `${fact.pile}.${fact.slug}`,
|
|
34773
|
+
localContent: onDisk,
|
|
34774
|
+
canonicalBody: rendered,
|
|
34775
|
+
writeCanonical: (body) => writeFact(args, fact.pile, fact.slug, body),
|
|
34776
|
+
factId: fact.id,
|
|
34777
|
+
now,
|
|
34778
|
+
output
|
|
34779
|
+
});
|
|
34780
|
+
state = refusal.state;
|
|
34781
|
+
registry3[relPath] = { id: fact.id, body_hash: renderedHash };
|
|
34782
|
+
refused++;
|
|
34783
|
+
continue;
|
|
34784
|
+
}
|
|
34785
|
+
writeFact(args, fact.pile, fact.slug, rendered);
|
|
34786
|
+
registry3[relPath] = { id: fact.id, body_hash: renderedHash };
|
|
34787
|
+
written++;
|
|
34788
|
+
}
|
|
34789
|
+
let deleted = 0;
|
|
34790
|
+
for (const { pile, slug } of listMemoryFactFiles(workspaceSlug)) {
|
|
34791
|
+
const relPath = memoryFactRelPath(pile, slug);
|
|
34792
|
+
if (live.has(relPath))
|
|
34793
|
+
continue;
|
|
34794
|
+
if (acked[relPath] === undefined)
|
|
34795
|
+
continue;
|
|
34796
|
+
const abs = memoryFactFilePath(workspaceSlug, pile, slug);
|
|
34797
|
+
suppressor?.registerDelete(abs);
|
|
34798
|
+
if (deleteMemoryFactFile(workspaceSlug, pile, slug))
|
|
34799
|
+
deleted++;
|
|
34800
|
+
state = clearMemoryGeneratedRefusal(state, workspaceSlug, abs);
|
|
34801
|
+
}
|
|
34802
|
+
const { state: next, changed } = setMemoryFactsState(state, {
|
|
34803
|
+
fetched_at: now().toISOString(),
|
|
34804
|
+
facts: registry3
|
|
34805
|
+
});
|
|
34806
|
+
state = changed ? commitState(state, next) : state;
|
|
34807
|
+
if (written > 0 || deleted > 0 || refused > 0) {
|
|
34808
|
+
output?.log(`[MEMORY_FACTS ws=${workspaceSlug} facts=${facts.length} written=${written} deleted=${deleted} refused=${refused}]`);
|
|
34809
|
+
}
|
|
34810
|
+
return { state, written, deleted, refused, facts: facts.length };
|
|
34811
|
+
}
|
|
34812
|
+
function writeFact(args, pile, slug, content) {
|
|
34813
|
+
const abs = memoryFactFilePath(args.workspaceSlug, pile, slug);
|
|
34814
|
+
args.suppressor?.registerWrite(abs, {
|
|
34815
|
+
bodyHash: stableHash(content),
|
|
34816
|
+
frontmatterHash: ""
|
|
34817
|
+
});
|
|
34818
|
+
writeMemoryFactFile(args.workspaceSlug, pile, slug, content);
|
|
34819
|
+
}
|
|
34820
|
+
function classifyMemoryFactPath(args) {
|
|
34821
|
+
let root;
|
|
34822
|
+
try {
|
|
34823
|
+
root = memoryFactsSubtreeRoot(args.workspaceSlug);
|
|
34824
|
+
} catch {
|
|
34825
|
+
return null;
|
|
34826
|
+
}
|
|
34827
|
+
const rel = relative6(root, args.absolutePath);
|
|
34828
|
+
if (rel === "" || rel.startsWith("..") || isAbsolute2(rel))
|
|
34829
|
+
return null;
|
|
34830
|
+
const mirrorRel = `${MEMORY_FACTS_DIR}/${rel.split(sep4).join("/")}`;
|
|
34831
|
+
return { relPath: mirrorRel, fact: parseMemoryFactRelPath(mirrorRel) };
|
|
34832
|
+
}
|
|
34833
|
+
function classifyMemoryFactEvent(args) {
|
|
34834
|
+
if (args.path === null)
|
|
34835
|
+
return { kind: "ignore" };
|
|
34836
|
+
if (args.ownEcho)
|
|
34837
|
+
return { kind: "echo" };
|
|
34838
|
+
if (args.kind !== "upsert")
|
|
34839
|
+
return { kind: "ignore" };
|
|
34840
|
+
if (args.path.fact !== null && args.known) {
|
|
34841
|
+
return { kind: "generated", relPath: args.path.relPath, pile: args.path.fact.pile };
|
|
34842
|
+
}
|
|
34843
|
+
return { kind: "unowned", relPath: args.path.relPath };
|
|
34844
|
+
}
|
|
34845
|
+
function memoryFactEventWarning(v) {
|
|
34846
|
+
if (v.kind === "generated") {
|
|
34847
|
+
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)}.`;
|
|
34848
|
+
}
|
|
34849
|
+
if (v.kind === "unowned") {
|
|
34850
|
+
return `${v.relPath} is not synced: \`memory/\` holds files Plinth generates from ` + `memory facts. Put your own notes in \`files/\` instead.`;
|
|
34851
|
+
}
|
|
34852
|
+
return null;
|
|
34853
|
+
}
|
|
34854
|
+
|
|
33960
34855
|
// src/lib/files/destructive-gate.ts
|
|
33961
34856
|
function isFileGutting(priorSize, newSize) {
|
|
33962
34857
|
if (priorSize === undefined)
|
|
@@ -34091,7 +34986,7 @@ class FileDestructiveGate {
|
|
|
34091
34986
|
import { readFileSync as readFileSync18, statSync as statSync10 } from "node:fs";
|
|
34092
34987
|
|
|
34093
34988
|
// src/lib/unsynced.ts
|
|
34094
|
-
import { join as join12, relative as
|
|
34989
|
+
import { join as join12, relative as relative7 } from "node:path";
|
|
34095
34990
|
var RECORDABLE_REASONS = new Set([
|
|
34096
34991
|
"rate_limit",
|
|
34097
34992
|
"server",
|
|
@@ -34111,7 +35006,7 @@ function recordEntityUnsynced(args) {
|
|
|
34111
35006
|
const cat = categorizeError(args.err);
|
|
34112
35007
|
let path5;
|
|
34113
35008
|
try {
|
|
34114
|
-
path5 =
|
|
35009
|
+
path5 = relative7(workspaceMirrorPath(args.workspaceSlug), args.absolutePath);
|
|
34115
35010
|
} catch {
|
|
34116
35011
|
path5 = args.absolutePath;
|
|
34117
35012
|
}
|
|
@@ -34136,7 +35031,7 @@ function recordFileUnsynced(args) {
|
|
|
34136
35031
|
});
|
|
34137
35032
|
}
|
|
34138
35033
|
async function drainUnsyncedOps(args) {
|
|
34139
|
-
const { workspaceSlug, baseUrl, pat, getState, setState, fetchImpl, mutex, output } = args;
|
|
35034
|
+
const { workspaceSlug, baseUrl, pat, getState, setState, fetchImpl, mutex, output, echo } = args;
|
|
34140
35035
|
const now = args.now ?? (() => new Date);
|
|
34141
35036
|
const ops = listUnsynced(getState());
|
|
34142
35037
|
let attempted = 0;
|
|
@@ -34169,7 +35064,7 @@ async function drainUnsyncedOps(args) {
|
|
|
34169
35064
|
state: getState(),
|
|
34170
35065
|
exists: op.op === "upsert",
|
|
34171
35066
|
unlinkContext: null,
|
|
34172
|
-
deps: { fetchImpl, now, mutex, output }
|
|
35067
|
+
deps: { fetchImpl, now, mutex, output, echo }
|
|
34173
35068
|
});
|
|
34174
35069
|
setState(result.state);
|
|
34175
35070
|
} else {
|
|
@@ -35210,11 +36105,11 @@ function createPattern(matcher) {
|
|
|
35210
36105
|
if (matcher.path === string4)
|
|
35211
36106
|
return true;
|
|
35212
36107
|
if (matcher.recursive) {
|
|
35213
|
-
const
|
|
35214
|
-
if (!
|
|
36108
|
+
const relative10 = sp2.relative(matcher.path, string4);
|
|
36109
|
+
if (!relative10) {
|
|
35215
36110
|
return false;
|
|
35216
36111
|
}
|
|
35217
|
-
return !
|
|
36112
|
+
return !relative10.startsWith("..") && !sp2.isAbsolute(relative10);
|
|
35218
36113
|
}
|
|
35219
36114
|
return false;
|
|
35220
36115
|
};
|
|
@@ -35844,7 +36739,7 @@ var chokidar_default = { watch, FSWatcher };
|
|
|
35844
36739
|
|
|
35845
36740
|
// src/lib/watcher.ts
|
|
35846
36741
|
import { statSync as statSync9 } from "node:fs";
|
|
35847
|
-
import { sep as
|
|
36742
|
+
import { sep as sep5 } from "node:path";
|
|
35848
36743
|
|
|
35849
36744
|
// src/lib/rename-classifier.ts
|
|
35850
36745
|
class RenameClassifier {
|
|
@@ -35975,7 +36870,7 @@ function watcherIgnoreMatcher(workspaceRoot) {
|
|
|
35975
36870
|
return (absolutePath) => patterns.some((re) => re.test(absolutePath));
|
|
35976
36871
|
}
|
|
35977
36872
|
function escSep() {
|
|
35978
|
-
return
|
|
36873
|
+
return sep5 === "/" ? "/" : "\\\\";
|
|
35979
36874
|
}
|
|
35980
36875
|
|
|
35981
36876
|
class DebounceMap {
|
|
@@ -36326,7 +37221,7 @@ async function reconcileFilesSubtree(args) {
|
|
|
36326
37221
|
}
|
|
36327
37222
|
|
|
36328
37223
|
// src/lib/artist-memory/sync.ts
|
|
36329
|
-
import { relative as
|
|
37224
|
+
import { relative as relative10 } from "node:path";
|
|
36330
37225
|
|
|
36331
37226
|
// src/lib/context/secret-gate.ts
|
|
36332
37227
|
function applySecretGate(args) {
|
|
@@ -36367,6 +37262,10 @@ var ENTITY_TYPE = "artists";
|
|
|
36367
37262
|
function conflictSlug(slug) {
|
|
36368
37263
|
return `${slug}.memory`;
|
|
36369
37264
|
}
|
|
37265
|
+
var GENERATED_PILE = "shared";
|
|
37266
|
+
function memoryRelPath(slug) {
|
|
37267
|
+
return `artists/${slug}/${ARTIST_MEMORY_ANCHOR}`;
|
|
37268
|
+
}
|
|
36370
37269
|
function persistEntry2(state, entry2) {
|
|
36371
37270
|
const { state: next, changed } = upsertEntry(state, ENTITY_TYPE, entry2);
|
|
36372
37271
|
if (!changed)
|
|
@@ -36393,7 +37292,7 @@ function classifyArtistMemoryPath(args) {
|
|
|
36393
37292
|
} catch {
|
|
36394
37293
|
return null;
|
|
36395
37294
|
}
|
|
36396
|
-
const rel =
|
|
37295
|
+
const rel = relative10(root, args.absolutePath);
|
|
36397
37296
|
const parts = rel.split("/");
|
|
36398
37297
|
if (parts.length !== 3)
|
|
36399
37298
|
return null;
|
|
@@ -36415,25 +37314,55 @@ function classifyArtistMemoryPath(args) {
|
|
|
36415
37314
|
}
|
|
36416
37315
|
function applyArtistMemoryOnPull(args) {
|
|
36417
37316
|
const { workspaceSlug, live, ackedMemoryHash, suppressor, now, out } = args;
|
|
37317
|
+
let state = args.state;
|
|
36418
37318
|
const slug = live.slug;
|
|
36419
37319
|
const cloudMemory = live.memory;
|
|
37320
|
+
const generated = live.memory_generated === true;
|
|
36420
37321
|
const local = readArtistMemoryFile(workspaceSlug, slug);
|
|
37322
|
+
const refuse = (localContent, canonical) => {
|
|
37323
|
+
const refused = refuseGeneratedMemoryEdit({
|
|
37324
|
+
state,
|
|
37325
|
+
workspaceSlug,
|
|
37326
|
+
absolutePath: artistMemoryFilePath(workspaceSlug, slug),
|
|
37327
|
+
relPath: memoryRelPath(slug),
|
|
37328
|
+
pile: GENERATED_PILE,
|
|
37329
|
+
conflictEntity: ENTITY_TYPE,
|
|
37330
|
+
conflictSlug: conflictSlug(slug),
|
|
37331
|
+
localContent,
|
|
37332
|
+
canonicalBody: canonical,
|
|
37333
|
+
writeCanonical: (body) => writeMemory(workspaceSlug, slug, body, suppressor),
|
|
37334
|
+
now,
|
|
37335
|
+
output: out
|
|
37336
|
+
});
|
|
37337
|
+
state = refused.state;
|
|
37338
|
+
};
|
|
36421
37339
|
if (cloudMemory === null) {
|
|
36422
37340
|
if (local !== null && isPristineMemoryStub(local)) {
|
|
36423
37341
|
const abs = artistMemoryFilePath(workspaceSlug, slug);
|
|
36424
37342
|
suppressor?.registerDelete(abs);
|
|
36425
37343
|
deleteArtistMemoryFile(workspaceSlug, slug);
|
|
36426
|
-
return { memoryHash: undefined };
|
|
37344
|
+
return { memoryHash: undefined, state };
|
|
36427
37345
|
}
|
|
36428
|
-
return { memoryHash: ackedMemoryHash };
|
|
37346
|
+
return { memoryHash: ackedMemoryHash, state };
|
|
36429
37347
|
}
|
|
36430
37348
|
const cloudHash = stableHash(cloudMemory);
|
|
36431
37349
|
if (cloudHash === ackedMemoryHash && local !== null) {
|
|
36432
|
-
|
|
37350
|
+
if (generated && stableHash(local) !== ackedMemoryHash) {
|
|
37351
|
+
refuse(local, cloudMemory);
|
|
37352
|
+
return { memoryHash: cloudHash, state };
|
|
37353
|
+
}
|
|
37354
|
+
if (generated) {
|
|
37355
|
+
state = clearMemoryGeneratedRefusal(state, workspaceSlug, artistMemoryFilePath(workspaceSlug, slug));
|
|
37356
|
+
}
|
|
37357
|
+
return { memoryHash: ackedMemoryHash, state };
|
|
36433
37358
|
}
|
|
36434
37359
|
if (local !== null) {
|
|
36435
37360
|
const localHash = stableHash(local);
|
|
36436
37361
|
if (localHash !== ackedMemoryHash && localHash !== cloudHash) {
|
|
37362
|
+
if (generated) {
|
|
37363
|
+
refuse(local, cloudMemory);
|
|
37364
|
+
return { memoryHash: cloudHash, state };
|
|
37365
|
+
}
|
|
36437
37366
|
const stashPath = stashConflict({
|
|
36438
37367
|
workspaceSlug,
|
|
36439
37368
|
entityType: ENTITY_TYPE,
|
|
@@ -36446,7 +37375,7 @@ function applyArtistMemoryOnPull(args) {
|
|
|
36446
37375
|
}
|
|
36447
37376
|
}
|
|
36448
37377
|
writeMemory(workspaceSlug, slug, cloudMemory, suppressor);
|
|
36449
|
-
return { memoryHash: cloudHash };
|
|
37378
|
+
return { memoryHash: cloudHash, state };
|
|
36450
37379
|
}
|
|
36451
37380
|
function writeMemory(workspaceSlug, slug, content, suppressor) {
|
|
36452
37381
|
const abs = artistMemoryFilePath(workspaceSlug, slug);
|
|
@@ -36489,8 +37418,46 @@ async function pushArtistMemory(args) {
|
|
|
36489
37418
|
if (gate.held)
|
|
36490
37419
|
return { state: base2, outcome: { kind: "secret_held", slug } };
|
|
36491
37420
|
if (alreadyAcked) {
|
|
37421
|
+
if (entry2.memory_generated === true) {
|
|
37422
|
+
base2 = clearMemoryGeneratedRefusal(base2, workspaceSlug, artistMemoryFilePath(workspaceSlug, slug));
|
|
37423
|
+
}
|
|
36492
37424
|
return { state: base2, outcome: { kind: "noop", slug } };
|
|
36493
37425
|
}
|
|
37426
|
+
if (entry2.memory_generated === true) {
|
|
37427
|
+
const row = await getEntity({
|
|
37428
|
+
strategy: artistsStrategy,
|
|
37429
|
+
baseUrl,
|
|
37430
|
+
pat,
|
|
37431
|
+
id: entry2.id,
|
|
37432
|
+
fetchImpl
|
|
37433
|
+
});
|
|
37434
|
+
const canonical = row.memory ?? "";
|
|
37435
|
+
const refused = refuseGeneratedMemoryEdit({
|
|
37436
|
+
state: base2,
|
|
37437
|
+
workspaceSlug,
|
|
37438
|
+
absolutePath: artistMemoryFilePath(workspaceSlug, slug),
|
|
37439
|
+
relPath: memoryRelPath(slug),
|
|
37440
|
+
pile: GENERATED_PILE,
|
|
37441
|
+
conflictEntity: ENTITY_TYPE,
|
|
37442
|
+
conflictSlug: conflictSlug(slug),
|
|
37443
|
+
localContent: local,
|
|
37444
|
+
canonicalBody: canonical,
|
|
37445
|
+
writeCanonical: (body) => writeMemory(workspaceSlug, slug, body, suppressor),
|
|
37446
|
+
now,
|
|
37447
|
+
output
|
|
37448
|
+
});
|
|
37449
|
+
const stamped = {
|
|
37450
|
+
...entry2,
|
|
37451
|
+
updated_at: row.updated_at,
|
|
37452
|
+
revision: row.revision,
|
|
37453
|
+
memory_hash: stableHash(canonical),
|
|
37454
|
+
memory_generated: row.memory_generated
|
|
37455
|
+
};
|
|
37456
|
+
return {
|
|
37457
|
+
state: persistEntry2(refused.state, stamped),
|
|
37458
|
+
outcome: { kind: "generated_refused", slug, stashPath: refused.stashPath }
|
|
37459
|
+
};
|
|
37460
|
+
}
|
|
36494
37461
|
const acked = entry2.memory_hash;
|
|
36495
37462
|
let cas = entry2.updated_at;
|
|
36496
37463
|
const landed = (doc2, note = "") => {
|
|
@@ -36998,16 +37965,7 @@ async function fetchCalendar(args) {
|
|
|
36998
37965
|
return parsed.data;
|
|
36999
37966
|
}
|
|
37000
37967
|
|
|
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";
|
|
37968
|
+
// src/lib/calendar/day.ts
|
|
37011
37969
|
var WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
37012
37970
|
var MONTHS = [
|
|
37013
37971
|
"Jan",
|
|
@@ -37024,32 +37982,12 @@ var MONTHS = [
|
|
|
37024
37982
|
"Dec"
|
|
37025
37983
|
];
|
|
37026
37984
|
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
37985
|
function dateParts(value) {
|
|
37040
37986
|
const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(value);
|
|
37041
37987
|
if (!m)
|
|
37042
37988
|
return null;
|
|
37043
37989
|
return { y: Number(m[1]), m: Number(m[2]), d: Number(m[3]) };
|
|
37044
37990
|
}
|
|
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
37991
|
function utcMs(parts) {
|
|
37054
37992
|
return Date.UTC(parts.y, parts.m - 1, parts.d);
|
|
37055
37993
|
}
|
|
@@ -37063,6 +38001,116 @@ function formatDay(parts, referenceYear) {
|
|
|
37063
38001
|
const year = parts.y === referenceYear ? "" : ` ${parts.y}`;
|
|
37064
38002
|
return `${weekday} ${parts.d} ${month}${year}`;
|
|
37065
38003
|
}
|
|
38004
|
+
function localDayOf(now) {
|
|
38005
|
+
return { y: now.getFullYear(), m: now.getMonth() + 1, d: now.getDate() };
|
|
38006
|
+
}
|
|
38007
|
+
function daysBetween2(from, to) {
|
|
38008
|
+
return Math.round((utcMs(to) - utcMs(from)) / DAY_MS);
|
|
38009
|
+
}
|
|
38010
|
+
function isRealDay(parts) {
|
|
38011
|
+
if (parts.m < 1 || parts.m > 12 || parts.d < 1 || parts.d > 31)
|
|
38012
|
+
return false;
|
|
38013
|
+
const d = new Date(utcMs(parts));
|
|
38014
|
+
return d.getUTCFullYear() === parts.y && d.getUTCMonth() + 1 === parts.m && d.getUTCDate() === parts.d;
|
|
38015
|
+
}
|
|
38016
|
+
var WEEKDAY_WORD = "(Sun(?:day)?|Mon(?:day)?|Tue(?:s|sday)?|Wed(?:nesday)?|Thu(?:r|rs|rsday)?|Fri(?:day)?|Sat(?:urday)?)";
|
|
38017
|
+
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)?)";
|
|
38018
|
+
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");
|
|
38019
|
+
var YEAR_WINDOW = 10;
|
|
38020
|
+
function weekdayIndex(word) {
|
|
38021
|
+
const stem = word.slice(0, 3).toLowerCase();
|
|
38022
|
+
return WEEKDAYS.findIndex((w) => w.toLowerCase() === stem);
|
|
38023
|
+
}
|
|
38024
|
+
function monthIndex(word) {
|
|
38025
|
+
const stem = word.slice(0, 3).toLowerCase();
|
|
38026
|
+
return MONTHS.findIndex((m) => m.toLowerCase() === stem);
|
|
38027
|
+
}
|
|
38028
|
+
function inferYear(day, month, weekday, today) {
|
|
38029
|
+
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)));
|
|
38030
|
+
if (weekday !== null) {
|
|
38031
|
+
const match = candidates.find((c) => new Date(utcMs(c.parts)).getUTCDay() === weekday);
|
|
38032
|
+
if (match)
|
|
38033
|
+
return match.y;
|
|
38034
|
+
}
|
|
38035
|
+
return candidates[0]?.y ?? today.y;
|
|
38036
|
+
}
|
|
38037
|
+
function readDayMentions(text, today) {
|
|
38038
|
+
const out = [];
|
|
38039
|
+
for (const m of text.matchAll(MENTION_RE)) {
|
|
38040
|
+
let parts;
|
|
38041
|
+
let yearInferred = false;
|
|
38042
|
+
let length = m[0].length;
|
|
38043
|
+
if (m[1] !== undefined) {
|
|
38044
|
+
parts = { y: Number(m[1]), m: Number(m[2]), d: Number(m[3]) };
|
|
38045
|
+
} else {
|
|
38046
|
+
if (m[6] === "may")
|
|
38047
|
+
continue;
|
|
38048
|
+
const weekday = m[4] === undefined ? null : weekdayIndex(m[4]);
|
|
38049
|
+
const day = Number(m[5]);
|
|
38050
|
+
const month = monthIndex(m[6]) + 1;
|
|
38051
|
+
if (month < 1)
|
|
38052
|
+
continue;
|
|
38053
|
+
const written = m[7] === undefined ? null : Number(m[7]);
|
|
38054
|
+
if (written !== null && Math.abs(written - today.y) <= YEAR_WINDOW) {
|
|
38055
|
+
parts = { y: written, m: month, d: day };
|
|
38056
|
+
} else {
|
|
38057
|
+
if (written !== null)
|
|
38058
|
+
length -= /,?\s+\d{4}$/.exec(m[0])?.[0].length ?? 0;
|
|
38059
|
+
parts = { y: inferYear(day, month, weekday, today), m: month, d: day };
|
|
38060
|
+
yearInferred = true;
|
|
38061
|
+
}
|
|
38062
|
+
}
|
|
38063
|
+
if (!isRealDay(parts))
|
|
38064
|
+
continue;
|
|
38065
|
+
out.push({ parts, index: m.index, length, yearInferred });
|
|
38066
|
+
}
|
|
38067
|
+
return out;
|
|
38068
|
+
}
|
|
38069
|
+
function readAsOf(text, today) {
|
|
38070
|
+
const at = /\bas\s+of\b/i.exec(text);
|
|
38071
|
+
if (!at)
|
|
38072
|
+
return null;
|
|
38073
|
+
const after = text.slice(at.index + at[0].length);
|
|
38074
|
+
const first = readDayMentions(after, today)[0];
|
|
38075
|
+
if (!first)
|
|
38076
|
+
return null;
|
|
38077
|
+
if (!/^[\s:,\-(]*$/.test(after.slice(0, first.index)))
|
|
38078
|
+
return null;
|
|
38079
|
+
const ahead = utcMs(first.parts) > utcMs(today);
|
|
38080
|
+
const parts = first.yearInferred && ahead ? { ...first.parts, y: first.parts.y - 1 } : first.parts;
|
|
38081
|
+
return { parts, yearInferred: first.yearInferred };
|
|
38082
|
+
}
|
|
38083
|
+
|
|
38084
|
+
// src/lib/calendar/render.ts
|
|
38085
|
+
var CALENDAR_BODY_MARKER = "<!-- PLINTH:CALENDAR-BODY -->";
|
|
38086
|
+
var OWNERSHIP_NOTE2 = [
|
|
38087
|
+
"<!-- Generated by Plinth from the calendars mapped in this workspace. This file",
|
|
38088
|
+
" is DERIVED: one-way, overwritten on every refresh, and never uploaded.",
|
|
38089
|
+
" Your edits here are lost. Dates are changed in Plinth's calendar view, by a",
|
|
38090
|
+
" person - not from this file and not by an agent. -->"
|
|
38091
|
+
].join(`
|
|
38092
|
+
`);
|
|
38093
|
+
var DEFAULT_USER_LABEL = "the Plinth account this mirror syncs with";
|
|
38094
|
+
var FULL_BAND_DAYS = 14;
|
|
38095
|
+
function yamlString2(value) {
|
|
38096
|
+
return JSON.stringify(value);
|
|
38097
|
+
}
|
|
38098
|
+
function oneLine2(value, maxLength = 200) {
|
|
38099
|
+
const flattened = value.replace(/[\u0000-\u001F\u007F]+/g, " ").replace(/<!--/g, "<! --").replace(/\s+/g, " ").trim();
|
|
38100
|
+
if (flattened.length === 0)
|
|
38101
|
+
return "(untitled)";
|
|
38102
|
+
if (flattened.length <= maxLength)
|
|
38103
|
+
return flattened;
|
|
38104
|
+
return `${flattened.slice(0, maxLength - 3)}...`;
|
|
38105
|
+
}
|
|
38106
|
+
function timeParts(value) {
|
|
38107
|
+
const m = /T(\d{2}):(\d{2})(?::\d{2}(?:\.\d+)?)?(Z|[+-]\d{2}:\d{2})?$/.exec(value);
|
|
38108
|
+
if (!m)
|
|
38109
|
+
return null;
|
|
38110
|
+
const rawOffset = m[3];
|
|
38111
|
+
const offset = rawOffset === undefined ? "" : rawOffset === "Z" ? "UTC" : rawOffset;
|
|
38112
|
+
return { hh: m[1], mi: m[2], offset };
|
|
38113
|
+
}
|
|
37066
38114
|
function sortInstant(event) {
|
|
37067
38115
|
if (!event.all_day) {
|
|
37068
38116
|
const ms = Date.parse(event.start);
|
|
@@ -37199,12 +38247,12 @@ function renderArtistCalendar(args) {
|
|
|
37199
38247
|
"---",
|
|
37200
38248
|
"plinth_generated: true",
|
|
37201
38249
|
"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: ${
|
|
38250
|
+
`artist_slug: ${row.artist_slug === null ? "null" : yamlString2(row.artist_slug)}`,
|
|
38251
|
+
`artist_name: ${row.artist_name === null ? "null" : yamlString2(row.artist_name)}`,
|
|
38252
|
+
`calendar_name: ${yamlString2(row.calendar_name)}`,
|
|
38253
|
+
`generated_at: ${yamlString2(generatedAt)}`,
|
|
38254
|
+
`window_start: ${yamlString2(window2.time_min)}`,
|
|
38255
|
+
`window_end: ${yamlString2(window2.time_max)}`,
|
|
37208
38256
|
`status: ${row.status}`,
|
|
37209
38257
|
"fetch_state: fresh",
|
|
37210
38258
|
`entry_count: ${events.length}`,
|
|
@@ -37285,9 +38333,9 @@ function renderUpcoming(args) {
|
|
|
37285
38333
|
"plinth_generated: true",
|
|
37286
38334
|
"view: calendar",
|
|
37287
38335
|
"scope: workspace",
|
|
37288
|
-
`generated_at: ${
|
|
37289
|
-
`window_start: ${
|
|
37290
|
-
`window_end: ${
|
|
38336
|
+
`generated_at: ${yamlString2(generatedAt)}`,
|
|
38337
|
+
`window_start: ${yamlString2(window2.time_min)}`,
|
|
38338
|
+
`window_end: ${yamlString2(window2.time_max)}`,
|
|
37291
38339
|
`artist_count: ${artistCount}`,
|
|
37292
38340
|
`mapped_artist_count: ${mappedArtistCount}`,
|
|
37293
38341
|
`calendars_read: ${fetched.length}`,
|
|
@@ -37383,8 +38431,8 @@ function renderStaleFile(existing, args) {
|
|
|
37383
38431
|
"---",
|
|
37384
38432
|
...carried,
|
|
37385
38433
|
"fetch_state: stale",
|
|
37386
|
-
`last_refresh_attempt: ${
|
|
37387
|
-
`last_refresh_error: ${
|
|
38434
|
+
`last_refresh_attempt: ${yamlString2(args.attemptedAt)}`,
|
|
38435
|
+
`last_refresh_error: ${yamlString2(oneLine2(args.reason, 300))}`,
|
|
37388
38436
|
"---",
|
|
37389
38437
|
"",
|
|
37390
38438
|
OWNERSHIP_NOTE2,
|
|
@@ -37531,6 +38579,11 @@ class PullSuppressor {
|
|
|
37531
38579
|
expiresAt: this.now() + this.ttlMs
|
|
37532
38580
|
});
|
|
37533
38581
|
}
|
|
38582
|
+
cancelDelete(absPath) {
|
|
38583
|
+
const entry2 = this.entries.get(absPath);
|
|
38584
|
+
if (entry2?.kind === "delete")
|
|
38585
|
+
this.entries.delete(absPath);
|
|
38586
|
+
}
|
|
37534
38587
|
shouldSuppressUpsert(absPath, hashes) {
|
|
37535
38588
|
const entry2 = this.entries.get(absPath);
|
|
37536
38589
|
if (!entry2)
|
|
@@ -38099,7 +39152,7 @@ async function reconcileDivergedRows(args) {
|
|
|
38099
39152
|
state,
|
|
38100
39153
|
exists: true,
|
|
38101
39154
|
unlinkContext: null,
|
|
38102
|
-
deps: { fetchImpl, now, output }
|
|
39155
|
+
deps: { fetchImpl, now, output, echo: args.echo }
|
|
38103
39156
|
});
|
|
38104
39157
|
state = result.state;
|
|
38105
39158
|
switch (result.outcome.kind) {
|
|
@@ -38287,7 +39340,7 @@ async function reconcileDivergedRows(args) {
|
|
|
38287
39340
|
|
|
38288
39341
|
// src/lib/workspace-rules/sync.ts
|
|
38289
39342
|
import { existsSync as existsSync4 } from "node:fs";
|
|
38290
|
-
import { relative as
|
|
39343
|
+
import { relative as relative11 } from "node:path";
|
|
38291
39344
|
|
|
38292
39345
|
// src/lib/workspace-rules/api.ts
|
|
38293
39346
|
var MAX_WORKSPACE_RULES_BYTES = 256 * 1024;
|
|
@@ -38302,16 +39355,16 @@ var PreconditionSchema2 = exports_external.object({
|
|
|
38302
39355
|
code: exports_external.literal("precondition_failed"),
|
|
38303
39356
|
current_updated_at: exports_external.string()
|
|
38304
39357
|
});
|
|
38305
|
-
function
|
|
39358
|
+
function baseHeaders3(pat) {
|
|
38306
39359
|
return { Authorization: `Bearer ${pat}`, Accept: "application/json" };
|
|
38307
39360
|
}
|
|
38308
|
-
var
|
|
39361
|
+
var ROUTE2 = "/api/sync/workspace-rules";
|
|
38309
39362
|
async function getWorkspaceRules(args) {
|
|
38310
39363
|
const { baseUrl, pat, fetchImpl = fetch } = args;
|
|
38311
|
-
const url2 = `${baseUrl.replace(/\/$/, "")}${
|
|
39364
|
+
const url2 = `${baseUrl.replace(/\/$/, "")}${ROUTE2}`;
|
|
38312
39365
|
let res;
|
|
38313
39366
|
try {
|
|
38314
|
-
res = await fetchImpl(url2, { method: "GET", headers:
|
|
39367
|
+
res = await fetchImpl(url2, { method: "GET", headers: baseHeaders3(pat) });
|
|
38315
39368
|
} catch (err) {
|
|
38316
39369
|
const msg = err instanceof Error ? err.message : "network error";
|
|
38317
39370
|
throw new ApiError(0, `Could not reach ${url2}: ${msg}`);
|
|
@@ -38334,9 +39387,9 @@ async function getWorkspaceRules(args) {
|
|
|
38334
39387
|
}
|
|
38335
39388
|
async function putWorkspaceRules(args) {
|
|
38336
39389
|
const { baseUrl, pat, bodyMd, expectedUpdatedAt, fetchImpl = fetch } = args;
|
|
38337
|
-
const url2 = `${baseUrl.replace(/\/$/, "")}${
|
|
39390
|
+
const url2 = `${baseUrl.replace(/\/$/, "")}${ROUTE2}`;
|
|
38338
39391
|
const headers = {
|
|
38339
|
-
...
|
|
39392
|
+
...baseHeaders3(pat),
|
|
38340
39393
|
"Content-Type": "application/json"
|
|
38341
39394
|
};
|
|
38342
39395
|
if (expectedUpdatedAt)
|
|
@@ -38422,7 +39475,7 @@ function classifyWorkspaceRulesPath(args) {
|
|
|
38422
39475
|
} catch {
|
|
38423
39476
|
return false;
|
|
38424
39477
|
}
|
|
38425
|
-
return
|
|
39478
|
+
return relative11(root, args.absolutePath) === WORKSPACE_RULES_FILENAME;
|
|
38426
39479
|
}
|
|
38427
39480
|
function migrateHouseStyleFile(workspaceSlug) {
|
|
38428
39481
|
let target;
|
|
@@ -38573,7 +39626,7 @@ async function reconcileWorkspaceRules(args) {
|
|
|
38573
39626
|
var WORKSPACE_RULES_MUTEX_KEY = "workspace-rules";
|
|
38574
39627
|
|
|
38575
39628
|
// src/lib/workspace-memory/sync.ts
|
|
38576
|
-
import { relative as
|
|
39629
|
+
import { relative as relative12 } from "node:path";
|
|
38577
39630
|
|
|
38578
39631
|
// src/lib/workspace-memory/api.ts
|
|
38579
39632
|
var MAX_WORKSPACE_MEMORY_BYTES = 256 * 1024;
|
|
@@ -38582,22 +39635,23 @@ var WorkspaceMemoryProjectionSchema = exports_external.object({
|
|
|
38582
39635
|
is_empty: exports_external.boolean(),
|
|
38583
39636
|
updated_at: exports_external.string().datetime({ offset: true }).nullable(),
|
|
38584
39637
|
created_at: exports_external.string().datetime({ offset: true }).nullable(),
|
|
38585
|
-
last_edited_by: exports_external.string().uuid().nullable()
|
|
39638
|
+
last_edited_by: exports_external.string().uuid().nullable(),
|
|
39639
|
+
generated: exports_external.boolean().optional().catch(undefined)
|
|
38586
39640
|
});
|
|
38587
39641
|
var PreconditionSchema3 = exports_external.object({
|
|
38588
39642
|
code: exports_external.literal("precondition_failed"),
|
|
38589
39643
|
current_updated_at: exports_external.string()
|
|
38590
39644
|
});
|
|
38591
|
-
function
|
|
39645
|
+
function baseHeaders4(pat) {
|
|
38592
39646
|
return { Authorization: `Bearer ${pat}`, Accept: "application/json" };
|
|
38593
39647
|
}
|
|
38594
|
-
var
|
|
39648
|
+
var ROUTE3 = "/api/sync/workspace-memory";
|
|
38595
39649
|
async function getWorkspaceMemory(args) {
|
|
38596
39650
|
const { baseUrl, pat, fetchImpl = fetch } = args;
|
|
38597
|
-
const url2 = `${baseUrl.replace(/\/$/, "")}${
|
|
39651
|
+
const url2 = `${baseUrl.replace(/\/$/, "")}${ROUTE3}`;
|
|
38598
39652
|
let res;
|
|
38599
39653
|
try {
|
|
38600
|
-
res = await fetchImpl(url2, { method: "GET", headers:
|
|
39654
|
+
res = await fetchImpl(url2, { method: "GET", headers: baseHeaders4(pat) });
|
|
38601
39655
|
} catch (err) {
|
|
38602
39656
|
const msg = err instanceof Error ? err.message : "network error";
|
|
38603
39657
|
throw new ApiError(0, `Could not reach ${url2}: ${msg}`);
|
|
@@ -38620,9 +39674,9 @@ async function getWorkspaceMemory(args) {
|
|
|
38620
39674
|
}
|
|
38621
39675
|
async function putWorkspaceMemory(args) {
|
|
38622
39676
|
const { baseUrl, pat, bodyMd, expectedUpdatedAt, fetchImpl = fetch } = args;
|
|
38623
|
-
const url2 = `${baseUrl.replace(/\/$/, "")}${
|
|
39677
|
+
const url2 = `${baseUrl.replace(/\/$/, "")}${ROUTE3}`;
|
|
38624
39678
|
const headers = {
|
|
38625
|
-
...
|
|
39679
|
+
...baseHeaders4(pat),
|
|
38626
39680
|
"Content-Type": "application/json"
|
|
38627
39681
|
};
|
|
38628
39682
|
if (expectedUpdatedAt)
|
|
@@ -38657,6 +39711,14 @@ async function putWorkspaceMemory(args) {
|
|
|
38657
39711
|
}
|
|
38658
39712
|
if (res.status === 409) {
|
|
38659
39713
|
const json2 = await res.json();
|
|
39714
|
+
const generated = GeneratedRefusalSchema.safeParse(json2);
|
|
39715
|
+
if (generated.success) {
|
|
39716
|
+
return {
|
|
39717
|
+
ok: false,
|
|
39718
|
+
code: "memory_is_generated",
|
|
39719
|
+
serverMessage: generated.data.error
|
|
39720
|
+
};
|
|
39721
|
+
}
|
|
38660
39722
|
const parsed = PreconditionSchema3.safeParse(json2);
|
|
38661
39723
|
if (!parsed.success) {
|
|
38662
39724
|
throw new ApiError(409, `put workspace-memory 409 body did not match precondition_failed shape`);
|
|
@@ -38684,6 +39746,7 @@ async function putWorkspaceMemory(args) {
|
|
|
38684
39746
|
// src/lib/workspace-memory/sync.ts
|
|
38685
39747
|
var CONFLICT_ENTITY2 = "workspace-memory";
|
|
38686
39748
|
var CONFLICT_SLUG2 = "workspace-memory";
|
|
39749
|
+
var GENERATED_PILE2 = "shared";
|
|
38687
39750
|
function memoryFilePath(workspaceSlug) {
|
|
38688
39751
|
return contextFilePath(workspaceSlug, WORKSPACE_MEMORY_FILENAME);
|
|
38689
39752
|
}
|
|
@@ -38708,7 +39771,7 @@ function classifyWorkspaceMemoryPath(args) {
|
|
|
38708
39771
|
} catch {
|
|
38709
39772
|
return false;
|
|
38710
39773
|
}
|
|
38711
|
-
return
|
|
39774
|
+
return relative12(root, args.absolutePath) === WORKSPACE_MEMORY_FILENAME;
|
|
38712
39775
|
}
|
|
38713
39776
|
async function pullWorkspaceMemory(args) {
|
|
38714
39777
|
const { workspaceSlug, baseUrl, pat, state, fetchImpl, output, suppressor } = args;
|
|
@@ -38722,12 +39785,37 @@ async function pullWorkspaceMemory(args) {
|
|
|
38722
39785
|
const cloudHash = stableHash(cloud.body_md);
|
|
38723
39786
|
const ackedHash = getWorkspaceMemoryState(state)?.body_hash;
|
|
38724
39787
|
if (local !== null && stableHash(local) === cloudHash) {
|
|
38725
|
-
const { state:
|
|
39788
|
+
const { state: stamped } = setWorkspaceMemoryState(state, {
|
|
38726
39789
|
body_hash: cloudHash,
|
|
38727
|
-
updated_at: cloud.updated_at
|
|
39790
|
+
updated_at: cloud.updated_at,
|
|
39791
|
+
generated: cloud.generated
|
|
38728
39792
|
});
|
|
39793
|
+
const next2 = clearMemoryGeneratedRefusal(stamped, workspaceSlug, memoryFilePath(workspaceSlug));
|
|
38729
39794
|
return { state: next2, action: "noop" };
|
|
38730
39795
|
}
|
|
39796
|
+
if (cloud.generated === true && local !== null && !localEmpty && stableHash(local) !== ackedHash) {
|
|
39797
|
+
const refused = refuseGeneratedMemoryEdit({
|
|
39798
|
+
state,
|
|
39799
|
+
workspaceSlug,
|
|
39800
|
+
absolutePath: memoryFilePath(workspaceSlug),
|
|
39801
|
+
relPath: WORKSPACE_MEMORY_FILENAME,
|
|
39802
|
+
pile: GENERATED_PILE2,
|
|
39803
|
+
conflictEntity: CONFLICT_ENTITY2,
|
|
39804
|
+
conflictSlug: CONFLICT_SLUG2,
|
|
39805
|
+
localContent: local,
|
|
39806
|
+
canonicalBody: cloud.body_md,
|
|
39807
|
+
writeCanonical: (body) => writeLocal2(workspaceSlug, body, suppressor),
|
|
39808
|
+
now,
|
|
39809
|
+
output
|
|
39810
|
+
});
|
|
39811
|
+
const { state: next2 } = setWorkspaceMemoryState(refused.state, {
|
|
39812
|
+
body_hash: cloudHash,
|
|
39813
|
+
updated_at: cloud.updated_at,
|
|
39814
|
+
generated: true
|
|
39815
|
+
});
|
|
39816
|
+
output?.log(`[WORKSPACE_MEMORY_PULL_GENERATED_REFUSED ws=${workspaceSlug} stashed]`);
|
|
39817
|
+
return { state: next2, action: "written" };
|
|
39818
|
+
}
|
|
38731
39819
|
if (local !== null && ackedHash !== undefined && cloudHash === ackedHash) {
|
|
38732
39820
|
output?.log(`[WORKSPACE_MEMORY_PULL_PRESERVED ws=${workspaceSlug} reason=pending_push]`);
|
|
38733
39821
|
return { state, action: "preserved" };
|
|
@@ -38750,7 +39838,8 @@ async function pullWorkspaceMemory(args) {
|
|
|
38750
39838
|
writeLocal2(workspaceSlug, cloud.body_md, suppressor);
|
|
38751
39839
|
const { state: next } = setWorkspaceMemoryState(state, {
|
|
38752
39840
|
body_hash: cloudHash,
|
|
38753
|
-
updated_at: cloud.updated_at
|
|
39841
|
+
updated_at: cloud.updated_at,
|
|
39842
|
+
generated: cloud.generated
|
|
38754
39843
|
});
|
|
38755
39844
|
return { state: next, action: cloud.is_empty ? "materialised" : "written" };
|
|
38756
39845
|
}
|
|
@@ -38760,7 +39849,8 @@ async function restoreFromCloud2(args) {
|
|
|
38760
39849
|
writeLocal2(workspaceSlug, cloud.body_md, suppressor);
|
|
38761
39850
|
return persistMemory(state, {
|
|
38762
39851
|
body_hash: stableHash(cloud.body_md),
|
|
38763
|
-
updated_at: cloud.updated_at
|
|
39852
|
+
updated_at: cloud.updated_at,
|
|
39853
|
+
generated: cloud.generated
|
|
38764
39854
|
});
|
|
38765
39855
|
}
|
|
38766
39856
|
async function pushWorkspaceMemory(args) {
|
|
@@ -38773,6 +39863,32 @@ async function pushWorkspaceMemory(args) {
|
|
|
38773
39863
|
const localHash = stableHash(local);
|
|
38774
39864
|
const acked = getWorkspaceMemoryState(state);
|
|
38775
39865
|
const alreadyAcked = Boolean(acked && acked.body_hash === localHash);
|
|
39866
|
+
if (acked?.generated === true && !alreadyAcked) {
|
|
39867
|
+
const cloud = await getWorkspaceMemory({ baseUrl, pat, fetchImpl });
|
|
39868
|
+
const refused = refuseGeneratedMemoryEdit({
|
|
39869
|
+
state,
|
|
39870
|
+
workspaceSlug,
|
|
39871
|
+
absolutePath: memoryFilePath(workspaceSlug),
|
|
39872
|
+
relPath: WORKSPACE_MEMORY_FILENAME,
|
|
39873
|
+
pile: GENERATED_PILE2,
|
|
39874
|
+
conflictEntity: CONFLICT_ENTITY2,
|
|
39875
|
+
conflictSlug: CONFLICT_SLUG2,
|
|
39876
|
+
localContent: local,
|
|
39877
|
+
canonicalBody: cloud.body_md,
|
|
39878
|
+
writeCanonical: (body) => writeLocal2(workspaceSlug, body, args.suppressor),
|
|
39879
|
+
now,
|
|
39880
|
+
output
|
|
39881
|
+
});
|
|
39882
|
+
const next2 = persistMemory(refused.state, {
|
|
39883
|
+
body_hash: stableHash(cloud.body_md),
|
|
39884
|
+
updated_at: cloud.updated_at,
|
|
39885
|
+
generated: cloud.generated
|
|
39886
|
+
});
|
|
39887
|
+
return {
|
|
39888
|
+
state: next2,
|
|
39889
|
+
outcome: { kind: "generated_refused", stashPath: refused.stashPath }
|
|
39890
|
+
};
|
|
39891
|
+
}
|
|
38776
39892
|
const gate = applySecretGate({
|
|
38777
39893
|
state,
|
|
38778
39894
|
workspaceSlug,
|
|
@@ -38804,7 +39920,8 @@ async function pushWorkspaceMemory(args) {
|
|
|
38804
39920
|
if (res.ok) {
|
|
38805
39921
|
const next2 = persistMemory(gated, {
|
|
38806
39922
|
body_hash: localHash,
|
|
38807
|
-
updated_at: res.row.updated_at
|
|
39923
|
+
updated_at: res.row.updated_at,
|
|
39924
|
+
generated: res.row.generated
|
|
38808
39925
|
});
|
|
38809
39926
|
output?.log(`[WORKSPACE_MEMORY_PUT ws=${workspaceSlug} is_empty=${res.row.is_empty}]`);
|
|
38810
39927
|
return { state: next2, outcome: { kind: "pushed" } };
|
|
@@ -38813,6 +39930,33 @@ async function pushWorkspaceMemory(args) {
|
|
|
38813
39930
|
output?.warn(`${WORKSPACE_MEMORY_FILENAME} is over the ${Math.round(MAX_WORKSPACE_MEMORY_BYTES / 1024)}KB cap — not synced.`);
|
|
38814
39931
|
return { state: gated, outcome: { kind: "over_cap" } };
|
|
38815
39932
|
}
|
|
39933
|
+
if (res.code === "memory_is_generated") {
|
|
39934
|
+
const cloud = await getWorkspaceMemory({ baseUrl, pat, fetchImpl });
|
|
39935
|
+
const refused = refuseGeneratedMemoryEdit({
|
|
39936
|
+
state: gated,
|
|
39937
|
+
workspaceSlug,
|
|
39938
|
+
absolutePath: memoryFilePath(workspaceSlug),
|
|
39939
|
+
relPath: WORKSPACE_MEMORY_FILENAME,
|
|
39940
|
+
pile: GENERATED_PILE2,
|
|
39941
|
+
conflictEntity: CONFLICT_ENTITY2,
|
|
39942
|
+
conflictSlug: CONFLICT_SLUG2,
|
|
39943
|
+
localContent: local,
|
|
39944
|
+
canonicalBody: cloud.body_md,
|
|
39945
|
+
writeCanonical: (body) => writeLocal2(workspaceSlug, body, args.suppressor),
|
|
39946
|
+
now,
|
|
39947
|
+
output
|
|
39948
|
+
});
|
|
39949
|
+
output?.log(`[WORKSPACE_MEMORY_PUT_REFUSED ws=${workspaceSlug} code=memory_is_generated]`);
|
|
39950
|
+
const next2 = persistMemory(refused.state, {
|
|
39951
|
+
body_hash: stableHash(cloud.body_md),
|
|
39952
|
+
updated_at: cloud.updated_at,
|
|
39953
|
+
generated: cloud.generated
|
|
39954
|
+
});
|
|
39955
|
+
return {
|
|
39956
|
+
state: next2,
|
|
39957
|
+
outcome: { kind: "generated_refused", stashPath: refused.stashPath }
|
|
39958
|
+
};
|
|
39959
|
+
}
|
|
38816
39960
|
{
|
|
38817
39961
|
const probe2 = await getWorkspaceMemory({ baseUrl, pat, fetchImpl });
|
|
38818
39962
|
if (acked && stableHash(probe2.body_md) === acked.body_hash) {
|
|
@@ -38826,7 +39970,8 @@ async function pushWorkspaceMemory(args) {
|
|
|
38826
39970
|
if (retry.ok) {
|
|
38827
39971
|
const next2 = persistMemory(gated, {
|
|
38828
39972
|
body_hash: localHash,
|
|
38829
|
-
updated_at: retry.row.updated_at
|
|
39973
|
+
updated_at: retry.row.updated_at,
|
|
39974
|
+
generated: retry.row.generated
|
|
38830
39975
|
});
|
|
38831
39976
|
output?.log(`[WORKSPACE_MEMORY_PUT ws=${workspaceSlug} is_empty=${retry.row.is_empty}]`);
|
|
38832
39977
|
return { state: next2, outcome: { kind: "pushed" } };
|
|
@@ -38835,6 +39980,32 @@ async function pushWorkspaceMemory(args) {
|
|
|
38835
39980
|
output?.warn(`${WORKSPACE_MEMORY_FILENAME} is over the ${Math.round(MAX_WORKSPACE_MEMORY_BYTES / 1024)}KB cap — not synced.`);
|
|
38836
39981
|
return { state: gated, outcome: { kind: "over_cap" } };
|
|
38837
39982
|
}
|
|
39983
|
+
if (retry.code === "memory_is_generated") {
|
|
39984
|
+
const cloud = await getWorkspaceMemory({ baseUrl, pat, fetchImpl });
|
|
39985
|
+
const refused = refuseGeneratedMemoryEdit({
|
|
39986
|
+
state: gated,
|
|
39987
|
+
workspaceSlug,
|
|
39988
|
+
absolutePath: memoryFilePath(workspaceSlug),
|
|
39989
|
+
relPath: WORKSPACE_MEMORY_FILENAME,
|
|
39990
|
+
pile: GENERATED_PILE2,
|
|
39991
|
+
conflictEntity: CONFLICT_ENTITY2,
|
|
39992
|
+
conflictSlug: CONFLICT_SLUG2,
|
|
39993
|
+
localContent: local,
|
|
39994
|
+
canonicalBody: cloud.body_md,
|
|
39995
|
+
writeCanonical: (body) => writeLocal2(workspaceSlug, body, args.suppressor),
|
|
39996
|
+
now,
|
|
39997
|
+
output
|
|
39998
|
+
});
|
|
39999
|
+
const next2 = persistMemory(refused.state, {
|
|
40000
|
+
body_hash: stableHash(cloud.body_md),
|
|
40001
|
+
updated_at: cloud.updated_at,
|
|
40002
|
+
generated: cloud.generated
|
|
40003
|
+
});
|
|
40004
|
+
return {
|
|
40005
|
+
state: next2,
|
|
40006
|
+
outcome: { kind: "generated_refused", stashPath: refused.stashPath }
|
|
40007
|
+
};
|
|
40008
|
+
}
|
|
38838
40009
|
}
|
|
38839
40010
|
}
|
|
38840
40011
|
const stashPath = stashConflict({
|
|
@@ -38860,6 +40031,7 @@ var SingletonProjectionSchema = exports_external.object({
|
|
|
38860
40031
|
body_md: exports_external.string(),
|
|
38861
40032
|
is_empty: exports_external.boolean(),
|
|
38862
40033
|
status: SingletonStatusSchema.optional().catch(undefined),
|
|
40034
|
+
generated: exports_external.boolean().optional().catch(undefined),
|
|
38863
40035
|
updated_at: exports_external.string().datetime({ offset: true }).nullable(),
|
|
38864
40036
|
created_at: exports_external.string().datetime({ offset: true }).nullable(),
|
|
38865
40037
|
last_edited_by: exports_external.string().uuid().nullable()
|
|
@@ -38870,7 +40042,7 @@ var PreconditionSchema4 = exports_external.object({
|
|
|
38870
40042
|
code: exports_external.literal("precondition_failed"),
|
|
38871
40043
|
current_updated_at: exports_external.string()
|
|
38872
40044
|
});
|
|
38873
|
-
function
|
|
40045
|
+
function baseHeaders5(pat) {
|
|
38874
40046
|
return { Authorization: `Bearer ${pat}`, Accept: "application/json" };
|
|
38875
40047
|
}
|
|
38876
40048
|
async function serverErrorMessage(res, fallback) {
|
|
@@ -38888,7 +40060,7 @@ function createSingletonApi(config2) {
|
|
|
38888
40060
|
const url2 = `${baseUrl.replace(/\/$/, "")}${route}`;
|
|
38889
40061
|
let res;
|
|
38890
40062
|
try {
|
|
38891
|
-
res = await fetchImpl(url2, { method: "GET", headers:
|
|
40063
|
+
res = await fetchImpl(url2, { method: "GET", headers: baseHeaders5(pat) });
|
|
38892
40064
|
} catch (err) {
|
|
38893
40065
|
const msg = err instanceof Error ? err.message : "network error";
|
|
38894
40066
|
throw new ApiError(0, `Could not reach ${url2}: ${msg}`);
|
|
@@ -38913,7 +40085,7 @@ function createSingletonApi(config2) {
|
|
|
38913
40085
|
const { baseUrl, pat, bodyMd, expectedUpdatedAt, fetchImpl = fetch } = args;
|
|
38914
40086
|
const url2 = `${baseUrl.replace(/\/$/, "")}${route}`;
|
|
38915
40087
|
const headers = {
|
|
38916
|
-
...
|
|
40088
|
+
...baseHeaders5(pat),
|
|
38917
40089
|
"Content-Type": "application/json"
|
|
38918
40090
|
};
|
|
38919
40091
|
if (expectedUpdatedAt)
|
|
@@ -38948,6 +40120,14 @@ function createSingletonApi(config2) {
|
|
|
38948
40120
|
}
|
|
38949
40121
|
if (res.status === 409) {
|
|
38950
40122
|
const json2 = await res.json();
|
|
40123
|
+
const generated = GeneratedRefusalSchema.safeParse(json2);
|
|
40124
|
+
if (generated.success) {
|
|
40125
|
+
return {
|
|
40126
|
+
ok: false,
|
|
40127
|
+
code: "memory_is_generated",
|
|
40128
|
+
serverMessage: generated.data.error
|
|
40129
|
+
};
|
|
40130
|
+
}
|
|
38951
40131
|
const parsed = PreconditionSchema4.safeParse(json2);
|
|
38952
40132
|
if (!parsed.success) {
|
|
38953
40133
|
throw new ApiError(409, `put ${surfaceName} 409 body did not match precondition_failed shape`);
|
|
@@ -38970,7 +40150,7 @@ function createSingletonApi(config2) {
|
|
|
38970
40150
|
}
|
|
38971
40151
|
|
|
38972
40152
|
// src/lib/singleton/engine.ts
|
|
38973
|
-
import { relative as
|
|
40153
|
+
import { relative as relative13 } from "node:path";
|
|
38974
40154
|
function filePath(descriptor, workspaceSlug) {
|
|
38975
40155
|
return contextFilePath(workspaceSlug, descriptor.filename);
|
|
38976
40156
|
}
|
|
@@ -38986,7 +40166,8 @@ function ackedFrom(projection, bodyHash) {
|
|
|
38986
40166
|
return {
|
|
38987
40167
|
body_hash: bodyHash,
|
|
38988
40168
|
updated_at: projection.updated_at,
|
|
38989
|
-
status: projection.status
|
|
40169
|
+
status: projection.status,
|
|
40170
|
+
generated: projection.generated
|
|
38990
40171
|
};
|
|
38991
40172
|
}
|
|
38992
40173
|
function persist2(descriptor, state, entry2) {
|
|
@@ -39002,7 +40183,7 @@ function classifySingletonPath(descriptor, args) {
|
|
|
39002
40183
|
} catch {
|
|
39003
40184
|
return false;
|
|
39004
40185
|
}
|
|
39005
|
-
return
|
|
40186
|
+
return relative13(root, args.absolutePath) === descriptor.filename;
|
|
39006
40187
|
}
|
|
39007
40188
|
async function pullSingleton(descriptor, args) {
|
|
39008
40189
|
const { workspaceSlug, baseUrl, pat, state, fetchImpl, output, suppressor } = args;
|
|
@@ -39017,9 +40198,29 @@ async function pullSingleton(descriptor, args) {
|
|
|
39017
40198
|
const ackedHash = descriptor.getState(state)?.body_hash;
|
|
39018
40199
|
const localHash = local !== null ? stableHash(local) : null;
|
|
39019
40200
|
if (localHash !== null && localHash === cloudHash) {
|
|
39020
|
-
const { state:
|
|
40201
|
+
const { state: stamped } = descriptor.setState(state, ackedFrom(cloud, cloudHash));
|
|
40202
|
+
const next2 = descriptor.generatedPile === null ? stamped : clearMemoryGeneratedRefusal(stamped, workspaceSlug, filePath(descriptor, workspaceSlug));
|
|
39021
40203
|
return { state: next2, action: "noop" };
|
|
39022
40204
|
}
|
|
40205
|
+
if (descriptor.generatedPile !== null && cloud.generated === true && local !== null && !localEmpty && localHash !== ackedHash) {
|
|
40206
|
+
const refused = refuseGeneratedMemoryEdit({
|
|
40207
|
+
state,
|
|
40208
|
+
workspaceSlug,
|
|
40209
|
+
absolutePath: filePath(descriptor, workspaceSlug),
|
|
40210
|
+
relPath: descriptor.filename,
|
|
40211
|
+
pile: descriptor.generatedPile,
|
|
40212
|
+
conflictEntity: descriptor.conflictEntity,
|
|
40213
|
+
conflictSlug: descriptor.conflictSlug,
|
|
40214
|
+
localContent: local,
|
|
40215
|
+
canonicalBody: cloud.body_md,
|
|
40216
|
+
writeCanonical: (body) => writeLocal3(descriptor, workspaceSlug, body, suppressor),
|
|
40217
|
+
now,
|
|
40218
|
+
output
|
|
40219
|
+
});
|
|
40220
|
+
const { state: next2 } = descriptor.setState(refused.state, ackedFrom(cloud, cloudHash));
|
|
40221
|
+
output?.log(`[${descriptor.logTag}_PULL_GENERATED_REFUSED ws=${workspaceSlug} stashed]`);
|
|
40222
|
+
return { state: next2, action: "written" };
|
|
40223
|
+
}
|
|
39023
40224
|
if (local !== null && ackedHash !== undefined && cloudHash === ackedHash) {
|
|
39024
40225
|
output?.log(`[${descriptor.logTag}_PULL_PRESERVED ws=${workspaceSlug} reason=pending_push]`);
|
|
39025
40226
|
return { state, action: "preserved" };
|
|
@@ -39058,6 +40259,28 @@ async function pushSingleton(descriptor, args) {
|
|
|
39058
40259
|
const localHash = stableHash(local);
|
|
39059
40260
|
const acked = descriptor.getState(state);
|
|
39060
40261
|
const alreadyAcked = Boolean(acked && acked.body_hash === localHash);
|
|
40262
|
+
if (descriptor.generatedPile !== null && acked?.generated === true && !alreadyAcked) {
|
|
40263
|
+
const cloud = await descriptor.api.get({ baseUrl, pat, fetchImpl });
|
|
40264
|
+
const refused = refuseGeneratedMemoryEdit({
|
|
40265
|
+
state,
|
|
40266
|
+
workspaceSlug,
|
|
40267
|
+
absolutePath: filePath(descriptor, workspaceSlug),
|
|
40268
|
+
relPath: descriptor.filename,
|
|
40269
|
+
pile: descriptor.generatedPile,
|
|
40270
|
+
conflictEntity: descriptor.conflictEntity,
|
|
40271
|
+
conflictSlug: descriptor.conflictSlug,
|
|
40272
|
+
localContent: local,
|
|
40273
|
+
canonicalBody: cloud.body_md,
|
|
40274
|
+
writeCanonical: (body) => writeLocal3(descriptor, workspaceSlug, body, args.suppressor),
|
|
40275
|
+
now,
|
|
40276
|
+
output
|
|
40277
|
+
});
|
|
40278
|
+
const next2 = persist2(descriptor, refused.state, ackedFrom(cloud, stableHash(cloud.body_md)));
|
|
40279
|
+
return {
|
|
40280
|
+
state: next2,
|
|
40281
|
+
outcome: { kind: "generated_refused", stashPath: refused.stashPath }
|
|
40282
|
+
};
|
|
40283
|
+
}
|
|
39061
40284
|
let gated = state;
|
|
39062
40285
|
if (descriptor.secretScanned) {
|
|
39063
40286
|
const gate = applySecretGate({
|
|
@@ -39098,6 +40321,29 @@ async function pushSingleton(descriptor, args) {
|
|
|
39098
40321
|
output?.warn(`${descriptor.filename} is over the ${Math.round(descriptor.maxBytes / 1024)}KB cap — not synced.`);
|
|
39099
40322
|
return { state: gated, outcome: { kind: "over_cap" } };
|
|
39100
40323
|
}
|
|
40324
|
+
if (res.code === "memory_is_generated") {
|
|
40325
|
+
const cloud = await descriptor.api.get({ baseUrl, pat, fetchImpl });
|
|
40326
|
+
const refused = refuseGeneratedMemoryEdit({
|
|
40327
|
+
state: gated,
|
|
40328
|
+
workspaceSlug,
|
|
40329
|
+
absolutePath: filePath(descriptor, workspaceSlug),
|
|
40330
|
+
relPath: descriptor.filename,
|
|
40331
|
+
pile: descriptor.generatedPile ?? "shared",
|
|
40332
|
+
conflictEntity: descriptor.conflictEntity,
|
|
40333
|
+
conflictSlug: descriptor.conflictSlug,
|
|
40334
|
+
localContent: local,
|
|
40335
|
+
canonicalBody: cloud.body_md,
|
|
40336
|
+
writeCanonical: (body) => writeLocal3(descriptor, workspaceSlug, body, args.suppressor),
|
|
40337
|
+
now,
|
|
40338
|
+
output
|
|
40339
|
+
});
|
|
40340
|
+
output?.log(`[${descriptor.logTag}_PUT_REFUSED ws=${workspaceSlug} code=memory_is_generated]`);
|
|
40341
|
+
const next2 = persist2(descriptor, refused.state, ackedFrom(cloud, stableHash(cloud.body_md)));
|
|
40342
|
+
return {
|
|
40343
|
+
state: next2,
|
|
40344
|
+
outcome: { kind: "generated_refused", stashPath: refused.stashPath }
|
|
40345
|
+
};
|
|
40346
|
+
}
|
|
39101
40347
|
{
|
|
39102
40348
|
const probe2 = await descriptor.api.get({ baseUrl, pat, fetchImpl });
|
|
39103
40349
|
if (acked && stableHash(probe2.body_md) === acked.body_hash) {
|
|
@@ -39117,6 +40363,28 @@ async function pushSingleton(descriptor, args) {
|
|
|
39117
40363
|
output?.warn(`${descriptor.filename} is over the ${Math.round(descriptor.maxBytes / 1024)}KB cap — not synced.`);
|
|
39118
40364
|
return { state: gated, outcome: { kind: "over_cap" } };
|
|
39119
40365
|
}
|
|
40366
|
+
if (retry.code === "memory_is_generated") {
|
|
40367
|
+
const cloud = await descriptor.api.get({ baseUrl, pat, fetchImpl });
|
|
40368
|
+
const refused = refuseGeneratedMemoryEdit({
|
|
40369
|
+
state: gated,
|
|
40370
|
+
workspaceSlug,
|
|
40371
|
+
absolutePath: filePath(descriptor, workspaceSlug),
|
|
40372
|
+
relPath: descriptor.filename,
|
|
40373
|
+
pile: descriptor.generatedPile ?? "shared",
|
|
40374
|
+
conflictEntity: descriptor.conflictEntity,
|
|
40375
|
+
conflictSlug: descriptor.conflictSlug,
|
|
40376
|
+
localContent: local,
|
|
40377
|
+
canonicalBody: cloud.body_md,
|
|
40378
|
+
writeCanonical: (body) => writeLocal3(descriptor, workspaceSlug, body, args.suppressor),
|
|
40379
|
+
now,
|
|
40380
|
+
output
|
|
40381
|
+
});
|
|
40382
|
+
const next2 = persist2(descriptor, refused.state, ackedFrom(cloud, stableHash(cloud.body_md)));
|
|
40383
|
+
return {
|
|
40384
|
+
state: next2,
|
|
40385
|
+
outcome: { kind: "generated_refused", stashPath: refused.stashPath }
|
|
40386
|
+
};
|
|
40387
|
+
}
|
|
39120
40388
|
}
|
|
39121
40389
|
}
|
|
39122
40390
|
const stashPath = stashConflict({
|
|
@@ -39150,6 +40418,7 @@ var USER_MEMORY_DESCRIPTOR = {
|
|
|
39150
40418
|
logTag: "USER_MEMORY",
|
|
39151
40419
|
maxBytes: MAX_USER_MEMORY_BYTES,
|
|
39152
40420
|
secretScanned: true,
|
|
40421
|
+
generatedPile: "private",
|
|
39153
40422
|
extractActive: extractUserMemory,
|
|
39154
40423
|
getState: getUserMemoryState,
|
|
39155
40424
|
setState: setUserMemoryState,
|
|
@@ -39183,6 +40452,7 @@ var VOICE_DESCRIPTOR = {
|
|
|
39183
40452
|
logTag: "VOICE",
|
|
39184
40453
|
maxBytes: MAX_USER_VOICE_BYTES,
|
|
39185
40454
|
secretScanned: false,
|
|
40455
|
+
generatedPile: null,
|
|
39186
40456
|
extractActive: extractVoiceProfile,
|
|
39187
40457
|
getState: getUserVoiceState,
|
|
39188
40458
|
setState: setUserVoiceState,
|
|
@@ -39329,6 +40599,11 @@ import { join as join16 } from "node:path";
|
|
|
39329
40599
|
// src/lib/conflicts-view.ts
|
|
39330
40600
|
import { existsSync as existsSync5 } from "node:fs";
|
|
39331
40601
|
import { join as join15 } from "node:path";
|
|
40602
|
+
var NO_GENERATED_MEMORY = {
|
|
40603
|
+
artistMemoryGenerated: () => false,
|
|
40604
|
+
workspaceMemoryGenerated: () => false,
|
|
40605
|
+
userMemoryGenerated: () => false
|
|
40606
|
+
};
|
|
39332
40607
|
var FOOTER = [
|
|
39333
40608
|
"---",
|
|
39334
40609
|
"",
|
|
@@ -39351,7 +40626,7 @@ function absolute(date5) {
|
|
|
39351
40626
|
function shellQuote2(s) {
|
|
39352
40627
|
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
39353
40628
|
}
|
|
39354
|
-
function resolveConflictRows(workspaceSlug, stashes) {
|
|
40629
|
+
function resolveConflictRows(workspaceSlug, stashes, generatedMemory = NO_GENERATED_MEMORY) {
|
|
39355
40630
|
let root;
|
|
39356
40631
|
try {
|
|
39357
40632
|
root = workspaceMirrorPath(workspaceSlug);
|
|
@@ -39359,6 +40634,9 @@ function resolveConflictRows(workspaceSlug, stashes) {
|
|
|
39359
40634
|
root = null;
|
|
39360
40635
|
}
|
|
39361
40636
|
return stashes.map((stash) => {
|
|
40637
|
+
const pile = isGeneratedMemoryStash(stash.label, generatedMemory);
|
|
40638
|
+
if (pile !== null)
|
|
40639
|
+
return { kind: "generated-memory", stash, pile };
|
|
39362
40640
|
if (root === null || !stash.label.startsWith("files/")) {
|
|
39363
40641
|
return { kind: "entity", stash };
|
|
39364
40642
|
}
|
|
@@ -39424,6 +40702,8 @@ function renderRow(row) {
|
|
|
39424
40702
|
out.push("Your version is at:", "", "```", stash.absPath, "```", "");
|
|
39425
40703
|
if (row.kind === "unsafe-path") {
|
|
39426
40704
|
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.");
|
|
40705
|
+
} else if (row.kind === "generated-memory") {
|
|
40706
|
+
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
40707
|
} else {
|
|
39428
40708
|
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
40709
|
}
|
|
@@ -39476,7 +40756,7 @@ ${FOOTER}
|
|
|
39476
40756
|
function refreshConflictsView(workspaceSlug) {
|
|
39477
40757
|
try {
|
|
39478
40758
|
const stashes = listConflictStashes(workspaceSlug);
|
|
39479
|
-
const rows = resolveConflictRows(workspaceSlug, stashes);
|
|
40759
|
+
const rows = resolveConflictRows(workspaceSlug, stashes, generatedMemoryStashDepsFrom(loadState(workspaceSlug)));
|
|
39480
40760
|
const body = renderConflictsView(rows);
|
|
39481
40761
|
if (readConflictsView(workspaceSlug) === body) {
|
|
39482
40762
|
return { stashes: rows.length, written: false };
|
|
@@ -39499,7 +40779,7 @@ import { existsSync as existsSync6, readFileSync as readFileSync22, statSync as
|
|
|
39499
40779
|
// src/lib/files/extracted-text.ts
|
|
39500
40780
|
import { createHash as createHash3 } from "node:crypto";
|
|
39501
40781
|
var DERIVED_TEXT_VERSION = 2;
|
|
39502
|
-
function
|
|
40782
|
+
function yamlString3(value) {
|
|
39503
40783
|
return JSON.stringify(value);
|
|
39504
40784
|
}
|
|
39505
40785
|
var PARTIAL_WARNING = [
|
|
@@ -39538,8 +40818,8 @@ function renderExtractedText(doc2) {
|
|
|
39538
40818
|
const frontmatter = [
|
|
39539
40819
|
"---",
|
|
39540
40820
|
"plinth_generated: true",
|
|
39541
|
-
`source_file: ${doc2.bytesInMirror ?
|
|
39542
|
-
`source_uri: ${doc2.sourceUri === undefined ? "unknown" : doc2.sourceUri === null ? "null" :
|
|
40821
|
+
`source_file: ${doc2.bytesInMirror ? yamlString3(`files/${doc2.relPath}`) : "null"}`,
|
|
40822
|
+
`source_uri: ${doc2.sourceUri === undefined ? "unknown" : doc2.sourceUri === null ? "null" : yamlString3(doc2.sourceUri)}`,
|
|
39543
40823
|
`content_quality: ${doc2.quality ?? "unchecked"}`,
|
|
39544
40824
|
`body_withheld: ${withheld}`,
|
|
39545
40825
|
`extracted_chars: ${withheld ? 0 : doc2.text.length}`,
|
|
@@ -40373,7 +41653,13 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
40373
41653
|
let cursor = startCursor;
|
|
40374
41654
|
let lastAdvancedCursor = startCursor;
|
|
40375
41655
|
let cursorBlocked = false;
|
|
40376
|
-
const ctr = {
|
|
41656
|
+
const ctr = {
|
|
41657
|
+
fetched: 0,
|
|
41658
|
+
written: 0,
|
|
41659
|
+
deleted: 0,
|
|
41660
|
+
moved: 0,
|
|
41661
|
+
refused: 0
|
|
41662
|
+
};
|
|
40377
41663
|
while (true) {
|
|
40378
41664
|
const page = await syncEntity({
|
|
40379
41665
|
strategy,
|
|
@@ -40407,6 +41693,12 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
40407
41693
|
ctr.written++;
|
|
40408
41694
|
else if (outcome.action === "deleted")
|
|
40409
41695
|
ctr.deleted++;
|
|
41696
|
+
else if (outcome.action === "moved")
|
|
41697
|
+
ctr.moved = (ctr.moved ?? 0) + 1;
|
|
41698
|
+
else if (outcome.action === "slug_refused") {
|
|
41699
|
+
ctr.refused = (ctr.refused ?? 0) + 1;
|
|
41700
|
+
cursorBlocked = true;
|
|
41701
|
+
}
|
|
40410
41702
|
}
|
|
40411
41703
|
commitPending();
|
|
40412
41704
|
if (page.next_cursor === null)
|
|
@@ -40526,6 +41818,27 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
40526
41818
|
const voiceWarning = voiceMalformedWarning(state, slug);
|
|
40527
41819
|
if (voiceWarning)
|
|
40528
41820
|
out.warn(voiceWarning);
|
|
41821
|
+
const factsResult = await syncMemoryFacts({
|
|
41822
|
+
workspaceSlug: slug,
|
|
41823
|
+
baseUrl: entry2.api_base_url,
|
|
41824
|
+
pat,
|
|
41825
|
+
state,
|
|
41826
|
+
fetchImpl,
|
|
41827
|
+
now,
|
|
41828
|
+
output: out,
|
|
41829
|
+
suppressor: deps.suppressor
|
|
41830
|
+
});
|
|
41831
|
+
if (factsResult.state !== state) {
|
|
41832
|
+
state = factsResult.state;
|
|
41833
|
+
}
|
|
41834
|
+
commitPending();
|
|
41835
|
+
if (factsResult.written > 0 || factsResult.deleted > 0) {
|
|
41836
|
+
counters["memory-facts"] = {
|
|
41837
|
+
fetched: factsResult.facts,
|
|
41838
|
+
written: factsResult.written,
|
|
41839
|
+
deleted: factsResult.deleted
|
|
41840
|
+
};
|
|
41841
|
+
}
|
|
40529
41842
|
} catch (err) {
|
|
40530
41843
|
out.log(pullFailedLine(categorizeError(err)));
|
|
40531
41844
|
if (err instanceof ApiError) {
|
|
@@ -40607,7 +41920,9 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
40607
41920
|
await syncFileRejections();
|
|
40608
41921
|
const totalWritten = Object.values(counters).reduce((sum, c) => sum + c.written, 0);
|
|
40609
41922
|
const totalDeleted = Object.values(counters).reduce((sum, c) => sum + c.deleted, 0);
|
|
40610
|
-
|
|
41923
|
+
const totalMoved = Object.values(counters).reduce((sum, c) => sum + (c.moved ?? 0), 0);
|
|
41924
|
+
const totalRefused = Object.values(counters).reduce((sum, c) => sum + (c.refused ?? 0), 0);
|
|
41925
|
+
if (totalWritten === 0 && totalDeleted === 0 && totalMoved === 0 && totalRefused === 0) {
|
|
40611
41926
|
out.success("Already up to date.");
|
|
40612
41927
|
return { exitCode: 0, applied: 0 };
|
|
40613
41928
|
}
|
|
@@ -40618,12 +41933,16 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
40618
41933
|
const parts = [`${c.fetched} fetched`];
|
|
40619
41934
|
if (c.written > 0)
|
|
40620
41935
|
parts.push(`${c.written} written`);
|
|
41936
|
+
if ((c.moved ?? 0) > 0)
|
|
41937
|
+
parts.push(`${c.moved} renamed`);
|
|
41938
|
+
if ((c.refused ?? 0) > 0)
|
|
41939
|
+
parts.push(`${c.refused} rename held`);
|
|
40621
41940
|
if (c.deleted > 0)
|
|
40622
41941
|
parts.push(`${c.deleted} deleted`);
|
|
40623
41942
|
out.info(` ${type}: ${parts.join(", ")}`);
|
|
40624
41943
|
}
|
|
40625
41944
|
out.success(`Done in ${elapsed}s.`);
|
|
40626
|
-
return { exitCode: 0, applied: totalWritten + totalDeleted };
|
|
41945
|
+
return { exitCode: 0, applied: totalWritten + totalMoved + totalDeleted };
|
|
40627
41946
|
}
|
|
40628
41947
|
function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = () => new Date, isFrozen, loggedTombstoneSkips) {
|
|
40629
41948
|
if (isFrozen?.(strategy.type, row.id)) {
|
|
@@ -40683,26 +42002,88 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
|
|
|
40683
42002
|
if (strategy.shouldMirror && !strategy.shouldMirror(live) && !existingEntry) {
|
|
40684
42003
|
return { state, action: "skipped" };
|
|
40685
42004
|
}
|
|
40686
|
-
|
|
42005
|
+
let relocation = { kind: "not_needed", writeSlug: live.slug };
|
|
42006
|
+
if (existingEntry && existingEntry.slug !== live.slug) {
|
|
42007
|
+
const echo = suppressor ? { watcher: "live", suppressor } : { watcher: "none" };
|
|
42008
|
+
relocation = relocateForSlugChange({
|
|
42009
|
+
workspaceSlug,
|
|
42010
|
+
strategy,
|
|
42011
|
+
fromSlug: existingEntry.slug,
|
|
42012
|
+
toSlug: live.slug,
|
|
42013
|
+
acked: {
|
|
42014
|
+
bodyHash: existingEntry.body_hash,
|
|
42015
|
+
frontmatterHash: existingEntry.frontmatter_hash
|
|
42016
|
+
},
|
|
42017
|
+
echo,
|
|
42018
|
+
now
|
|
42019
|
+
});
|
|
42020
|
+
const move = `entityType=${strategy.type} from=${existingEntry.slug} to=${live.slug} id=${live.id}`;
|
|
42021
|
+
switch (relocation.kind) {
|
|
42022
|
+
case "moved":
|
|
42023
|
+
out.log(`[SLUG_MOVE ${move}]`);
|
|
42024
|
+
state = clearSlugClash(state, [
|
|
42025
|
+
strategy.mirrorRelativePath(existingEntry.slug),
|
|
42026
|
+
strategy.mirrorRelativePath(live.slug)
|
|
42027
|
+
]);
|
|
42028
|
+
break;
|
|
42029
|
+
case "refused":
|
|
42030
|
+
out.log(`[SLUG_MOVE_REFUSED ${move} reason=${relocation.reason}]`);
|
|
42031
|
+
const occupantTracked = getEntryBySlug(state, strategy.type, live.slug) !== undefined;
|
|
42032
|
+
state = recordEntitySlugClash({
|
|
42033
|
+
state,
|
|
42034
|
+
mirrorRelativePath: strategy.mirrorRelativePath(existingEntry.slug),
|
|
42035
|
+
entityType: strategy.type,
|
|
42036
|
+
fromSlug: existingEntry.slug,
|
|
42037
|
+
toSlug: live.slug,
|
|
42038
|
+
reason: relocation.reason,
|
|
42039
|
+
occupantTracked,
|
|
42040
|
+
now
|
|
42041
|
+
});
|
|
42042
|
+
out.warn(relocation.reason === "source_unreadable" ? `${strategy.type}/${existingEntry.slug} was renamed to ${live.slug} in ` + `Plinth, but ${strategy.mirrorRelativePath(existingEntry.slug)} could ` + `not be read — it was left exactly where it is. Check its permissions.` : occupantTracked ? `${strategy.type}/${existingEntry.slug} was renamed to ${live.slug} in ` + `Plinth, but ${strategy.mirrorRelativePath(live.slug)} belongs to ` + `another ${strategy.type} — nothing was moved or overwritten. This ` + `usually clears itself once that row's own rename arrives; do not ` + `delete the file, which would delete that record in Plinth.` : `${strategy.type}/${existingEntry.slug} was renamed to ${live.slug} in ` + `Plinth, but ${strategy.mirrorRelativePath(live.slug)} already exists ` + `here — nothing was moved or overwritten. Rename or remove that file ` + `and Plinth will move this one into place.`);
|
|
42043
|
+
return { state, action: "slug_refused" };
|
|
42044
|
+
case "skipped":
|
|
42045
|
+
out.log(`[SLUG_MOVE_SKIP ${move} reason=${relocation.reason}]`);
|
|
42046
|
+
break;
|
|
42047
|
+
case "absent":
|
|
42048
|
+
case "not_needed":
|
|
42049
|
+
break;
|
|
42050
|
+
}
|
|
42051
|
+
const stashPath = relocation.kind === "moved" ? relocation.stashPath : undefined;
|
|
42052
|
+
if (stashPath) {
|
|
42053
|
+
out.warn(`Unsynced local edit on ${strategy.type}/${existingEntry.slug} → stashed ` + `to ${stashPath} (the row was renamed in Plinth; the cloud copy is ` + `authoritative).`);
|
|
42054
|
+
}
|
|
42055
|
+
}
|
|
42056
|
+
const writeSlug = relocation.writeSlug;
|
|
42057
|
+
if (existingEntry && existingEntry.updated_at === live.updated_at && relocation.kind === "not_needed") {
|
|
40687
42058
|
const path5 = entityFilePath(workspaceSlug, strategy.type, live.slug);
|
|
40688
42059
|
if (existsSync7(path5)) {
|
|
40689
42060
|
if (strategy.type === "artists") {
|
|
40690
|
-
|
|
42061
|
+
const artistLive = live;
|
|
42062
|
+
state = applyArtistMemoryOnPull({
|
|
40691
42063
|
workspaceSlug,
|
|
40692
|
-
live,
|
|
42064
|
+
live: artistLive,
|
|
40693
42065
|
ackedMemoryHash: existingEntry.memory_hash,
|
|
42066
|
+
state,
|
|
40694
42067
|
suppressor,
|
|
40695
42068
|
now,
|
|
40696
42069
|
out
|
|
40697
|
-
});
|
|
42070
|
+
}).state;
|
|
42071
|
+
if (existingEntry.memory_generated !== artistLive.memory_generated) {
|
|
42072
|
+
const bumped = upsertEntry(state, strategy.type, {
|
|
42073
|
+
...existingEntry,
|
|
42074
|
+
memory_generated: artistLive.memory_generated
|
|
42075
|
+
});
|
|
42076
|
+
if (bumped.changed)
|
|
42077
|
+
state = bumped.state;
|
|
42078
|
+
}
|
|
40698
42079
|
}
|
|
40699
42080
|
return { state, action: "noop_overwrite" };
|
|
40700
42081
|
}
|
|
40701
42082
|
}
|
|
40702
|
-
if (!suppressor && existingEntry && existingEntry.updated_at !== live.updated_at) {
|
|
42083
|
+
if (!suppressor && existingEntry && existingEntry.updated_at !== live.updated_at && relocation.kind !== "moved") {
|
|
40703
42084
|
let localRaw = null;
|
|
40704
42085
|
try {
|
|
40705
|
-
localRaw = readFileSync24(entityFilePath(workspaceSlug, strategy.type,
|
|
42086
|
+
localRaw = readFileSync24(entityFilePath(workspaceSlug, strategy.type, writeSlug), "utf8");
|
|
40706
42087
|
} catch {
|
|
40707
42088
|
localRaw = null;
|
|
40708
42089
|
}
|
|
@@ -40722,37 +42103,45 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
|
|
|
40722
42103
|
}
|
|
40723
42104
|
}
|
|
40724
42105
|
}
|
|
40725
|
-
const content = writeMirrorWithBodyPreserve(workspaceSlug, strategy, live, suppressor);
|
|
42106
|
+
const content = writeMirrorWithBodyPreserve(workspaceSlug, strategy, live, suppressor, writeSlug);
|
|
40726
42107
|
const hashes = computeMirrorHashes(strategy, content);
|
|
40727
42108
|
let inode;
|
|
40728
42109
|
try {
|
|
40729
|
-
inode = statSync16(entityFilePath(workspaceSlug, strategy.type,
|
|
42110
|
+
inode = statSync16(entityFilePath(workspaceSlug, strategy.type, writeSlug)).ino;
|
|
40730
42111
|
} catch {
|
|
40731
42112
|
inode = undefined;
|
|
40732
42113
|
}
|
|
40733
42114
|
let memoryHash;
|
|
42115
|
+
let memoryGenerated;
|
|
40734
42116
|
if (strategy.type === "artists") {
|
|
40735
|
-
|
|
42117
|
+
const artistLive = live;
|
|
42118
|
+
const applied = applyArtistMemoryOnPull({
|
|
40736
42119
|
workspaceSlug,
|
|
40737
|
-
live,
|
|
42120
|
+
live: artistLive,
|
|
40738
42121
|
ackedMemoryHash: existingEntry?.memory_hash,
|
|
42122
|
+
state,
|
|
40739
42123
|
suppressor,
|
|
40740
42124
|
now,
|
|
40741
42125
|
out
|
|
40742
|
-
})
|
|
42126
|
+
});
|
|
42127
|
+
memoryHash = applied.memoryHash;
|
|
42128
|
+
state = applied.state;
|
|
42129
|
+
memoryGenerated = artistLive.memory_generated;
|
|
40743
42130
|
}
|
|
40744
42131
|
const { state: nextState } = upsertEntry(state, strategy.type, {
|
|
40745
42132
|
id: live.id,
|
|
40746
|
-
slug:
|
|
42133
|
+
slug: writeSlug,
|
|
40747
42134
|
updated_at: live.updated_at,
|
|
40748
42135
|
revision: live.revision,
|
|
40749
42136
|
body_hash: hashes.bodyHash,
|
|
40750
42137
|
frontmatter_hash: hashes.frontmatterHash,
|
|
40751
42138
|
inode,
|
|
40752
42139
|
body_len: hashes.bodyLen,
|
|
40753
|
-
memory_hash: memoryHash
|
|
42140
|
+
memory_hash: memoryHash,
|
|
42141
|
+
memory_generated: memoryGenerated
|
|
40754
42142
|
});
|
|
40755
|
-
|
|
42143
|
+
const action = relocation.kind === "moved" ? "moved" : "written";
|
|
42144
|
+
return { state: nextState, action };
|
|
40756
42145
|
}
|
|
40757
42146
|
var syncCommand = defineCommand({
|
|
40758
42147
|
meta: {
|
|
@@ -41228,6 +42617,22 @@ function makeWatchHandler(deps) {
|
|
|
41228
42617
|
});
|
|
41229
42618
|
if (!classifiedFrom)
|
|
41230
42619
|
return;
|
|
42620
|
+
if (suppressor?.shouldSuppressDelete(event.fromPath)) {
|
|
42621
|
+
const strategy = getStrategy(classifiedFrom.entityType);
|
|
42622
|
+
let toRaw = null;
|
|
42623
|
+
try {
|
|
42624
|
+
toRaw = readFileSync25(event.toPath, "utf8");
|
|
42625
|
+
} catch {
|
|
42626
|
+
toRaw = null;
|
|
42627
|
+
}
|
|
42628
|
+
const isEcho = strategy !== undefined && toRaw !== null && suppressor.shouldSuppressUpsert(event.toPath, computeMirrorHashes(strategy, toRaw));
|
|
42629
|
+
if (isEcho) {
|
|
42630
|
+
output?.log(`[RENAME_SUPPRESSED entityType=${classifiedFrom.entityType} ` + `from=${event.fromSlug} id=${event.id} reason=pull_echo]`);
|
|
42631
|
+
return;
|
|
42632
|
+
}
|
|
42633
|
+
await handle({ kind: "upsert", absolutePath: event.toPath });
|
|
42634
|
+
return;
|
|
42635
|
+
}
|
|
41231
42636
|
const result2 = await pushRename({
|
|
41232
42637
|
workspaceSlug,
|
|
41233
42638
|
baseUrl,
|
|
@@ -41239,7 +42644,13 @@ function makeWatchHandler(deps) {
|
|
|
41239
42644
|
expectedUpdatedAt: event.expectedUpdatedAt,
|
|
41240
42645
|
fromSlug: event.fromSlug,
|
|
41241
42646
|
state: getState(),
|
|
41242
|
-
deps: {
|
|
42647
|
+
deps: {
|
|
42648
|
+
fetchImpl,
|
|
42649
|
+
now,
|
|
42650
|
+
mutex,
|
|
42651
|
+
output,
|
|
42652
|
+
echo: suppressor ? { watcher: "live", suppressor } : undefined
|
|
42653
|
+
}
|
|
41243
42654
|
});
|
|
41244
42655
|
setState(result2.state);
|
|
41245
42656
|
reportOutcome(spinner, workspaceSlug, result2.state, result2.outcome);
|
|
@@ -41272,6 +42683,30 @@ function makeWatchHandler(deps) {
|
|
|
41272
42683
|
await handleArtistMemoryEvent(memoryMatch.slug, absolutePath, event.kind === "upsert");
|
|
41273
42684
|
return;
|
|
41274
42685
|
}
|
|
42686
|
+
const factPath = classifyMemoryFactPath({ workspaceSlug, absolutePath });
|
|
42687
|
+
if (factPath) {
|
|
42688
|
+
let ownEcho = false;
|
|
42689
|
+
if (event.kind === "upsert") {
|
|
42690
|
+
try {
|
|
42691
|
+
ownEcho = suppressor?.shouldSuppressUpsert(absolutePath, {
|
|
42692
|
+
bodyHash: stableHash(readFileSync25(absolutePath, "utf8")),
|
|
42693
|
+
frontmatterHash: ""
|
|
42694
|
+
}) === true;
|
|
42695
|
+
} catch {
|
|
42696
|
+
ownEcho = true;
|
|
42697
|
+
}
|
|
42698
|
+
}
|
|
42699
|
+
const warning2 = memoryFactEventWarning(classifyMemoryFactEvent({
|
|
42700
|
+
path: factPath,
|
|
42701
|
+
kind: event.kind === "upsert" ? "upsert" : "unlink",
|
|
42702
|
+
ownEcho,
|
|
42703
|
+
known: getMemoryFactEntry(getState(), factPath.relPath) !== undefined
|
|
42704
|
+
}));
|
|
42705
|
+
if (warning2)
|
|
42706
|
+
spinner.warn(warning2);
|
|
42707
|
+
spinner.update(`Watching ${workspaceSlug} · ${watchingSummary(getState())}`);
|
|
42708
|
+
return;
|
|
42709
|
+
}
|
|
41275
42710
|
const classified = classifyMirrorPath({ workspaceSlug, absolutePath });
|
|
41276
42711
|
if (!classified) {
|
|
41277
42712
|
const key = entitySymlinkRefusalKey({ workspaceSlug, absolutePath });
|
|
@@ -41347,7 +42782,14 @@ function makeWatchHandler(deps) {
|
|
|
41347
42782
|
slug: event.stateEntry.slug,
|
|
41348
42783
|
updated_at: event.stateEntry.updated_at
|
|
41349
42784
|
} : null,
|
|
41350
|
-
deps: {
|
|
42785
|
+
deps: {
|
|
42786
|
+
fetchImpl,
|
|
42787
|
+
now,
|
|
42788
|
+
mutex,
|
|
42789
|
+
output,
|
|
42790
|
+
suppressor,
|
|
42791
|
+
echo: suppressor ? { watcher: "live", suppressor } : undefined
|
|
42792
|
+
}
|
|
41351
42793
|
});
|
|
41352
42794
|
setState(clearEntityRejected(result.state, workspaceSlug, absolutePath));
|
|
41353
42795
|
reportOutcome(spinner, workspaceSlug, getState(), result.outcome);
|
|
@@ -41408,7 +42850,8 @@ async function flushHeldEntityOp(deps) {
|
|
|
41408
42850
|
fetchImpl: deps.fetchImpl,
|
|
41409
42851
|
now,
|
|
41410
42852
|
mutex: deps.mutex,
|
|
41411
|
-
output: deps.output
|
|
42853
|
+
output: deps.output,
|
|
42854
|
+
echo: deps.echo
|
|
41412
42855
|
}
|
|
41413
42856
|
});
|
|
41414
42857
|
deps.setState(result.state);
|
|
@@ -41544,6 +42987,7 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
41544
42987
|
frozenFiles = computeFrozenFiles(slug);
|
|
41545
42988
|
};
|
|
41546
42989
|
const recon = await reconcileDivergedRows({
|
|
42990
|
+
echo: { watcher: "none" },
|
|
41547
42991
|
workspaceSlug: slug,
|
|
41548
42992
|
baseUrl: entry2.api_base_url,
|
|
41549
42993
|
pat,
|
|
@@ -41757,6 +43201,7 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
41757
43201
|
spinner.start(`Watching ${slug} · ${watchingSummary(state)}`);
|
|
41758
43202
|
const mutex = new PerIdMutex;
|
|
41759
43203
|
const bootDrain = await drainResolvedBatches({
|
|
43204
|
+
echo: { watcher: "none" },
|
|
41760
43205
|
workspaceSlug: slug,
|
|
41761
43206
|
baseUrl: entry2.api_base_url,
|
|
41762
43207
|
pat,
|
|
@@ -41774,6 +43219,7 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
41774
43219
|
out.info(`Resolved ${bootDrain.applied + bootDrain.discarded} held batch${bootDrain.applied + bootDrain.discarded === 1 ? "" : "es"}: ${bootDrain.applied} applied, ${bootDrain.discarded} discarded.`);
|
|
41775
43220
|
}
|
|
41776
43221
|
const bootUnsynced = await drainUnsyncedOps({
|
|
43222
|
+
echo: { watcher: "none" },
|
|
41777
43223
|
workspaceSlug: slug,
|
|
41778
43224
|
baseUrl: entry2.api_base_url,
|
|
41779
43225
|
pat,
|
|
@@ -41811,6 +43257,7 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
41811
43257
|
};
|
|
41812
43258
|
const suppressor = new PullSuppressor;
|
|
41813
43259
|
const flushHeldOp = (op) => flushHeldEntityOp({
|
|
43260
|
+
echo: { watcher: "live", suppressor },
|
|
41814
43261
|
workspaceSlug: slug,
|
|
41815
43262
|
baseUrl: entry2.api_base_url,
|
|
41816
43263
|
pat,
|
|
@@ -41998,6 +43445,7 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
41998
43445
|
draining = true;
|
|
41999
43446
|
try {
|
|
42000
43447
|
const drained = await drainResolvedBatches({
|
|
43448
|
+
echo: { watcher: "live", suppressor },
|
|
42001
43449
|
workspaceSlug: slug,
|
|
42002
43450
|
baseUrl: entry2.api_base_url,
|
|
42003
43451
|
pat,
|
|
@@ -42015,6 +43463,7 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
42015
43463
|
spinner.update(`Watching ${slug} · ${watchingSummary(state)}`);
|
|
42016
43464
|
}
|
|
42017
43465
|
const redriven = await drainUnsyncedOps({
|
|
43466
|
+
echo: { watcher: "live", suppressor },
|
|
42018
43467
|
workspaceSlug: slug,
|
|
42019
43468
|
baseUrl: entry2.api_base_url,
|
|
42020
43469
|
pat,
|
|
@@ -42459,6 +43908,305 @@ var startCommand = defineCommand({
|
|
|
42459
43908
|
// src/commands/status.ts
|
|
42460
43909
|
import { statSync as statSync19 } from "node:fs";
|
|
42461
43910
|
|
|
43911
|
+
// src/lib/context/context-walls.ts
|
|
43912
|
+
function prepare(raw) {
|
|
43913
|
+
const text = stripContactsBlock(raw.replace(/^---\r?\n(?:[\s\S]*?\r?\n)?---\r?\n/, ""));
|
|
43914
|
+
return text.replace(/<!--[\s\S]*?-->/g, "").replace(/<!--[\s\S]*$/, "");
|
|
43915
|
+
}
|
|
43916
|
+
function indentOf(line) {
|
|
43917
|
+
let cols = 0;
|
|
43918
|
+
for (const ch of line) {
|
|
43919
|
+
if (ch === " ")
|
|
43920
|
+
cols += 1;
|
|
43921
|
+
else if (ch === "\t")
|
|
43922
|
+
cols += 4;
|
|
43923
|
+
else
|
|
43924
|
+
break;
|
|
43925
|
+
}
|
|
43926
|
+
return cols;
|
|
43927
|
+
}
|
|
43928
|
+
var HEADING_RE = /^(#{1,6})\s+(.*?)\s*#*\s*$/;
|
|
43929
|
+
var ITEM_RE = /^(\s*)(?:[-*+]|\d{1,3}[.)])\s+(.*)$/;
|
|
43930
|
+
var FENCE_RE = /^\s*(?:`{3,}|~{3,})/;
|
|
43931
|
+
var TABLE_RE = /^\s*\|/;
|
|
43932
|
+
var LABEL_RE = /^[^\p{L}\p{N}*_`]*\*\*(.+?)\*\*/u;
|
|
43933
|
+
var UPCOMING_RE = /upcoming/i;
|
|
43934
|
+
var CURRENT_STATE_RE = /^current[\s-]+state\b/i;
|
|
43935
|
+
var SESSION_LOG_RE = /^session[\s-]+log$/i;
|
|
43936
|
+
function latestOf(dates) {
|
|
43937
|
+
let latest = null;
|
|
43938
|
+
for (const d of dates) {
|
|
43939
|
+
if (latest === null || utcMs(d) > utcMs(latest))
|
|
43940
|
+
latest = d;
|
|
43941
|
+
}
|
|
43942
|
+
return latest;
|
|
43943
|
+
}
|
|
43944
|
+
function openingOf(text) {
|
|
43945
|
+
const safe = terminalSafeText(text, 400);
|
|
43946
|
+
const chars = [...safe];
|
|
43947
|
+
if (chars.length <= 60)
|
|
43948
|
+
return safe;
|
|
43949
|
+
return `${chars.slice(0, 60).join("").trimEnd()}…`;
|
|
43950
|
+
}
|
|
43951
|
+
function safeLabel(text) {
|
|
43952
|
+
return terminalSafeText(text, 120);
|
|
43953
|
+
}
|
|
43954
|
+
function scanContextText(raw, today, opts = {}) {
|
|
43955
|
+
const scanner = new ContextScanner(today, opts.initialHeading ?? null);
|
|
43956
|
+
for (const line of prepare(raw).split(/\r?\n/))
|
|
43957
|
+
scanner.line(line);
|
|
43958
|
+
return scanner.finish();
|
|
43959
|
+
}
|
|
43960
|
+
|
|
43961
|
+
class ContextScanner {
|
|
43962
|
+
today;
|
|
43963
|
+
todayMs;
|
|
43964
|
+
heading;
|
|
43965
|
+
block = null;
|
|
43966
|
+
blockDates = [];
|
|
43967
|
+
blockRegion = null;
|
|
43968
|
+
blockIsLabel = false;
|
|
43969
|
+
open = [];
|
|
43970
|
+
fence = false;
|
|
43971
|
+
lastBlank = true;
|
|
43972
|
+
sessionLogLevel = null;
|
|
43973
|
+
walls = [];
|
|
43974
|
+
played = [];
|
|
43975
|
+
asOf = [];
|
|
43976
|
+
constructor(today, initialHeading) {
|
|
43977
|
+
this.today = today;
|
|
43978
|
+
this.todayMs = utcMs(today);
|
|
43979
|
+
this.heading = initialHeading;
|
|
43980
|
+
}
|
|
43981
|
+
line(line) {
|
|
43982
|
+
if (FENCE_RE.test(line)) {
|
|
43983
|
+
this.fence = !this.fence;
|
|
43984
|
+
this.flushBlock();
|
|
43985
|
+
this.lastBlank = false;
|
|
43986
|
+
return;
|
|
43987
|
+
}
|
|
43988
|
+
if (this.fence)
|
|
43989
|
+
return;
|
|
43990
|
+
if (/^\s*$/.test(line)) {
|
|
43991
|
+
this.flushBlock();
|
|
43992
|
+
this.lastBlank = true;
|
|
43993
|
+
return;
|
|
43994
|
+
}
|
|
43995
|
+
const h = HEADING_RE.exec(line);
|
|
43996
|
+
if (h) {
|
|
43997
|
+
this.onHeading(h[1].length, h[2]);
|
|
43998
|
+
return;
|
|
43999
|
+
}
|
|
44000
|
+
if (this.sessionLogLevel !== null)
|
|
44001
|
+
return;
|
|
44002
|
+
if (TABLE_RE.test(line)) {
|
|
44003
|
+
this.onTableRow(line);
|
|
44004
|
+
return;
|
|
44005
|
+
}
|
|
44006
|
+
const item = ITEM_RE.exec(line);
|
|
44007
|
+
if (item) {
|
|
44008
|
+
this.onItem(line, item[2]);
|
|
44009
|
+
return;
|
|
44010
|
+
}
|
|
44011
|
+
this.onText(line);
|
|
44012
|
+
}
|
|
44013
|
+
finish() {
|
|
44014
|
+
this.flushBlock();
|
|
44015
|
+
this.closeWhere(() => true);
|
|
44016
|
+
return { walls: this.walls, played: this.played, asOf: this.asOf };
|
|
44017
|
+
}
|
|
44018
|
+
onHeading(level, label) {
|
|
44019
|
+
this.flushBlock();
|
|
44020
|
+
this.closeWhere((r) => r.kind === "item" || r.depth >= level);
|
|
44021
|
+
this.lastBlank = false;
|
|
44022
|
+
if (this.sessionLogLevel !== null && level > this.sessionLogLevel)
|
|
44023
|
+
return;
|
|
44024
|
+
this.sessionLogLevel = SESSION_LOG_RE.test(label.trim()) ? level : null;
|
|
44025
|
+
if (this.sessionLogLevel !== null)
|
|
44026
|
+
return;
|
|
44027
|
+
this.open.push({
|
|
44028
|
+
kind: "heading",
|
|
44029
|
+
depth: level,
|
|
44030
|
+
label,
|
|
44031
|
+
upcoming: UPCOMING_RE.test(label),
|
|
44032
|
+
played: [],
|
|
44033
|
+
childBlocks: 0,
|
|
44034
|
+
ownLatest: null
|
|
44035
|
+
});
|
|
44036
|
+
this.heading = label;
|
|
44037
|
+
if (CURRENT_STATE_RE.test(label))
|
|
44038
|
+
this.noteAsOf(label);
|
|
44039
|
+
}
|
|
44040
|
+
noteAsOf(label) {
|
|
44041
|
+
const dated = readAsOf(label, this.today);
|
|
44042
|
+
if (dated === null) {
|
|
44043
|
+
this.asOf.push({ heading: safeLabel(label), daysOld: null });
|
|
44044
|
+
return;
|
|
44045
|
+
}
|
|
44046
|
+
const age = daysBetween2(dated.parts, this.today);
|
|
44047
|
+
if (age > CURRENT_STATE_AS_OF_MAX_DAYS || age < 0) {
|
|
44048
|
+
this.asOf.push({ heading: safeLabel(label), daysOld: age });
|
|
44049
|
+
}
|
|
44050
|
+
}
|
|
44051
|
+
onTableRow(line) {
|
|
44052
|
+
this.flushBlock();
|
|
44053
|
+
if (this.lastBlank) {
|
|
44054
|
+
const indent = indentOf(line);
|
|
44055
|
+
this.closeWhere((r) => r.kind === "item" && r.depth >= indent);
|
|
44056
|
+
}
|
|
44057
|
+
this.judgeEntry(this.upcomingRegion(), readDayMentions(line, this.today).map((m) => m.parts), false);
|
|
44058
|
+
this.lastBlank = false;
|
|
44059
|
+
}
|
|
44060
|
+
onItem(line, body) {
|
|
44061
|
+
this.flushBlock();
|
|
44062
|
+
const indent = indentOf(line);
|
|
44063
|
+
this.closeWhere((r) => r.kind === "item" && r.depth >= indent);
|
|
44064
|
+
const labelMatch = LABEL_RE.exec(body);
|
|
44065
|
+
const label = labelMatch ? labelMatch[1] : null;
|
|
44066
|
+
const upcoming = label !== null && UPCOMING_RE.test(label);
|
|
44067
|
+
const region = { kind: "item", depth: indent, label, upcoming, played: [], childBlocks: 0, ownLatest: null };
|
|
44068
|
+
this.open.push(region);
|
|
44069
|
+
this.block = { heading: this.heading, text: line.trim() };
|
|
44070
|
+
this.blockRegion = this.upcomingRegion();
|
|
44071
|
+
this.blockIsLabel = this.blockRegion === region;
|
|
44072
|
+
this.collectDates(labelMatch && upcoming ? body.slice(labelMatch[0].length) : body);
|
|
44073
|
+
this.lastBlank = false;
|
|
44074
|
+
}
|
|
44075
|
+
onText(line) {
|
|
44076
|
+
const indent = indentOf(line);
|
|
44077
|
+
if (this.lastBlank) {
|
|
44078
|
+
this.closeWhere((r) => r.kind === "item" && r.depth >= indent);
|
|
44079
|
+
}
|
|
44080
|
+
if (this.block === null) {
|
|
44081
|
+
this.block = { heading: this.heading, text: line.trim() };
|
|
44082
|
+
this.blockRegion = this.upcomingRegion();
|
|
44083
|
+
this.blockIsLabel = false;
|
|
44084
|
+
} else {
|
|
44085
|
+
this.block.text += `
|
|
44086
|
+
${line.trim()}`;
|
|
44087
|
+
}
|
|
44088
|
+
this.collectDates(line);
|
|
44089
|
+
this.lastBlank = false;
|
|
44090
|
+
}
|
|
44091
|
+
flushBlock() {
|
|
44092
|
+
if (this.block === null)
|
|
44093
|
+
return;
|
|
44094
|
+
const chars = [...this.block.text].length;
|
|
44095
|
+
if (chars > CONTEXT_BLOCK_BAR_CHARS) {
|
|
44096
|
+
this.walls.push({
|
|
44097
|
+
heading: this.block.heading === null ? null : safeLabel(this.block.heading),
|
|
44098
|
+
opening: openingOf(this.block.text),
|
|
44099
|
+
chars
|
|
44100
|
+
});
|
|
44101
|
+
}
|
|
44102
|
+
this.judgeEntry(this.blockRegion, this.blockDates, this.blockIsLabel);
|
|
44103
|
+
this.block = null;
|
|
44104
|
+
this.blockDates = [];
|
|
44105
|
+
this.blockRegion = null;
|
|
44106
|
+
this.blockIsLabel = false;
|
|
44107
|
+
}
|
|
44108
|
+
judgeEntry(region, dates, isLabel) {
|
|
44109
|
+
if (region === null)
|
|
44110
|
+
return;
|
|
44111
|
+
const latest = latestOf(dates);
|
|
44112
|
+
const played = latest !== null && utcMs(latest) < this.todayMs;
|
|
44113
|
+
if (isLabel) {
|
|
44114
|
+
region.ownLatest = played ? latest : null;
|
|
44115
|
+
return;
|
|
44116
|
+
}
|
|
44117
|
+
region.childBlocks += 1;
|
|
44118
|
+
if (played)
|
|
44119
|
+
region.played.push(latest);
|
|
44120
|
+
}
|
|
44121
|
+
closeRegion(r) {
|
|
44122
|
+
if (!r.upcoming)
|
|
44123
|
+
return;
|
|
44124
|
+
const entries = r.childBlocks === 0 && r.ownLatest !== null ? [r.ownLatest] : r.played;
|
|
44125
|
+
const latest = latestOf(entries);
|
|
44126
|
+
if (latest === null)
|
|
44127
|
+
return;
|
|
44128
|
+
this.played.push({ label: safeLabel(r.label ?? ""), playedEntries: entries.length, latest });
|
|
44129
|
+
}
|
|
44130
|
+
closeWhere(reject) {
|
|
44131
|
+
for (let i = this.open.length - 1;i >= 0; i -= 1) {
|
|
44132
|
+
const r = this.open[i];
|
|
44133
|
+
if (reject(r)) {
|
|
44134
|
+
this.closeRegion(r);
|
|
44135
|
+
this.open.splice(i, 1);
|
|
44136
|
+
}
|
|
44137
|
+
}
|
|
44138
|
+
}
|
|
44139
|
+
upcomingRegion() {
|
|
44140
|
+
for (let i = this.open.length - 1;i >= 0; i -= 1) {
|
|
44141
|
+
if (this.open[i].upcoming)
|
|
44142
|
+
return this.open[i];
|
|
44143
|
+
}
|
|
44144
|
+
return null;
|
|
44145
|
+
}
|
|
44146
|
+
collectDates(segment) {
|
|
44147
|
+
if (this.blockRegion === null)
|
|
44148
|
+
return;
|
|
44149
|
+
for (const mention of readDayMentions(segment, this.today))
|
|
44150
|
+
this.blockDates.push(mention.parts);
|
|
44151
|
+
}
|
|
44152
|
+
}
|
|
44153
|
+
function assessContextWalls(workspaceSlug, deps = {}) {
|
|
44154
|
+
const readAnchor = deps.readArtistAnchor ?? readArtistAnchorFile;
|
|
44155
|
+
const readContext = deps.readContext ?? readContextFile;
|
|
44156
|
+
const rosterOf = deps.readRosterFn ?? readRoster;
|
|
44157
|
+
const today = localDayOf((deps.now ?? (() => new Date))());
|
|
44158
|
+
const walls = [];
|
|
44159
|
+
const played = [];
|
|
44160
|
+
const asOf = [];
|
|
44161
|
+
let unreadable = 0;
|
|
44162
|
+
const take = (path5, result) => {
|
|
44163
|
+
for (const w of result.walls)
|
|
44164
|
+
walls.push({ path: path5, ...w });
|
|
44165
|
+
for (const p of result.played)
|
|
44166
|
+
played.push({ path: path5, ...p });
|
|
44167
|
+
for (const a of result.asOf)
|
|
44168
|
+
asOf.push({ path: path5, ...a });
|
|
44169
|
+
};
|
|
44170
|
+
let roster;
|
|
44171
|
+
try {
|
|
44172
|
+
roster = rosterOf(workspaceSlug);
|
|
44173
|
+
} catch {
|
|
44174
|
+
roster = [];
|
|
44175
|
+
}
|
|
44176
|
+
for (const artist of roster) {
|
|
44177
|
+
let raw = null;
|
|
44178
|
+
try {
|
|
44179
|
+
raw = readAnchor(workspaceSlug, artist.slug);
|
|
44180
|
+
} catch {
|
|
44181
|
+
unreadable += 1;
|
|
44182
|
+
continue;
|
|
44183
|
+
}
|
|
44184
|
+
if (raw === null)
|
|
44185
|
+
continue;
|
|
44186
|
+
take(`artists/${artist.slug}/${ARTIST_ANCHOR_FILENAME}`, scanContextText(raw, today));
|
|
44187
|
+
}
|
|
44188
|
+
let root = null;
|
|
44189
|
+
try {
|
|
44190
|
+
root = readContext(workspaceSlug, CLAUDE_MD);
|
|
44191
|
+
} catch {
|
|
44192
|
+
root = null;
|
|
44193
|
+
}
|
|
44194
|
+
if (root !== null) {
|
|
44195
|
+
const block = extractUserBlock(root);
|
|
44196
|
+
if (block !== null) {
|
|
44197
|
+
take(CLAUDE_MD, scanContextText(block, today, { initialHeading: "the customisable block" }));
|
|
44198
|
+
}
|
|
44199
|
+
}
|
|
44200
|
+
walls.sort((a, b) => b.chars - a.chars || a.path.localeCompare(b.path));
|
|
44201
|
+
played.sort((a, b) => b.playedEntries - a.playedEntries || a.path.localeCompare(b.path));
|
|
44202
|
+
asOf.sort((a, b) => {
|
|
44203
|
+
if (a.daysOld === null !== (b.daysOld === null))
|
|
44204
|
+
return a.daysOld === null ? -1 : 1;
|
|
44205
|
+
return (b.daysOld ?? 0) - (a.daysOld ?? 0) || a.path.localeCompare(b.path);
|
|
44206
|
+
});
|
|
44207
|
+
return { walls, played, asOf, unreadable };
|
|
44208
|
+
}
|
|
44209
|
+
|
|
42462
44210
|
// src/lib/build-stamp.ts
|
|
42463
44211
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
42464
44212
|
import { existsSync as existsSync9, mkdirSync as mkdirSync11, readFileSync as readFileSync26, writeFileSync as writeFileSync8 } from "node:fs";
|
|
@@ -42547,8 +44295,8 @@ function checkBuildStaleness(deps = {}) {
|
|
|
42547
44295
|
|
|
42548
44296
|
// src/lib/memory-links.ts
|
|
42549
44297
|
import { readFileSync as readFileSync27 } from "node:fs";
|
|
42550
|
-
import { relative as
|
|
42551
|
-
var MEMORY_FILE_RE = /^(workspace-memory\.md|artists\/[^/]+\/memory\.md)$/i;
|
|
44298
|
+
import { relative as relative14, sep as sep6 } from "node:path";
|
|
44299
|
+
var MEMORY_FILE_RE = /^(workspace-memory\.md|user-memory\.md|artists\/[^/]+\/memory\.md|memory\/[^/]+\.md|memory\/private\/[^/]+\.md)$/i;
|
|
42552
44300
|
function scanMemoryLinks(deps) {
|
|
42553
44301
|
const listFiles = deps.listFiles ?? ((r) => walkSubtreeFiles(r, isVcsInternal));
|
|
42554
44302
|
const readFile = deps.readFile ?? ((p) => readFileSync27(p, "utf8"));
|
|
@@ -42560,7 +44308,7 @@ function scanMemoryLinks(deps) {
|
|
|
42560
44308
|
} catch {
|
|
42561
44309
|
return { scanned: 0, unreadable: [], dead: [] };
|
|
42562
44310
|
}
|
|
42563
|
-
const relPaths = absPaths.map((p) =>
|
|
44311
|
+
const relPaths = absPaths.map((p) => relative14(root, p).split(sep6).join("/"));
|
|
42564
44312
|
const artists = buildArtistIndex(relPaths, root, (abs) => readFile(abs));
|
|
42565
44313
|
const dead = [];
|
|
42566
44314
|
const unreadable = [];
|
|
@@ -42596,7 +44344,7 @@ init_output();
|
|
|
42596
44344
|
|
|
42597
44345
|
// src/lib/pending-writes.ts
|
|
42598
44346
|
import { readFileSync as readFileSync28, statSync as statSync18 } from "node:fs";
|
|
42599
|
-
import { relative as
|
|
44347
|
+
import { relative as relative15 } from "node:path";
|
|
42600
44348
|
var import_gray_matter14 = __toESM(require_gray_matter(), 1);
|
|
42601
44349
|
var EMPTY2 = { writes: [], count: 0 };
|
|
42602
44350
|
function reportedElsewhere(state) {
|
|
@@ -42625,7 +44373,7 @@ function scanPendingWrites(args) {
|
|
|
42625
44373
|
const writes = [];
|
|
42626
44374
|
const discovered = [];
|
|
42627
44375
|
for (const absolutePath of walkSubtreeFiles(mirrorRootPath, ignored)) {
|
|
42628
|
-
const relPath =
|
|
44376
|
+
const relPath = relative15(mirrorRootPath, absolutePath);
|
|
42629
44377
|
const fileMatch = classifyFilesPath({ workspaceSlug, absolutePath });
|
|
42630
44378
|
if (fileMatch) {
|
|
42631
44379
|
if (frozenFiles.has(fileMatch.relPath))
|
|
@@ -43102,8 +44850,11 @@ async function runStatus(args, deps = {}) {
|
|
|
43102
44850
|
if (stashes.length === 0) {
|
|
43103
44851
|
out.info(row("Conflicts", "No conflict stashes."));
|
|
43104
44852
|
} else {
|
|
43105
|
-
const
|
|
43106
|
-
const
|
|
44853
|
+
const generatedDeps = generatedMemoryStashDepsFrom(localState);
|
|
44854
|
+
const generatedSide = stashes.filter((s) => isGeneratedMemoryStash(s.label, generatedDeps) !== null);
|
|
44855
|
+
const generatedLabels = new Set(generatedSide.map((s) => s.label));
|
|
44856
|
+
const cloudSide = stashes.filter((s) => s.side === "cloud" && !generatedLabels.has(s.label));
|
|
44857
|
+
const localSide = stashes.filter((s) => s.side !== "cloud" && !generatedLabels.has(s.label));
|
|
43107
44858
|
out.info(row("Conflicts", `${stashes.length} stashed cop${stashes.length === 1 ? "y" : "ies"} in ${conflictsRoot(slug)}`));
|
|
43108
44859
|
const newest = stashes[0];
|
|
43109
44860
|
const when = newest.stashedAt ? `${formatAbsolute(newest.stashedAt)} (${formatRelative(now.getTime() - newest.stashedAt.getTime())})` : "time unknown";
|
|
@@ -43112,6 +44863,9 @@ async function runStatus(args, deps = {}) {
|
|
|
43112
44863
|
if (localSide.length > 0) {
|
|
43113
44864
|
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
44865
|
}
|
|
44866
|
+
if (generatedSide.length > 0) {
|
|
44867
|
+
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.`));
|
|
44868
|
+
}
|
|
43115
44869
|
if (cloudSide.length > 0) {
|
|
43116
44870
|
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
44871
|
}
|
|
@@ -43359,6 +45113,16 @@ async function runStatus(args, deps = {}) {
|
|
|
43359
45113
|
}
|
|
43360
45114
|
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
45115
|
}
|
|
45116
|
+
let context = { walls: [], played: [], asOf: [], unreadable: 0 };
|
|
45117
|
+
try {
|
|
45118
|
+
context = assessContextWalls(slug, { now: () => now });
|
|
45119
|
+
} catch {
|
|
45120
|
+
context = { walls: [], played: [], asOf: [], unreadable: 0 };
|
|
45121
|
+
}
|
|
45122
|
+
renderWalls(out, context);
|
|
45123
|
+
renderPlayed(out, context);
|
|
45124
|
+
renderAsOf(out, context);
|
|
45125
|
+
renderUnchecked(out, context);
|
|
43362
45126
|
const anchor = localState.size_anchor;
|
|
43363
45127
|
if (anchor !== undefined) {
|
|
43364
45128
|
let now2 = {
|
|
@@ -43399,6 +45163,53 @@ async function runStatus(args, deps = {}) {
|
|
|
43399
45163
|
out.info("");
|
|
43400
45164
|
return { exitCode: 0 };
|
|
43401
45165
|
}
|
|
45166
|
+
var num = (v) => v.toLocaleString("en-GB");
|
|
45167
|
+
function renderWalls(out, context) {
|
|
45168
|
+
const total = context.walls.length;
|
|
45169
|
+
if (total === 0)
|
|
45170
|
+
return;
|
|
45171
|
+
const named = context.walls.slice(0, MAX_NAMED_CONTEXT_FINDINGS);
|
|
45172
|
+
out.info(row("Walls", `${total} block${total === 1 ? "" : "s"} of master context ${total === 1 ? "has" : "have"} ` + `grown past ${num(CONTEXT_BLOCK_BAR_CHARS)} characters.`));
|
|
45173
|
+
for (const w of named) {
|
|
45174
|
+
const where = w.heading === null ? "before its first heading" : `under "${w.heading}"`;
|
|
45175
|
+
out.info(cont(`${w.path}, ${where}: ${num(w.chars)} characters, opening "${w.opening}"`));
|
|
45176
|
+
}
|
|
45177
|
+
if (total > named.length)
|
|
45178
|
+
out.info(cont(`And ${total - named.length} more, not named here.`));
|
|
45179
|
+
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."));
|
|
45180
|
+
}
|
|
45181
|
+
function renderPlayed(out, context) {
|
|
45182
|
+
const total = context.played.length;
|
|
45183
|
+
if (total === 0)
|
|
45184
|
+
return;
|
|
45185
|
+
const named = context.played.slice(0, MAX_NAMED_CONTEXT_FINDINGS);
|
|
45186
|
+
out.info(row("Played", `${total} label${total === 1 ? "" : "s"} that ${total === 1 ? "says" : "say"} upcoming still ` + `${total === 1 ? "holds" : "hold"} entries that have played.`));
|
|
45187
|
+
for (const p of named) {
|
|
45188
|
+
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)}.`));
|
|
45189
|
+
}
|
|
45190
|
+
if (total > named.length)
|
|
45191
|
+
out.info(cont(`And ${total - named.length} more, not named here.`));
|
|
45192
|
+
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."));
|
|
45193
|
+
}
|
|
45194
|
+
function renderAsOf(out, context) {
|
|
45195
|
+
const total = context.asOf.length;
|
|
45196
|
+
if (total === 0)
|
|
45197
|
+
return;
|
|
45198
|
+
const named = context.asOf.slice(0, MAX_NAMED_CONTEXT_FINDINGS);
|
|
45199
|
+
out.info(row("As of", `${total} current-state section${total === 1 ? "" : "s"} cannot be checked against a date.`));
|
|
45200
|
+
for (const a of named) {
|
|
45201
|
+
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.`));
|
|
45202
|
+
}
|
|
45203
|
+
if (total > named.length)
|
|
45204
|
+
out.info(cont(`And ${total - named.length} more, not named here.`));
|
|
45205
|
+
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."));
|
|
45206
|
+
}
|
|
45207
|
+
function renderUnchecked(out, context) {
|
|
45208
|
+
const n = context.unreadable;
|
|
45209
|
+
if (n === 0)
|
|
45210
|
+
return;
|
|
45211
|
+
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"}.`));
|
|
45212
|
+
}
|
|
43402
45213
|
var statusCommand = defineCommand({
|
|
43403
45214
|
meta: {
|
|
43404
45215
|
name: "status",
|
|
@@ -43737,7 +45548,7 @@ function denyUngated(detail) {
|
|
|
43737
45548
|
// src/lib/voice-gate/payload.ts
|
|
43738
45549
|
import { readFileSync as readFileSync30, statSync as statSync20 } from "node:fs";
|
|
43739
45550
|
import { homedir as homedir2 } from "node:os";
|
|
43740
|
-
import { isAbsolute as
|
|
45551
|
+
import { isAbsolute as isAbsolute4, resolve as resolve4 } from "node:path";
|
|
43741
45552
|
var MAX_BODY_BYTES = 256 * 1024;
|
|
43742
45553
|
var HELPER_BASENAME = "push_gmail_draft.py";
|
|
43743
45554
|
var DRAFT_FLAGS = ["--to", "--body-file", "--body-stdin"];
|
|
@@ -43927,7 +45738,7 @@ function applyCd(tokens, state) {
|
|
|
43927
45738
|
return { dir: state.dir, known: false };
|
|
43928
45739
|
}
|
|
43929
45740
|
const expanded = expandHome(target);
|
|
43930
|
-
if (
|
|
45741
|
+
if (isAbsolute4(expanded))
|
|
43931
45742
|
return { dir: expanded, known: true };
|
|
43932
45743
|
if (!state.known)
|
|
43933
45744
|
return state;
|
|
@@ -43958,12 +45769,12 @@ function classifyHelperSegment(tokens, dirState, readBodyFile) {
|
|
|
43958
45769
|
};
|
|
43959
45770
|
}
|
|
43960
45771
|
const expanded = expandHome(bodyFile);
|
|
43961
|
-
if (!
|
|
45772
|
+
if (!isAbsolute4(expanded) && !dirState.known) {
|
|
43962
45773
|
return {
|
|
43963
45774
|
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
45775
|
};
|
|
43965
45776
|
}
|
|
43966
|
-
const resolved =
|
|
45777
|
+
const resolved = isAbsolute4(expanded) ? expanded : resolve4(dirState.dir, expanded);
|
|
43967
45778
|
let body;
|
|
43968
45779
|
try {
|
|
43969
45780
|
body = readBodyFile(resolved);
|