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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/ship.ts +52 -22
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursedops",
3
- "version": "0.20.0",
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 `kind: "task"` board claim
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 f of files) {
190
- let c: BoardClaim;
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(join(where.forge, "tasks", "In_Progress", c.task), "utf8"));
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;