@augurworks/augur 0.15.1 → 0.15.3

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/scripts/read.mjs CHANGED
@@ -4,8 +4,7 @@
4
4
  // refuses edits there. `augur close` inside it removes it. See docs/drafts-that-land.md §7.
5
5
  import path from "node:path";
6
6
  import { target } from "./lib/store.mjs";
7
- import { markPathFor } from "./lib/marks.mjs";
8
- import { unitClient, doRead, readDirFor } from "./lib/draft.mjs";
7
+ import { unitClient, doRead, readDirFor, unitPathFor } from "./lib/draft.mjs";
9
8
  import { normUnit } from "../src/unit-core.mjs";
10
9
 
11
10
  const log = (m) => console.error(`\x1b[35m[read]\x1b[0m ${m}`);
@@ -15,7 +14,7 @@ const opt = (n) => { const i = argv.indexOf(n); return i > -1 ? argv[i + 1] : nu
15
14
  const positional = argv.filter((a, i) => !a.startsWith("--") && !(i > 0 && argv[i - 1].startsWith("--")));
16
15
  const raw = positional[0];
17
16
  if (!raw) die("name a prototype: `augur read <opportunity>/<prototype>`.");
18
- const unit = normUnit(markPathFor(raw) || raw);
17
+ const unit = normUnit(unitPathFor(raw) || raw);
19
18
  if (!unit) die(`"${raw}" is not a prototype path.`);
20
19
  let origin, token;
21
20
  try { ({ origin, token } = target({ needToken: true })); } catch (e) { die(e.message); }
@@ -16,7 +16,6 @@ import { execFileSync } from "node:child_process";
16
16
  import { existsSync, readFileSync, readdirSync } from "node:fs";
17
17
  import path from "node:path";
18
18
  import { resolveOrigin, resolveToken, apiClient, buildStamp, ENGINE_ROOT } from "./lib/store.mjs";
19
- import { fetchMarks, markLine } from "./lib/marks.mjs";
20
19
  import { registryList, unitClient, draftsReport } from "./lib/draft.mjs";
21
20
 
22
21
  const args = process.argv.slice(2);
@@ -123,28 +122,11 @@ console.log("");
123
122
  console.log(` ${C.dim}engine chrome ${eng.sha ? eng.sha.slice(0, 12) : "—"}` +
124
123
  `${eng.version ? ` (v${eng.version})` : ""}${eng.publishedAt ? ` · shipped ${eng.publishedAt}` : ""}${C.off}`);
125
124
 
126
- // ── who is working where, right now ─────────────────────────────────────────
127
- //
128
- // `F-presence-marks`. The three numbers above answer "is what I have what is live"; they
129
- // cannot answer "is somebody else in here", and that is the question that decides whether
130
- // the next hour of work is going to be merged or forked.
131
- //
132
- // NEEDS A TOKEN and does not ask for one. `status` has always been the command that runs
133
- // with nothing configured — the build stamp is public — so a missing credential quietly
134
- // costs the marks section and nothing else. It affects the exit code in no way: marks
135
- // refuse nothing, and a mark is not a problem.
136
- const markToken = resolveToken(origin);
137
- if (markToken) {
138
- const marks = await fetchMarks(apiClient(origin, markToken));
139
- if (marks.length) {
140
- console.log("");
141
- console.log(` ${C.dim}being worked on right now${C.off}`);
142
- for (const m of marks) console.log(` ${C.dim} ${markLine(m)}${C.off}`);
143
- }
144
- } else {
145
- // The one thing a fresh agent needs to know and has no other way to learn: this machine
146
- // holds no token, and the way to get one is pairing, not a password. Same words as the
147
- // instance's own /llms.txt. Costs nothing here — status never needed the token.
125
+ // ── not paired? say how, once ───────────────────────────────────────────────
126
+ // The one thing a fresh agent needs to know and has no other way to learn: this machine
127
+ // holds no token, and the way to get one is pairing, not a password. Same words as the
128
+ // instance's own /llms.txt. Costs nothing here status never needed the token.
129
+ if (!resolveToken(origin)) {
148
130
  console.log("");
149
131
  console.log(` ${C.dim}not paired with ${origin}. Publishing from here needs a token; get one without a password:${C.off}`);
150
132
  console.log(` ${C.dim} augur connect --origin ${origin} (the owner approves a code in a signed-in browser)${C.off}`);
package/seed/README.md CHANGED
@@ -59,7 +59,7 @@ Three things are substituted on the way in:
59
59
 
60
60
  | What | Where | Substituted with |
61
61
  | --- | --- | --- |
62
- | The connect command | `CONNECT_COMMAND` in `start-here/prototypes/connect-your-terminal/index.html` | `npx augur connect --origin https://<label><suffix>`, the workspace's real address, filled the moment the page is published |
62
+ | The connect command | `CONNECT_COMMAND` in `start-here/prototypes/connect-your-terminal/index.html` | `npx @augurworks/augur connect --origin https://<label><suffix>`, the workspace's real address, filled the moment the page is published |
63
63
  | Comment timestamps | `at` in `threads.json` | the provisioning time, so day-one threads do not read as months old |
64
64
  | File timestamps | `editedAt` on every file in the manifest | the same provisioning time, all of them |
65
65
 
@@ -91,7 +91,7 @@
91
91
  <h2 class="s-h2">Run this where your agent runs</h2>
92
92
  <p class="s-prose">In the folder you want this workspace to live in, run:</p>
93
93
  <div class="s-cmd">
94
- <code class="s-cmd__text" id="cmd-connect">npx augur connect your-workspace</code>
94
+ <code class="s-cmd__text" id="cmd-connect">npx @augurworks/augur connect your-workspace</code>
95
95
  <button type="button" class="s-cmd__btn" data-s-copy="#cmd-connect">Copy</button>
96
96
  </div>
97
97
  <p class="s-prose" style="margin-top: var(--starter-s4)">A browser tab opens and asks you to
@@ -105,7 +105,7 @@
105
105
  <div>
106
106
  <p class="ask__why">Paste this into your agent instead of running the command yourself.</p>
107
107
  <div class="s-cmd s-cmd--paste">
108
- <code class="s-cmd__text s-cmd__text--plain" id="ask-agent">Run `npx augur connect your-workspace` in this folder. It will open a browser tab for me to approve — tell me when it is waiting, and tell me exactly what it prints when it finishes.</code>
108
+ <code class="s-cmd__text s-cmd__text--plain" id="ask-agent">Run `npx @augurworks/augur connect your-workspace` in this folder. It will open a browser tab for me to approve — tell me when it is waiting, and tell me exactly what it prints when it finishes.</code>
109
109
  <button type="button" class="s-cmd__btn" data-s-copy="#ask-agent">Copy</button>
110
110
  </div>
111
111
  </div>
@@ -311,7 +311,7 @@
311
311
  <p class="s-prose">Last one. Ask the agent to connect, in plain language — it runs the
312
312
  command for you:</p>
313
313
  <div class="s-cmd s-cmd--paste">
314
- <code class="s-cmd__text s-cmd__text--plain" id="ask-5-do">Run `npx augur connect your-workspace` in this folder. It will open a browser tab for me to approve — tell me when it is waiting, and tell me exactly what it prints when it finishes.</code>
314
+ <code class="s-cmd__text s-cmd__text--plain" id="ask-5-do">Run `npx @augurworks/augur connect your-workspace` in this folder. It will open a browser tab for me to approve — tell me when it is waiting, and tell me exactly what it prints when it finishes.</code>
315
315
  <button type="button" class="s-cmd__btn" data-s-copy="#ask-5-do">Copy</button>
316
316
  </div>
317
317
  <p class="s-prose">Approve the tab that opens. The terminal now has a key of its own, so
@@ -369,7 +369,7 @@
369
369
  var host = location.hostname || "";
370
370
  var label = host.split(".")[0];
371
371
  var named = host.indexOf(".") > 0 && label && label !== "localhost" && !/^\d+$/.test(label);
372
- return named ? "npx augur connect --origin https://" + host : "npx augur connect your-workspace";
372
+ return named ? "npx @augurworks/augur connect --origin https://" + host : "npx @augurworks/augur connect your-workspace";
373
373
  }
374
374
 
375
375
  var cmd = connectCommand();
@@ -377,7 +377,7 @@
377
377
  if (cmdEl) cmdEl.textContent = cmd;
378
378
  ["ask-agent", "ask-5-do"].forEach(function (id) {
379
379
  var el = document.getElementById(id);
380
- if (el) el.textContent = el.textContent.replace(/npx augur connect [\w-]+/, cmd);
380
+ if (el) el.textContent = el.textContent.replace(/npx @augurworks/augur connect [\w-]+/, cmd);
381
381
  });
382
382
 
383
383
  /* ── Which path you are on ────────────────────────────────────────────────
package/src/_worker.js CHANGED
@@ -5453,46 +5453,6 @@ async function publishApi(tctx, request, url, env) {
5453
5453
  const [spaceId, op, arg] = url.pathname.slice("/__publish/".length).split("/");
5454
5454
  if (!spaceId || !op || !/^[a-z0-9_][a-z0-9-]*$/.test(spaceId)) return jsonResponse({ error: "bad-path" }, 400);
5455
5455
 
5456
- // ── working marks (`F-presence-marks`) ─────────────────────────────────────
5457
- //
5458
- // AHEAD OF THE BUNDLE-STORE GUARD, deliberately: a mark is not published content and
5459
- // holds nothing the store knows about, so an instance serving from ASSETS — `augur dev`,
5460
- // `npm run offline`, a raw engine build — is exactly where two agents most need to stay
5461
- // out of each other's way, and 501 there would be an accident of where the check sits.
5462
- //
5463
- // ANY VALID PUBLISH TOKEN, whatever its scope. A mark names a path, not a space, and a
5464
- // space-scoped token holder is precisely the person whose work-start is worth announcing.
5465
- // The capability gate still applies, so a restricted credential (the control plane's
5466
- // purge token) reaches this no more than it reaches anything else.
5467
- if (spaceId === "_marks") {
5468
- const a = await publishAuthDetailed(tctx, request, env, spaceId, true);
5469
- if (!a.entry) return jsonResponse(publishRefusalBody(a.refusal), 403);
5470
- if (capabilityRefusal(a.entry, spaceId, op)) {
5471
- return jsonResponse({ error: "forbidden", reason: "capability-not-granted" }, 403);
5472
- }
5473
- // WHO, from the credential and never from the body. `augur login` labels a token with
5474
- // the holder's address; a token an admin minted by hand carries whatever they typed,
5475
- // which hashes to a stable id that resolves to no roster face — honest, and better
5476
- // than letting the caller name itself.
5477
- const who = { personId: personId(a.entry.label || "") };
5478
- if (op === "list" && request.method === "GET") {
5479
- return jsonResponse({ ...(await readMarks(tctx, env)), ttlMs: MARK_TTL_MS, maxTtlMs: MARK_TTL_MAX_MS });
5480
- }
5481
- let body = null;
5482
- if (request.method === "POST") {
5483
- try { body = await request.json(); } catch (e) { return jsonResponse({ error: "bad-json" }, 400); }
5484
- }
5485
- if (op === "set" && request.method === "POST") {
5486
- const out = await writeMark(tctx, env, who, { path: body && body.path, ttl: body && body.ttl });
5487
- return out.error ? jsonResponse(out, out.error === "bad-input" ? 400 : 503) : jsonResponse(out);
5488
- }
5489
- if (op === "clear" && request.method === "POST") {
5490
- const out = await clearMark(tctx, env, who, { path: body && body.path });
5491
- return out.error ? jsonResponse(out, out.error === "bad-input" ? 400 : 503) : jsonResponse(out);
5492
- }
5493
- return jsonResponse({ error: "unknown-op" }, 400);
5494
- }
5495
-
5496
5456
  if (!env.BUNDLES) return jsonResponse({ error: "bundle-store-not-configured" }, 501);
5497
5457
  // This workspace's view of the store. `blobs/` is content-addressed and shared, so the
5498
5458
  // three `blobs/…` operations below deliberately keep using the binding directly — the
@@ -5975,7 +5935,7 @@ async function publishApi(tctx, request, url, env) {
5975
5935
  clientProtocol,
5976
5936
  minProtocol: tctx.MIN_CLIENT_PROTOCOL,
5977
5937
  protocol: PUBLISH_PROTOCOL,
5978
- upgrade: "npx augur@latest",
5938
+ upgrade: "npx @augurworks/augur@latest",
5979
5939
  }, 426);
5980
5940
  }
5981
5941
  // Prune any publicPrefixes entry THIS manifest declares without backing it with
@@ -7949,16 +7909,6 @@ const OVERLAY_KV_KEYS = Object.freeze({
7949
7909
  boards: Object.freeze({ doc: "board", layout: "keyed", workspaceScoped: true }),
7950
7910
  // Canvas image METADATA — the bytes are in R2. See assetApi.
7951
7911
  assets: Object.freeze({ doc: "basset-meta", layout: "keyed" }),
7952
- // WORKING MARKS — "something is editing here right now". One row per path, and the row
7953
- // is only meaningful until its own TTL runs out. See the marks section below.
7954
- //
7955
- // `map` RATHER THAN `keyed`, and the trade is worth naming because it looks backwards.
7956
- // Keyed would give one KV document per path, so two marks written in the same window
7957
- // could not lose each other. It would also turn every READ into a kv.list plus a get per
7958
- // row — and marks are read by a gallery page as well as by the CLI, which puts a listing
7959
- // in front of ordinary page loads on a store whose daily get budget has been exhausted
7960
- // before. One document is one get. What it costs is stated on `writeMark`.
7961
- marks: Object.freeze({ doc: "marks", layout: "map" }),
7962
7912
  // WHICH UNITS HAVE OPEN DRAFTS — a hint the gallery reads so it can ask the right unit
7963
7913
  // objects and no others (`draftsIndexApi`). One row per unit, `{n, at}`, written from
7964
7914
  // the object's own presence answer after open, land and discard. NEVER THE TRUTH: the
@@ -9149,7 +9099,7 @@ function doorFacts(tctx, url, env) {
9149
9099
  origin: url.origin,
9150
9100
  engine: tctx.INSTANCE_ENGINE_VERSION ? { version: tctx.INSTANCE_ENGINE_VERSION } : {},
9151
9101
  pairing: { enabled: pairing, start: "/__publish/_pair/start", approve: "/__connect" },
9152
- connect: pairing ? `npx augur connect --origin ${url.origin}` : null,
9102
+ connect: pairing ? `npx @augurworks/augur connect --origin ${url.origin}` : null,
9153
9103
  docs: DOOR_DOCS,
9154
9104
  // Where drafts are served, a prototype is changed by opening it, not by shipping a
9155
9105
  // tree — and the door is where a cold agent learns which of the two this instance is.
@@ -9169,11 +9119,9 @@ function doorText(f) {
9169
9119
  + ` ${f.connect}\n\n`
9170
9120
  + `It prints a link and a code. The owner of this workspace opens the link in a\n`
9171
9121
  + `browser they are already signed in to and enters the code. The token lands on\n`
9172
- + `that machine, and \`augur publish\` / \`augur ship\` use it from then on. With no\n`
9173
- + `source tree yet, \`npx augur clone --space ${f.workspace}\` then fetches one (it reads\n`
9122
+ + `that machine, and every verb below uses it from then on. With no\n`
9123
+ + `source tree yet, \`npx @augurworks/augur clone --space ${f.workspace}\` then fetches one (it reads\n`
9174
9124
  + `the origin from the pairing).\n\n`
9175
- + `Not on npm yet? The engine clone sits next to every workspace that publishes:\n\n`
9176
- + ` node <engine>/scripts/cli.mjs connect --origin ${f.origin}\n\n`
9177
9125
  : `Device pairing is switched off on this workspace. Ask an admin for an invite;\n`
9178
9126
  + `once you have signed in, \`augur login\` (email and password, meant for CI)\n`
9179
9127
  + `trades that for a publish token.\n\n`;
@@ -10335,235 +10283,6 @@ async function nameApi(tctx, request, url, env) {
10335
10283
  return jsonResponse({ error: "method-not-allowed" }, 405);
10336
10284
  }
10337
10285
 
10338
- // ---- Working marks (KV-backed, single key) ----------------------------------
10339
- //
10340
- // `F-presence-marks`. Nothing anywhere said what was already being worked on. Two
10341
- // collaborators' tools — usually two agents, on two machines, told to improve "the
10342
- // checkout flow" — would each open the same folder, each edit it, and find out at publish
10343
- // time, where the answer is a fork and a conflict file nobody asked for.
10344
- //
10345
- // ⚠️ THIS IS DELIBERATELY NOT A LOCK, and every line below is written so it cannot become
10346
- // one. A mark REFUSES NOTHING. It is not consulted by the gate, by the publish handler, by
10347
- // the commit CAS or by anything else that could say no. It is a note left where the next
10348
- // reader will look, and the whole protocol is: write one before you start, read them
10349
- // before you start. Enforcement when coordination fails is the composed publish's job
10350
- // (`src/publish-compose.mjs`), which is the only place in this engine allowed to refuse a
10351
- // write over a collision — and it does it on evidence, after the fact, never on a claim.
10352
- //
10353
- // THE PROTOCOL IS AGENT-FIRST. The badge a person sees on a gallery card is the byproduct,
10354
- // not the point: as an agent's edit shrinks toward seconds, a mark is FELT almost never and
10355
- // READ always. So the write side is the CLI (`augur mark`, over a publish token, see the
10356
- // `_marks` branch in publishApi) and the browser side is read-only — a person editing in a
10357
- // tab is not running a work-start step and inventing one for them would be a lie about who
10358
- // wrote what.
10359
- //
10360
- // ⚠️ A MARK EXPIRES BY ITSELF AND IS NEVER TRUSTED TO BE CLEARED. The thing that leaves a
10361
- // mark is a process that can be killed — Ctrl-C, an OOM, a laptop lid — and a claim that
10362
- // outlives the claimant is worse than no claim at all, because the next reader believes it.
10363
- // So EXPIRY IS A READ-TIME FILTER (`liveMarks`), not a cleanup job: the moment `startedAt +
10364
- // ttl` is in the past the mark is gone from every answer, whether or not anything ever runs
10365
- // again. `sweepExpired` below only reclaims the BYTES, opportunistically, and correctness
10366
- // never depends on it having run.
10367
-
10368
- /** How long a mark is good for when the caller does not say. */
10369
- const MARK_TTL_MS = 10 * 60_000;
10370
- /**
10371
- * The longest a caller may ask for. An agent that wants four hours is describing a lock,
10372
- * and the answer to a lock is a shorter mark re-written as the work continues.
10373
- */
10374
- const MARK_TTL_MAX_MS = 60 * 60_000;
10375
- /** The shortest, so a `--ttl 0` cannot write a mark that is already dead. */
10376
- const MARK_TTL_MIN_MS = 5_000;
10377
- /**
10378
- * How many lapsed rows one write may reclaim. Bounded low because on the KV backing each
10379
- * row delete is a whole-document read and put, so the sweep can cost more than the litter
10380
- * it collects; an unbounded one would turn a work-start step into a hundred writes on a
10381
- * workspace nobody has marked in a month. Nothing depends on it running at all.
10382
- */
10383
- const MARK_SWEEP_MAX = 4;
10384
- /** Belt and braces: a workspace cannot be filled with marks by a loop. */
10385
- const MARK_MAX_ROWS = 200;
10386
-
10387
- /**
10388
- * One spelling of a path, so two tools that mean the same folder agree.
10389
- *
10390
- * Leading and trailing slash, always: a mark names a UNIT — the prototype folder a URL
10391
- * names and a person edits — and `unitOfPath` in src/publish-units.mjs decides containment
10392
- * by prefix, which only works when a folder ends in a slash. `/a/b` and `/a/bc/` would
10393
- * otherwise overlap.
10394
- */
10395
- function normalizeMarkPath(p) {
10396
- const s = clamp(p, 300);
10397
- if (!s) return "";
10398
- const trimmed = s.trim().replace(/^\.\//, "").replace(/\/{2,}/g, "/");
10399
- if (!trimmed || trimmed === "/") return "/";
10400
- return `/${trimmed.replace(/^\/+/, "").replace(/\/+$/, "")}/`;
10401
- }
10402
-
10403
- /**
10404
- * Do these two paths describe overlapping work? Containment in either direction — a mark
10405
- * on `/checkout/` covers `/checkout/step-two/`, and a mark on `/checkout/step-two/` is
10406
- * worth showing to somebody about to take `/checkout/`.
10407
- */
10408
- function markPathsOverlap(a, b) {
10409
- const x = normalizeMarkPath(a), y = normalizeMarkPath(b);
10410
- if (!x || !y) return false;
10411
- return x === y || x.startsWith(y) || y.startsWith(x);
10412
- }
10413
-
10414
- /** The instant a mark stops meaning anything. Pure, and the only definition of expiry. */
10415
- function markExpiresAt(m) {
10416
- const started = Date.parse((m && m.startedAt) || "");
10417
- if (!Number.isFinite(started)) return 0;
10418
- const ttl = Number.isFinite(+(m && m.ttl)) ? +m.ttl : MARK_TTL_MS;
10419
- return started + Math.min(Math.max(ttl, MARK_TTL_MIN_MS), MARK_TTL_MAX_MS);
10420
- }
10421
-
10422
- /**
10423
- * The live marks in a stored map, newest first. THE expiry rule — every reader goes
10424
- * through here, so a lapsed mark cannot be reported by one surface and hidden by another.
10425
- */
10426
- function liveMarks(map, now = Date.now()) {
10427
- return Object.entries(map || {})
10428
- .map(([path, m]) => (m && typeof m === "object" ? { ...m, path: m.path || path } : null))
10429
- .filter((m) => m && markExpiresAt(m) > now)
10430
- .sort((a, b) => Date.parse(b.startedAt || 0) - Date.parse(a.startedAt || 0));
10431
- }
10432
-
10433
- /**
10434
- * What a mark looks like on the wire: the stored row plus two things a reader would
10435
- * otherwise have to compute, and one it could not — the display name behind the id.
10436
- *
10437
- * The NAME IS RESOLVED, NEVER STORED. `personId` is the same one-way hash a comment
10438
- * carries, so a mark holds no address; the roster turns it back into a face at read time,
10439
- * which also means a rename shows through and an ex-member resolves to nobody.
10440
- */
10441
- function decorateMark(m, users, now = Date.now()) {
10442
- const u = (users || []).find((x) => x && personId(x.email) === m.personId);
10443
- return {
10444
- path: m.path,
10445
- personId: m.personId || null,
10446
- startedAt: m.startedAt,
10447
- ttl: markExpiresAt(m) - Date.parse(m.startedAt),
10448
- by: u ? u.name || nameFromEmail(u.email) : null,
10449
- initials: u ? u.initials || initialsFor(u.name || u.email) : null,
10450
- color: u ? u.color || null : null,
10451
- expiresIn: Math.max(0, markExpiresAt(m) - now),
10452
- };
10453
- }
10454
-
10455
- /** Read the live marks for a workspace, decorated. `null` store answers with nothing. */
10456
- async function readMarks(tctx, env) {
10457
- const store = overlayFor(env, tctx);
10458
- if (!store) return { marks: [], warning: "no-kv-binding" };
10459
- const map = await store.read("marks");
10460
- const now = Date.now();
10461
- return { marks: liveMarks(map, now).map((m) => decorateMark(m, tctx.USERS, now)), now };
10462
- }
10463
-
10464
- /**
10465
- * Reclaim the bytes of rows that lapsed. NOT the expiry mechanism — `liveMarks` already
10466
- * stopped reporting these, and this runs only so a one-way author id does not sit in the
10467
- * store for months after it stopped meaning anything. Per-key deletes, never a whole-family
10468
- * `replace`: a replace computed from a read taken moments ago would drop a mark another
10469
- * agent wrote in between, and on the workspace object it would delete rows it never read.
10470
- *
10471
- * It swallows its own failures on purpose. Reclaiming bytes may never be the reason a
10472
- * work-start step reports a failure, because the mark it was announcing is already written.
10473
- */
10474
- async function sweepExpired(store, map, now, keep) {
10475
- let swept = 0;
10476
- for (const [path, m] of Object.entries(map || {})) {
10477
- if (swept >= MARK_SWEEP_MAX) break;
10478
- if (path === keep) continue;
10479
- if (markExpiresAt(m) > now) continue;
10480
- try { await store.set("marks", "", path, null); swept++; } catch (e) { break; }
10481
- }
10482
- return swept;
10483
- }
10484
-
10485
- /**
10486
- * Write one mark. `who` is resolved by the caller from a credential — a session or a
10487
- * publish token — and NEVER from the request body, exactly like a comment's authorship.
10488
- *
10489
- * ⚠️ ON THE KV BACKING TWO MARKS WRITTEN IN THE SAME WINDOW CAN LOSE EACH OTHER, and that
10490
- * is a known cost rather than an oversight. A `map` family is one document: `set` reads it,
10491
- * changes one key and puts it back, so a mark written between the read and the put is
10492
- * overwritten — and KV reads converge globally rather than instantly, which makes the
10493
- * window as wide as the convergence, not as wide as the round trip. The overlay's own
10494
- * header says the same thing about statuses, names and pins; the workspace object closes
10495
- * it for all of them at once by making each key a row.
10496
- *
10497
- * WHY IT IS SURVIVABLE HERE AND WOULD NOT BE IN A LOCK. A lost mark costs the next reader
10498
- * a hint. It cannot cost anybody work, because nothing anywhere asks a mark for permission:
10499
- * the loser of the race is still editing, still publishing, and still protected by the
10500
- * composed publish, which settles a real collision on evidence. A lock that lost a write
10501
- * would hand two writers the same exclusive claim, which is why this is not one.
10502
- */
10503
- async function writeMark(tctx, env, who, { path, ttl }) {
10504
- const store = overlayFor(env, tctx);
10505
- if (!store) return { error: "no-kv-binding" };
10506
- const p = normalizeMarkPath(path);
10507
- if (!p) return { error: "bad-input" };
10508
- const ms = Number.isFinite(+ttl) && +ttl > 0
10509
- ? Math.min(Math.max(+ttl, MARK_TTL_MIN_MS), MARK_TTL_MAX_MS)
10510
- : MARK_TTL_MS;
10511
- const now = Date.now();
10512
- const before = await store.read("marks");
10513
- // The only refusal on this route, and it is a runaway-loop guard rather than a policy:
10514
- // this many things being worked on at once in one workspace is a script, not a team.
10515
- const live = liveMarks(before, now);
10516
- if (live.length >= MARK_MAX_ROWS && !live.some((m) => m.path === p)) {
10517
- return { error: "too-many-marks" };
10518
- }
10519
- // ⚠️ THE ROW CARRIES NO ADDRESS — not in the value and not in the `owner` column. A mark
10520
- // is read by more things than a comment thread is (a gallery page stamps a badge from
10521
- // it), and `personId` is exactly enough to put a face on it.
10522
- const mark = { path: p, personId: who.personId, startedAt: new Date(now).toISOString(), ttl: ms };
10523
- await store.set("marks", "", p, mark, null);
10524
- const swept = await sweepExpired(store, before, now, p);
10525
- // The answer is COMPUTED from what was just written, never re-read. A second read costs
10526
- // a round trip to say what this function already knows, and on KV it can come back
10527
- // STALER than the write it was meant to confirm — a work-start step that printed "your
10528
- // mark is not there" right after writing it would teach people to distrust the tool.
10529
- const marks = [mark, ...live.filter((m) => m.path !== p)];
10530
- return {
10531
- mark: decorateMark(mark, tctx.USERS, now),
10532
- marks: marks.map((m) => decorateMark(m, tctx.USERS, now)),
10533
- swept,
10534
- };
10535
- }
10536
-
10537
- /**
10538
- * Release a mark early. A COURTESY, never the guarantee — the TTL is the guarantee, and a
10539
- * tool that is killed never reaches this. Only the mark's own author may clear it: taking
10540
- * somebody else's mark down would turn the note into something worth fighting over.
10541
- */
10542
- async function clearMark(tctx, env, who, { path }) {
10543
- const store = overlayFor(env, tctx);
10544
- if (!store) return { error: "no-kv-binding" };
10545
- const p = normalizeMarkPath(path);
10546
- if (!p) return { error: "bad-input" };
10547
- const map = await store.read("marks");
10548
- const cur = (map || {})[p];
10549
- const now = Date.now();
10550
- if (!cur || markExpiresAt(cur) <= now) return { cleared: false, reason: "no-mark" };
10551
- if (cur.personId !== who.personId) return { cleared: false, reason: "not-yours" };
10552
- await store.set("marks", "", p, null);
10553
- const marks = liveMarks(map, now).filter((m) => m.path !== p);
10554
- return { cleared: true, marks: marks.map((m) => decorateMark(m, tctx.USERS, now)) };
10555
- }
10556
-
10557
- /**
10558
- * The browser's read. GET only, on purpose — see the header: the badge is the byproduct of
10559
- * an agent protocol, and a tab is not a work-start step.
10560
- */
10561
- async function marksApi(tctx, request, url, env) {
10562
- if (request.method !== "GET") return jsonResponse({ error: "method-not-allowed" }, 405);
10563
- const out = await readMarks(tctx, env);
10564
- return jsonResponse({ ...out, ttlMs: MARK_TTL_MS });
10565
- }
10566
-
10567
10286
  // ---- Prototype deletion (repo-write via dispatch webhook) -------------------
10568
10287
  // "Delete forever" on a prototype card. The worker holds NO repo credentials —
10569
10288
  // it forwards the request to a per-instance webhook (a GitHub repository_dispatch
@@ -12413,13 +12132,6 @@ async function handleRequest(request, env, ctx, url, trace) {
12413
12132
  if (!authed) return jsonResponse({ error: "unauthorized" }, 401);
12414
12133
  return pinsApi(tctx, request, url, env, me);
12415
12134
  }
12416
- // Working marks — READ ONLY here. Who is working where is workspace-internal, so it
12417
- // asks for a session like the gallery around it; the WRITE side is a publish token and
12418
- // lives under /__publish/_marks, because a work-start step is something a tool runs.
12419
- if (url.pathname === "/__marks") {
12420
- if (!authed) return jsonResponse({ error: "unauthorized" }, 401);
12421
- return marksApi(tctx, request, url, env);
12422
- }
12423
12135
  if (url.pathname === "/__name") {
12424
12136
  if (!authed) return jsonResponse({ error: "unauthorized" }, 401);
12425
12137
  const denied = viewerWriteRefusal(request, url, me, "name", tctx.SPACES);
@@ -12625,9 +12337,6 @@ export const __testables = Object.freeze({
12625
12337
  nextPublishVersion, overlayFor, overlayKvKey, statusApi, nameApi, pinsApi,
12626
12338
  currencyApi, currencyRows, freshness, whenWords, parseSince, unitKey, unitProvenance,
12627
12339
  STALE_AFTER_DAYS, STATUS_LABELS, VALID_STATUS,
12628
- marksApi, readMarks, writeMark, clearMark, liveMarks, markExpiresAt, decorateMark,
12629
- normalizeMarkPath, markPathsOverlap, sweepExpired,
12630
- MARK_TTL_MS, MARK_TTL_MAX_MS, MARK_TTL_MIN_MS, MARK_SWEEP_MAX, MARK_MAX_ROWS,
12631
12340
  exportState, importState,
12632
12341
  quotaBump, quotaMinute, quotaDay, workspaceStatus, touchWorkspaceActivity,
12633
12342
  deleteWorkspace, purgeDue, blobGc, clearFamilies, NEVER_CLEARED,
package/src/seed-pack.mjs CHANGED
@@ -44,7 +44,7 @@
44
44
  //
45
45
  // ONE SUBSTITUTION. connect-your-terminal ships with an empty `CONNECT_COMMAND` slot and
46
46
  // derives a command from the URL it is served on. Provisioning fills the slot with the
47
- // workspace's REAL command — `npx augur connect --origin https://<label><suffix>` — so the
47
+ // workspace's REAL command — `npx @augurworks/augur connect --origin https://<label><suffix>` — so the
48
48
  // page is exact rather than merely not wrong. The filled page hashes differently, so it is
49
49
  // the one blob that is per-workspace rather than shared; everything else dedups.
50
50
 
@@ -60,7 +60,7 @@ export const SEED_CONNECT_SLOT = 'var CONNECT_COMMAND = "";';
60
60
  /** The one-line command a person runs to pair a terminal with their workspace. */
61
61
  export function connectCommandFor(origin) {
62
62
  const o = String(origin || "").replace(/\/+$/, "");
63
- return o ? `npx augur connect --origin ${o}` : "";
63
+ return o ? `npx @augurworks/augur connect --origin ${o}` : "";
64
64
  }
65
65
 
66
66
  /**
@@ -123,10 +123,6 @@ export const STATE_INVENTORY = Object.freeze([
123
123
  id: "board:", store: "kv", kind: "prefix", to: "workspace",
124
124
  why: "A canvas board document, spelled `board:<path>` or `board:<workspace>:<path>` where the deployment serves its own rooms (src/board-key.mjs). Same table. NOTE this is a MIRROR: while the realtime worker is separate, the authoritative copy is that worker's own BoardRoom storage, and a DO's storage belongs to the script that created it — so a board carried by an export is carried as of the last mirror write, which lags the room and can lag it by a great deal more than the write cadence alone. What travels the whole board is `scripts/board-snapshot.mjs`, which reads the room over a WebSocket join and seeds the new one; run it per board AFTER a move, and take its lag report rather than a clock as the sign that a board is quiet enough to have been copied correctly.",
125
125
  },
126
- {
127
- id: "marks", store: "kv", kind: "key", to: "drop",
128
- why: "Working marks: one row per path saying something is editing there right now, holding the path, the one-way author id and the time, and good only until its own TTL runs out (ten minutes by default, an hour at the most). Transient by construction — a mark is not a promise to anybody, it grants nothing and refuses nothing, and the next work-start recreates it. DROPPED rather than carried, for the same reason `health:report` is: a mark restored onto a new home would say somebody is working somewhere at a moment that has passed, and a false claim is worse than a missing one. Nothing is lost: the marks a copy would have carried are all expired by the time anybody reads the copy.",
129
- },
130
126
  {
131
127
  id: "drafts", store: "kv", kind: "key", to: "drop",
132
128
  why: "The open-drafts hint: one row per unit saying how many drafts its object reports open, written after open, land and discard so the gallery knows which unit objects to ask (`draftsIndexApi` in src/_worker.js) and asks no others. Transient by construction and recreated by the next open, land or discard on that unit; never the truth — every read re-checks the object and drops a row it contradicts. DROPPED on a copy because the unit objects it points at do not travel (see `/__unit/`), so a carried row would name drafts the destination cannot serve; the first open on the new home writes a true one.",
@@ -1,86 +0,0 @@
1
- # Working marks — say what you are about to touch, read what everyone else is
2
-
3
- Nothing in a workspace used to say what was already being worked on. Two people's
4
- agents, on two machines, both told to improve "the checkout flow", would each open
5
- the same folder, each edit it, and find out at publish time — where the answer is a
6
- fork, a conflict file and an afternoon nobody planned.
7
-
8
- A **mark** is a note on a path saying *something is editing here right now*. It
9
- carries the path, a one-way id for who left it, when it started, and how long it is
10
- good for. That is all it is.
11
-
12
- Where the workspace serves drafts, a draft IS the mark: `augur open` tells you who else
13
- has the prototype open and shows you on their chips, with nothing to leave and nothing to
14
- expire — see [drafts.md](./drafts.md). Marks stay for workspaces without drafts.
15
-
16
- ## ⚠️ It is not a lock, and you must not treat it as one
17
-
18
- A mark **grants nothing and refuses nothing**. A marked path can still be opened,
19
- edited, published and shipped by anybody, including you. Nothing in the engine asks
20
- a mark for permission — not the gate, not the publish handler, not the commit.
21
- `augur mark` exits 0 whatever it finds.
22
-
23
- So do not write a script that blocks on a mark, retries until one clears, or treats
24
- one as a failure. When coordination genuinely fails, the composed publish settles it
25
- on evidence after the fact (see [publishing.md](./publishing.md)) — that is the part
26
- that is allowed to refuse, and it is the only part.
27
-
28
- ## The protocol, in one line
29
-
30
- **Read the marks before you start. Leave one when you do.**
31
-
32
- ```
33
- augur mark what is being worked on right now
34
- augur mark <path> [--ttl <seconds>] leave a mark on it, then start
35
- augur mark <path> --clear take yours down early
36
- augur mark … --json the same answer, for a tool to read
37
- ```
38
-
39
- A path is a URL path (`/checkout/flow/`). The repo folder is accepted and
40
- translated, so `checkout/prototypes/flow` — the folder you were just editing —
41
- marks the URL it publishes to. The line printed back is always the instance's own
42
- spelling; do not assume yours won.
43
-
44
- Marking a path that somebody else already marked **still works**, and prints their
45
- mark next to yours. That is the whole design: you now know, and you choose. Pick a
46
- different path, wait it out, or carry on knowing you will be merging.
47
-
48
- ## It expires by itself
49
-
50
- A mark is good for ten minutes by default and an hour at the most. The instance
51
- stops reporting it the moment that passes, whether or not anything ever clears it.
52
-
53
- That is the point rather than a detail. The thing leaving marks is a process that
54
- can be killed — an interrupt, an out-of-memory, a closed laptop — and a claim that
55
- outlives the claimant is worse than no claim at all, because the next reader
56
- believes it. **`--clear` is a courtesy, never the guarantee.** There is no cleanup
57
- job to run and none to forget.
58
-
59
- If the work is genuinely still going when the mark lapses, mark it again. An agent
60
- that wants a four-hour mark is describing a lock, and the answer to a lock is a
61
- short mark re-written as the work continues.
62
-
63
- ## Where marks show up without being asked for
64
-
65
- - `augur status` prints what is being worked on, under the live-vs-clone table.
66
- - `augur clone` and `augur pull` print the marks on the paths they are about to
67
- write, before the first byte lands — and write anyway, with the same exit code
68
- as always.
69
- - A gallery card carries a small badge (*"… is working on this"*) while a mark on
70
- that exact URL is live, and drops it when the mark lapses.
71
-
72
- The badge is the **byproduct**, not the point. As an agent's edit shrinks toward
73
- seconds, a mark is felt almost never and read always — which is why the write side
74
- is the CLI and the browser side only ever reads.
75
-
76
- ## What is stored
77
-
78
- `{path, personId, startedAt, ttl}` — and `personId` is the same one-way fingerprint
79
- a comment carries, so a mark holds **no address**. The display name beside it is
80
- resolved from the roster at read time, never stored, so a rename shows through and
81
- somebody who has left resolves to "Someone".
82
-
83
- Marks are workspace state, not published content: they are covered by
84
- `augur export --full`, and a restore deliberately drops them (see
85
- `src/state-inventory.mjs`) rather than reinstating a claim about a moment that has
86
- passed.