cursedops 0.20.0 → 0.20.1
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/package.json +1 -1
- package/src/ship.ts +52 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursedops",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.1",
|
|
4
4
|
"description": "The build-and-ops answers this generation's apps wrote independently and identically: finding a generation's roots — and printing a command that runs when pasted — without knowing a path, the generation's whole-tree laws run over one repo from a checkout or a worktree, macOS launchd agent install/replace/remove and the live port a job serves, the check that a launchd job imports only what the LOCKED versions of its packages export (a lib-preview overlay's extra subpath or name is a job that dies at the next forced reinstall), the scaffolding and verdicts of a deployed smoke (origin probe, the smoke's own environment, a network that lies about DNS, a settled version), the static-serving helpers eight apps copied — the path-traversal guard among them — the API floor that keeps an unmatched /api/... from ever being answered with the app shell, the commit and dirty flag a checkout-served process reports, the Cloudflare Worker deploy toolkit four apps copied (the deploy sequence, exact-set secrets over a pipe, origin-first rollback, the curl edge fetch, the row-for-row D1 import proof, the billed-CPU tail check around a deploy's walk and smoke, and each app's worker:secrets and worker:smoke main as one function of its data), the relay a Worker fronts a Mac-bound app with (the Durable Object, the frames, the Mac's dialer and key rotation — lifted from station for roms — and the signed-in stage walk's skeleton and the relay app's whole worker:deploy), the Worker import-graph and await-port checks every Worker app's suite runs over its own source, and the public-surface ratchet three published libraries each carried a forked copy of. Mechanism only — no app knows its name from here. Bun, zero runtime dependencies (typescript is an optional peer, for public-surface only), ships source.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
package/src/ship.ts
CHANGED
|
@@ -160,11 +160,12 @@ export interface ShipNowWhere {
|
|
|
160
160
|
|
|
161
161
|
/**
|
|
162
162
|
* Is `FORGE_SHIP_NOW` honored here? Outside a batch, whenever it is asked: that is the owner's own
|
|
163
|
-
* shell. Under `$AUTOPILOT_BATCH`, only when that batch holds a live
|
|
164
|
-
* (`$FORGE_STATE/state/board/*.json`, the pid alive and not expired) on a task in
|
|
163
|
+
* shell. Under `$AUTOPILOT_BATCH`, only when that batch holds a live task claim on a task in
|
|
165
164
|
* `$FORGE/tasks/In_Progress/` whose `escalation-key:` is one of {@link SHIP_NOW_KEYS}, AND
|
|
166
165
|
* `FORGE_SHIP_NOW_WHY` says why. Anything it cannot read is a refusal — a refused ship-now only
|
|
167
166
|
* makes the deploy later, which is the safe direction.
|
|
167
|
+
*
|
|
168
|
+
* A claim is read from either record a batch's task claim lives in ({@link heldClaims}).
|
|
168
169
|
*/
|
|
169
170
|
export function shipNowVerdict(where: ShipNowWhere, env: NodeJS.ProcessEnv = process.env): ShipNowVerdict {
|
|
170
171
|
const asked = shipNowAsked(env);
|
|
@@ -176,30 +177,15 @@ export function shipNowVerdict(where: ShipNowWhere, env: NodeJS.ProcessEnv = pro
|
|
|
176
177
|
const refuse = (why: string): ShipNowVerdict => ({ ...base, honoured: false, why });
|
|
177
178
|
if (!reason) return refuse(`under batch ${batch}, ${SHIP_NOW_VAR} needs ${SHIP_NOW_WHY_VAR}="<why this cannot wait for the ship>"`);
|
|
178
179
|
if (!where.state || !where.forge) return refuse(`under batch ${batch}, no generation to read the board from`);
|
|
179
|
-
const board = join(where.state, "state", "board");
|
|
180
|
-
const now = where.now ?? Date.now();
|
|
181
|
-
const isAlive = where.isAlive ?? pidAlive;
|
|
182
|
-
let files: string[] = [];
|
|
183
|
-
try {
|
|
184
|
-
files = readdirSync(board).filter((f) => f.startsWith("task-") && f.endsWith(".json"));
|
|
185
|
-
} catch {
|
|
186
|
-
/* no board → no claim */
|
|
187
|
-
}
|
|
188
180
|
const held: string[] = [];
|
|
189
|
-
for (const
|
|
190
|
-
|
|
191
|
-
try {
|
|
192
|
-
c = JSON.parse(readFileSync(join(board, f), "utf8")) as BoardClaim;
|
|
193
|
-
} catch {
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
if (c.kind !== "task" || c.batch !== batch || typeof c.task !== "string" || typeof c.pid !== "number") continue;
|
|
197
|
-
if (typeof c.expiresAt === "string" && Date.parse(c.expiresAt) < now) continue;
|
|
198
|
-
if (!isAlive(c.pid)) continue;
|
|
181
|
+
for (const c of heldClaims(where.state, batch, where.now ?? Date.now(), where.isAlive ?? pidAlive)) {
|
|
182
|
+
if (held.includes(c.task)) continue;
|
|
199
183
|
held.push(c.task);
|
|
184
|
+
const stem = c.task.slice(0, -".md".length);
|
|
185
|
+
const text = [c.task, join(stem, c.task)].map((f) => join(where.forge as string, "tasks", "In_Progress", f)).find((f) => existsSync(f));
|
|
200
186
|
let key: string | null = null;
|
|
201
187
|
try {
|
|
202
|
-
key = escalationKey(readFileSync(
|
|
188
|
+
key = text ? escalationKey(readFileSync(text, "utf8")) : null;
|
|
203
189
|
} catch {
|
|
204
190
|
continue;
|
|
205
191
|
}
|
|
@@ -210,6 +196,50 @@ export function shipNowVerdict(where: ShipNowWhere, env: NodeJS.ProcessEnv = pro
|
|
|
210
196
|
);
|
|
211
197
|
}
|
|
212
198
|
|
|
199
|
+
/**
|
|
200
|
+
* The live task claims `batch` holds, from both records a claim lives in — the file shapes are the
|
|
201
|
+
* interface, so this never imports the runner:
|
|
202
|
+
* · autopilot-runner's claim record, `$FORGE_STATE/state/autopilot/claims/<task>.json`
|
|
203
|
+
* (`{pid, batch, at}`), where `autopilot mine` claims since forge task 3738. It holds while its
|
|
204
|
+
* pid lives; it names a task DIRECTORY without its `.md`. Until 3791 this was not read, and every
|
|
205
|
+
* batch's ship-now — the ship task's own publish — was refused;
|
|
206
|
+
* · the board's `state/board/task-*.json` (`kind: "task"`, an `expiresAt`), which a `finish`
|
|
207
|
+
* hand-off still writes.
|
|
208
|
+
* Each `task` is named with its `.md`; a task directory's file is `In_Progress/<stem>/<task>`.
|
|
209
|
+
*/
|
|
210
|
+
function heldClaims(state: string, batch: string, now: number, isAlive: (pid: number) => boolean): { task: string }[] {
|
|
211
|
+
const out: { task: string }[] = [];
|
|
212
|
+
const read = (dir: string, keep: (name: string) => boolean): [string, BoardClaim][] => {
|
|
213
|
+
let names: string[] = [];
|
|
214
|
+
try {
|
|
215
|
+
names = readdirSync(dir).filter((n) => n.endsWith(".json") && keep(n));
|
|
216
|
+
} catch {
|
|
217
|
+
return [];
|
|
218
|
+
}
|
|
219
|
+
const claims: [string, BoardClaim][] = [];
|
|
220
|
+
for (const n of names) {
|
|
221
|
+
try {
|
|
222
|
+
claims.push([n, JSON.parse(readFileSync(join(dir, n), "utf8")) as BoardClaim]);
|
|
223
|
+
} catch {
|
|
224
|
+
/* unreadable → no claim */
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return claims;
|
|
228
|
+
};
|
|
229
|
+
for (const [name, c] of read(join(state, "state", "autopilot", "claims"), () => true)) {
|
|
230
|
+
if (c.batch !== batch || typeof c.pid !== "number" || !isAlive(c.pid)) continue;
|
|
231
|
+
const stem = name.slice(0, -".json".length);
|
|
232
|
+
out.push({ task: stem.endsWith(".md") ? stem : `${stem}.md` });
|
|
233
|
+
}
|
|
234
|
+
for (const [, c] of read(join(state, "state", "board"), (n) => n.startsWith("task-"))) {
|
|
235
|
+
if (c.kind !== "task" || c.batch !== batch || typeof c.task !== "string" || typeof c.pid !== "number") continue;
|
|
236
|
+
if (typeof c.expiresAt === "string" && Date.parse(c.expiresAt) < now) continue;
|
|
237
|
+
if (!isAlive(c.pid)) continue;
|
|
238
|
+
out.push({ task: c.task });
|
|
239
|
+
}
|
|
240
|
+
return out;
|
|
241
|
+
}
|
|
242
|
+
|
|
213
243
|
/** The ledger line every ship-now ask leaves — honored or refused. */
|
|
214
244
|
export interface ShipNowEventLine {
|
|
215
245
|
at: string;
|