@plinth-music/cli 0.12.0 → 0.13.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 +60 -0
- package/README.md +10 -6
- package/dist/build-stamp.json +2 -2
- package/dist/cli.js +713 -118
- 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.13.0",
|
|
7588
7588
|
description: "Plinth capstone sync daemon and CLI",
|
|
7589
7589
|
type: "module",
|
|
7590
7590
|
private: false,
|
|
@@ -26170,7 +26170,7 @@ function composeGroundingBlock(today, workspaceRules, workspaceMemory, userMemor
|
|
|
26170
26170
|
add("grounding", `Plinth session grounding:`, ``, `Today's date: ${today}. You have no inherent sense of the current date and ` + `your training has a knowledge cutoff, so treat this injected date as ` + `authoritative for anything time-relative: deadlines, what counts as ` + `"recent" or "upcoming", and the age of any data or of your own knowledge.`);
|
|
26171
26171
|
if (identity !== null)
|
|
26172
26172
|
add("identity", ``, identity);
|
|
26173
|
-
add("grounding", ``, `Resolve Plinth entities before answering about them. For any answer grounded ` + `in a specific artist, project, task, or other workspace entity, first look ` + `the name up in THIS workspace with the Plinth MCP tools ` + `(\`mcp__plinth__search_entities\`, \`mcp__plinth__list_artists\`, or ` + `\`mcp__plinth__list_projects\`) and ground your answer in what they return. ` + `Do not answer from name recognition alone: a globally famous name may not ` + `exist in this workspace, and an entity here may differ from the public ` + `figure of the same name. If the lookup returns nothing, say so instead of ` + `inventing details.`, ``, `Plan before you write. The first time you call any mutating Plinth MCP tool ` + `in a session (create_*, update_*, delete_*, mark_*: anything that changes ` + `workspace data), do not commit silently: state exactly what you would write, ` + `naming the tool, the entity, and the fields and values, and proceed only after ` + `the user confirms or amends.`, ``, `Write through the canonical surface. In this cockpit the file tree is your edit ` + `surface, so reach for it before the most direct tool. Change prose (document ` + `bodies, free-form notes, anything textual) by editing the mirror markdown ` + `file; the daemon syncs it up. An artist's master context is the clearest case: ` + `it lives in the body of that artist's \`artists/<slug>/_artist.md\` mirror file, ` + `the file the web context pane renders, and one of only two files under ` + `\`artists/<slug>/\` the daemon syncs both ways (\`memory.md\` is the other), ` + `so change it by editing that body, never by creating ` + `a separate document or context entity (e.g. \`create_document\`) the pane won't ` + `render. One carve-out inside that file: a block labelled as rendered from ` + `Plinth is regenerated on every sync and your edits to it are discarded, so ` + `write those through the tool the block names. Change structured fields (task ` + `status, priority, assignees, due dates, a project's category) through the ` + `Plinth MCP (\`mcp__plinth__*\`) or the task file's frontmatter, which is ` + `row-canonical but still written through the validated API. Make a bulk or ` + `destructive change (a mass delete, a gutting overwrite) by editing the file ` + `path, so the daemon's confirm-gate can quarantine it for \`plinth confirm\`. ` + `Never write the database directly, which rules out raw SQL and ` + `\`execute_sql\`, \`apply_migration\` and \`psql\`: those bypass the daemon, the ` + `mirror, the confirm-gate, canonicality, and API validation.`, ``, `Dates belong on the calendar, not in \`key_dates\`. The date surface for an artist ` + `is their mapped Google Calendar; the \`key_dates\` table is retired. Reading ` + `dates is yours, and this mirror already holds them: the daemon projects every ` + `mapped calendar into \`views/calendar/upcoming.md\` and ` + `\`views/calendar/<slug>.md\`, so read those before reaching for a tool ` + `(\`list_artist_calendars\`). They are derived and overwritten, ` + `so they are current only as of the \`generated_at\` in their header - quote that ` + `stamp, never edit them, and check the status line, because a calendar Plinth ` + `could not reach is unknown rather than empty. When a tool is the only way, ` + `resolve the calendar through \`mcp__plinth__list_artist_calendars\` and use its ` + `\`calendar_id\` field, not the mapping row's \`id\`, because the default ` + `\`primary\` is almost never theirs.`, ``, `Writing a date is not yours, by any route: not a calendar tool, and not a ` + `shell command line either, the Google Workspace one included. The calendar ` + `tools are refused for you where Plinth has configured this workspace. ` + `Nothing refuses a shell, anywhere, and where Plinth has not configured ` + `this workspace nothing is refused at all: you are the only thing holding ` + `those. The way ` + `through is a person, not a workaround, so when a date needs adding or ` + `correcting say plainly what the change is, say that it goes in through ` + `Plinth's calendar view, and say that a person makes it. Say why, too: a date ` + `left looking handled is worse than one openly outstanding, and a calendar ` + `write goes out to everyone invited the moment it lands and cannot be recalled.`, ``, `Draft email. Never send it. Writing the draft is yours: use the draft tools ` + `(\`create_draft\`, \`update_draft\`), then say where the draft is and what it ` + `says, so the person can read it and send it in their own name. Sending, ` + `replying and
|
|
26173
|
+
add("grounding", ``, `Resolve Plinth entities before answering about them. For any answer grounded ` + `in a specific artist, project, task, or other workspace entity, first look ` + `the name up in THIS workspace with the Plinth MCP tools ` + `(\`mcp__plinth__search_entities\`, \`mcp__plinth__list_artists\`, or ` + `\`mcp__plinth__list_projects\`) and ground your answer in what they return. ` + `Do not answer from name recognition alone: a globally famous name may not ` + `exist in this workspace, and an entity here may differ from the public ` + `figure of the same name. If the lookup returns nothing, say so instead of ` + `inventing details.`, ``, `Plan before you write. The first time you call any mutating Plinth MCP tool ` + `in a session (create_*, update_*, delete_*, mark_*: anything that changes ` + `workspace data), do not commit silently: state exactly what you would write, ` + `naming the tool, the entity, and the fields and values, and proceed only after ` + `the user confirms or amends.`, ``, `Write through the canonical surface. In this cockpit the file tree is your edit ` + `surface, so reach for it before the most direct tool. Change prose (document ` + `bodies, free-form notes, anything textual) by editing the mirror markdown ` + `file; the daemon syncs it up. An artist's master context is the clearest case: ` + `it lives in the body of that artist's \`artists/<slug>/_artist.md\` mirror file, ` + `the file the web context pane renders, and one of only two files under ` + `\`artists/<slug>/\` the daemon syncs both ways (\`memory.md\` is the other), ` + `so change it by editing that body, never by creating ` + `a separate document or context entity (e.g. \`create_document\`) the pane won't ` + `render. One carve-out inside that file: a block labelled as rendered from ` + `Plinth is regenerated on every sync and your edits to it are discarded, so ` + `write those through the tool the block names. Change structured fields (task ` + `status, priority, assignees, due dates, a project's category) through the ` + `Plinth MCP (\`mcp__plinth__*\`) or the task file's frontmatter, which is ` + `row-canonical but still written through the validated API. Make a bulk or ` + `destructive change (a mass delete, a gutting overwrite) by editing the file ` + `path, so the daemon's confirm-gate can quarantine it for \`plinth confirm\`. ` + `Never write the database directly, which rules out raw SQL and ` + `\`execute_sql\`, \`apply_migration\` and \`psql\`: those bypass the daemon, the ` + `mirror, the confirm-gate, canonicality, and API validation.`, ``, `Dates belong on the calendar, not in \`key_dates\`. The date surface for an artist ` + `is their mapped Google Calendar; the \`key_dates\` table is retired. Reading ` + `dates is yours, and this mirror already holds them: the daemon projects every ` + `mapped calendar into \`views/calendar/upcoming.md\` and ` + `\`views/calendar/<slug>.md\`, so read those before reaching for a tool ` + `(\`list_artist_calendars\`). They are derived and overwritten, ` + `so they are current only as of the \`generated_at\` in their header - quote that ` + `stamp, never edit them, and check the status line, because a calendar Plinth ` + `could not reach is unknown rather than empty. When a tool is the only way, ` + `resolve the calendar through \`mcp__plinth__list_artist_calendars\` and use its ` + `\`calendar_id\` field, not the mapping row's \`id\`, because the default ` + `\`primary\` is almost never theirs.`, ``, `Writing a date is not yours, by any route: not a calendar tool, and not a ` + `shell command line either, the Google Workspace one included. The calendar ` + `tools are refused for you where Plinth has configured this workspace. ` + `Nothing refuses a shell, anywhere, and where Plinth has not configured ` + `this workspace nothing is refused at all: you are the only thing holding ` + `those. The way ` + `through is a person, not a workaround, so when a date needs adding or ` + `correcting say plainly what the change is, say that it goes in through ` + `Plinth's calendar view, and say that a person makes it. Say why, too: a date ` + `left looking handled is worse than one openly outstanding, and a calendar ` + `write goes out to everyone invited the moment it lands and cannot be recalled.`, ``, `Draft email. Never send it. Writing the draft is yours: use the draft tools ` + `(\`create_draft\`, \`update_draft\`), then say where the draft is and what it ` + `says, so the person can read it and send it in their own name. Sending, ` + `replying, forwarding and deleting are not yours, by any route: not a mail tool, and ` + `not a shell command line either. The mail tools are refused for you where ` + `Plinth has configured this workspace. Nothing refuses a shell, anywhere, ` + `and where Plinth has not configured this workspace nothing is refused at ` + `all: you are the only thing holding those. This is not a comment on your ` + `writing. It is that a message which has left the building cannot be recalled, ` + `and one deleted out of a mailbox is not yours to lose; the person whose name ` + `is on it is the one who decides.`);
|
|
26174
26174
|
if (workspaceRules === null && voiceProfile === null) {
|
|
26175
26175
|
add("voice", ``, `Check the voice yourself, because nothing else checks it now. Two files carry ` + `it: \`workspace-rules.md\` at the mirror root holds this workspace's house ` + `style, and \`${VOICE_FILENAME}\` beside it holds your own voice profile, the ` + `sign-off and the habits that belong to one member rather than the team. Read ` + `them before you write and follow them as written, rather than drafting in ` + `your own register and correcting after. Nothing inspects a draft between you ` + `and the person reading it, so the rules in those files are the whole of it.`);
|
|
26176
26176
|
}
|
|
@@ -26458,7 +26458,25 @@ var RejectedWriteSchema = exports_external.object({
|
|
|
26458
26458
|
reason: exports_external.string().min(1),
|
|
26459
26459
|
detail: exports_external.string().optional(),
|
|
26460
26460
|
message: exports_external.string().optional(),
|
|
26461
|
-
rejected_at: exports_external.string().datetime({ offset: true }).optional()
|
|
26461
|
+
rejected_at: exports_external.string().datetime({ offset: true }).optional(),
|
|
26462
|
+
size_bytes: exports_external.number().int().nonnegative().optional(),
|
|
26463
|
+
max_bytes: exports_external.number().int().nonnegative().optional(),
|
|
26464
|
+
reported_at: exports_external.string().datetime({ offset: true }).optional()
|
|
26465
|
+
});
|
|
26466
|
+
var TeamFileRejectionSchema = exports_external.object({
|
|
26467
|
+
path: exports_external.string().min(1),
|
|
26468
|
+
reason: exports_external.string().min(1),
|
|
26469
|
+
detail: exports_external.string().nullish(),
|
|
26470
|
+
size_bytes: exports_external.number().int().nonnegative().nullish(),
|
|
26471
|
+
max_bytes: exports_external.number().int().nonnegative().nullish(),
|
|
26472
|
+
reported_by: exports_external.string().nullish(),
|
|
26473
|
+
reported_by_name: exports_external.string().nullish(),
|
|
26474
|
+
first_seen_at: exports_external.string().nullish(),
|
|
26475
|
+
last_seen_at: exports_external.string().nullish()
|
|
26476
|
+
});
|
|
26477
|
+
var TeamRejectedSchema = exports_external.object({
|
|
26478
|
+
fetched_at: exports_external.string().datetime({ offset: true }),
|
|
26479
|
+
rows: exports_external.array(TeamFileRejectionSchema)
|
|
26462
26480
|
});
|
|
26463
26481
|
var WorkspaceStateV2Schema = exports_external.object({
|
|
26464
26482
|
version: exports_external.literal(2),
|
|
@@ -26472,7 +26490,8 @@ var WorkspaceStateV2Schema = exports_external.object({
|
|
|
26472
26490
|
user_memory: UserMemoryStateSchema.optional(),
|
|
26473
26491
|
user_voice: UserVoiceStateSchema.optional(),
|
|
26474
26492
|
derived_text_version: exports_external.number().int().nonnegative().optional(),
|
|
26475
|
-
frontmatter_id_backfill_version: exports_external.number().int().nonnegative().optional()
|
|
26493
|
+
frontmatter_id_backfill_version: exports_external.number().int().nonnegative().optional(),
|
|
26494
|
+
team_rejected: TeamRejectedSchema.optional()
|
|
26476
26495
|
});
|
|
26477
26496
|
var WorkspaceStateV1Schema = exports_external.object({
|
|
26478
26497
|
version: exports_external.literal(1),
|
|
@@ -26654,7 +26673,8 @@ function commitState(before, after) {
|
|
|
26654
26673
|
user_memory: userMemory.value,
|
|
26655
26674
|
user_voice: userVoice.value,
|
|
26656
26675
|
derived_text_version: mergeScalar(fresh.derived_text_version, before.derived_text_version, after.derived_text_version),
|
|
26657
|
-
frontmatter_id_backfill_version: mergeScalar(fresh.frontmatter_id_backfill_version, before.frontmatter_id_backfill_version, after.frontmatter_id_backfill_version)
|
|
26676
|
+
frontmatter_id_backfill_version: mergeScalar(fresh.frontmatter_id_backfill_version, before.frontmatter_id_backfill_version, after.frontmatter_id_backfill_version),
|
|
26677
|
+
team_rejected: mergeScalar(fresh.team_rejected, before.team_rejected, after.team_rejected)
|
|
26658
26678
|
};
|
|
26659
26679
|
writeState(merged);
|
|
26660
26680
|
const singletonOverruled = rules.overruled || memory.overruled || userMemory.overruled || userVoice.overruled;
|
|
@@ -26825,6 +26845,7 @@ function upsertRejected(state, candidate) {
|
|
|
26825
26845
|
const list = state.rejected ?? [];
|
|
26826
26846
|
const idx = list.findIndex((o) => o.target === candidate.target && o.path === candidate.path);
|
|
26827
26847
|
const existing = idx >= 0 ? list[idx] : undefined;
|
|
26848
|
+
const payloadMoved = existing !== undefined && (existing.reason !== candidate.reason || (existing.detail ?? existing.message) !== (candidate.detail ?? candidate.message) || existing.size_bytes !== candidate.size_bytes || existing.max_bytes !== candidate.max_bytes);
|
|
26828
26849
|
const merged = {
|
|
26829
26850
|
target: candidate.target,
|
|
26830
26851
|
op: candidate.op,
|
|
@@ -26834,7 +26855,10 @@ function upsertRejected(state, candidate) {
|
|
|
26834
26855
|
reason: candidate.reason,
|
|
26835
26856
|
detail: candidate.detail,
|
|
26836
26857
|
message: candidate.message,
|
|
26837
|
-
rejected_at: existing?.rejected_at ?? candidate.rejected_at
|
|
26858
|
+
rejected_at: existing?.rejected_at ?? candidate.rejected_at,
|
|
26859
|
+
size_bytes: candidate.size_bytes,
|
|
26860
|
+
max_bytes: candidate.max_bytes,
|
|
26861
|
+
reported_at: payloadMoved ? undefined : existing?.reported_at
|
|
26838
26862
|
};
|
|
26839
26863
|
const next = [...list];
|
|
26840
26864
|
if (idx >= 0)
|
|
@@ -26850,6 +26874,53 @@ function removeRejected(state, target, path3) {
|
|
|
26850
26874
|
return { state, changed: false };
|
|
26851
26875
|
return { state: { ...state, rejected: next }, changed: true };
|
|
26852
26876
|
}
|
|
26877
|
+
function markRejectedReported(state, path3, reportedAt) {
|
|
26878
|
+
const list = state.rejected ?? [];
|
|
26879
|
+
const idx = list.findIndex((o) => o.target === "file" && o.path === path3);
|
|
26880
|
+
if (idx < 0)
|
|
26881
|
+
return { state, changed: false };
|
|
26882
|
+
if (list[idx].reported_at === reportedAt)
|
|
26883
|
+
return { state, changed: false };
|
|
26884
|
+
const next = [...list];
|
|
26885
|
+
next[idx] = { ...list[idx], reported_at: reportedAt };
|
|
26886
|
+
return { state: { ...state, rejected: next }, changed: true };
|
|
26887
|
+
}
|
|
26888
|
+
function clearRejectedReported(state, path3) {
|
|
26889
|
+
const list = state.rejected ?? [];
|
|
26890
|
+
const idx = list.findIndex((o) => o.target === "file" && o.path === path3);
|
|
26891
|
+
if (idx < 0)
|
|
26892
|
+
return { state, changed: false };
|
|
26893
|
+
if (list[idx].reported_at === undefined)
|
|
26894
|
+
return { state, changed: false };
|
|
26895
|
+
const next = [...list];
|
|
26896
|
+
const { reported_at: _dropped, ...rest } = list[idx];
|
|
26897
|
+
next[idx] = rest;
|
|
26898
|
+
return { state: { ...state, rejected: next }, changed: true };
|
|
26899
|
+
}
|
|
26900
|
+
function listTeamRejected(state) {
|
|
26901
|
+
return state.team_rejected?.rows ?? [];
|
|
26902
|
+
}
|
|
26903
|
+
function teamRejectedFetchedAt(state) {
|
|
26904
|
+
return state.team_rejected?.fetched_at;
|
|
26905
|
+
}
|
|
26906
|
+
function setTeamRejected(state, rows, fetchedAt) {
|
|
26907
|
+
const existing = state.team_rejected;
|
|
26908
|
+
if (existing && rowsEqual(existing.rows, rows)) {
|
|
26909
|
+
return { state, changed: false };
|
|
26910
|
+
}
|
|
26911
|
+
return {
|
|
26912
|
+
state: { ...state, team_rejected: { fetched_at: fetchedAt, rows } },
|
|
26913
|
+
changed: true
|
|
26914
|
+
};
|
|
26915
|
+
}
|
|
26916
|
+
function rowsEqual(a, b) {
|
|
26917
|
+
if (a.length !== b.length)
|
|
26918
|
+
return false;
|
|
26919
|
+
return a.every((x, i) => {
|
|
26920
|
+
const y = b[i];
|
|
26921
|
+
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;
|
|
26922
|
+
});
|
|
26923
|
+
}
|
|
26853
26924
|
function getWorkspaceRulesState(state) {
|
|
26854
26925
|
return state.workspace_rules;
|
|
26855
26926
|
}
|
|
@@ -28555,16 +28626,24 @@ var MANAGED_COMMENT = [
|
|
|
28555
28626
|
"entries in this file (they are preserved verbatim, including their matchers)",
|
|
28556
28627
|
"or in .claude/settings.local.json.",
|
|
28557
28628
|
"The permissions.deny entries below are managed the same way. They stop an agent",
|
|
28558
|
-
"in this workspace sending, replying to or forwarding mail
|
|
28559
|
-
"
|
|
28629
|
+
"in this workspace sending, replying to or forwarding mail, deleting or trashing",
|
|
28630
|
+
"it, marking it spam, and writing a calendar; drafting, filing, labelling and",
|
|
28631
|
+
"reading stay allowed. The refusal covers a draft the agent staged itself: on a",
|
|
28632
|
+
"Gmail mount there is no delete-draft tool, so trashing was the only way to",
|
|
28633
|
+
"discard one and it is now refused. Fix a wrong draft with update_draft, which",
|
|
28634
|
+
"keeps its thread. They match by VERB FAMILY rather than by",
|
|
28560
28635
|
"tool name - send_mail, sendEmail, send-mail and outlook_send_message are one",
|
|
28561
28636
|
"rule set, not four - and the matcher cannot express an alternation, so each",
|
|
28562
28637
|
"spelling needs its own line. That width also catches a few reads whose name",
|
|
28563
|
-
"merely begins with a send verb - getForwardingSettings is the live example
|
|
28638
|
+
"merely begins with a send verb - getForwardingSettings is the live example -",
|
|
28639
|
+
"and it stops an agent trashing any file, not only mail: nothing here can tell",
|
|
28640
|
+
"a mailbox's trash from a drive's.",
|
|
28564
28641
|
"You cannot re-open one of these here: a deny beats an allow, including yours,",
|
|
28565
28642
|
"and this list is rewritten every sync. Tell us and we will narrow the pattern.",
|
|
28566
28643
|
"We took that trade knowingly - a refusal is visible and recoverable, and a",
|
|
28567
|
-
"message that has gone cannot be recalled.",
|
|
28644
|
+
"message that has gone cannot be recalled. Nor is a deleted one yours to lose:",
|
|
28645
|
+
"it is somebody else's mailbox, spam-marking is deletion on a thirty-day fuse,",
|
|
28646
|
+
"and neither is undone by noticing afterwards.",
|
|
28568
28647
|
"One more entry stops an agent EDITING voice.md, the",
|
|
28569
28648
|
"voice profile it is judged against: that file is yours to write and theirs to read.",
|
|
28570
28649
|
"Your own allow list is untouched, and your own",
|
|
@@ -28643,6 +28722,36 @@ var MANAGED_DENY = [
|
|
|
28643
28722
|
"mcp__*__*_cancel*Event*",
|
|
28644
28723
|
"mcp__*__*-cancel*Event*",
|
|
28645
28724
|
"mcp__*__*Cancel*Event*",
|
|
28725
|
+
"mcp__*__trash*",
|
|
28726
|
+
"mcp__*__*_trash*",
|
|
28727
|
+
"mcp__*__*-trash*",
|
|
28728
|
+
"mcp__*__*Trash*",
|
|
28729
|
+
"mcp__*__delete*message*",
|
|
28730
|
+
"mcp__*__*_delete*message*",
|
|
28731
|
+
"mcp__*__*-delete*message*",
|
|
28732
|
+
"mcp__*__*Delete*message*",
|
|
28733
|
+
"mcp__*__delete*Message*",
|
|
28734
|
+
"mcp__*__*_delete*Message*",
|
|
28735
|
+
"mcp__*__*-delete*Message*",
|
|
28736
|
+
"mcp__*__*Delete*Message*",
|
|
28737
|
+
"mcp__*__delete*thread*",
|
|
28738
|
+
"mcp__*__*_delete*thread*",
|
|
28739
|
+
"mcp__*__*-delete*thread*",
|
|
28740
|
+
"mcp__*__*Delete*thread*",
|
|
28741
|
+
"mcp__*__delete*Thread*",
|
|
28742
|
+
"mcp__*__*_delete*Thread*",
|
|
28743
|
+
"mcp__*__*-delete*Thread*",
|
|
28744
|
+
"mcp__*__*Delete*Thread*",
|
|
28745
|
+
"mcp__*__delete*mail*",
|
|
28746
|
+
"mcp__*__*_delete*mail*",
|
|
28747
|
+
"mcp__*__*-delete*mail*",
|
|
28748
|
+
"mcp__*__*Delete*mail*",
|
|
28749
|
+
"mcp__*__delete*Mail*",
|
|
28750
|
+
"mcp__*__*_delete*Mail*",
|
|
28751
|
+
"mcp__*__*-delete*Mail*",
|
|
28752
|
+
"mcp__*__*Delete*Mail*",
|
|
28753
|
+
"mcp__*__*spam*",
|
|
28754
|
+
"mcp__*__*Spam*",
|
|
28646
28755
|
"Edit(//**/voice.md)"
|
|
28647
28756
|
];
|
|
28648
28757
|
var RETIRED_DENY = [
|
|
@@ -28865,7 +28974,7 @@ function ensureSessionHook(workspaceSlug, baseRelDir, out, resolved) {
|
|
|
28865
28974
|
parsed = raw;
|
|
28866
28975
|
} catch (err) {
|
|
28867
28976
|
const msg = err instanceof Error ? err.message : "unknown error";
|
|
28868
|
-
out.error(`${rel} could not be parsed (${msg}), so the Plinth session-grounding ` + `hook, the session-visibility hook and the permissions.deny list were ` + `NOT installed there. Fix or remove the file and re-run — until then ` + `this workspace is ungrounded and an agent here can SEND mail
|
|
28977
|
+
out.error(`${rel} could not be parsed (${msg}), so the Plinth session-grounding ` + `hook, the session-visibility hook and the permissions.deny list were ` + `NOT installed there. Fix or remove the file and re-run — until then ` + `this workspace is ungrounded and an agent here can SEND mail, ` + `DELETE mail and write calendars.`);
|
|
28869
28978
|
return "skipped";
|
|
28870
28979
|
}
|
|
28871
28980
|
}
|
|
@@ -28874,7 +28983,7 @@ function ensureSessionHook(workspaceSlug, baseRelDir, out, resolved) {
|
|
|
28874
28983
|
generated = buildSettingsJson(resolved.invocation, workspaceSlug, parsed);
|
|
28875
28984
|
} catch (err) {
|
|
28876
28985
|
const msg = err instanceof Error ? err.message : "unknown error";
|
|
28877
|
-
out.error(`${rel} has a structure the generator could not merge into (${msg}), so ` + `the Plinth session-grounding hook, the session-visibility hook and the ` + `permissions.deny list were NOT installed there. This workspace is ` + `ungrounded and an agent here can SEND mail and write calendars. Fix or ` + `remove the file and re-run.`);
|
|
28986
|
+
out.error(`${rel} has a structure the generator could not merge into (${msg}), so ` + `the Plinth session-grounding hook, the session-visibility hook and the ` + `permissions.deny list were NOT installed there. This workspace is ` + `ungrounded and an agent here can SEND mail, DELETE mail and write ` + `calendars. Fix or ` + `remove the file and re-run.`);
|
|
28878
28987
|
return "skipped";
|
|
28879
28988
|
}
|
|
28880
28989
|
if (existing === generated)
|
|
@@ -28900,7 +29009,7 @@ function reportResolution(out, resolved) {
|
|
|
28900
29009
|
}
|
|
28901
29010
|
}
|
|
28902
29011
|
function warnUnresolvedBinary(out) {
|
|
28903
|
-
out.error("Could not locate an installed `plinth` binary, so no Claude Code settings " + "were written for this workspace — sessions will be UNGROUNDED and an " + "agent here can SEND mail and write calendars. Install the CLI globally " + "(`npm i -g @plinth-music/cli`) so `command -v plinth` resolves, then " + "re-run `plinth refresh-context`.");
|
|
29012
|
+
out.error("Could not locate an installed `plinth` binary, so no Claude Code settings " + "were written for this workspace — sessions will be UNGROUNDED and an " + "agent here can SEND mail, DELETE mail and write calendars. Install the " + "CLI globally " + "(`npm i -g @plinth-music/cli`) so `command -v plinth` resolves, then " + "re-run `plinth refresh-context`.");
|
|
28904
29013
|
}
|
|
28905
29014
|
|
|
28906
29015
|
// src/lib/context/artist-context.ts
|
|
@@ -29208,6 +29317,8 @@ function buildCloseCommandMd(workspaceSlug) {
|
|
|
29208
29317
|
"",
|
|
29209
29318
|
"**A session that touched no artist writes one dated note under `documents/` instead**, with the same content. That is a new file rather than an edit to a tracked one, so it reaches the cloud only with the sync running - Step 2 is where you already saw whether it is.",
|
|
29210
29319
|
"",
|
|
29320
|
+
"**Something rule-shaped is not a loose end and is not memory. Sort it here, before Step 4 offers it a memory index. This close writes none of them** - name which of these four it is in the closing summary and write no prose for it. *Mechanical* (a tool to refuse, or a check a program could run): a deny rule or a hook. *Procedural* (a sequence somebody repeats): a skill, or a step in the customisable block at the foot of these commands. *Workspace-wide and stable*: `workspace-rules.md`, and **the member writes it, not you** - it reaches every teammate on every machine. *A preference dressed as a rule*: say so and let it go.",
|
|
29321
|
+
"",
|
|
29211
29322
|
"## Step 4: bank what was learned, by type first and scope second",
|
|
29212
29323
|
"",
|
|
29213
29324
|
"**Two sorts, in this order, and the order is the whole of it.** First: does this fact already have a surface of its own? Then, and only then: whose fact is it? Sorting by scope alone is how a contact record ends up in a memory file wearing a memory line's clothes.",
|
|
@@ -29229,6 +29340,12 @@ function buildCloseCommandMd(workspaceSlug) {
|
|
|
29229
29340
|
"",
|
|
29230
29341
|
"### Step 4b: two piles, and the artist pile comes first",
|
|
29231
29342
|
"",
|
|
29343
|
+
"**Before either pile, the gate. These four are never banked:** *raw transcript* - what it taught you may be a fact, what was said is not · *a guess, or anything only true today* - live state, which Step 3 routes · *a duplicate of a line already in the index* · *anything the generated `CLAUDE.md` or this session's grounding already states*. **Every line you do bank names its source** - who told you, or which tool read returned it - with an absolute date. **If you cannot name the source, write `source: unknown` - never infer one.**",
|
|
29344
|
+
"",
|
|
29345
|
+
"**Read the index before you add to it, and reconcile what disagrees.** If two lines on your subject contradict each other, **which pair it is decides what you may do**. *One is the line you are adding now*: do not add a third - edit the older line until it reads as superseded, then write yours. **It may well be a teammate's, so mark it superseded, never delete it, and name whose line you changed in the closing summary.** *Both were there before this session*: **propose the supersede and let the member confirm it**, as Step 6 does. Neither is yours and this step has no confirmation gate, so **resolving somebody else's is not yours to do unasked**.",
|
|
29346
|
+
"",
|
|
29347
|
+
"**When two lines disagree, this order decides which is current:** a line the member confirmed outranks a newer one the agent wrote; then the newer line; then one verified against a tool read; then one repeated across sessions; mark the loser superseded in place, never delete it silently.",
|
|
29348
|
+
"",
|
|
29232
29349
|
"**Most of what a session learns is true of exactly one artist**, and that is the pile that gets forgotten. The other is true of the member in front of you. Write both directly: they are scoped to the person or the act they describe, and a wrong line in either is theirs to prune.",
|
|
29233
29350
|
"",
|
|
29234
29351
|
"**The sorting test: would a teammate who never touches that artist be worse off without this line?** If not, it is the artist pile.",
|
|
@@ -29239,7 +29356,7 @@ function buildCloseCommandMd(workspaceSlug) {
|
|
|
29239
29356
|
"",
|
|
29240
29357
|
"That artist's rate card, a contact of theirs whose behaviour you had to learn, how their kit actually works, what a venue reliably does. It syncs to that artist's Plinth record, so a teammate opening that artist gets it. **List what you wrote here in the closing summary.**",
|
|
29241
29358
|
"",
|
|
29242
|
-
"**Follow the conventions in the file's own header comment**, which states them and is the version that stays current. Do not work from a copy of them here.",
|
|
29359
|
+
"**Follow the conventions in the file's own header comment**, which states them and is the version that stays current. Do not work from a copy of them here. **The source convention and the conflict order above are the exception**: they reach a header only on a file this command creates, so they hold wherever an existing header is silent.",
|
|
29243
29360
|
"",
|
|
29244
29361
|
"**Re-read the file at the moment you write it, not when you were planning.** The daemon can pull a `memory.md` down from Plinth at any point in a session, so a file that was missing minutes ago may exist now - and if it does, add your line to it rather than creating it over the top. This is not hypothetical: a close on 26 Aug 2026 checked, found nothing, and clobbered a copy the daemon had pulled in ninety seconds earlier.",
|
|
29245
29362
|
"",
|
|
@@ -29251,6 +29368,11 @@ function buildCloseCommandMd(workspaceSlug) {
|
|
|
29251
29368
|
"<!-- Durable facts about <Artist> that hold across sessions. One line per fact, with",
|
|
29252
29369
|
" provenance and an absolute date. SUPERSEDE, do not accumulate - when a fact here",
|
|
29253
29370
|
" becomes wrong, edit that line in place rather than adding a contradicting one.",
|
|
29371
|
+
" If you cannot name the source, write `source: unknown` - never infer one.",
|
|
29372
|
+
" When two lines disagree, this order decides which is current: a line the member",
|
|
29373
|
+
" confirmed outranks a newer one the agent wrote; then the newer line; then one",
|
|
29374
|
+
" verified against a tool read; then one repeated across sessions; mark the loser",
|
|
29375
|
+
" superseded in place, never delete it silently.",
|
|
29254
29376
|
" Live state does NOT belong here - that goes in the body of `_artist.md`. -->",
|
|
29255
29377
|
"```",
|
|
29256
29378
|
"",
|
|
@@ -29278,7 +29400,9 @@ function buildCloseCommandMd(workspaceSlug) {
|
|
|
29278
29400
|
"",
|
|
29279
29401
|
"**Anything named there was NOT banked, whatever the write appeared to return.** Say so plainly, name the file, and point the user at the restore command rather than reporting the work as done. A close that reports a stashed session record as banked is the one failure this step exists to prevent, because the record was the point of the close.",
|
|
29280
29402
|
"",
|
|
29281
|
-
"**Five to seven lines**, and no file-by-file dump: **where the session record went, named per artist**, what now lives on a durable surface, what is still in flight and on whom, **what was banked to memory and to which of the two piles**, anything you named as having no home yet, any fact you judged true of the whole team and left unwritten, any date you handed to a person, and the obvious next move. **This is how the user sees what was stored without opening a file**, so name the files rather than saying that memory was updated.",
|
|
29403
|
+
"**Five to seven lines**, and no file-by-file dump: **where the session record went, named per artist**, what now lives on a durable surface, what is still in flight and on whom, **what was banked to memory and to which of the two piles**, anything you named as having no home yet, any fact you judged true of the whole team and left unwritten, **anything rule-shaped you are handing back**, **any contradicting pair you found**, any date you handed to a person, and the obvious next move. **This is how the user sees what was stored without opening a file**, so name the files rather than saying that memory was updated.",
|
|
29404
|
+
"",
|
|
29405
|
+
"**When more than seven lines is needed, what this close could NOT do comes first** - a proposal handed back, a contradiction unresolved, a fact with no home. Those exist nowhere else once this session ends.",
|
|
29282
29406
|
"",
|
|
29283
29407
|
"## Step 6: offer to trim what has outgrown its file, and do not trim it",
|
|
29284
29408
|
"",
|
|
@@ -29294,20 +29418,38 @@ function buildCloseCommandMd(workspaceSlug) {
|
|
|
29294
29418
|
"",
|
|
29295
29419
|
"**Running the trim on your own initiative is the failure this step exists to prevent**, and it is the same failure as reporting a sweep nobody asked for. A `no` ends it here, in one line, and the next close offers it again in the same clause without arguing.",
|
|
29296
29420
|
"",
|
|
29297
|
-
"**On yes: propose
|
|
29421
|
+
"**On yes: propose it line by line, and write nothing yet.** Take one file at a time. **This is a list of proposals, not a file operation.** The member accepts or rejects each line on its own, and a line they reject stays exactly as it is. For every line you want to remove or rewrite, give these three fields and no others; a line you leave alone still appears in the listing:",
|
|
29422
|
+
"",
|
|
29423
|
+
"1. **The line itself, verbatim.**",
|
|
29424
|
+
"2. **Its age**, counted from the absolute date the line carries to the date the session hook injected. **A line carrying no date has no age - say so rather than estimating one.**",
|
|
29425
|
+
"3. **What you propose, and one reason.** **To REMOVE**, the reason is one of these four and no others: **superseded** by a later line you name · **duplicate** of another line here · **live state** belonging in the body of `artists/<slug>/_artist.md` · **over-long**, whose remedy is a split and never a cut. **To KEEP but change**, say which rewrite it is: **shortened**, the fact whole and only the wording gone, or **merged** with a line you name. A rewrite is not a removal and needs none of the four.",
|
|
29426
|
+
"",
|
|
29427
|
+
"**Shortening is what actually clears the bar, so reach for it first** - a split makes the file longer, and the four removal reasons are about lines that should not be there rather than about size. **A file whose lines are all correct, current and singular is trimmed by shortening or not at all**: say so rather than manufacturing a removal to reach a number. **An undated line is the member's call** - you cannot show it is over 30 days, so the floor cannot clear it: say it has no date, give your reason, let them decide. **Never treat undated as recent and drop it silently.**",
|
|
29428
|
+
"",
|
|
29429
|
+
"Then the rules the proposal itself obeys:",
|
|
29298
29430
|
"",
|
|
29299
29431
|
"- **Every surviving fact appears either unchanged or beside its rewrite**, so the member is reading a diff rather than trusting a summary of one.",
|
|
29300
29432
|
"- **A fact that is now wrong is edited where it stands, not deleted** - the correction and the date it changed are the record. The file's own header comment states its conventions and is the version that stays current; follow it rather than a copy.",
|
|
29301
29433
|
"- **Provenance and the absolute date survive on every line you keep.** A fact whose source you drop while shortening it has been damaged, not trimmed.",
|
|
29302
29434
|
"- **Nothing banked in the last 30 days is dropped**, measured against the date the session hook injected. Recent facts have not had time to prove whether they were durable, and a trim is not the place to decide.",
|
|
29303
29435
|
"- **Merging two facts into one is a rewrite and shows as one**, with both original lines visible in the proposal.",
|
|
29436
|
+
"- **A contradicting pair is a supersede, not a cut**, resolved by Step 4b's order; the loser stays in the file.",
|
|
29304
29437
|
"",
|
|
29305
29438
|
"**Then write only what the member confirms**, and only to the files this command can reach:",
|
|
29306
29439
|
"",
|
|
29307
|
-
"- `artists/<slug>/memory.md` syncs, and a teammate may be in the same file. **Re-read it at the moment you write**, exactly as Step 4b requires, and change only the lines the proposal named. This is the only file this step
|
|
29440
|
+
"- `artists/<slug>/memory.md` syncs, and a teammate may be in the same file. **Re-read it at the moment you write**, exactly as Step 4b requires, and change only the lines the proposal named. **This is the only file this step writes that reaches Plinth.**",
|
|
29308
29441
|
`- \`${VOICE_FILENAME}\` is named and nothing else. You are denied editing it, and that is deliberate: the profile a draft is judged against is not the drafter's to rewrite. Say its size, say the member edits it themselves, and propose nothing.`,
|
|
29442
|
+
"- **`workspace-rules.md` is reported here and never edited, and nothing in the harness stops you.** It is injected whole into every teammate's session, so **it is the member's to change** - Step 3's call, and a size row does not change it. Say how far over it is and propose nothing.",
|
|
29309
29443
|
"- **A memory file the whole workspace shares is not this command's to trim.** `plinth status` names it and says how far over it is; trimming it is a deliberate act by a person who can see who else it belongs to, not a step at the end of somebody's session.",
|
|
29310
29444
|
"",
|
|
29445
|
+
"**Every line the trim changes is preserved, rewrites included.** The ORIGINAL of each line you remove, shorten or merge goes verbatim into `artists/<slug>/memory-archive.md` beside the store, under a heading carrying the date the session hook injected. Create it if absent, with this first line:",
|
|
29446
|
+
"",
|
|
29447
|
+
"```md",
|
|
29448
|
+
"# <Artist> - RETIRED facts, removed from `memory.md`. Not current, and not synced.",
|
|
29449
|
+
"```",
|
|
29450
|
+
"",
|
|
29451
|
+
"**Write the archive first and `artists/<slug>/memory.md` second, never the other way round** - two files cannot be one edit, and in this order a failure between them over-claims rather than losing lines. **Say plainly that the archive stays on this machine**: Plinth never syncs, reads or rewrites it, so a teammate sees the facts gone from `memory.md` and no trace of where they went. Undoing a trim is a copy back by hand. **The proposal is the control; the archive is only the undo.** **Report it when you report the trim, not in the Step 5 summary**, which is already written - name the file, and **if the `memory.md` write was set aside say so**, because the archive then lists lines still in the store.",
|
|
29452
|
+
"",
|
|
29311
29453
|
"**Anything you write here lands after Step 5 looked at the conflicts list**, so it can be set aside within seconds like any other write. If you wrote a trim, run the Step 5 check once more on what you wrote and report it the same way.",
|
|
29312
29454
|
"",
|
|
29313
29455
|
"## Step 7: end the session",
|
|
@@ -29896,6 +30038,18 @@ function fileOverCapLine(f) {
|
|
|
29896
30038
|
function fileConflictLine(f) {
|
|
29897
30039
|
return `[FILE_CONFLICT path=${f.path} stash=${f.stash}]`;
|
|
29898
30040
|
}
|
|
30041
|
+
function fileRejectionSyncFailedLine(f) {
|
|
30042
|
+
const parts = [`[FILE_REJECTION_SYNC_FAILED verb=${f.verb}`];
|
|
30043
|
+
if (f.path !== undefined)
|
|
30044
|
+
parts.push(`path=${f.path}`);
|
|
30045
|
+
parts.push(`reason=${f.reason}`);
|
|
30046
|
+
if (f.status !== undefined)
|
|
30047
|
+
parts.push(`status=${f.status}`);
|
|
30048
|
+
return `${parts.join(" ")}]`;
|
|
30049
|
+
}
|
|
30050
|
+
function fileRejectionWithdrawnLine(f) {
|
|
30051
|
+
return `[FILE_REJECTION_WITHDRAWN path=${f.path} disposition=${f.disposition}]`;
|
|
30052
|
+
}
|
|
29899
30053
|
function conflictsViewFailedLine(f) {
|
|
29900
30054
|
const msg = f.message === undefined ? "" : sanitizeServerMessage(f.message);
|
|
29901
30055
|
const msgPart = msg === "" ? "" : ` message="${msg.replace(/"/g, "'")}"`;
|
|
@@ -30005,7 +30159,7 @@ function humanBytes(bytes) {
|
|
|
30005
30159
|
function recordFileOverCap(args) {
|
|
30006
30160
|
const withoutStale = removeUnsynced(args.state, "file", args.relPath).state;
|
|
30007
30161
|
const ceiling = args.source === "server" ? `the ${humanBytes(args.max)} server limit` : `the ${humanBytes(args.max)} upload ceiling (measured platform limit)`;
|
|
30008
|
-
const message = args.tracked ? `
|
|
30162
|
+
const message = args.tracked ? `A local change to this file (${humanBytes(args.size)}) exceeds ${ceiling}, ` + `so it cannot be pushed and the copy in Plinth is unchanged. ` + `Your edit is on disk here only.` : `${humanBytes(args.size)} exceeds ${ceiling}. The file is still on disk ` + `here but is not in Plinth; shrink or split it to sync it.`;
|
|
30009
30163
|
return persist(withoutStale, upsertRejected(withoutStale, {
|
|
30010
30164
|
target: "file",
|
|
30011
30165
|
op: "upsert",
|
|
@@ -30013,6 +30167,8 @@ function recordFileOverCap(args) {
|
|
|
30013
30167
|
status: 413,
|
|
30014
30168
|
reason: "over_cap",
|
|
30015
30169
|
message,
|
|
30170
|
+
size_bytes: args.size,
|
|
30171
|
+
max_bytes: args.max,
|
|
30016
30172
|
rejected_at: args.now().toISOString()
|
|
30017
30173
|
}));
|
|
30018
30174
|
}
|
|
@@ -30066,6 +30222,12 @@ function clearFileRejected(state, relPath) {
|
|
|
30066
30222
|
return state;
|
|
30067
30223
|
return commitState(state, next.state);
|
|
30068
30224
|
}
|
|
30225
|
+
function clearReportedSafeFileRejected(state, relPath) {
|
|
30226
|
+
const existing = (state.rejected ?? []).find((o) => o.target === "file" && o.path === relPath);
|
|
30227
|
+
if (existing?.reported_at !== undefined)
|
|
30228
|
+
return state;
|
|
30229
|
+
return clearFileRejected(state, relPath);
|
|
30230
|
+
}
|
|
30069
30231
|
|
|
30070
30232
|
// src/lib/context/workspace-claude-md.ts
|
|
30071
30233
|
var EXTRACTED_TEXT_DIR = extractedTextRelPath("x").replace(/x\.md$/, "");
|
|
@@ -30937,7 +31099,7 @@ var sessionCommand = defineCommand({
|
|
|
30937
31099
|
|
|
30938
31100
|
// src/commands/start.ts
|
|
30939
31101
|
var import_gray_matter13 = __toESM(require_gray_matter(), 1);
|
|
30940
|
-
import { existsSync as existsSync8, readFileSync as
|
|
31102
|
+
import { existsSync as existsSync8, readFileSync as readFileSync25, statSync as statSync17 } from "node:fs";
|
|
30941
31103
|
|
|
30942
31104
|
// src/lib/daemon-log.ts
|
|
30943
31105
|
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync9, renameSync as renameSync7, statSync as statSync4 } from "node:fs";
|
|
@@ -31190,9 +31352,9 @@ function conflictsRoot(workspaceSlug) {
|
|
|
31190
31352
|
return join11(plinthHome(), "_conflicts", workspaceSlug);
|
|
31191
31353
|
}
|
|
31192
31354
|
function conflictFilePath(args) {
|
|
31193
|
-
const { workspaceSlug, entityType, entitySlug, timestamp } = args;
|
|
31355
|
+
const { workspaceSlug, entityType, entitySlug, timestamp, side } = args;
|
|
31194
31356
|
const iso = timestamp.toISOString().replace(/[:.]/g, "-");
|
|
31195
|
-
return join11(conflictsRoot(workspaceSlug), `${entityType}.${entitySlug}.${iso}.md`);
|
|
31357
|
+
return join11(conflictsRoot(workspaceSlug), `${entityType}.${entitySlug}.${iso}.${side}.md`);
|
|
31196
31358
|
}
|
|
31197
31359
|
function stashConflict(args) {
|
|
31198
31360
|
const target = conflictFilePath(args);
|
|
@@ -31201,10 +31363,10 @@ function stashConflict(args) {
|
|
|
31201
31363
|
return target;
|
|
31202
31364
|
}
|
|
31203
31365
|
function fileConflictPath(args) {
|
|
31204
|
-
const { workspaceSlug, relPath, timestamp } = args;
|
|
31366
|
+
const { workspaceSlug, relPath, timestamp, side } = args;
|
|
31205
31367
|
const iso = timestamp.toISOString().replace(/[:.]/g, "-");
|
|
31206
31368
|
const safe = relPath.replace(/[/\\]/g, "__");
|
|
31207
|
-
return join11(conflictsRoot(workspaceSlug), `files.${safe}.${iso}`);
|
|
31369
|
+
return join11(conflictsRoot(workspaceSlug), `files.${safe}.${iso}.${side}`);
|
|
31208
31370
|
}
|
|
31209
31371
|
function stashFileConflict(args) {
|
|
31210
31372
|
const target = fileConflictPath(args);
|
|
@@ -31212,7 +31374,7 @@ function stashFileConflict(args) {
|
|
|
31212
31374
|
writeFileSync7(target, args.bytes, { mode: 384 });
|
|
31213
31375
|
return target;
|
|
31214
31376
|
}
|
|
31215
|
-
var STAMP_RE2 = /\.(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z)(
|
|
31377
|
+
var STAMP_RE2 = /\.(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z)(?:\.(local|cloud))?(?:\.md)?$/;
|
|
31216
31378
|
function parseStamp(stamp) {
|
|
31217
31379
|
const iso = stamp.replace(/T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z$/, "T$1:$2:$3.$4Z");
|
|
31218
31380
|
const d = new Date(iso);
|
|
@@ -31253,11 +31415,13 @@ function listConflictStashes(workspaceSlug) {
|
|
|
31253
31415
|
if (file2.startsWith("."))
|
|
31254
31416
|
continue;
|
|
31255
31417
|
const m = STAMP_RE2.exec(file2);
|
|
31418
|
+
const side = m?.[2];
|
|
31256
31419
|
out.push({
|
|
31257
31420
|
file: file2,
|
|
31258
31421
|
absPath,
|
|
31259
31422
|
label: labelFor(file2),
|
|
31260
31423
|
stashedAt: m ? parseStamp(m[1]) : null,
|
|
31424
|
+
side: side === "local" || side === "cloud" ? side : null,
|
|
31261
31425
|
mtimeMs: st.mtimeMs
|
|
31262
31426
|
});
|
|
31263
31427
|
}
|
|
@@ -31512,7 +31676,7 @@ async function pushFilesUpsert(args) {
|
|
|
31512
31676
|
cloudRow = undefined;
|
|
31513
31677
|
}
|
|
31514
31678
|
}
|
|
31515
|
-
const stashPath = stashFileConflict({ workspaceSlug, relPath, timestamp: now(), bytes });
|
|
31679
|
+
const stashPath = stashFileConflict({ workspaceSlug, relPath, timestamp: now(), bytes, side: "local" });
|
|
31516
31680
|
output?.log(fileConflictLine({ path: relPath, stash: stashPath }));
|
|
31517
31681
|
if (existing) {
|
|
31518
31682
|
nextState = await restoreFileFromCloud({
|
|
@@ -32002,7 +32166,8 @@ async function reconcileContextDocConflict(args) {
|
|
|
32002
32166
|
entityType: strategy.type,
|
|
32003
32167
|
entitySlug: fileSlug,
|
|
32004
32168
|
timestamp: now(),
|
|
32005
|
-
contents: raw
|
|
32169
|
+
contents: raw,
|
|
32170
|
+
side: "local"
|
|
32006
32171
|
});
|
|
32007
32172
|
const nextState = await pullAndWriteMirror({
|
|
32008
32173
|
strategy,
|
|
@@ -32034,12 +32199,13 @@ async function reconcileContextDocConflict(args) {
|
|
|
32034
32199
|
state: nextState
|
|
32035
32200
|
};
|
|
32036
32201
|
}
|
|
32037
|
-
const
|
|
32202
|
+
const stashPreLandCloudCopy = () => stashConflict({
|
|
32038
32203
|
workspaceSlug,
|
|
32039
32204
|
entityType: strategy.type,
|
|
32040
32205
|
entitySlug: existingSlug,
|
|
32041
32206
|
timestamp: now(),
|
|
32042
|
-
contents: strategy.serialise(existing)
|
|
32207
|
+
contents: strategy.serialise(existing),
|
|
32208
|
+
side: "cloud"
|
|
32043
32209
|
});
|
|
32044
32210
|
const fm = parsed.data ?? {};
|
|
32045
32211
|
const lifted = splitUserFrontmatter(strategy, fm);
|
|
@@ -32073,6 +32239,7 @@ async function reconcileContextDocConflict(args) {
|
|
|
32073
32239
|
body_len: hashBody.length
|
|
32074
32240
|
};
|
|
32075
32241
|
state = persistEntry(state, strategy.type, updated);
|
|
32242
|
+
const preLandStash = stashPreLandCloudCopy();
|
|
32076
32243
|
output?.log(contextDocConflictLine({
|
|
32077
32244
|
entityType: strategy.type,
|
|
32078
32245
|
localSlug: fileSlug,
|
|
@@ -32088,7 +32255,8 @@ async function reconcileContextDocConflict(args) {
|
|
|
32088
32255
|
outcome: {
|
|
32089
32256
|
kind: "pushed",
|
|
32090
32257
|
entityType: strategy.type,
|
|
32091
|
-
entitySlug: updated.slug
|
|
32258
|
+
entitySlug: updated.slug,
|
|
32259
|
+
stashPath: preLandStash
|
|
32092
32260
|
},
|
|
32093
32261
|
state
|
|
32094
32262
|
};
|
|
@@ -32112,7 +32280,8 @@ async function reconcileContextDocConflict(args) {
|
|
|
32112
32280
|
entityType: strategy.type,
|
|
32113
32281
|
entitySlug: fileSlug,
|
|
32114
32282
|
timestamp: now(),
|
|
32115
|
-
contents: raw
|
|
32283
|
+
contents: raw,
|
|
32284
|
+
side: "local"
|
|
32116
32285
|
});
|
|
32117
32286
|
if (staleEntryId && staleEntryId !== existingId) {
|
|
32118
32287
|
state = dropEntry(state, strategy.type, staleEntryId);
|
|
@@ -32384,7 +32553,8 @@ async function runPushUpsert(args) {
|
|
|
32384
32553
|
entityType: strategy.type,
|
|
32385
32554
|
entitySlug: liveEntry.slug,
|
|
32386
32555
|
timestamp: args.now(),
|
|
32387
|
-
contents: args.raw
|
|
32556
|
+
contents: args.raw,
|
|
32557
|
+
side: "local"
|
|
32388
32558
|
});
|
|
32389
32559
|
const nextState = await pullAndWriteMirror({
|
|
32390
32560
|
strategy,
|
|
@@ -32422,16 +32592,18 @@ async function reconcileTaskConflict(args) {
|
|
|
32422
32592
|
output
|
|
32423
32593
|
} = args;
|
|
32424
32594
|
let state = args.state;
|
|
32425
|
-
const
|
|
32595
|
+
const stashLocalSide = () => stashConflict({
|
|
32426
32596
|
workspaceSlug,
|
|
32427
32597
|
entityType: strategy.type,
|
|
32428
32598
|
entitySlug: liveEntry.slug,
|
|
32429
32599
|
timestamp: now(),
|
|
32430
|
-
contents: raw
|
|
32600
|
+
contents: raw,
|
|
32601
|
+
side: "local"
|
|
32431
32602
|
});
|
|
32432
32603
|
const localProse = stripSyncErrorBlock(stripCommentsBlock(parsed.content));
|
|
32433
32604
|
const wireBody = normaliseWireBody(strategy.bodyToWire(localProse).body);
|
|
32434
32605
|
if (wireBody == null) {
|
|
32606
|
+
const stashPath2 = stashLocalSide();
|
|
32435
32607
|
state = await pullAndWriteMirror({
|
|
32436
32608
|
strategy,
|
|
32437
32609
|
workspaceSlug,
|
|
@@ -32446,11 +32618,37 @@ async function reconcileTaskConflict(args) {
|
|
|
32446
32618
|
kind: "conflict",
|
|
32447
32619
|
entityType: strategy.type,
|
|
32448
32620
|
entitySlug: liveEntry.slug,
|
|
32449
|
-
stashPath
|
|
32621
|
+
stashPath: stashPath2
|
|
32622
|
+
},
|
|
32623
|
+
state
|
|
32624
|
+
};
|
|
32625
|
+
}
|
|
32626
|
+
let cloudRow = null;
|
|
32627
|
+
try {
|
|
32628
|
+
cloudRow = await getEntity({ strategy, baseUrl, pat, id: liveEntry.id, fetchImpl });
|
|
32629
|
+
} catch (err) {
|
|
32630
|
+
if (!(err instanceof ApiError) || err.status !== 404)
|
|
32631
|
+
throw err;
|
|
32632
|
+
}
|
|
32633
|
+
if (cloudRow === null) {
|
|
32634
|
+
stashLocalSide();
|
|
32635
|
+
return {
|
|
32636
|
+
outcome: {
|
|
32637
|
+
kind: "not_found",
|
|
32638
|
+
entityType: strategy.type,
|
|
32639
|
+
entitySlug: liveEntry.slug
|
|
32450
32640
|
},
|
|
32451
32641
|
state
|
|
32452
32642
|
};
|
|
32453
32643
|
}
|
|
32644
|
+
const stashCloudSide = () => stashConflict({
|
|
32645
|
+
workspaceSlug,
|
|
32646
|
+
entityType: strategy.type,
|
|
32647
|
+
entitySlug: liveEntry.slug,
|
|
32648
|
+
timestamp: now(),
|
|
32649
|
+
contents: strategy.serialise(cloudRow),
|
|
32650
|
+
side: "cloud"
|
|
32651
|
+
});
|
|
32454
32652
|
const patchResult = await patchEntity({
|
|
32455
32653
|
strategy,
|
|
32456
32654
|
baseUrl,
|
|
@@ -32461,6 +32659,7 @@ async function reconcileTaskConflict(args) {
|
|
|
32461
32659
|
fetchImpl
|
|
32462
32660
|
});
|
|
32463
32661
|
if (patchResult.ok) {
|
|
32662
|
+
const stashPath2 = stashCloudSide();
|
|
32464
32663
|
writeMirrorWithBodyPreserve(workspaceSlug, strategy, patchResult.doc);
|
|
32465
32664
|
state = stampStateFromMirror({
|
|
32466
32665
|
strategy,
|
|
@@ -32474,12 +32673,14 @@ async function reconcileTaskConflict(args) {
|
|
|
32474
32673
|
outcome: {
|
|
32475
32674
|
kind: "pushed",
|
|
32476
32675
|
entityType: strategy.type,
|
|
32477
|
-
entitySlug: patchResult.doc.slug
|
|
32676
|
+
entitySlug: patchResult.doc.slug,
|
|
32677
|
+
stashPath: stashPath2
|
|
32478
32678
|
},
|
|
32479
32679
|
state
|
|
32480
32680
|
};
|
|
32481
32681
|
}
|
|
32482
32682
|
if (patchResult.code === 404) {
|
|
32683
|
+
stashLocalSide();
|
|
32483
32684
|
return {
|
|
32484
32685
|
outcome: {
|
|
32485
32686
|
kind: "not_found",
|
|
@@ -32489,6 +32690,7 @@ async function reconcileTaskConflict(args) {
|
|
|
32489
32690
|
state
|
|
32490
32691
|
};
|
|
32491
32692
|
}
|
|
32693
|
+
const stashPath = stashLocalSide();
|
|
32492
32694
|
state = await pullAndWriteMirror({
|
|
32493
32695
|
strategy,
|
|
32494
32696
|
workspaceSlug,
|
|
@@ -32650,7 +32852,8 @@ async function runPushCreate(args) {
|
|
|
32650
32852
|
entityType: strategy.type,
|
|
32651
32853
|
entitySlug: args.slug,
|
|
32652
32854
|
timestamp: args.now(),
|
|
32653
|
-
contents: args.raw
|
|
32855
|
+
contents: args.raw,
|
|
32856
|
+
side: "local"
|
|
32654
32857
|
});
|
|
32655
32858
|
const nextState = await pullAndWriteMirror({
|
|
32656
32859
|
strategy,
|
|
@@ -32876,7 +33079,8 @@ async function pushRename(args) {
|
|
|
32876
33079
|
entityType,
|
|
32877
33080
|
entitySlug: fromSlug,
|
|
32878
33081
|
timestamp: now(),
|
|
32879
|
-
contents: contents2
|
|
33082
|
+
contents: contents2,
|
|
33083
|
+
side: "local"
|
|
32880
33084
|
});
|
|
32881
33085
|
const afterColliding = await pullAndWriteMirror({
|
|
32882
33086
|
strategy,
|
|
@@ -32916,7 +33120,8 @@ async function pushRename(args) {
|
|
|
32916
33120
|
entityType,
|
|
32917
33121
|
entitySlug: fromSlug,
|
|
32918
33122
|
timestamp: now(),
|
|
32919
|
-
contents
|
|
33123
|
+
contents,
|
|
33124
|
+
side: "local"
|
|
32920
33125
|
});
|
|
32921
33126
|
const nextState = await pullAndWriteMirror({
|
|
32922
33127
|
strategy,
|
|
@@ -33040,7 +33245,8 @@ async function discardBatchOps(batch, deps) {
|
|
|
33040
33245
|
entityType: op.entityType,
|
|
33041
33246
|
entitySlug: op.slug,
|
|
33042
33247
|
timestamp: (deps.now ?? (() => new Date))(),
|
|
33043
|
-
contents: local
|
|
33248
|
+
contents: local,
|
|
33249
|
+
side: "local"
|
|
33044
33250
|
});
|
|
33045
33251
|
}
|
|
33046
33252
|
}
|
|
@@ -33073,7 +33279,8 @@ async function discardFileOp(op, deps) {
|
|
|
33073
33279
|
workspaceSlug: deps.workspaceSlug,
|
|
33074
33280
|
relPath: op.path,
|
|
33075
33281
|
timestamp: (deps.now ?? (() => new Date))(),
|
|
33076
|
-
bytes: local
|
|
33282
|
+
bytes: local,
|
|
33283
|
+
side: "local"
|
|
33077
33284
|
});
|
|
33078
33285
|
}
|
|
33079
33286
|
const next = await restoreFileFromCloud({
|
|
@@ -35328,6 +35535,7 @@ async function reconcileFilesSubtree(args) {
|
|
|
35328
35535
|
case "created":
|
|
35329
35536
|
case "updated":
|
|
35330
35537
|
countPushed();
|
|
35538
|
+
state = clearFileRejected(state, relPath);
|
|
35331
35539
|
break;
|
|
35332
35540
|
case "conflict":
|
|
35333
35541
|
conflicts++;
|
|
@@ -35567,7 +35775,8 @@ function applyArtistMemoryOnPull(args) {
|
|
|
35567
35775
|
entityType: ENTITY_TYPE,
|
|
35568
35776
|
entitySlug: conflictSlug(slug),
|
|
35569
35777
|
timestamp: now(),
|
|
35570
|
-
contents: local
|
|
35778
|
+
contents: local,
|
|
35779
|
+
side: "local"
|
|
35571
35780
|
});
|
|
35572
35781
|
out.warn(`Conflict on artists/${slug}/memory.md → stashed local edit to ${stashPath} (cloud-wins).`);
|
|
35573
35782
|
}
|
|
@@ -35647,7 +35856,8 @@ async function pushArtistMemory(args) {
|
|
|
35647
35856
|
entityType: ENTITY_TYPE,
|
|
35648
35857
|
entitySlug: conflictSlug(slug),
|
|
35649
35858
|
timestamp: now(),
|
|
35650
|
-
contents: local
|
|
35859
|
+
contents: local,
|
|
35860
|
+
side: "local"
|
|
35651
35861
|
});
|
|
35652
35862
|
const nextState = adoptCloudMemory({ workspaceSlug, slug, cloud, state: base2, suppressor });
|
|
35653
35863
|
output?.log(`[ARTIST_MEMORY_CONFLICT slug=${slug} id=${entry2.id} action=cloud_wins_stash]`);
|
|
@@ -37192,6 +37402,7 @@ async function reconcileDivergedRows(args) {
|
|
|
37192
37402
|
let scanned = 0;
|
|
37193
37403
|
let diverged = 0;
|
|
37194
37404
|
let pushed = 0;
|
|
37405
|
+
let merged = 0;
|
|
37195
37406
|
let conflicts = 0;
|
|
37196
37407
|
let noop = 0;
|
|
37197
37408
|
let failed = 0;
|
|
@@ -37214,7 +37425,10 @@ async function reconcileDivergedRows(args) {
|
|
|
37214
37425
|
state = result.state;
|
|
37215
37426
|
switch (result.outcome.kind) {
|
|
37216
37427
|
case "pushed":
|
|
37217
|
-
|
|
37428
|
+
if (result.outcome.stashPath)
|
|
37429
|
+
merged++;
|
|
37430
|
+
else
|
|
37431
|
+
pushed++;
|
|
37218
37432
|
break;
|
|
37219
37433
|
case "conflict":
|
|
37220
37434
|
case "create_slug_conflict":
|
|
@@ -37382,6 +37596,7 @@ async function reconcileDivergedRows(args) {
|
|
|
37382
37596
|
scanned,
|
|
37383
37597
|
diverged,
|
|
37384
37598
|
pushed,
|
|
37599
|
+
merged,
|
|
37385
37600
|
conflicts,
|
|
37386
37601
|
noop,
|
|
37387
37602
|
failed,
|
|
@@ -37578,7 +37793,8 @@ async function pullWorkspaceRules(args) {
|
|
|
37578
37793
|
entityType: CONFLICT_ENTITY,
|
|
37579
37794
|
entitySlug: CONFLICT_SLUG,
|
|
37580
37795
|
timestamp: now(),
|
|
37581
|
-
contents: local
|
|
37796
|
+
contents: local,
|
|
37797
|
+
side: "local"
|
|
37582
37798
|
});
|
|
37583
37799
|
output?.warn(`Conflict on ${WORKSPACE_RULES_FILENAME} -> stashed local edit to ${stashPath} (cloud-wins).`);
|
|
37584
37800
|
output?.log(`[WORKSPACE_RULES_PULL_CONFLICT ws=${workspaceSlug} stashed]`);
|
|
@@ -37664,7 +37880,8 @@ async function pushWorkspaceRules(args) {
|
|
|
37664
37880
|
entityType: CONFLICT_ENTITY,
|
|
37665
37881
|
entitySlug: CONFLICT_SLUG,
|
|
37666
37882
|
timestamp: now(),
|
|
37667
|
-
contents: local
|
|
37883
|
+
contents: local,
|
|
37884
|
+
side: "local"
|
|
37668
37885
|
});
|
|
37669
37886
|
const next = await restoreFromCloud(args);
|
|
37670
37887
|
output?.warn(`Conflict on ${WORKSPACE_RULES_FILENAME} -> stashed local edit to ${stashPath} (cloud-wins; cloud copy restored).`);
|
|
@@ -37844,7 +38061,8 @@ async function pullWorkspaceMemory(args) {
|
|
|
37844
38061
|
entityType: CONFLICT_ENTITY2,
|
|
37845
38062
|
entitySlug: CONFLICT_SLUG2,
|
|
37846
38063
|
timestamp: now(),
|
|
37847
|
-
contents: local
|
|
38064
|
+
contents: local,
|
|
38065
|
+
side: "local"
|
|
37848
38066
|
});
|
|
37849
38067
|
output?.warn(`Conflict on ${WORKSPACE_MEMORY_FILENAME} -> stashed local edit to ${stashPath} (cloud-wins).`);
|
|
37850
38068
|
output?.log(`[WORKSPACE_MEMORY_PULL_CONFLICT ws=${workspaceSlug} stashed]`);
|
|
@@ -37930,7 +38148,8 @@ async function pushWorkspaceMemory(args) {
|
|
|
37930
38148
|
entityType: CONFLICT_ENTITY2,
|
|
37931
38149
|
entitySlug: CONFLICT_SLUG2,
|
|
37932
38150
|
timestamp: now(),
|
|
37933
|
-
contents: local
|
|
38151
|
+
contents: local,
|
|
38152
|
+
side: "local"
|
|
37934
38153
|
});
|
|
37935
38154
|
const next = await restoreFromCloud2(args);
|
|
37936
38155
|
output?.warn(`Conflict on ${WORKSPACE_MEMORY_FILENAME} -> stashed local edit to ${stashPath} (cloud-wins; cloud copy restored).`);
|
|
@@ -38118,7 +38337,8 @@ async function pullSingleton(descriptor, args) {
|
|
|
38118
38337
|
entityType: descriptor.conflictEntity,
|
|
38119
38338
|
entitySlug: descriptor.conflictSlug,
|
|
38120
38339
|
timestamp: now(),
|
|
38121
|
-
contents: local
|
|
38340
|
+
contents: local,
|
|
38341
|
+
side: "local"
|
|
38122
38342
|
});
|
|
38123
38343
|
output?.warn(`Conflict on ${descriptor.filename} -> stashed local edit to ${stashPath} (cloud-wins).`);
|
|
38124
38344
|
output?.log(`[${descriptor.logTag}_PULL_CONFLICT ws=${workspaceSlug} stashed]`);
|
|
@@ -38192,7 +38412,8 @@ async function pushSingleton(descriptor, args) {
|
|
|
38192
38412
|
entityType: descriptor.conflictEntity,
|
|
38193
38413
|
entitySlug: descriptor.conflictSlug,
|
|
38194
38414
|
timestamp: now(),
|
|
38195
|
-
contents: local
|
|
38415
|
+
contents: local,
|
|
38416
|
+
side: "local"
|
|
38196
38417
|
});
|
|
38197
38418
|
const next = await restoreFromCloud3(descriptor, args);
|
|
38198
38419
|
output?.warn(`Conflict on ${descriptor.filename} -> stashed local edit to ${stashPath} (cloud-wins; cloud copy restored).`);
|
|
@@ -38385,8 +38606,8 @@ function createThrottledFetch(baseFetch, opts = {}) {
|
|
|
38385
38606
|
import {
|
|
38386
38607
|
existsSync as existsSync7,
|
|
38387
38608
|
readdirSync as readdirSync6,
|
|
38388
|
-
readFileSync as
|
|
38389
|
-
statSync as
|
|
38609
|
+
readFileSync as readFileSync24,
|
|
38610
|
+
statSync as statSync16,
|
|
38390
38611
|
unlinkSync as unlinkSync8
|
|
38391
38612
|
} from "node:fs";
|
|
38392
38613
|
import { join as join16 } from "node:path";
|
|
@@ -38405,8 +38626,8 @@ var FOOTER = [
|
|
|
38405
38626
|
var NOTHING_SET_ASIDE = [
|
|
38406
38627
|
"# Conflicts",
|
|
38407
38628
|
"",
|
|
38408
|
-
"**Nothing has been set aside.**
|
|
38409
|
-
"
|
|
38629
|
+
"**Nothing has been set aside.** Nothing in this workspace has changed in two",
|
|
38630
|
+
"places at once and had one side put aside."
|
|
38410
38631
|
].join(`
|
|
38411
38632
|
`);
|
|
38412
38633
|
function absolute(date5) {
|
|
@@ -38443,9 +38664,47 @@ function resolveConflictRows(workspaceSlug, stashes) {
|
|
|
38443
38664
|
};
|
|
38444
38665
|
});
|
|
38445
38666
|
}
|
|
38667
|
+
function entityNoun(label) {
|
|
38668
|
+
const head = label.slice(0, label.indexOf("/"));
|
|
38669
|
+
switch (head) {
|
|
38670
|
+
case "tasks":
|
|
38671
|
+
return "task";
|
|
38672
|
+
case "documents":
|
|
38673
|
+
return "document";
|
|
38674
|
+
case "projects":
|
|
38675
|
+
return "project";
|
|
38676
|
+
case "artists":
|
|
38677
|
+
return "artist";
|
|
38678
|
+
default:
|
|
38679
|
+
return "record";
|
|
38680
|
+
}
|
|
38681
|
+
}
|
|
38446
38682
|
function renderRow(row) {
|
|
38447
38683
|
const { stash } = row;
|
|
38448
38684
|
const when = stash.stashedAt === null ? "at a time its filename does not record" : `on ${absolute(stash.stashedAt)}`;
|
|
38685
|
+
if (stash.side === "cloud") {
|
|
38686
|
+
const noun = entityNoun(stash.label);
|
|
38687
|
+
return [
|
|
38688
|
+
`## ${stash.label}`,
|
|
38689
|
+
"",
|
|
38690
|
+
`Set aside ${when}.`,
|
|
38691
|
+
"",
|
|
38692
|
+
"**This one is the cloud's copy, not yours.** This " + noun + " changed in",
|
|
38693
|
+
"two places at once and Plinth merged it field by field: your writing won",
|
|
38694
|
+
"and is in your file now, the cloud's other fields won, and what is set",
|
|
38695
|
+
"aside below is the version your writing replaced. That is someone else's",
|
|
38696
|
+
"work, and it is nowhere else.",
|
|
38697
|
+
"",
|
|
38698
|
+
"It is at:",
|
|
38699
|
+
"",
|
|
38700
|
+
"```",
|
|
38701
|
+
stash.absPath,
|
|
38702
|
+
"```",
|
|
38703
|
+
"",
|
|
38704
|
+
`Read it, fold anything worth keeping into the ${noun}, then delete the stash.`
|
|
38705
|
+
].join(`
|
|
38706
|
+
`);
|
|
38707
|
+
}
|
|
38449
38708
|
const out = [`## ${stash.label}`, "", `Set aside ${when}.`, ""];
|
|
38450
38709
|
if (row.kind !== "restorable") {
|
|
38451
38710
|
out.push("Your version is at:", "", "```", stash.absPath, "```", "");
|
|
@@ -38471,15 +38730,24 @@ function renderConflictsView(rows) {
|
|
|
38471
38730
|
${FOOTER}
|
|
38472
38731
|
`;
|
|
38473
38732
|
const n = rows.length;
|
|
38733
|
+
const cloudSide = rows.filter((r) => r.stash.side === "cloud").length;
|
|
38734
|
+
const localSide = n - cloudSide;
|
|
38474
38735
|
const head = [
|
|
38475
38736
|
"# Conflicts",
|
|
38476
38737
|
"",
|
|
38477
|
-
`**${n}
|
|
38478
|
-
`
|
|
38738
|
+
`**${n} ${n === 1 ? "copy was" : "copies were"} set aside when the same thing changed in two places at once.**`,
|
|
38739
|
+
`Nothing here is lost. ${n === 1 ? "It is" : "They are"} listed below, each saying which side it holds.`,
|
|
38479
38740
|
"",
|
|
38480
|
-
|
|
38481
|
-
|
|
38482
|
-
|
|
38741
|
+
...localSide > 0 ? [
|
|
38742
|
+
"For most files Plinth keeps the cloud version in your mirror and puts your",
|
|
38743
|
+
"version aside rather than choosing between them."
|
|
38744
|
+
] : [],
|
|
38745
|
+
...localSide > 0 && cloudSide > 0 ? [""] : [],
|
|
38746
|
+
...cloudSide > 0 ? [
|
|
38747
|
+
"Some records are merged field by field instead: your writing wins and the",
|
|
38748
|
+
"cloud's other fields win. There the copy set aside is the CLOUD's, which",
|
|
38749
|
+
"your version replaced. Each of those says so in its own section."
|
|
38750
|
+
] : []
|
|
38483
38751
|
].join(`
|
|
38484
38752
|
`);
|
|
38485
38753
|
return `${head}
|
|
@@ -38882,7 +39150,7 @@ async function applyFileRow(args) {
|
|
|
38882
39150
|
try {
|
|
38883
39151
|
const localBytes = readFileSync22(abs);
|
|
38884
39152
|
if (sha256Bytes(localBytes) !== existing.content_hash) {
|
|
38885
|
-
const stashPath = stashFileConflict({ workspaceSlug, relPath, timestamp: now(), bytes: localBytes });
|
|
39153
|
+
const stashPath = stashFileConflict({ workspaceSlug, relPath, timestamp: now(), bytes: localBytes, side: "local" });
|
|
38886
39154
|
output?.warn(`Conflict on files/${relPath} → stashed local edit to ${stashPath} (cloud-wins on boot).`);
|
|
38887
39155
|
}
|
|
38888
39156
|
} catch {}
|
|
@@ -38988,6 +39256,203 @@ function textIsCurrent(existing, row, workspaceSlug, relPath) {
|
|
|
38988
39256
|
return extractedTextHash(onDisk) === existing.text_hash;
|
|
38989
39257
|
}
|
|
38990
39258
|
|
|
39259
|
+
// src/lib/files/rejection-sync.ts
|
|
39260
|
+
import { readFileSync as readFileSync23, statSync as statSync15 } from "node:fs";
|
|
39261
|
+
|
|
39262
|
+
// src/lib/files/rejections-api.ts
|
|
39263
|
+
var RejectionsFeedSchema = exports_external.object({
|
|
39264
|
+
rejections: exports_external.array(TeamFileRejectionSchema)
|
|
39265
|
+
});
|
|
39266
|
+
|
|
39267
|
+
class RejectionFeedShapeError extends Error {
|
|
39268
|
+
constructor(message) {
|
|
39269
|
+
super(message);
|
|
39270
|
+
this.name = "RejectionFeedShapeError";
|
|
39271
|
+
}
|
|
39272
|
+
}
|
|
39273
|
+
var WithdrawResponseSchema = exports_external.object({
|
|
39274
|
+
cleared: exports_external.number().int().nonnegative()
|
|
39275
|
+
});
|
|
39276
|
+
function headers(pat) {
|
|
39277
|
+
return {
|
|
39278
|
+
authorization: `Bearer ${pat}`,
|
|
39279
|
+
"content-type": "application/json"
|
|
39280
|
+
};
|
|
39281
|
+
}
|
|
39282
|
+
function endpoint(baseUrl) {
|
|
39283
|
+
return `${baseUrl.replace(/\/$/, "")}/api/sync/files/rejections`;
|
|
39284
|
+
}
|
|
39285
|
+
async function listFileRejections(args) {
|
|
39286
|
+
const doFetch = args.fetchImpl ?? fetch;
|
|
39287
|
+
const res = await doFetch(endpoint(args.baseUrl), {
|
|
39288
|
+
method: "GET",
|
|
39289
|
+
headers: headers(args.pat)
|
|
39290
|
+
});
|
|
39291
|
+
if (!res.ok) {
|
|
39292
|
+
throw new ApiError(res.status, `file-rejections feed failed with HTTP ${res.status}`);
|
|
39293
|
+
}
|
|
39294
|
+
const parsed = RejectionsFeedSchema.safeParse(await res.json());
|
|
39295
|
+
if (!parsed.success) {
|
|
39296
|
+
throw new RejectionFeedShapeError("file-rejections feed did not match the expected shape");
|
|
39297
|
+
}
|
|
39298
|
+
return parsed.data.rejections;
|
|
39299
|
+
}
|
|
39300
|
+
async function postFileRejection(args) {
|
|
39301
|
+
const doFetch = args.fetchImpl ?? fetch;
|
|
39302
|
+
const res = await doFetch(endpoint(args.baseUrl), {
|
|
39303
|
+
method: "POST",
|
|
39304
|
+
headers: headers(args.pat),
|
|
39305
|
+
body: JSON.stringify({
|
|
39306
|
+
path: args.path,
|
|
39307
|
+
reason: args.reason,
|
|
39308
|
+
detail: args.detail ?? null,
|
|
39309
|
+
size_bytes: args.sizeBytes ?? null,
|
|
39310
|
+
max_bytes: args.maxBytes ?? null
|
|
39311
|
+
})
|
|
39312
|
+
});
|
|
39313
|
+
if (!res.ok) {
|
|
39314
|
+
throw new ApiError(res.status, `recording the file rejection failed with HTTP ${res.status}`);
|
|
39315
|
+
}
|
|
39316
|
+
}
|
|
39317
|
+
async function deleteFileRejection(args) {
|
|
39318
|
+
const doFetch = args.fetchImpl ?? fetch;
|
|
39319
|
+
const url2 = `${endpoint(args.baseUrl)}?path=${encodeURIComponent(args.path)}`;
|
|
39320
|
+
const res = await doFetch(url2, {
|
|
39321
|
+
method: "DELETE",
|
|
39322
|
+
headers: headers(args.pat)
|
|
39323
|
+
});
|
|
39324
|
+
if (!res.ok) {
|
|
39325
|
+
throw new ApiError(res.status, `withdrawing the file rejection failed with HTTP ${res.status}`);
|
|
39326
|
+
}
|
|
39327
|
+
const parsed = WithdrawResponseSchema.safeParse(await res.json());
|
|
39328
|
+
return parsed.success ? parsed.data.cleared : 0;
|
|
39329
|
+
}
|
|
39330
|
+
|
|
39331
|
+
// src/lib/files/rejection-sync.ts
|
|
39332
|
+
var TEAM_VISIBLE_REJECT_REASONS = new Set([
|
|
39333
|
+
"over_cap",
|
|
39334
|
+
"bad_request",
|
|
39335
|
+
"invalid_frontmatter"
|
|
39336
|
+
]);
|
|
39337
|
+
async function drainFileRejectionReports(args) {
|
|
39338
|
+
let state = args.state;
|
|
39339
|
+
const pending = listRejected(state).filter((r) => r.target === "file" && !r.reported_at && TEAM_VISIBLE_REJECT_REASONS.has(r.reason));
|
|
39340
|
+
for (const r of pending) {
|
|
39341
|
+
try {
|
|
39342
|
+
await postFileRejection({
|
|
39343
|
+
baseUrl: args.baseUrl,
|
|
39344
|
+
pat: args.pat,
|
|
39345
|
+
path: r.path,
|
|
39346
|
+
reason: r.reason,
|
|
39347
|
+
detail: r.detail ?? r.message ?? null,
|
|
39348
|
+
sizeBytes: r.size_bytes ?? null,
|
|
39349
|
+
maxBytes: r.max_bytes ?? null,
|
|
39350
|
+
fetchImpl: args.fetchImpl
|
|
39351
|
+
});
|
|
39352
|
+
const next = markRejectedReported(state, r.path, args.now().toISOString());
|
|
39353
|
+
if (next.changed)
|
|
39354
|
+
state = commitState(state, next.state);
|
|
39355
|
+
} catch (err) {
|
|
39356
|
+
args.onError?.(r.path, err);
|
|
39357
|
+
}
|
|
39358
|
+
}
|
|
39359
|
+
return state;
|
|
39360
|
+
}
|
|
39361
|
+
async function fetchTeamRejections(args) {
|
|
39362
|
+
const rows = await listFileRejections({
|
|
39363
|
+
baseUrl: args.baseUrl,
|
|
39364
|
+
pat: args.pat,
|
|
39365
|
+
fetchImpl: args.fetchImpl
|
|
39366
|
+
});
|
|
39367
|
+
const next = setTeamRejected(args.state, rows, args.now().toISOString());
|
|
39368
|
+
return next.changed ? commitState(args.state, next.state) : args.state;
|
|
39369
|
+
}
|
|
39370
|
+
function settledDisposition(args) {
|
|
39371
|
+
const entry2 = getFileEntry(args.state, args.relPath);
|
|
39372
|
+
let absolute2;
|
|
39373
|
+
try {
|
|
39374
|
+
absolute2 = filesSubtreePath(args.workspaceSlug, args.relPath);
|
|
39375
|
+
} catch {
|
|
39376
|
+
return null;
|
|
39377
|
+
}
|
|
39378
|
+
let size = null;
|
|
39379
|
+
try {
|
|
39380
|
+
const st = statSync15(absolute2);
|
|
39381
|
+
size = st.isFile() ? st.size : null;
|
|
39382
|
+
} catch {
|
|
39383
|
+
size = null;
|
|
39384
|
+
}
|
|
39385
|
+
if (size === null)
|
|
39386
|
+
return entry2 === undefined ? "gone" : null;
|
|
39387
|
+
if (entry2 === undefined)
|
|
39388
|
+
return null;
|
|
39389
|
+
if (size !== entry2.size_bytes)
|
|
39390
|
+
return null;
|
|
39391
|
+
let bytes;
|
|
39392
|
+
try {
|
|
39393
|
+
bytes = readFileSync23(absolute2);
|
|
39394
|
+
} catch {
|
|
39395
|
+
return null;
|
|
39396
|
+
}
|
|
39397
|
+
return sha256Bytes(bytes) === entry2.content_hash ? "synced" : null;
|
|
39398
|
+
}
|
|
39399
|
+
async function withdrawSettledRejections(args) {
|
|
39400
|
+
let state = args.state;
|
|
39401
|
+
const live = new Set(listTeamRejected(state).map((r) => r.path));
|
|
39402
|
+
const owned = listRejected(state).filter((r) => r.target === "file" && r.reported_at !== undefined);
|
|
39403
|
+
const withdrawn = new Set;
|
|
39404
|
+
for (const record2 of owned) {
|
|
39405
|
+
if (!live.has(record2.path))
|
|
39406
|
+
continue;
|
|
39407
|
+
const disposition = settledDisposition({
|
|
39408
|
+
state,
|
|
39409
|
+
workspaceSlug: args.workspaceSlug,
|
|
39410
|
+
relPath: record2.path
|
|
39411
|
+
});
|
|
39412
|
+
if (disposition === null)
|
|
39413
|
+
continue;
|
|
39414
|
+
try {
|
|
39415
|
+
await deleteFileRejection({
|
|
39416
|
+
baseUrl: args.baseUrl,
|
|
39417
|
+
pat: args.pat,
|
|
39418
|
+
path: record2.path,
|
|
39419
|
+
fetchImpl: args.fetchImpl
|
|
39420
|
+
});
|
|
39421
|
+
withdrawn.add(record2.path);
|
|
39422
|
+
const cleared = removeRejected(state, "file", record2.path);
|
|
39423
|
+
if (cleared.changed)
|
|
39424
|
+
state = commitState(state, cleared.state);
|
|
39425
|
+
args.onWithdrawn?.(record2.path, disposition);
|
|
39426
|
+
} catch (err) {
|
|
39427
|
+
args.onError?.(record2.path, err);
|
|
39428
|
+
}
|
|
39429
|
+
}
|
|
39430
|
+
if (withdrawn.size > 0) {
|
|
39431
|
+
const fetchedAt = state.team_rejected?.fetched_at;
|
|
39432
|
+
if (fetchedAt) {
|
|
39433
|
+
const kept = listTeamRejected(state).filter((r) => !withdrawn.has(r.path));
|
|
39434
|
+
const pruned = setTeamRejected(state, kept, fetchedAt);
|
|
39435
|
+
if (pruned.changed)
|
|
39436
|
+
state = commitState(state, pruned.state);
|
|
39437
|
+
}
|
|
39438
|
+
}
|
|
39439
|
+
return state;
|
|
39440
|
+
}
|
|
39441
|
+
function rearmMissingReports(state) {
|
|
39442
|
+
const live = new Set(listTeamRejected(state).map((r) => r.path));
|
|
39443
|
+
let next = state;
|
|
39444
|
+
for (const r of listRejected(state)) {
|
|
39445
|
+
if (r.reported_at === undefined)
|
|
39446
|
+
continue;
|
|
39447
|
+
if (live.has(r.path))
|
|
39448
|
+
continue;
|
|
39449
|
+
const cleared = clearRejectedReported(next, r.path);
|
|
39450
|
+
if (cleared.changed)
|
|
39451
|
+
next = commitState(next, cleared.state);
|
|
39452
|
+
}
|
|
39453
|
+
return next;
|
|
39454
|
+
}
|
|
39455
|
+
|
|
38991
39456
|
// src/commands/sync.ts
|
|
38992
39457
|
init_output();
|
|
38993
39458
|
var memberRefreshFailedAt = new Map;
|
|
@@ -39041,7 +39506,7 @@ function unlinkIfProvablyOurs(workspaceSlug, strategy, entry2, suppressor) {
|
|
|
39041
39506
|
}
|
|
39042
39507
|
let raw;
|
|
39043
39508
|
try {
|
|
39044
|
-
raw =
|
|
39509
|
+
raw = readFileSync24(target, "utf8");
|
|
39045
39510
|
} catch (err) {
|
|
39046
39511
|
if (err?.code === "ENOENT") {
|
|
39047
39512
|
return { didUnlink: false };
|
|
@@ -39115,6 +39580,66 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
39115
39580
|
}));
|
|
39116
39581
|
out.warn(`Conflict list could not be written to ${CONFLICTS_VIEW_REL_PATH}: ${res.failed}`);
|
|
39117
39582
|
};
|
|
39583
|
+
const syncFileRejections = async () => {
|
|
39584
|
+
const logFailure = (verb) => (path5, err) => {
|
|
39585
|
+
if (err instanceof RejectionFeedShapeError) {
|
|
39586
|
+
out.log(fileRejectionSyncFailedLine({ verb, path: path5, reason: "shape" }));
|
|
39587
|
+
return;
|
|
39588
|
+
}
|
|
39589
|
+
const cat = categorizeError(err);
|
|
39590
|
+
out.log(fileRejectionSyncFailedLine({
|
|
39591
|
+
verb,
|
|
39592
|
+
path: path5,
|
|
39593
|
+
reason: cat.reason,
|
|
39594
|
+
status: cat.status
|
|
39595
|
+
}));
|
|
39596
|
+
};
|
|
39597
|
+
let feedRead = false;
|
|
39598
|
+
try {
|
|
39599
|
+
await fetchTeamRejections({
|
|
39600
|
+
state: loadState(slug),
|
|
39601
|
+
baseUrl: entry2.api_base_url,
|
|
39602
|
+
pat,
|
|
39603
|
+
now,
|
|
39604
|
+
fetchImpl
|
|
39605
|
+
});
|
|
39606
|
+
feedRead = true;
|
|
39607
|
+
} catch (err) {
|
|
39608
|
+
logFailure("feed")(undefined, err);
|
|
39609
|
+
}
|
|
39610
|
+
if (feedRead) {
|
|
39611
|
+
try {
|
|
39612
|
+
rearmMissingReports(loadState(slug));
|
|
39613
|
+
} catch (err) {
|
|
39614
|
+
logFailure("report")(undefined, err);
|
|
39615
|
+
}
|
|
39616
|
+
}
|
|
39617
|
+
try {
|
|
39618
|
+
await drainFileRejectionReports({
|
|
39619
|
+
state: loadState(slug),
|
|
39620
|
+
baseUrl: entry2.api_base_url,
|
|
39621
|
+
pat,
|
|
39622
|
+
now,
|
|
39623
|
+
fetchImpl,
|
|
39624
|
+
onError: (path5, err) => logFailure("report")(path5, err)
|
|
39625
|
+
});
|
|
39626
|
+
} catch (err) {
|
|
39627
|
+
logFailure("report")(undefined, err);
|
|
39628
|
+
}
|
|
39629
|
+
try {
|
|
39630
|
+
await withdrawSettledRejections({
|
|
39631
|
+
state: loadState(slug),
|
|
39632
|
+
workspaceSlug: slug,
|
|
39633
|
+
baseUrl: entry2.api_base_url,
|
|
39634
|
+
pat,
|
|
39635
|
+
fetchImpl,
|
|
39636
|
+
onError: (path5, err) => logFailure("withdraw")(path5, err),
|
|
39637
|
+
onWithdrawn: (path5, disposition) => out.log(fileRejectionWithdrawnLine({ path: path5, disposition }))
|
|
39638
|
+
});
|
|
39639
|
+
} catch (err) {
|
|
39640
|
+
logFailure("withdraw")(undefined, err);
|
|
39641
|
+
}
|
|
39642
|
+
};
|
|
39118
39643
|
refreshBanner();
|
|
39119
39644
|
out.info(`Syncing ${slug}…`);
|
|
39120
39645
|
const startedAt = now();
|
|
@@ -39349,6 +39874,7 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
39349
39874
|
out.warn(`Per-artist context generation skipped: ${msg}`);
|
|
39350
39875
|
}
|
|
39351
39876
|
refreshBanner();
|
|
39877
|
+
await syncFileRejections();
|
|
39352
39878
|
const totalWritten = Object.values(counters).reduce((sum, c) => sum + c.written, 0);
|
|
39353
39879
|
const totalDeleted = Object.values(counters).reduce((sum, c) => sum + c.deleted, 0);
|
|
39354
39880
|
if (totalWritten === 0 && totalDeleted === 0) {
|
|
@@ -39446,7 +39972,7 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
|
|
|
39446
39972
|
if (!suppressor && existingEntry && existingEntry.updated_at !== live.updated_at) {
|
|
39447
39973
|
let localRaw = null;
|
|
39448
39974
|
try {
|
|
39449
|
-
localRaw =
|
|
39975
|
+
localRaw = readFileSync24(entityFilePath(workspaceSlug, strategy.type, live.slug), "utf8");
|
|
39450
39976
|
} catch {
|
|
39451
39977
|
localRaw = null;
|
|
39452
39978
|
}
|
|
@@ -39459,7 +39985,8 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
|
|
|
39459
39985
|
entityType: strategy.type,
|
|
39460
39986
|
entitySlug: live.slug,
|
|
39461
39987
|
timestamp: now(),
|
|
39462
|
-
contents: localRaw
|
|
39988
|
+
contents: localRaw,
|
|
39989
|
+
side: "local"
|
|
39463
39990
|
});
|
|
39464
39991
|
out.warn(`Conflict on ${strategy.type}/${live.slug} → stashed local edit to ${stashPath} (cloud-wins on boot).`);
|
|
39465
39992
|
}
|
|
@@ -39469,7 +39996,7 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
|
|
|
39469
39996
|
const hashes = computeMirrorHashes(strategy, content);
|
|
39470
39997
|
let inode;
|
|
39471
39998
|
try {
|
|
39472
|
-
inode =
|
|
39999
|
+
inode = statSync16(entityFilePath(workspaceSlug, strategy.type, live.slug)).ino;
|
|
39473
40000
|
} catch {
|
|
39474
40001
|
inode = undefined;
|
|
39475
40002
|
}
|
|
@@ -39562,7 +40089,7 @@ var syncCommand = defineCommand({
|
|
|
39562
40089
|
// src/commands/start.ts
|
|
39563
40090
|
var SHUTDOWN_DRAIN_TIMEOUT_MS = 5000;
|
|
39564
40091
|
var PULL_HEARTBEAT_MS = 900000;
|
|
39565
|
-
function backfillStateInodes(workspaceSlug, state, statFn = (p) =>
|
|
40092
|
+
function backfillStateInodes(workspaceSlug, state, statFn = (p) => statSync17(p)) {
|
|
39566
40093
|
let changed = false;
|
|
39567
40094
|
const nextEntries = {};
|
|
39568
40095
|
for (const [entityType, bucket] of Object.entries(state.entries)) {
|
|
@@ -39595,7 +40122,7 @@ function backfillStateInodes(workspaceSlug, state, statFn = (p) => statSync16(p)
|
|
|
39595
40122
|
return { state, changed: false };
|
|
39596
40123
|
return { state: { ...state, entries: nextEntries }, changed: true };
|
|
39597
40124
|
}
|
|
39598
|
-
function backfillBodyLengths(workspaceSlug, state, readFn = (p) =>
|
|
40125
|
+
function backfillBodyLengths(workspaceSlug, state, readFn = (p) => readFileSync25(p, "utf8")) {
|
|
39599
40126
|
let changed = false;
|
|
39600
40127
|
const nextEntries = {};
|
|
39601
40128
|
for (const [entityType, bucket] of Object.entries(state.entries)) {
|
|
@@ -39682,14 +40209,14 @@ function makeWatchHandler(deps) {
|
|
|
39682
40209
|
state: getState(),
|
|
39683
40210
|
deps: { fetchImpl, now, output, suppressor }
|
|
39684
40211
|
});
|
|
39685
|
-
setState(
|
|
40212
|
+
setState(clearReportedSafeFileRejected(result.state, relPath));
|
|
39686
40213
|
reportFileOutcome(spinner, workspaceSlug, getState(), result.outcome);
|
|
39687
40214
|
});
|
|
39688
40215
|
return;
|
|
39689
40216
|
}
|
|
39690
40217
|
let st;
|
|
39691
40218
|
try {
|
|
39692
|
-
st =
|
|
40219
|
+
st = statSync17(absolutePath);
|
|
39693
40220
|
} catch (err) {
|
|
39694
40221
|
if (err?.code === "ENOENT")
|
|
39695
40222
|
return;
|
|
@@ -39720,7 +40247,7 @@ function makeWatchHandler(deps) {
|
|
|
39720
40247
|
spinner.update(`Watching ${workspaceSlug} · ${watchingSummary(getState())}`);
|
|
39721
40248
|
return;
|
|
39722
40249
|
}
|
|
39723
|
-
const bytes =
|
|
40250
|
+
const bytes = readFileSync25(absolutePath);
|
|
39724
40251
|
const hash2 = sha256Bytes(bytes);
|
|
39725
40252
|
if (suppressor?.shouldSuppressUpsert(absolutePath, {
|
|
39726
40253
|
bodyHash: hash2,
|
|
@@ -39778,7 +40305,7 @@ function makeWatchHandler(deps) {
|
|
|
39778
40305
|
if (suppressor) {
|
|
39779
40306
|
let raw = null;
|
|
39780
40307
|
try {
|
|
39781
|
-
raw =
|
|
40308
|
+
raw = readFileSync25(absolutePath, "utf8");
|
|
39782
40309
|
} catch {
|
|
39783
40310
|
raw = null;
|
|
39784
40311
|
}
|
|
@@ -39816,7 +40343,7 @@ function makeWatchHandler(deps) {
|
|
|
39816
40343
|
if (suppressor) {
|
|
39817
40344
|
let raw = null;
|
|
39818
40345
|
try {
|
|
39819
|
-
raw =
|
|
40346
|
+
raw = readFileSync25(absolutePath, "utf8");
|
|
39820
40347
|
} catch {
|
|
39821
40348
|
raw = null;
|
|
39822
40349
|
}
|
|
@@ -39854,7 +40381,7 @@ function makeWatchHandler(deps) {
|
|
|
39854
40381
|
if (suppressor) {
|
|
39855
40382
|
let raw = null;
|
|
39856
40383
|
try {
|
|
39857
|
-
raw =
|
|
40384
|
+
raw = readFileSync25(absolutePath, "utf8");
|
|
39858
40385
|
} catch {
|
|
39859
40386
|
raw = null;
|
|
39860
40387
|
}
|
|
@@ -39892,7 +40419,7 @@ function makeWatchHandler(deps) {
|
|
|
39892
40419
|
if (suppressor) {
|
|
39893
40420
|
let raw = null;
|
|
39894
40421
|
try {
|
|
39895
|
-
raw =
|
|
40422
|
+
raw = readFileSync25(absolutePath, "utf8");
|
|
39896
40423
|
} catch {
|
|
39897
40424
|
raw = null;
|
|
39898
40425
|
}
|
|
@@ -39930,7 +40457,7 @@ function makeWatchHandler(deps) {
|
|
|
39930
40457
|
if (suppressor) {
|
|
39931
40458
|
let raw = null;
|
|
39932
40459
|
try {
|
|
39933
|
-
raw =
|
|
40460
|
+
raw = readFileSync25(absolutePath, "utf8");
|
|
39934
40461
|
} catch {
|
|
39935
40462
|
raw = null;
|
|
39936
40463
|
}
|
|
@@ -40045,7 +40572,7 @@ function makeWatchHandler(deps) {
|
|
|
40045
40572
|
if (strategy) {
|
|
40046
40573
|
let raw = null;
|
|
40047
40574
|
try {
|
|
40048
|
-
raw =
|
|
40575
|
+
raw = readFileSync25(absolutePath, "utf8");
|
|
40049
40576
|
} catch {
|
|
40050
40577
|
raw = null;
|
|
40051
40578
|
}
|
|
@@ -40305,21 +40832,9 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
40305
40832
|
if (backfillWorthReporting(idBackfill)) {
|
|
40306
40833
|
daemonLog.log(frontmatterIdBackfillLine(idBackfill));
|
|
40307
40834
|
}
|
|
40308
|
-
|
|
40309
|
-
|
|
40310
|
-
|
|
40311
|
-
parts.push(`${recon.pushed} pushed`);
|
|
40312
|
-
if (recon.conflicts > 0)
|
|
40313
|
-
parts.push(`${recon.conflicts} stashed to ~/.plinth/_conflicts/`);
|
|
40314
|
-
if (recon.syncErrors > 0)
|
|
40315
|
-
parts.push(`${recon.syncErrors} with sync errors (see # sync-error in the file)`);
|
|
40316
|
-
if (recon.quarantined > 0)
|
|
40317
|
-
parts.push(`${recon.quarantined} held for \`plinth confirm\``);
|
|
40318
|
-
if (recon.failed > 0)
|
|
40319
|
-
parts.push(`${recon.failed} deferred`);
|
|
40320
|
-
const acted = recon.diverged + recon.quarantined;
|
|
40321
|
-
out.info(`Reconciled ${acted} offline edit${acted === 1 ? "" : "s"}${parts.length ? `: ${parts.join(", ")}` : ""}.`);
|
|
40322
|
-
}
|
|
40835
|
+
const reconLine = reconcileSummaryLine(recon);
|
|
40836
|
+
if (reconLine !== null)
|
|
40837
|
+
out.info(reconLine);
|
|
40323
40838
|
const disc = recon.discovery;
|
|
40324
40839
|
if (disc.created > 0 || disc.renamed > 0 || disc.collisions > 0 || disc.renameConflicts > 0) {
|
|
40325
40840
|
const parts = [];
|
|
@@ -40987,11 +41502,34 @@ function watchingSummary(state) {
|
|
|
40987
41502
|
const parts = types.map((t) => `${counts[t] ?? 0} ${t}`);
|
|
40988
41503
|
return parts.join(" · ");
|
|
40989
41504
|
}
|
|
41505
|
+
function reconcileSummaryLine(recon) {
|
|
41506
|
+
if (recon.diverged === 0 && recon.quarantined === 0)
|
|
41507
|
+
return null;
|
|
41508
|
+
const parts = [];
|
|
41509
|
+
if (recon.pushed > 0)
|
|
41510
|
+
parts.push(`${recon.pushed} pushed`);
|
|
41511
|
+
if (recon.merged > 0)
|
|
41512
|
+
parts.push(`${recon.merged} merged field by field (the cloud's description stashed to ~/.plinth/_conflicts/)`);
|
|
41513
|
+
if (recon.conflicts > 0)
|
|
41514
|
+
parts.push(`${recon.conflicts} stashed to ~/.plinth/_conflicts/`);
|
|
41515
|
+
if (recon.syncErrors > 0)
|
|
41516
|
+
parts.push(`${recon.syncErrors} with sync errors (see # sync-error in the file)`);
|
|
41517
|
+
if (recon.quarantined > 0)
|
|
41518
|
+
parts.push(`${recon.quarantined} held for \`plinth confirm\``);
|
|
41519
|
+
if (recon.failed > 0)
|
|
41520
|
+
parts.push(`${recon.failed} deferred`);
|
|
41521
|
+
const acted = recon.diverged + recon.quarantined;
|
|
41522
|
+
return `Reconciled ${acted} offline edit${acted === 1 ? "" : "s"}${parts.length ? `: ${parts.join(", ")}` : ""}.`;
|
|
41523
|
+
}
|
|
40990
41524
|
function reportOutcome(spinner, slug, state, outcome) {
|
|
40991
41525
|
const summary = watchingSummary(state);
|
|
40992
41526
|
switch (outcome.kind) {
|
|
40993
41527
|
case "pushed":
|
|
40994
|
-
|
|
41528
|
+
if (outcome.stashPath) {
|
|
41529
|
+
spinner.warn(`Merged ${outcome.entityType}/${outcome.entitySlug} field by field → the cloud's copy is at ${outcome.stashPath}`);
|
|
41530
|
+
} else {
|
|
41531
|
+
spinner.success(`Pushed ${outcome.entityType}/${outcome.entitySlug}`);
|
|
41532
|
+
}
|
|
40995
41533
|
spinner.update(`Watching ${slug} · ${summary}`);
|
|
40996
41534
|
return;
|
|
40997
41535
|
case "created":
|
|
@@ -41186,7 +41724,7 @@ var startCommand = defineCommand({
|
|
|
41186
41724
|
});
|
|
41187
41725
|
|
|
41188
41726
|
// src/commands/status.ts
|
|
41189
|
-
import { statSync as
|
|
41727
|
+
import { statSync as statSync19 } from "node:fs";
|
|
41190
41728
|
|
|
41191
41729
|
// src/lib/context/memory-bars.ts
|
|
41192
41730
|
var RULE_FILE_BAR_BYTES = 6000;
|
|
@@ -41280,7 +41818,7 @@ function assessMemoryBars(workspaceSlug, deps = {}) {
|
|
|
41280
41818
|
|
|
41281
41819
|
// src/lib/build-stamp.ts
|
|
41282
41820
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
41283
|
-
import { existsSync as existsSync9, mkdirSync as mkdirSync11, readFileSync as
|
|
41821
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync11, readFileSync as readFileSync26, writeFileSync as writeFileSync8 } from "node:fs";
|
|
41284
41822
|
import { dirname as dirname10, join as join17 } from "node:path";
|
|
41285
41823
|
import { fileURLToPath } from "node:url";
|
|
41286
41824
|
var BUILD_STAMP_FILENAME = "build-stamp.json";
|
|
@@ -41304,7 +41842,7 @@ function readHead(repoDir) {
|
|
|
41304
41842
|
}
|
|
41305
41843
|
function readBuildStamp(distDir) {
|
|
41306
41844
|
try {
|
|
41307
|
-
const parsed = JSON.parse(
|
|
41845
|
+
const parsed = JSON.parse(readFileSync26(stampPath(distDir), "utf8"));
|
|
41308
41846
|
if (parsed && typeof parsed.sha === "string" && parsed.sha.length > 0 && typeof parsed.dirty === "boolean" && typeof parsed.built_at === "string") {
|
|
41309
41847
|
return { sha: parsed.sha, dirty: parsed.dirty, built_at: parsed.built_at };
|
|
41310
41848
|
}
|
|
@@ -41365,12 +41903,12 @@ function checkBuildStaleness(deps = {}) {
|
|
|
41365
41903
|
}
|
|
41366
41904
|
|
|
41367
41905
|
// src/lib/memory-links.ts
|
|
41368
|
-
import { readFileSync as
|
|
41906
|
+
import { readFileSync as readFileSync27 } from "node:fs";
|
|
41369
41907
|
import { relative as relative13, sep as sep5 } from "node:path";
|
|
41370
41908
|
var MEMORY_FILE_RE = /^(workspace-memory\.md|artists\/[^/]+\/memory\.md)$/i;
|
|
41371
41909
|
function scanMemoryLinks(deps) {
|
|
41372
41910
|
const listFiles = deps.listFiles ?? ((r) => walkSubtreeFiles(r, isVcsInternal));
|
|
41373
|
-
const readFile = deps.readFile ?? ((p) =>
|
|
41911
|
+
const readFile = deps.readFile ?? ((p) => readFileSync27(p, "utf8"));
|
|
41374
41912
|
let root;
|
|
41375
41913
|
let absPaths;
|
|
41376
41914
|
try {
|
|
@@ -41414,7 +41952,7 @@ function scanMemoryLinks(deps) {
|
|
|
41414
41952
|
init_output();
|
|
41415
41953
|
|
|
41416
41954
|
// src/lib/pending-writes.ts
|
|
41417
|
-
import { readFileSync as
|
|
41955
|
+
import { readFileSync as readFileSync28, statSync as statSync18 } from "node:fs";
|
|
41418
41956
|
import { relative as relative14 } from "node:path";
|
|
41419
41957
|
var import_gray_matter14 = __toESM(require_gray_matter(), 1);
|
|
41420
41958
|
var EMPTY2 = { writes: [], count: 0 };
|
|
@@ -41454,7 +41992,7 @@ function scanPendingWrites(args) {
|
|
|
41454
41992
|
const entry3 = getFileEntry(state, fileMatch.relPath);
|
|
41455
41993
|
let size;
|
|
41456
41994
|
try {
|
|
41457
|
-
size =
|
|
41995
|
+
size = statSync18(absolutePath).size;
|
|
41458
41996
|
} catch {
|
|
41459
41997
|
continue;
|
|
41460
41998
|
}
|
|
@@ -41462,7 +42000,7 @@ function scanPendingWrites(args) {
|
|
|
41462
42000
|
continue;
|
|
41463
42001
|
let hash2;
|
|
41464
42002
|
try {
|
|
41465
|
-
hash2 = sha256Bytes(
|
|
42003
|
+
hash2 = sha256Bytes(readFileSync28(absolutePath));
|
|
41466
42004
|
} catch {
|
|
41467
42005
|
continue;
|
|
41468
42006
|
}
|
|
@@ -41483,7 +42021,7 @@ function scanPendingWrites(args) {
|
|
|
41483
42021
|
continue;
|
|
41484
42022
|
let liveInode;
|
|
41485
42023
|
try {
|
|
41486
|
-
liveInode =
|
|
42024
|
+
liveInode = statSync18(absolutePath).ino;
|
|
41487
42025
|
} catch {
|
|
41488
42026
|
liveInode = undefined;
|
|
41489
42027
|
}
|
|
@@ -41493,7 +42031,7 @@ function scanPendingWrites(args) {
|
|
|
41493
42031
|
continue;
|
|
41494
42032
|
let content;
|
|
41495
42033
|
try {
|
|
41496
|
-
content =
|
|
42034
|
+
content = readFileSync28(absolutePath, "utf8");
|
|
41497
42035
|
} catch {
|
|
41498
42036
|
continue;
|
|
41499
42037
|
}
|
|
@@ -41635,6 +42173,40 @@ function row(label, value) {
|
|
|
41635
42173
|
function cont(value) {
|
|
41636
42174
|
return `${" ".repeat(LABEL_WIDTH)} ${value}`;
|
|
41637
42175
|
}
|
|
42176
|
+
function teamRejectedHeadline(count, state) {
|
|
42177
|
+
const noun = count === 1 ? "file is" : "files are";
|
|
42178
|
+
const at = teamRejectedFetchedAt(state);
|
|
42179
|
+
const when = at ? readStamp(at) : undefined;
|
|
42180
|
+
const freshness = when ? `read from Plinth at the last sync, ${when}` : `read from Plinth at an unrecorded time`;
|
|
42181
|
+
return `${count} more ${noun} also rejected on other machines (${freshness} — start the daemon for a fresher list):`;
|
|
42182
|
+
}
|
|
42183
|
+
function teamRejectedDetail(r) {
|
|
42184
|
+
const why = r.detail ? firstSentence(r.detail) : r.reason;
|
|
42185
|
+
const who = r.reported_by_name ?? "another machine";
|
|
42186
|
+
const since = r.first_seen_at ? shortDate(r.first_seen_at) : undefined;
|
|
42187
|
+
return since ? `${why} — ${who}, first seen ${since}` : `${why} — ${who}`;
|
|
42188
|
+
}
|
|
42189
|
+
function firstSentence(text) {
|
|
42190
|
+
const end = text.indexOf(". ");
|
|
42191
|
+
return end === -1 ? text : text.slice(0, end + 1);
|
|
42192
|
+
}
|
|
42193
|
+
function readStamp(iso, today = new Date) {
|
|
42194
|
+
const d = new Date(iso);
|
|
42195
|
+
if (Number.isNaN(d.getTime()))
|
|
42196
|
+
return;
|
|
42197
|
+
const hhmm = `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
42198
|
+
const day = shortDate(iso);
|
|
42199
|
+
return day === localDate(today) ? hhmm : `${day} ${hhmm}`;
|
|
42200
|
+
}
|
|
42201
|
+
function shortDate(iso) {
|
|
42202
|
+
const d = new Date(iso);
|
|
42203
|
+
if (Number.isNaN(d.getTime()))
|
|
42204
|
+
return;
|
|
42205
|
+
return localDate(d);
|
|
42206
|
+
}
|
|
42207
|
+
function localDate(d) {
|
|
42208
|
+
return `${d.getFullYear()}-` + `${String(d.getMonth() + 1).padStart(2, "0")}-` + `${String(d.getDate()).padStart(2, "0")}`;
|
|
42209
|
+
}
|
|
41638
42210
|
function uncheckedLines(items) {
|
|
41639
42211
|
const n = items.length;
|
|
41640
42212
|
const head = `${n} generated command file${n === 1 ? "" : "s"} could not be checked, ` + `so ${n === 1 ? "its block was" : "their blocks were"} not compared.`;
|
|
@@ -41692,7 +42264,7 @@ function renderBuild(out, staleness) {
|
|
|
41692
42264
|
}
|
|
41693
42265
|
function lastActivity(now) {
|
|
41694
42266
|
try {
|
|
41695
|
-
const mtimeMs =
|
|
42267
|
+
const mtimeMs = statSync19(daemonLogPath()).mtimeMs;
|
|
41696
42268
|
return formatRelative(now.getTime() - mtimeMs);
|
|
41697
42269
|
} catch {
|
|
41698
42270
|
return null;
|
|
@@ -41802,13 +42374,13 @@ async function runStatus(args, deps = {}) {
|
|
|
41802
42374
|
const heldArtists = pending.writes.filter((w) => w.held === "artist");
|
|
41803
42375
|
const heldCloudRows = pending.writes.filter((w) => w.held === "cloud-row");
|
|
41804
42376
|
const heldRenames = pending.writes.filter((w) => w.held === "rename-taken");
|
|
41805
|
-
const
|
|
42377
|
+
const MAX_SHOWN2 = 10;
|
|
41806
42378
|
const listPaths = (writes) => {
|
|
41807
|
-
for (const w of writes.slice(0,
|
|
42379
|
+
for (const w of writes.slice(0, MAX_SHOWN2)) {
|
|
41808
42380
|
out.info(cont(`${w.path} (${w.kind})`));
|
|
41809
42381
|
}
|
|
41810
|
-
if (writes.length >
|
|
41811
|
-
out.info(cont(`…and ${writes.length -
|
|
42382
|
+
if (writes.length > MAX_SHOWN2) {
|
|
42383
|
+
out.info(cont(`…and ${writes.length - MAX_SHOWN2} more.`));
|
|
41812
42384
|
}
|
|
41813
42385
|
};
|
|
41814
42386
|
let labelled = false;
|
|
@@ -41849,11 +42421,16 @@ async function runStatus(args, deps = {}) {
|
|
|
41849
42421
|
}
|
|
41850
42422
|
out.info("");
|
|
41851
42423
|
const rejected = listRejected(localState);
|
|
41852
|
-
|
|
42424
|
+
const localRejectedPaths = new Set(rejected.filter((r) => r.target === "file").map((r) => r.path));
|
|
42425
|
+
const teamRejected = listTeamRejected(localState).filter((r) => !localRejectedPaths.has(r.path));
|
|
42426
|
+
const MAX_SHOWN = 10;
|
|
42427
|
+
if (rejected.length === 0 && teamRejected.length === 0) {
|
|
41853
42428
|
out.info(row("Rejected", "No writes rejected."));
|
|
42429
|
+
} else if (rejected.length === 0) {
|
|
42430
|
+
out.info(row("Rejected", "No writes rejected on this machine."));
|
|
41854
42431
|
} else {
|
|
41855
|
-
|
|
41856
|
-
|
|
42432
|
+
const scope = teamRejected.length > 0 ? " on this machine" : "";
|
|
42433
|
+
out.info(row("Rejected", `${rejected.length} write${rejected.length === 1 ? "" : "s"} rejected${scope} and not retried. Each line says what to do.`));
|
|
41857
42434
|
for (const r of rejected.slice(0, MAX_SHOWN)) {
|
|
41858
42435
|
const why = r.detail ?? r.message ?? r.reason;
|
|
41859
42436
|
out.info(cont(`${r.path} — ${why}`));
|
|
@@ -41862,16 +42439,34 @@ async function runStatus(args, deps = {}) {
|
|
|
41862
42439
|
out.info(cont(`…and ${rejected.length - MAX_SHOWN} more (see ~/.plinth/daemon.log).`));
|
|
41863
42440
|
}
|
|
41864
42441
|
}
|
|
42442
|
+
if (teamRejected.length > 0) {
|
|
42443
|
+
out.info(cont(""));
|
|
42444
|
+
out.info(cont(teamRejectedHeadline(teamRejected.length, localState)));
|
|
42445
|
+
for (const r of teamRejected.slice(0, MAX_SHOWN)) {
|
|
42446
|
+
out.info(cont(`${r.path} — ${teamRejectedDetail(r)}`));
|
|
42447
|
+
}
|
|
42448
|
+
if (teamRejected.length > MAX_SHOWN) {
|
|
42449
|
+
out.info(cont(`…and ${teamRejected.length - MAX_SHOWN} more.`));
|
|
42450
|
+
}
|
|
42451
|
+
}
|
|
41865
42452
|
out.info("");
|
|
41866
42453
|
const stashes = listConflictStashes(slug);
|
|
41867
42454
|
if (stashes.length === 0) {
|
|
41868
42455
|
out.info(row("Conflicts", "No conflict stashes."));
|
|
41869
42456
|
} else {
|
|
41870
|
-
|
|
42457
|
+
const cloudSide = stashes.filter((s) => s.side === "cloud");
|
|
42458
|
+
const localSide = stashes.filter((s) => s.side !== "cloud");
|
|
42459
|
+
out.info(row("Conflicts", `${stashes.length} stashed cop${stashes.length === 1 ? "y" : "ies"} in ${conflictsRoot(slug)}`));
|
|
41871
42460
|
const newest = stashes[0];
|
|
41872
42461
|
const when = newest.stashedAt ? `${formatAbsolute(newest.stashedAt)} (${formatRelative(now.getTime() - newest.stashedAt.getTime())})` : "time unknown";
|
|
41873
42462
|
out.info(cont(`Newest: ${newest.label} — ${when}`));
|
|
41874
|
-
|
|
42463
|
+
const lead = (n) => n === stashes.length ? "Each is" : n === 1 ? "1 is" : `${n} are`;
|
|
42464
|
+
if (localSide.length > 0) {
|
|
42465
|
+
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.`));
|
|
42466
|
+
}
|
|
42467
|
+
if (cloudSide.length > 0) {
|
|
42468
|
+
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.`));
|
|
42469
|
+
}
|
|
41875
42470
|
out.info(cont(`Full list with restore commands: ${CONFLICTS_VIEW_REL_PATH} (written by the sync, so this line is the fresher of the two)`));
|
|
41876
42471
|
}
|
|
41877
42472
|
out.info("");
|
|
@@ -42076,7 +42671,7 @@ async function runStatus(args, deps = {}) {
|
|
|
42076
42671
|
var statusCommand = defineCommand({
|
|
42077
42672
|
meta: {
|
|
42078
42673
|
name: "status",
|
|
42079
|
-
description: "Print local sync state: daemon liveness, last sync + per-type counts, held destructive batches,
|
|
42674
|
+
description: "Print local sync state: daemon liveness, last sync + per-type counts, held destructive batches, the voice profile's status, and files rejected anywhere in the workspace (rejections are grouped by the member who reported them; two machines signed in as the same person cannot be told apart)"
|
|
42080
42675
|
},
|
|
42081
42676
|
args: {
|
|
42082
42677
|
workspace: {
|
|
@@ -42110,7 +42705,7 @@ var versionCommand = defineCommand({
|
|
|
42110
42705
|
});
|
|
42111
42706
|
|
|
42112
42707
|
// src/commands/voice-gate.ts
|
|
42113
|
-
import { readFileSync as
|
|
42708
|
+
import { readFileSync as readFileSync31 } from "node:fs";
|
|
42114
42709
|
|
|
42115
42710
|
// src/lib/voice-gate/api.ts
|
|
42116
42711
|
var VoiceGateSurfaces = [
|
|
@@ -42215,7 +42810,7 @@ async function callVoiceGate(req) {
|
|
|
42215
42810
|
import { createHash as createHash4 } from "node:crypto";
|
|
42216
42811
|
import {
|
|
42217
42812
|
mkdirSync as mkdirSync12,
|
|
42218
|
-
readFileSync as
|
|
42813
|
+
readFileSync as readFileSync29,
|
|
42219
42814
|
renameSync as renameSync8,
|
|
42220
42815
|
unlinkSync as unlinkSync9,
|
|
42221
42816
|
writeFileSync as writeFileSync9
|
|
@@ -42246,7 +42841,7 @@ function emptyFile() {
|
|
|
42246
42841
|
}
|
|
42247
42842
|
function load2(workspaceSlug) {
|
|
42248
42843
|
try {
|
|
42249
|
-
const raw =
|
|
42844
|
+
const raw = readFileSync29(attemptsPath(workspaceSlug), "utf8");
|
|
42250
42845
|
const parsed = AttemptsFileSchema.safeParse(JSON.parse(raw));
|
|
42251
42846
|
return parsed.success ? parsed.data : emptyFile();
|
|
42252
42847
|
} catch {
|
|
@@ -42409,7 +43004,7 @@ function denyUngated(detail) {
|
|
|
42409
43004
|
}
|
|
42410
43005
|
|
|
42411
43006
|
// src/lib/voice-gate/payload.ts
|
|
42412
|
-
import { readFileSync as
|
|
43007
|
+
import { readFileSync as readFileSync30, statSync as statSync20 } from "node:fs";
|
|
42413
43008
|
import { homedir as homedir2 } from "node:os";
|
|
42414
43009
|
import { isAbsolute as isAbsolute3, resolve as resolve4 } from "node:path";
|
|
42415
43010
|
var MAX_BODY_BYTES = 256 * 1024;
|
|
@@ -42566,11 +43161,11 @@ function isHelperInvocation(tokens) {
|
|
|
42566
43161
|
return helperIndex === start + 1 && /(^|\/)python[0-9.]*$/.test(prev);
|
|
42567
43162
|
}
|
|
42568
43163
|
function defaultReadBodyFile(path5) {
|
|
42569
|
-
const stat4 =
|
|
43164
|
+
const stat4 = statSync20(path5);
|
|
42570
43165
|
if (stat4.size > MAX_BODY_BYTES) {
|
|
42571
43166
|
throw new Error(`draft body is ${stat4.size} bytes, over the ${MAX_BODY_BYTES}-byte gate limit`);
|
|
42572
43167
|
}
|
|
42573
|
-
return
|
|
43168
|
+
return readFileSync30(path5, "utf8");
|
|
42574
43169
|
}
|
|
42575
43170
|
function parseRecipients(...raw) {
|
|
42576
43171
|
const all = raw.filter((v) => typeof v === "string").flatMap((v) => v.split(",")).map((v) => v.trim().toLowerCase()).filter((v) => v.length > 0);
|
|
@@ -42930,7 +43525,7 @@ async function runVoiceGateDirect(args, deps = {}) {
|
|
|
42930
43525
|
let text;
|
|
42931
43526
|
if (args.file) {
|
|
42932
43527
|
try {
|
|
42933
|
-
text =
|
|
43528
|
+
text = readFileSync31(args.file, "utf8");
|
|
42934
43529
|
} catch (err) {
|
|
42935
43530
|
const msg = err instanceof Error ? err.message : "unknown error";
|
|
42936
43531
|
out.error(`Could not read ${args.file}: ${msg}`);
|