@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/INSTALL.md +3 -2
- package/README.md +4 -2
- package/agents/README.md +6 -7
- package/agents/drafts.md +5 -6
- package/agents/identity.md +1 -1
- package/agents/prototype-contract.md +1 -1
- package/agents/publishing.md +23 -16
- package/build.js +1 -97
- package/changelog.md +5 -0
- package/package.json +16 -16
- package/scripts/cli.mjs +11 -5
- package/scripts/clone.mjs +0 -20
- package/scripts/connect.mjs +1 -1
- package/scripts/init.mjs +2 -2
- package/scripts/lib/adapters.mjs +23 -3
- package/scripts/lib/draft.mjs +12 -0
- package/scripts/open.mjs +2 -3
- package/scripts/publish.mjs +19 -0
- package/scripts/read.mjs +2 -3
- package/scripts/status.mjs +5 -23
- package/seed/README.md +1 -1
- package/seed/start-here/prototypes/connect-your-terminal/index.html +5 -5
- package/src/_worker.js +4 -295
- package/src/seed-pack.mjs +2 -2
- package/src/state-inventory.mjs +0 -4
- package/agents/working-marks.md +0 -86
- package/scripts/lib/marks.mjs +0 -107
- package/scripts/mark.mjs +0 -112
- package/scripts/ship.mjs +0 -460
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 {
|
|
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(
|
|
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); }
|
package/scripts/status.mjs
CHANGED
|
@@ -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
|
-
// ──
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
|
|
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
|
|
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
|
/**
|
package/src/state-inventory.mjs
CHANGED
|
@@ -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.",
|
package/agents/working-marks.md
DELETED
|
@@ -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.
|