syntaur 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -9672,6 +9672,65 @@ function createServersRouter(serversDir2, projectsDir2, assignmentsDir2) {
9672
9672
  import { Router as Router3 } from "express";
9673
9673
  import { resolve as resolve16 } from "path";
9674
9674
  init_fs();
9675
+
9676
+ // src/utils/transcript.ts
9677
+ import { open } from "fs/promises";
9678
+ var MAX_LINES_SCANNED = 50;
9679
+ async function derivePathFromTranscript(transcriptPath) {
9680
+ if (!transcriptPath) return null;
9681
+ let handle;
9682
+ try {
9683
+ handle = await open(transcriptPath, "r");
9684
+ } catch {
9685
+ return null;
9686
+ }
9687
+ try {
9688
+ const stream = handle.createReadStream({ encoding: "utf-8" });
9689
+ let buffer = "";
9690
+ let scanned = 0;
9691
+ for await (const chunk of stream) {
9692
+ buffer += chunk;
9693
+ let nl = buffer.indexOf("\n");
9694
+ while (nl !== -1) {
9695
+ const line = buffer.slice(0, nl);
9696
+ buffer = buffer.slice(nl + 1);
9697
+ const cwd = extractCwd(line);
9698
+ if (cwd) {
9699
+ stream.destroy();
9700
+ return cwd;
9701
+ }
9702
+ scanned++;
9703
+ if (scanned >= MAX_LINES_SCANNED) {
9704
+ stream.destroy();
9705
+ return null;
9706
+ }
9707
+ nl = buffer.indexOf("\n");
9708
+ }
9709
+ }
9710
+ if (buffer.length > 0) {
9711
+ const cwd = extractCwd(buffer);
9712
+ if (cwd) return cwd;
9713
+ }
9714
+ return null;
9715
+ } finally {
9716
+ await handle.close().catch(() => {
9717
+ });
9718
+ }
9719
+ }
9720
+ function extractCwd(line) {
9721
+ const trimmed = line.trim();
9722
+ if (trimmed.length === 0 || trimmed[0] !== "{") return null;
9723
+ try {
9724
+ const parsed = JSON.parse(trimmed);
9725
+ if (typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
9726
+ return parsed.cwd;
9727
+ }
9728
+ } catch {
9729
+ }
9730
+ return null;
9731
+ }
9732
+
9733
+ // src/dashboard/api-agent-sessions.ts
9675
9734
  function createAgentSessionsRouter(projectsDir2, broadcast, assignmentsDir2) {
9676
9735
  const router = Router3();
9677
9736
  router.get("/", async (_req, res) => {
@@ -9719,6 +9778,8 @@ function createAgentSessionsRouter(projectsDir2, broadcast, assignmentsDir2) {
9719
9778
  return;
9720
9779
  }
9721
9780
  }
9781
+ const derivedPath = await derivePathFromTranscript(transcriptPath);
9782
+ const recordedPath = derivedPath ?? path ?? "";
9722
9783
  const session = {
9723
9784
  projectSlug: projectSlug || null,
9724
9785
  assignmentSlug: assignmentSlug || null,
@@ -9726,7 +9787,7 @@ function createAgentSessionsRouter(projectsDir2, broadcast, assignmentsDir2) {
9726
9787
  sessionId,
9727
9788
  started: (/* @__PURE__ */ new Date()).toISOString(),
9728
9789
  status: "active",
9729
- path: path || "",
9790
+ path: recordedPath,
9730
9791
  description: description || null,
9731
9792
  transcriptPath: transcriptPath || null
9732
9793
  };
@@ -10216,6 +10277,33 @@ function listLeases(filter) {
10216
10277
  ORDER BY granted_at DESC`
10217
10278
  ).all(...params2);
10218
10279
  }
10280
+ function listMembers(inventory_slug) {
10281
+ const database = getLeasesDb();
10282
+ const inv = database.prepare("SELECT slug FROM inventories WHERE slug = ?").get(inventory_slug);
10283
+ if (!inv) throw new NotFoundError(inventory_slug);
10284
+ return database.prepare(
10285
+ `SELECT inventory_slug, member_id, status, generation, metadata_json, last_used_at, retired_at
10286
+ FROM inventory_members WHERE inventory_slug = ?
10287
+ ORDER BY member_id`
10288
+ ).all(inventory_slug);
10289
+ }
10290
+ function getLeaseEvents(lease_id, limit = 50) {
10291
+ const database = getLeasesDb();
10292
+ if (lease_id) {
10293
+ return database.prepare(
10294
+ `SELECT id, lease_id, event, at, detail_json
10295
+ FROM lease_events WHERE lease_id = ?
10296
+ ORDER BY at ASC, id ASC
10297
+ LIMIT ?`
10298
+ ).all(lease_id, limit);
10299
+ }
10300
+ return database.prepare(
10301
+ `SELECT id, lease_id, event, at, detail_json
10302
+ FROM lease_events
10303
+ ORDER BY at DESC, id DESC
10304
+ LIMIT ?`
10305
+ ).all(limit);
10306
+ }
10219
10307
  function gcExpiredLeases() {
10220
10308
  const database = getLeasesDb();
10221
10309
  try {
@@ -10289,6 +10377,96 @@ function forceReleaseLease(lease_id) {
10289
10377
  throw err2;
10290
10378
  }
10291
10379
  }
10380
+ function updateInventory(slug, input4) {
10381
+ if ("kind" in input4) {
10382
+ throw new Error("inventory kind is immutable");
10383
+ }
10384
+ if (input4.default_ttl_s === void 0 && input4.display_name === void 0) {
10385
+ throw new Error("nothing to update");
10386
+ }
10387
+ if (input4.default_ttl_s !== void 0 && input4.default_ttl_s <= 0) {
10388
+ throw new Error("default_ttl_s must be positive");
10389
+ }
10390
+ const database = getLeasesDb();
10391
+ const fn = database.transaction(() => {
10392
+ const existing = database.prepare("SELECT slug FROM inventories WHERE slug = ?").get(slug);
10393
+ if (!existing) throw new NotFoundError(slug);
10394
+ const sets = [];
10395
+ const params2 = [];
10396
+ if (input4.default_ttl_s !== void 0) {
10397
+ sets.push("default_ttl_s = ?");
10398
+ params2.push(input4.default_ttl_s);
10399
+ }
10400
+ if (input4.display_name !== void 0) {
10401
+ sets.push("display_name = ?");
10402
+ params2.push(input4.display_name);
10403
+ }
10404
+ params2.push(slug);
10405
+ database.prepare(`UPDATE inventories SET ${sets.join(", ")} WHERE slug = ?`).run(...params2);
10406
+ return database.prepare(
10407
+ `SELECT slug, kind, display_name, default_ttl_s, created_at
10408
+ FROM inventories WHERE slug = ?`
10409
+ ).get(slug);
10410
+ });
10411
+ try {
10412
+ return fn.immediate();
10413
+ } catch (err2) {
10414
+ if (isBusyError(err2)) throw new LeaseContentionError(slug);
10415
+ throw err2;
10416
+ }
10417
+ }
10418
+ function deleteInventory(slug, opts = {}) {
10419
+ const database = getLeasesDb();
10420
+ let revoked = 0;
10421
+ const fn = database.transaction(() => {
10422
+ const existing = database.prepare("SELECT slug FROM inventories WHERE slug = ?").get(slug);
10423
+ if (!existing) throw new NotFoundError(slug);
10424
+ const activeLeases = database.prepare(
10425
+ `SELECT lease_id FROM leases
10426
+ WHERE inventory_slug = ? AND state = 'active'`
10427
+ ).all(slug);
10428
+ if (activeLeases.length > 0 && !opts.force) {
10429
+ throw new MemberInUseError(slug, "*");
10430
+ }
10431
+ revoked = activeLeases.length;
10432
+ database.prepare(
10433
+ `DELETE FROM lease_events
10434
+ WHERE lease_id IN (SELECT lease_id FROM leases WHERE inventory_slug = ?)`
10435
+ ).run(slug);
10436
+ database.prepare("DELETE FROM leases WHERE inventory_slug = ?").run(slug);
10437
+ database.prepare("DELETE FROM inventory_members WHERE inventory_slug = ?").run(slug);
10438
+ database.prepare("DELETE FROM inventories WHERE slug = ?").run(slug);
10439
+ });
10440
+ try {
10441
+ fn.immediate();
10442
+ } catch (err2) {
10443
+ if (isBusyError(err2)) throw new LeaseContentionError(slug);
10444
+ throw err2;
10445
+ }
10446
+ return { deleted: true, revoked };
10447
+ }
10448
+ function releaseLeasesByRequestedFor(tag) {
10449
+ const database = getLeasesDb();
10450
+ const rows = database.prepare(
10451
+ `SELECT lease_id FROM leases
10452
+ WHERE state = 'active' AND requested_for = ?`
10453
+ ).all(tag);
10454
+ const released = [];
10455
+ const stale = [];
10456
+ for (const { lease_id } of rows) {
10457
+ try {
10458
+ releaseLease(lease_id);
10459
+ released.push(lease_id);
10460
+ } catch (err2) {
10461
+ if (err2 instanceof StaleLeaseError) {
10462
+ stale.push(lease_id);
10463
+ continue;
10464
+ }
10465
+ throw err2;
10466
+ }
10467
+ }
10468
+ return { released, stale };
10469
+ }
10292
10470
 
10293
10471
  // src/dashboard/api-leases.ts
10294
10472
  function createLeasesRouter(broadcast) {
@@ -12061,7 +12239,7 @@ init_fs();
12061
12239
  init_config2();
12062
12240
  import { execFile as execFile2 } from "child_process";
12063
12241
  import { promisify as promisify2 } from "util";
12064
- import { cp, mkdtemp, rm as rm2, readFile as readFile15, writeFile as writeFile4, unlink as unlink3, stat, open, rename as rename5 } from "fs/promises";
12242
+ import { cp, mkdtemp, rm as rm2, readFile as readFile15, writeFile as writeFile4, unlink as unlink3, stat, open as open2, rename as rename5 } from "fs/promises";
12065
12243
  import { resolve as resolve21, join as join2 } from "path";
12066
12244
  import { tmpdir } from "os";
12067
12245
  var exec2 = promisify2(execFile2);
@@ -12116,7 +12294,7 @@ async function acquireLock() {
12116
12294
  const lockPath = resolve21(syntaurRoot(), LOCK_FILE_NAME);
12117
12295
  await ensureDir(syntaurRoot());
12118
12296
  try {
12119
- const handle = await open(lockPath, "wx");
12297
+ const handle = await open2(lockPath, "wx");
12120
12298
  await handle.write(String(process.pid));
12121
12299
  await handle.close();
12122
12300
  return lockPath;
@@ -14382,7 +14560,11 @@ var KNOWN_SKILL_NAMES = [
14382
14560
  "add-memory",
14383
14561
  "list-assignments",
14384
14562
  "log-progress",
14385
- "set-workspace"
14563
+ "set-workspace",
14564
+ "claim-resource",
14565
+ "release-resource",
14566
+ "extend-resource",
14567
+ "list-resources"
14386
14568
  ];
14387
14569
  var KNOWN_SKILLS = KNOWN_SKILL_NAMES;
14388
14570
  async function getSkillsDir() {
@@ -14445,8 +14627,8 @@ async function skillMatches(srcDir, destDir) {
14445
14627
  }
14446
14628
  async function isSymlink(path) {
14447
14629
  try {
14448
- const stat9 = await lstat2(path);
14449
- return stat9.isSymbolicLink();
14630
+ const stat10 = await lstat2(path);
14631
+ return stat10.isSymbolicLink();
14450
14632
  } catch {
14451
14633
  return false;
14452
14634
  }
@@ -15631,6 +15813,8 @@ async function trackSessionCommand(options) {
15631
15813
  }
15632
15814
  initSessionDb();
15633
15815
  const { sessionId } = options;
15816
+ const derivedPath = await derivePathFromTranscript(options.transcriptPath);
15817
+ const recordedPath = derivedPath ?? options.path ?? process.cwd();
15634
15818
  await appendSession("", {
15635
15819
  projectSlug: options.project || null,
15636
15820
  assignmentSlug: options.assignment || null,
@@ -15638,7 +15822,7 @@ async function trackSessionCommand(options) {
15638
15822
  sessionId,
15639
15823
  started: (/* @__PURE__ */ new Date()).toISOString(),
15640
15824
  status: "active",
15641
- path: options.path || process.cwd(),
15825
+ path: recordedPath,
15642
15826
  description: options.description || null,
15643
15827
  transcriptPath: options.transcriptPath ?? null
15644
15828
  });
@@ -19281,7 +19465,7 @@ ${entry}`;
19281
19465
 
19282
19466
  // src/commands/capture.ts
19283
19467
  import { resolve as resolve52, relative as relative4 } from "path";
19284
- import { copyFile as copyFile3, mkdir as mkdir7, realpath as realpath2, stat as stat6 } from "fs/promises";
19468
+ import { copyFile as copyFile3, mkdir as mkdir7, realpath as realpath2, stat as stat7 } from "fs/promises";
19285
19469
 
19286
19470
  // src/utils/assignment-target.ts
19287
19471
  init_paths();
@@ -19461,6 +19645,80 @@ function isArtifactKind(value) {
19461
19645
  // src/commands/capture.ts
19462
19646
  init_fs();
19463
19647
 
19648
+ // src/utils/screencapture.ts
19649
+ import { spawn as spawn4 } from "child_process";
19650
+ import { mkdtemp as mkdtemp2, rm as rm6, stat as stat6 } from "fs/promises";
19651
+ import { tmpdir as tmpdir2 } from "os";
19652
+ import { join as join7 } from "path";
19653
+ function argsFor(mode, pngPath) {
19654
+ switch (mode) {
19655
+ case "interactive":
19656
+ return ["-i", pngPath];
19657
+ case "window":
19658
+ return ["-iWo", pngPath];
19659
+ case "fullscreen":
19660
+ return ["-x", pngPath];
19661
+ }
19662
+ }
19663
+ function runScreencapture(args) {
19664
+ return new Promise((resolvePromise, reject) => {
19665
+ const child = spawn4("screencapture", args, { stdio: ["ignore", "pipe", "pipe"] });
19666
+ let settled = false;
19667
+ child.once("error", (err2) => {
19668
+ if (settled) return;
19669
+ settled = true;
19670
+ reject(err2);
19671
+ });
19672
+ child.once("close", (code) => {
19673
+ if (settled) return;
19674
+ settled = true;
19675
+ resolvePromise(code ?? -1);
19676
+ });
19677
+ });
19678
+ }
19679
+ async function captureScreenshot(mode) {
19680
+ if (process.platform !== "darwin") {
19681
+ throw new Error(
19682
+ "screencapture is only available on macOS. Use --file <path> to attach an existing image."
19683
+ );
19684
+ }
19685
+ const tmpDir = await mkdtemp2(join7(tmpdir2(), "syntaur-screenshot-"));
19686
+ const pngPath = join7(tmpDir, "shot.png");
19687
+ const cleanup = async () => {
19688
+ await rm6(tmpDir, { recursive: true, force: true }).catch(() => {
19689
+ });
19690
+ };
19691
+ try {
19692
+ let code;
19693
+ try {
19694
+ code = await runScreencapture(argsFor(mode, pngPath));
19695
+ } catch (err2) {
19696
+ if (err2 && typeof err2 === "object" && err2.code === "ENOENT") {
19697
+ throw new Error(
19698
+ "screencapture binary not found. Is this macOS with the system utility on PATH?"
19699
+ );
19700
+ }
19701
+ throw err2;
19702
+ }
19703
+ if (code !== 0) {
19704
+ throw new Error(`Screenshot canceled or failed (exit ${code}).`);
19705
+ }
19706
+ let size = 0;
19707
+ try {
19708
+ size = (await stat6(pngPath)).size;
19709
+ } catch {
19710
+ throw new Error("screencapture exited 0 but produced no image.");
19711
+ }
19712
+ if (size === 0) {
19713
+ throw new Error("screencapture exited 0 but produced no image.");
19714
+ }
19715
+ return { pngPath, cleanup };
19716
+ } catch (err2) {
19717
+ await cleanup();
19718
+ throw err2;
19719
+ }
19720
+ }
19721
+
19464
19722
  // src/db/proof-db.ts
19465
19723
  init_paths();
19466
19724
  import Database4 from "better-sqlite3";
@@ -19565,6 +19823,27 @@ async function captureCommand(target, options = {}) {
19565
19823
  );
19566
19824
  }
19567
19825
  const kind = options.kind;
19826
+ const shelloutFlagCount = [
19827
+ options.interactive,
19828
+ options.window,
19829
+ options.fullscreen
19830
+ ].filter(Boolean).length;
19831
+ if (shelloutFlagCount > 1) {
19832
+ throw new Error("--interactive, --window, --fullscreen are mutually exclusive.");
19833
+ }
19834
+ const shelloutMode = options.interactive ? "interactive" : options.window ? "window" : options.fullscreen ? "fullscreen" : null;
19835
+ if (shelloutMode) {
19836
+ if (kind !== "screenshot") {
19837
+ throw new Error(
19838
+ "--interactive, --window, and --fullscreen require --kind=screenshot."
19839
+ );
19840
+ }
19841
+ if (options.file) {
19842
+ throw new Error(
19843
+ "--file cannot be combined with --interactive, --window, or --fullscreen."
19844
+ );
19845
+ }
19846
+ }
19568
19847
  if (kind === "text") {
19569
19848
  if (options.file) {
19570
19849
  throw new Error("--kind=text forbids --file. Use --note for text payloads.");
@@ -19573,7 +19852,7 @@ async function captureCommand(target, options = {}) {
19573
19852
  throw new Error("--kind=text requires --note.");
19574
19853
  }
19575
19854
  } else {
19576
- if (kind !== "http" && !options.file) {
19855
+ if (kind !== "http" && !options.file && !shelloutMode) {
19577
19856
  throw new Error(`--kind=${kind} requires --file.`);
19578
19857
  }
19579
19858
  if (kind === "http" && !options.file && (!options.note || options.note.trim() === "")) {
@@ -19587,6 +19866,7 @@ async function captureCommand(target, options = {}) {
19587
19866
  cwd: options.cwd
19588
19867
  });
19589
19868
  let resolvedSource = null;
19869
+ let shelloutCleanup = null;
19590
19870
  if (options.file) {
19591
19871
  const expanded = options.file.startsWith("~/") ? resolve52(process.env.HOME ?? "", options.file.slice(2)) : resolve52(options.file);
19592
19872
  if (!await fileExists(expanded)) {
@@ -19600,71 +19880,92 @@ async function captureCommand(target, options = {}) {
19600
19880
  `--file is unreadable: ${options.file} (${e instanceof Error ? e.message : String(e)})`
19601
19881
  );
19602
19882
  }
19603
- const st = await stat6(real);
19883
+ const st = await stat7(real);
19604
19884
  if (!st.isFile()) {
19605
19885
  throw new Error(`--file is not a regular file: ${options.file}`);
19606
19886
  }
19607
19887
  resolvedSource = real;
19888
+ } else if (shelloutMode) {
19889
+ const { pngPath, cleanup } = await captureScreenshot(shelloutMode);
19890
+ resolvedSource = pngPath;
19891
+ shelloutCleanup = cleanup;
19608
19892
  }
19609
- if (!resolved.id || resolved.id.trim() === "") {
19610
- throw new Error(
19611
- `Resolved assignment is missing a frontmatter \`id\`: ${resolved.assignmentDir}. Cannot record artifact.`
19612
- );
19613
- }
19614
- initProofDb();
19615
- const subdir = criterionIndex === null ? "untagged" : String(criterionIndex);
19616
- const destDir = resolve52(proofDir(resolved.assignmentDir), subdir);
19617
- if (resolvedSource) await mkdir7(destDir, { recursive: true });
19618
- const ext = resolvedSource ? extensionForKind(kind) : null;
19619
- let id = null;
19620
- let relativeFilePath = null;
19621
- let absPath = null;
19622
- let lastErr = null;
19623
- for (let attempt = 0; attempt < MAX_ID_RETRIES; attempt += 1) {
19624
- const candidate = generateArtifactId();
19625
- const candidateAbsPath = resolvedSource && ext ? resolve52(destDir, `${candidate}.${ext}`) : null;
19626
- const candidateRel = candidateAbsPath ? relative4(resolved.assignmentDir, candidateAbsPath) : null;
19627
- try {
19628
- insertArtifact({
19629
- id: candidate,
19630
- assignmentId: resolved.id,
19631
- assignmentDir: resolved.assignmentDir,
19632
- criterionIndex,
19633
- kind,
19634
- filePath: candidateRel,
19635
- note: options.note ?? null
19636
- });
19637
- id = candidate;
19638
- absPath = candidateAbsPath;
19639
- relativeFilePath = candidateRel;
19640
- break;
19641
- } catch (e) {
19642
- if (isUniqueConstraintError(e)) {
19643
- lastErr = e;
19644
- continue;
19893
+ try {
19894
+ if (!resolved.id || resolved.id.trim() === "") {
19895
+ throw new Error(
19896
+ `Resolved assignment is missing a frontmatter \`id\`: ${resolved.assignmentDir}. Cannot record artifact.`
19897
+ );
19898
+ }
19899
+ initProofDb();
19900
+ const subdir = criterionIndex === null ? "untagged" : String(criterionIndex);
19901
+ const destDir = resolve52(proofDir(resolved.assignmentDir), subdir);
19902
+ if (resolvedSource) await mkdir7(destDir, { recursive: true });
19903
+ const ext = resolvedSource ? extensionForKind(kind) : null;
19904
+ let id = null;
19905
+ let relativeFilePath = null;
19906
+ let absPath = null;
19907
+ let lastErr = null;
19908
+ for (let attempt = 0; attempt < MAX_ID_RETRIES; attempt += 1) {
19909
+ const candidate = generateArtifactId();
19910
+ const candidateAbsPath = resolvedSource && ext ? resolve52(destDir, `${candidate}.${ext}`) : null;
19911
+ const candidateRel = candidateAbsPath ? relative4(resolved.assignmentDir, candidateAbsPath) : null;
19912
+ try {
19913
+ insertArtifact({
19914
+ id: candidate,
19915
+ assignmentId: resolved.id,
19916
+ assignmentDir: resolved.assignmentDir,
19917
+ criterionIndex,
19918
+ kind,
19919
+ filePath: candidateRel,
19920
+ note: options.note ?? null
19921
+ });
19922
+ id = candidate;
19923
+ absPath = candidateAbsPath;
19924
+ relativeFilePath = candidateRel;
19925
+ break;
19926
+ } catch (e) {
19927
+ if (isUniqueConstraintError(e)) {
19928
+ lastErr = e;
19929
+ continue;
19930
+ }
19931
+ throw e;
19645
19932
  }
19646
- throw e;
19647
19933
  }
19648
- }
19649
- if (!id) {
19650
- throw new Error(
19651
- `Failed to generate a unique artifact id after ${MAX_ID_RETRIES} attempts: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`
19652
- );
19653
- }
19654
- if (resolvedSource && absPath) {
19655
- await copyFile3(resolvedSource, absPath);
19656
- }
19657
- const ref = resolved.standalone ? resolved.id : `${resolved.projectSlug}/${resolved.assignmentSlug}`;
19658
- const tagSuffix = criterionIndex === null ? "untagged" : `criterion ${criterionIndex}`;
19659
- console.log(`Captured artifact ${id} (${kind}) for ${ref} \u2014 ${tagSuffix}.`);
19660
- if (relativeFilePath) {
19661
- console.log(` file: ${resolve52(resolved.assignmentDir, relativeFilePath)}`);
19934
+ if (!id) {
19935
+ throw new Error(
19936
+ `Failed to generate a unique artifact id after ${MAX_ID_RETRIES} attempts: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`
19937
+ );
19938
+ }
19939
+ if (resolvedSource && absPath) {
19940
+ try {
19941
+ await copyFile3(resolvedSource, absPath);
19942
+ } catch (err2) {
19943
+ if (shelloutCleanup) {
19944
+ console.error(
19945
+ `Screenshot saved at ${resolvedSource} \u2014 DB row inserted but copy to proof dir failed. Recover with: mv ${resolvedSource} ${absPath}`
19946
+ );
19947
+ shelloutCleanup = null;
19948
+ }
19949
+ throw err2;
19950
+ }
19951
+ }
19952
+ const ref = resolved.standalone ? resolved.id : `${resolved.projectSlug}/${resolved.assignmentSlug}`;
19953
+ const tagSuffix = criterionIndex === null ? "untagged" : `criterion ${criterionIndex}`;
19954
+ console.log(`Captured artifact ${id} (${kind}) for ${ref} \u2014 ${tagSuffix}.`);
19955
+ if (relativeFilePath) {
19956
+ console.log(` file: ${resolve52(resolved.assignmentDir, relativeFilePath)}`);
19957
+ }
19958
+ } finally {
19959
+ if (shelloutCleanup) {
19960
+ await shelloutCleanup();
19961
+ shelloutCleanup = null;
19962
+ }
19662
19963
  }
19663
19964
  }
19664
19965
 
19665
19966
  // src/commands/proof.ts
19666
19967
  import { Command as Command5 } from "commander";
19667
- import { readFile as readFile34, writeFile as writeFile10, rename as rename7, stat as stat7 } from "fs/promises";
19968
+ import { readFile as readFile34, writeFile as writeFile10, rename as rename7, stat as stat8 } from "fs/promises";
19668
19969
  import { resolve as resolve53, relative as relative5, isAbsolute as isAbsolute8 } from "path";
19669
19970
  import { randomBytes as randomBytes3 } from "crypto";
19670
19971
 
@@ -19928,7 +20229,7 @@ async function loadInlineFiles(rows, assignmentDir) {
19928
20229
  out.set(r.file_path, null);
19929
20230
  continue;
19930
20231
  }
19931
- const st = await stat7(abs);
20232
+ const st = await stat8(abs);
19932
20233
  if (st.size > INLINE_TEXT_LIMIT_BYTES) {
19933
20234
  out.set(r.file_path, null);
19934
20235
  continue;
@@ -20075,38 +20376,75 @@ memberCommand.command("retire").description("Retire a member (no longer claimabl
20075
20376
  process.exit(1);
20076
20377
  }
20077
20378
  });
20078
- leaseCommand.command("claim").description("Claim an idle member of an inventory. Fail-fast if none idle.").argument("<inventory>", "Inventory slug").option("--ttl <duration>", "Lease TTL (overrides inventory default)").option("--for <tag>", "Free-form requester tag (session id, assignment slug, etc.)").option("--json", "Output JSON instead of a one-line summary").action(async (inventory, opts) => {
20079
- try {
20080
- initLeasesDb();
20081
- const detail = getInventoryDetail(inventory);
20082
- if (!detail) {
20083
- console.error(`Error: inventory '${inventory}' not found`);
20084
- process.exit(1);
20085
- return;
20086
- }
20087
- const ttl_s = parseDuration(opts.ttl, detail.inventory.default_ttl_s);
20088
- const result = claimLease(inventory, ttl_s, opts.for);
20089
- if (opts.json) {
20090
- console.log(JSON.stringify(result, null, 2));
20091
- } else {
20092
- console.log(
20093
- `Claimed ${result.member_id} as ${result.lease_id} (expires ${result.expires_at})`
20094
- );
20095
- }
20096
- } catch (error) {
20097
- if (error instanceof NoIdleMemberError) {
20098
- console.error(`Error: no idle members in '${error.inventorySlug}'`);
20099
- process.exit(1);
20100
- }
20101
- if (error instanceof LeaseContentionError) {
20102
- console.error(`Error: contention timeout on '${error.inventorySlug}'; retry`);
20379
+ leaseCommand.command("claim").description("Claim an idle member of an inventory. Fail-fast unless --wait is set.").argument("<inventory>", "Inventory slug").option("--ttl <duration>", "Lease TTL (overrides inventory default)").option("--for <tag>", "Free-form requester tag (session id, assignment slug, etc.)").option(
20380
+ "--wait <duration>",
20381
+ "Block up to <duration> waiting for an idle member. Backoff: 100ms \u2192 200ms \u2192 400ms \u2192 800ms \u2192 1s cap."
20382
+ ).option("--json", "Output JSON instead of a one-line summary").action(
20383
+ async (inventory, opts) => {
20384
+ try {
20385
+ initLeasesDb();
20386
+ const detail = getInventoryDetail(inventory);
20387
+ if (!detail) {
20388
+ console.error(`Error: inventory '${inventory}' not found`);
20389
+ process.exit(1);
20390
+ return;
20391
+ }
20392
+ const ttl_s = parseDuration(opts.ttl, detail.inventory.default_ttl_s);
20393
+ const waitBudgetMs = opts.wait !== void 0 ? parseDuration(opts.wait, 0) * 1e3 : 0;
20394
+ const tryClaim = () => claimLease(inventory, ttl_s, opts.for);
20395
+ let result;
20396
+ if (waitBudgetMs <= 0) {
20397
+ result = tryClaim();
20398
+ } else {
20399
+ const deadline = Date.now() + waitBudgetMs;
20400
+ const backoffSchedule = [100, 200, 400, 800];
20401
+ let attempt = 0;
20402
+ while (true) {
20403
+ try {
20404
+ result = tryClaim();
20405
+ break;
20406
+ } catch (err2) {
20407
+ if (!(err2 instanceof NoIdleMemberError)) throw err2;
20408
+ const remaining = deadline - Date.now();
20409
+ if (remaining <= 0) {
20410
+ console.error(
20411
+ `Error: timed out waiting ${opts.wait} for an idle member of '${inventory}'`
20412
+ );
20413
+ process.exit(1);
20414
+ return;
20415
+ }
20416
+ const delay = Math.min(
20417
+ backoffSchedule[Math.min(attempt, backoffSchedule.length - 1)] ?? 1e3,
20418
+ 1e3,
20419
+ remaining
20420
+ );
20421
+ attempt += 1;
20422
+ await new Promise((r) => setTimeout(r, delay));
20423
+ }
20424
+ }
20425
+ }
20426
+ if (opts.json) {
20427
+ console.log(JSON.stringify(result, null, 2));
20428
+ } else {
20429
+ console.log(
20430
+ `Claimed ${result.member_id} as ${result.lease_id} (expires ${result.expires_at})`
20431
+ );
20432
+ }
20433
+ } catch (error) {
20434
+ if (error instanceof NoIdleMemberError) {
20435
+ console.error(`Error: no idle members in '${error.inventorySlug}'`);
20436
+ process.exit(1);
20437
+ }
20438
+ if (error instanceof LeaseContentionError) {
20439
+ console.error(`Error: contention timeout on '${error.inventorySlug}'; retry`);
20440
+ process.exit(1);
20441
+ }
20442
+ const message = error instanceof Error ? error.message : String(error);
20443
+ console.error(`Error: ${message}`);
20103
20444
  process.exit(1);
20104
20445
  }
20105
- const message = error instanceof Error ? error.message : String(error);
20106
- console.error(`Error: ${message}`);
20107
- process.exit(1);
20108
20446
  }
20109
- });
20447
+ );
20110
20448
  leaseCommand.command("release").description("Release a lease by its lease_id").argument("<lease-id>", "Opaque lease id returned by `claim`").action(async (leaseId) => {
20111
20449
  try {
20112
20450
  initLeasesDb();
@@ -20239,6 +20577,186 @@ leaseCommand.command("gc").description("Sweep expired leases across all inventor
20239
20577
  process.exit(1);
20240
20578
  }
20241
20579
  });
20580
+ leaseCommand.command("revoke").description(
20581
+ "Force-release a lease (administrative). Idempotent \u2014 already-revoked exits 0."
20582
+ ).argument("<lease-id>", "Lease id to revoke").action(async (leaseId) => {
20583
+ try {
20584
+ initLeasesDb();
20585
+ const existing = getLease(leaseId);
20586
+ if (!existing) {
20587
+ console.error(`Error: lease ${leaseId} not found`);
20588
+ process.exit(1);
20589
+ return;
20590
+ }
20591
+ if (existing.state === "revoked") {
20592
+ console.log(`already revoked ${leaseId}`);
20593
+ return;
20594
+ }
20595
+ const res = forceReleaseLease(leaseId);
20596
+ console.log(`revoked ${leaseId} (member_freed=${res.member_freed})`);
20597
+ } catch (error) {
20598
+ if (error instanceof NotFoundError) {
20599
+ console.error(`Error: lease ${error.id} not found`);
20600
+ process.exit(1);
20601
+ }
20602
+ if (error instanceof LeaseContentionError) {
20603
+ console.error(`Error: contention timeout; retry`);
20604
+ process.exit(1);
20605
+ }
20606
+ const message = error instanceof Error ? error.message : String(error);
20607
+ console.error(`Error: ${message}`);
20608
+ process.exit(1);
20609
+ }
20610
+ });
20611
+ leaseCommand.command("release-all").description("Release every active lease matching --for <tag>.").requiredOption("--for <tag>", "Requester tag to match against `requested_for`").option("--json", "Output JSON {released, stale} as id arrays").action(async (opts) => {
20612
+ try {
20613
+ initLeasesDb();
20614
+ const res = releaseLeasesByRequestedFor(opts.for);
20615
+ if (opts.json) {
20616
+ console.log(JSON.stringify(res, null, 2));
20617
+ return;
20618
+ }
20619
+ for (const lid of res.released) {
20620
+ console.log(`released ${lid}`);
20621
+ }
20622
+ for (const lid of res.stale) {
20623
+ console.log(`skipped ${lid} (stale)`);
20624
+ }
20625
+ console.log(
20626
+ `released ${res.released.length} lease(s) for tag "${opts.for}" (${res.stale.length} stale skipped)`
20627
+ );
20628
+ } catch (error) {
20629
+ if (error instanceof LeaseContentionError) {
20630
+ console.error(`Error: contention timeout; retry`);
20631
+ process.exit(1);
20632
+ }
20633
+ const message = error instanceof Error ? error.message : String(error);
20634
+ console.error(`Error: ${message}`);
20635
+ process.exit(1);
20636
+ }
20637
+ });
20638
+ leaseCommand.command("history").description(
20639
+ "Show lease_events. With a lease-id, print that lease's timeline; without, print the last N events across all leases."
20640
+ ).argument("[lease-id]", "Optional lease id to filter on").option("--limit <n>", "Max events to return (default 50)", "50").option("--json", "Output JSON rows").action(
20641
+ async (leaseId, opts) => {
20642
+ try {
20643
+ initLeasesDb();
20644
+ const limit = Number.parseInt(opts.limit, 10);
20645
+ if (!Number.isFinite(limit) || limit <= 0) {
20646
+ throw new Error("--limit must be a positive integer");
20647
+ }
20648
+ const rows = getLeaseEvents(leaseId, limit);
20649
+ if (opts.json) {
20650
+ console.log(JSON.stringify(rows, null, 2));
20651
+ return;
20652
+ }
20653
+ if (rows.length === 0) {
20654
+ console.log("(no events)");
20655
+ return;
20656
+ }
20657
+ for (const r of rows) {
20658
+ const detail = r.detail_json ? ` ${r.detail_json}` : "";
20659
+ console.log(`${r.at} ${r.event.padEnd(16)} ${r.lease_id}${detail}`);
20660
+ }
20661
+ } catch (error) {
20662
+ const message = error instanceof Error ? error.message : String(error);
20663
+ console.error(`Error: ${message}`);
20664
+ process.exit(1);
20665
+ }
20666
+ }
20667
+ );
20668
+ var inventoryCommand = leaseCommand.command("inventory").description("Manage existing inventories (update, delete)");
20669
+ inventoryCommand.command("update").description(
20670
+ "Update mutable inventory fields. `kind` is immutable in v1."
20671
+ ).argument("<slug>", "Inventory slug").option("--default-ttl <duration>", "New default TTL (e.g. 30m, 2h)").option("--display-name <text>", "New display name").action(
20672
+ async (slug, opts) => {
20673
+ try {
20674
+ initLeasesDb();
20675
+ if (opts.defaultTtl === void 0 && opts.displayName === void 0) {
20676
+ throw new Error(
20677
+ "nothing to update \u2014 pass --default-ttl or --display-name"
20678
+ );
20679
+ }
20680
+ const patch = {};
20681
+ if (opts.defaultTtl !== void 0) {
20682
+ patch.default_ttl_s = parseDuration(opts.defaultTtl, 0);
20683
+ }
20684
+ if (opts.displayName !== void 0) {
20685
+ patch.display_name = opts.displayName;
20686
+ }
20687
+ const row = updateInventory(slug, patch);
20688
+ console.log(
20689
+ `Updated inventory '${row.slug}' (kind=${row.kind}, default_ttl=${row.default_ttl_s}s, display_name=${row.display_name ?? "(none)"}).`
20690
+ );
20691
+ } catch (error) {
20692
+ if (error instanceof NotFoundError) {
20693
+ console.error(`Error: inventory '${slug}' not found`);
20694
+ process.exit(1);
20695
+ }
20696
+ const message = error instanceof Error ? error.message : String(error);
20697
+ console.error(`Error: ${message}`);
20698
+ process.exit(1);
20699
+ }
20700
+ }
20701
+ );
20702
+ inventoryCommand.command("delete").description(
20703
+ "Delete an inventory. Refuses if any lease is active unless --force is set."
20704
+ ).argument("<slug>", "Inventory slug").option("--force", "Revoke all active leases first, then cascade delete").action(async (slug, opts) => {
20705
+ try {
20706
+ initLeasesDb();
20707
+ const res = deleteInventory(slug, { force: opts.force });
20708
+ console.log(
20709
+ `deleted "${slug}" (revoked ${res.revoked} active lease(s))`
20710
+ );
20711
+ } catch (error) {
20712
+ if (error instanceof MemberInUseError) {
20713
+ console.error(
20714
+ `Error: inventory "${slug}" has active leases \u2014 use --force to revoke and delete`
20715
+ );
20716
+ process.exit(1);
20717
+ }
20718
+ if (error instanceof NotFoundError) {
20719
+ console.error(`Error: inventory '${slug}' not found`);
20720
+ process.exit(1);
20721
+ }
20722
+ if (error instanceof LeaseContentionError) {
20723
+ console.error(`Error: contention timeout; retry`);
20724
+ process.exit(1);
20725
+ }
20726
+ const message = error instanceof Error ? error.message : String(error);
20727
+ console.error(`Error: ${message}`);
20728
+ process.exit(1);
20729
+ }
20730
+ });
20731
+ memberCommand.command("list").description("List all members of an inventory (pure roster \u2014 no lease state).").argument("<inventory>", "Inventory slug").option("--json", "Output JSON rows").action(async (inventory, opts) => {
20732
+ try {
20733
+ initLeasesDb();
20734
+ const rows = listMembers(inventory);
20735
+ if (opts.json) {
20736
+ console.log(JSON.stringify(rows, null, 2));
20737
+ return;
20738
+ }
20739
+ if (rows.length === 0) {
20740
+ console.log("(no members)");
20741
+ return;
20742
+ }
20743
+ for (const r of rows) {
20744
+ const meta = r.metadata_json ? ` ${r.metadata_json}` : "";
20745
+ const last = r.last_used_at ? ` last_used=${r.last_used_at}` : "";
20746
+ console.log(
20747
+ `${r.member_id.padEnd(24)} ${r.status.padEnd(8)} gen=${r.generation}${last}${meta}`
20748
+ );
20749
+ }
20750
+ } catch (error) {
20751
+ if (error instanceof NotFoundError) {
20752
+ console.error(`Error: inventory '${inventory}' not found`);
20753
+ process.exit(1);
20754
+ }
20755
+ const message = error instanceof Error ? error.message : String(error);
20756
+ console.error(`Error: ${message}`);
20757
+ process.exit(1);
20758
+ }
20759
+ });
20242
20760
 
20243
20761
  // src/commands/request.ts
20244
20762
  init_paths();
@@ -20515,7 +21033,7 @@ planCommand.command("version").description(
20515
21033
  // src/commands/session.ts
20516
21034
  init_fs();
20517
21035
  import { Command as Command8 } from "commander";
20518
- import { readFile as readFile37, readdir as readdir19, stat as stat8 } from "fs/promises";
21036
+ import { readFile as readFile37, readdir as readdir19, stat as stat9 } from "fs/promises";
20519
21037
  import { resolve as resolve56 } from "path";
20520
21038
  async function readContext(cwd) {
20521
21039
  const path = resolve56(cwd, ".syntaur", "context.json");
@@ -20536,7 +21054,7 @@ async function findLatestSessionSummary(assignmentDir) {
20536
21054
  if (!entry.isDirectory()) continue;
20537
21055
  const summaryPath = resolve56(sessionsRoot, entry.name, "summary.md");
20538
21056
  if (!await fileExists(summaryPath)) continue;
20539
- const st = await stat8(summaryPath);
21057
+ const st = await stat9(summaryPath);
20540
21058
  if (best === null || st.mtime.getTime() > best.mtime.getTime()) {
20541
21059
  best = { sessionId: entry.name, path: summaryPath, mtime: st.mtime };
20542
21060
  }
@@ -21127,19 +21645,19 @@ init_paths();
21127
21645
  init_fs();
21128
21646
  import { fileURLToPath as fileURLToPath8 } from "url";
21129
21647
  import { readFile as readFile42 } from "fs/promises";
21130
- import { dirname as dirname16, join as join8, resolve as resolve62 } from "path";
21131
- import { spawn as spawn4 } from "child_process";
21648
+ import { dirname as dirname16, join as join9, resolve as resolve62 } from "path";
21649
+ import { spawn as spawn5 } from "child_process";
21132
21650
  import { createInterface as createInterface2 } from "readline/promises";
21133
21651
 
21134
21652
  // src/utils/version.ts
21135
21653
  import { fileURLToPath as fileURLToPath7 } from "url";
21136
21654
  import { readFile as readFile41 } from "fs/promises";
21137
- import { dirname as dirname15, join as join7 } from "path";
21655
+ import { dirname as dirname15, join as join8 } from "path";
21138
21656
  async function readPackageVersion(scriptUrl) {
21139
21657
  try {
21140
21658
  const scriptPath = fileURLToPath7(scriptUrl);
21141
21659
  const pkgRoot = dirname15(dirname15(scriptPath));
21142
- const raw = await readFile41(join7(pkgRoot, "package.json"), "utf-8");
21660
+ const raw = await readFile41(join8(pkgRoot, "package.json"), "utf-8");
21143
21661
  const parsed = JSON.parse(raw);
21144
21662
  return typeof parsed.version === "string" ? parsed.version : null;
21145
21663
  } catch {
@@ -21183,7 +21701,7 @@ async function resolveNpmBin() {
21183
21701
  const nodeDir = dirname16(process.execPath);
21184
21702
  const isWin = process.platform === "win32";
21185
21703
  const npmName = isWin ? "npm.cmd" : "npm";
21186
- const nearNode = join8(nodeDir, npmName);
21704
+ const nearNode = join9(nodeDir, npmName);
21187
21705
  if (await fileExists(nearNode)) {
21188
21706
  return { cmd: nearNode, shell: false };
21189
21707
  }
@@ -21192,7 +21710,7 @@ async function resolveNpmBin() {
21192
21710
  async function installGlobally() {
21193
21711
  const { cmd, shell } = await resolveNpmBin();
21194
21712
  return new Promise((resolvePromise) => {
21195
- const child = spawn4(cmd, ["install", "-g", "syntaur"], {
21713
+ const child = spawn5(cmd, ["install", "-g", "syntaur"], {
21196
21714
  stdio: "inherit",
21197
21715
  shell
21198
21716
  });
@@ -21203,7 +21721,7 @@ async function installGlobally() {
21203
21721
  async function readGlobalVersion() {
21204
21722
  const { cmd, shell } = await resolveNpmBin();
21205
21723
  const rootPath = await new Promise((resolvePromise) => {
21206
- const child = spawn4(cmd, ["root", "-g"], {
21724
+ const child = spawn5(cmd, ["root", "-g"], {
21207
21725
  shell,
21208
21726
  stdio: ["ignore", "pipe", "ignore"]
21209
21727
  });
@@ -21226,7 +21744,7 @@ async function readGlobalVersion() {
21226
21744
  });
21227
21745
  if (!rootPath) return null;
21228
21746
  try {
21229
- const manifestPath = join8(rootPath, "syntaur", "package.json");
21747
+ const manifestPath = join9(rootPath, "syntaur", "package.json");
21230
21748
  if (!await fileExists(manifestPath)) return null;
21231
21749
  const raw = await readFile42(manifestPath, "utf-8");
21232
21750
  const parsed = JSON.parse(raw);
@@ -21398,7 +21916,7 @@ program.command("comment").description("Add a comment to an assignment (CLI-medi
21398
21916
  process.exit(1);
21399
21917
  }
21400
21918
  });
21401
- program.command("capture").description("Capture a typed proof artifact for an assignment").argument("[target]", "Assignment slug (with --project) or UUID; falls back to .syntaur/context.json").option("--kind <type>", "Artifact kind: screenshot | video | asciinema | http | text").option("--file <path>", "Source file to ingest (forbidden for --kind=text)").option("--criterion <index>", "Optional 0-based criterion index to tag").option("--note <text>", "Optional note (required for --kind=text)").option("--project <slug>", "Project slug if the target is project-nested").option("--dir <path>", "Override default project directory").action(async (target, options) => {
21919
+ program.command("capture").description("Capture a typed proof artifact for an assignment").argument("[target]", "Assignment slug (with --project) or UUID; falls back to .syntaur/context.json").option("--kind <type>", "Artifact kind: screenshot | video | asciinema | http | text").option("--file <path>", "Source file to ingest (forbidden for --kind=text)").option("--criterion <index>", "Optional 0-based criterion index to tag").option("--note <text>", "Optional note (required for --kind=text)").option("--project <slug>", "Project slug if the target is project-nested").option("--dir <path>", "Override default project directory").option("--interactive", "Screenshot mode: drag a region (macOS only)").option("--window", "Screenshot mode: window picker (macOS only)").option("--fullscreen", "Screenshot mode: silent full-screen capture (macOS only)").action(async (target, options) => {
21402
21920
  try {
21403
21921
  await captureCommand(target, options);
21404
21922
  } catch (error) {