syntaur 0.69.0 → 0.70.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "syntaur",
3
- "version": "0.69.0",
3
+ "version": "0.70.0",
4
4
  "description": "Syntaur protocol skills for AI coding agents — assignment, project, plan, and session lifecycle. Cross-agent (Claude Code, Codex, Cursor, OpenCode, Gemini CLI, and more via skills.sh).",
5
5
  "author": {
6
6
  "name": "Brennen",
@@ -122,7 +122,7 @@ function getTargetStatus(_from, command, table) {
122
122
  if (!table) {
123
123
  return DEFAULT_COMMAND_TARGETS.get(command) ?? null;
124
124
  }
125
- return table.get(command) ?? table.get(`${_from}:${command}`) ?? null;
125
+ return table.get(`${_from}:${command}`) ?? table.get(command) ?? null;
126
126
  }
127
127
  var DEFAULT_COMMAND_TARGETS, DEFAULT_TRANSITION_TABLE;
128
128
  var init_state_machine = __esm({
@@ -6322,8 +6322,7 @@ async function areDependenciesSatisfied(projectDir, dependsOn, terminalStatuses)
6322
6322
  if (!await fileExists(depPath)) return false;
6323
6323
  try {
6324
6324
  const content = await readFile9(depPath, "utf-8");
6325
- const m = content.match(/^status:\s*(.+)$/m);
6326
- const status = m ? m[1].trim() : "";
6325
+ const { status } = parseAssignmentFrontmatter(content);
6327
6326
  if (!terminalStatuses.has(status)) return false;
6328
6327
  } catch {
6329
6328
  return false;
@@ -6438,6 +6437,7 @@ var init_facts = __esm({
6438
6437
  init_fs();
6439
6438
  init_git_worktree();
6440
6439
  init_derive();
6440
+ init_frontmatter();
6441
6441
  HTML_COMMENT_RE = /<!--[\s\S]*?-->/g;
6442
6442
  PLAN_FILE_RE = /^plan(?:-v(\d+))?\.md$/;
6443
6443
  }
@@ -8175,8 +8175,8 @@ function scanKey(serversDir2, projectsDir, assignmentsDir2) {
8175
8175
  return `${serversDir2}\0${projectsDir}\0${assignmentsDir2 ?? ""}`;
8176
8176
  }
8177
8177
  function delay(ms) {
8178
- return new Promise((resolve47) => {
8179
- const timer3 = setTimeout(resolve47, ms);
8178
+ return new Promise((resolve48) => {
8179
+ const timer3 = setTimeout(resolve48, ms);
8180
8180
  if (typeof timer3.unref === "function") {
8181
8181
  timer3.unref();
8182
8182
  }
@@ -11991,6 +11991,22 @@ var init_assignment_todos = __esm({
11991
11991
  }
11992
11992
  });
11993
11993
 
11994
+ // src/utils/path-canon.ts
11995
+ import { realpathSync as realpathSync2 } from "fs";
11996
+ import { resolve as resolve43 } from "path";
11997
+ function canonicalPath(p) {
11998
+ try {
11999
+ return realpathSync2(resolve43(p));
12000
+ } catch {
12001
+ return resolve43(p).replace(/\/+$/, "");
12002
+ }
12003
+ }
12004
+ var init_path_canon = __esm({
12005
+ "src/utils/path-canon.ts"() {
12006
+ "use strict";
12007
+ }
12008
+ });
12009
+
11994
12010
  // src/targets/renderers.ts
11995
12011
  var RENDERERS;
11996
12012
  var init_renderers = __esm({
@@ -12011,7 +12027,7 @@ var init_renderers = __esm({
12011
12027
  });
12012
12028
 
12013
12029
  // src/targets/user-descriptors.ts
12014
- import { resolve as resolve43 } from "path";
12030
+ import { resolve as resolve44 } from "path";
12015
12031
  import { readFile as readFile30, readdir as readdir18 } from "fs/promises";
12016
12032
  var VALID_RENDERER_KEYS;
12017
12033
  var init_user_descriptors = __esm({
@@ -12026,20 +12042,20 @@ var init_user_descriptors = __esm({
12026
12042
 
12027
12043
  // src/targets/registry.ts
12028
12044
  import { homedir as homedir7 } from "os";
12029
- import { join as join10, resolve as resolve44 } from "path";
12045
+ import { join as join10, resolve as resolve45 } from "path";
12030
12046
  function home(...segments) {
12031
- return resolve44(homedir7(), ...segments);
12047
+ return resolve45(homedir7(), ...segments);
12032
12048
  }
12033
12049
  function hermesHome() {
12034
12050
  const env = process.env.HERMES_HOME;
12035
- return env && env.length > 0 ? resolve44(env) : home(".hermes");
12051
+ return env && env.length > 0 ? resolve45(env) : home(".hermes");
12036
12052
  }
12037
12053
  function hermesSkillsDir() {
12038
- return resolve44(hermesHome(), "skills");
12054
+ return resolve45(hermesHome(), "skills");
12039
12055
  }
12040
12056
  function codexHome() {
12041
12057
  const env = process.env.CODEX_HOME;
12042
- return env && env.length > 0 ? resolve44(env) : home(".codex");
12058
+ return env && env.length > 0 ? resolve45(env) : home(".codex");
12043
12059
  }
12044
12060
  function toDiscovered(meta) {
12045
12061
  if (!meta) return null;
@@ -12110,7 +12126,7 @@ var init_registry = __esm({
12110
12126
  skillsShAgentId: "codex",
12111
12127
  nativePlugin: "codex",
12112
12128
  detect: detectDir(codexHome()),
12113
- skillsDir: { global: resolve44(codexHome(), "skills") },
12129
+ skillsDir: { global: resolve45(codexHome(), "skills") },
12114
12130
  instructions: { files: [{ path: "AGENTS.md", renderer: "codexAgents" }] },
12115
12131
  sessions: codexSessions
12116
12132
  },
@@ -12179,7 +12195,7 @@ var init_registry = __esm({
12179
12195
  tier3: {
12180
12196
  kind: "hermes-plugin",
12181
12197
  source: "platforms/hermes/plugins/syntaur",
12182
- installDir: () => resolve44(hermesHome(), "plugins", "syntaur"),
12198
+ installDir: () => resolve45(hermesHome(), "plugins", "syntaur"),
12183
12199
  entry: "plugin.yaml"
12184
12200
  }
12185
12201
  }
@@ -12199,7 +12215,7 @@ import { execFile as execFile3, execFileSync as execFileSync4 } from "child_proc
12199
12215
  import { promisify as promisify3 } from "util";
12200
12216
  import { statSync as statSync4 } from "fs";
12201
12217
  import { readFile as readFile31 } from "fs/promises";
12202
- import { resolve as resolve45 } from "path";
12218
+ import { resolve as resolve46 } from "path";
12203
12219
  function emptySummary() {
12204
12220
  return { discovered: 0, inserted: 0, revived: 0, swept: 0, skipped: 0, changed: false };
12205
12221
  }
@@ -12252,10 +12268,15 @@ async function defaultOpenFiles(files) {
12252
12268
  }
12253
12269
  return open6;
12254
12270
  }
12271
+ function canonicalizeOpenSet(open6) {
12272
+ const out = /* @__PURE__ */ new Set();
12273
+ for (const p of open6) out.add(canonicalPath(p));
12274
+ return out;
12275
+ }
12255
12276
  async function readContextLink(cwd, cache3) {
12256
12277
  if (cache3.has(cwd)) return cache3.get(cwd);
12257
12278
  let link = null;
12258
- const path = resolve45(cwd, ".syntaur", "context.json");
12279
+ const path = resolve46(cwd, ".syntaur", "context.json");
12259
12280
  if (await fileExists(path)) {
12260
12281
  try {
12261
12282
  const parsed = JSON.parse(await readFile31(path, "utf-8"));
@@ -12307,7 +12328,9 @@ async function scanSessions(opts = {}, deps = {}) {
12307
12328
  }
12308
12329
  }
12309
12330
  summary.discovered = discovered.length;
12310
- const openSet = await openFiles(discovered.map((d) => d.transcriptPath));
12331
+ const openSet = canonicalizeOpenSet(
12332
+ await openFiles(discovered.map((d) => d.transcriptPath))
12333
+ );
12311
12334
  const contextCache = /* @__PURE__ */ new Map();
12312
12335
  for (const d of discovered) {
12313
12336
  const link = await readContextLink(d.cwd, contextCache);
@@ -12316,7 +12339,7 @@ async function scanSessions(opts = {}, deps = {}) {
12316
12339
  continue;
12317
12340
  }
12318
12341
  const mtime = statMtimeMs(d.transcriptPath);
12319
- const heldOpen = openSet.has(d.transcriptPath);
12342
+ const heldOpen = openSet.has(canonicalPath(d.transcriptPath));
12320
12343
  const isLive = heldOpen || mtime !== null && now() - mtime < FRESH_MTIME_MS;
12321
12344
  const prev = getSessionById(d.sessionId);
12322
12345
  const status = isLive ? "active" : prev?.status ?? "stopped";
@@ -12378,12 +12401,14 @@ async function scanSessions(opts = {}, deps = {}) {
12378
12401
  sweepCandidates.push({ sessionId: row.session_id, transcriptPath: null });
12379
12402
  }
12380
12403
  }
12381
- const sweepOpenSet = await openFiles(
12382
- sweepCandidates.map((c) => c.transcriptPath).filter((p) => p !== null)
12404
+ const sweepOpenSet = canonicalizeOpenSet(
12405
+ await openFiles(
12406
+ sweepCandidates.map((c) => c.transcriptPath).filter((p) => p !== null)
12407
+ )
12383
12408
  );
12384
12409
  for (const candidate of sweepCandidates) {
12385
12410
  if (candidate.transcriptPath) {
12386
- if (sweepOpenSet.has(candidate.transcriptPath)) continue;
12411
+ if (sweepOpenSet.has(canonicalPath(candidate.transcriptPath))) continue;
12387
12412
  const mtime = statMtimeMs(candidate.transcriptPath);
12388
12413
  if (mtime !== null && now() - mtime < FRESH_MTIME_MS) continue;
12389
12414
  const endedAt = mtime !== null ? new Date(mtime).toISOString() : void 0;
@@ -12404,6 +12429,7 @@ var init_scanner2 = __esm({
12404
12429
  "src/sessions/scanner.ts"() {
12405
12430
  "use strict";
12406
12431
  init_fs();
12432
+ init_path_canon();
12407
12433
  init_config2();
12408
12434
  init_session_id();
12409
12435
  init_registry();
@@ -12457,7 +12483,7 @@ init_assignment_resolver();
12457
12483
  init_agent_sessions();
12458
12484
  import express from "express";
12459
12485
  import { createServer } from "http";
12460
- import { resolve as resolve46 } from "path";
12486
+ import { resolve as resolve47 } from "path";
12461
12487
  import { writeFile as writeFile8, unlink as unlink9 } from "fs/promises";
12462
12488
  import { WebSocketServer, WebSocket } from "ws";
12463
12489
 
@@ -18195,6 +18221,12 @@ async function resolveSessionPlan(input, terminal) {
18195
18221
  }
18196
18222
  }
18197
18223
  }
18224
+ if (!cwd.trim()) {
18225
+ throw new LaunchError(
18226
+ "workspace-path-invalid",
18227
+ `Session ${input.id} has no recorded working directory and no linked assignment workspace resolved \u2014 refusing to launch in an unknown directory.`
18228
+ );
18229
+ }
18198
18230
  const agent = getAgents(input.config).find((a) => a.id === session.agent);
18199
18231
  if (!agent) {
18200
18232
  throw new LaunchError(
@@ -18271,7 +18303,7 @@ async function executeLaunchPlan(plan, spawnFn = realSpawn) {
18271
18303
  `Spawn failed: ${msg}. Verify the terminal is installed and on PATH.`
18272
18304
  );
18273
18305
  }
18274
- await new Promise((resolve47, reject) => {
18306
+ await new Promise((resolve48, reject) => {
18275
18307
  let settled = false;
18276
18308
  let stderr = "";
18277
18309
  const finishOk = () => {
@@ -18281,7 +18313,7 @@ async function executeLaunchPlan(plan, spawnFn = realSpawn) {
18281
18313
  child.unref();
18282
18314
  } catch {
18283
18315
  }
18284
- resolve47();
18316
+ resolve48();
18285
18317
  };
18286
18318
  const finishErr = (remediation) => {
18287
18319
  if (settled) return;
@@ -20044,8 +20076,7 @@ function predictReset(_provider, anchor) {
20044
20076
  }
20045
20077
  function verifyReset(provider, anchor, now) {
20046
20078
  const reset = predictReset(provider, anchor);
20047
- if (now.getTime() >= Date.parse(reset)) return { eligible: true };
20048
- return { eligible: false, rescheduleToIso: reset };
20079
+ return { eligible: now.getTime() >= Date.parse(reset) };
20049
20080
  }
20050
20081
 
20051
20082
  // src/schedules/triggers.ts
@@ -20069,13 +20100,15 @@ function evaluateKind(trigger, job, ctx) {
20069
20100
  switch (trigger.kind) {
20070
20101
  case "at": {
20071
20102
  const at = Date.parse(trigger.at);
20072
- const due = !Number.isNaN(at) && ctx.now.getTime() >= at;
20103
+ const beforeCreation = !Number.isNaN(createdAtMs) && at < createdAtMs;
20104
+ const due = !Number.isNaN(at) && ctx.now.getTime() >= at && !beforeCreation;
20073
20105
  return { due, dedupeKey: `at:${trigger.at}`, nextFireIso: trigger.at };
20074
20106
  }
20075
20107
  case "in": {
20076
20108
  const anchor = Date.parse(trigger.anchorIso);
20077
20109
  const fireAt = anchor + trigger.durationMs;
20078
- const due = !Number.isNaN(anchor) && ctx.now.getTime() >= fireAt;
20110
+ const beforeCreation = !Number.isNaN(createdAtMs) && fireAt < createdAtMs;
20111
+ const due = !Number.isNaN(anchor) && ctx.now.getTime() >= fireAt && !beforeCreation;
20079
20112
  const fireIso = Number.isNaN(anchor) ? null : iso(new Date(fireAt));
20080
20113
  return { due, dedupeKey: `in:${fireAt}`, nextFireIso: fireIso };
20081
20114
  }
@@ -20109,7 +20142,7 @@ function evaluateAfterReset(trigger, now) {
20109
20142
  if (v.eligible) {
20110
20143
  return { due: true, dedupeKey: `after-reset:${trigger.anchor.windowStartIso}`, nextFireIso: predicted };
20111
20144
  }
20112
- return { due: false, nextFireIso: predicted, rescheduleToIso: v.rescheduleToIso };
20145
+ return { due: false, nextFireIso: predicted };
20113
20146
  }
20114
20147
  function evaluateWhenStatus(trigger, job, ctx, createdAtMs) {
20115
20148
  const fm = ctx.assignment;
@@ -22809,7 +22842,7 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
22809
22842
  router.post("/:workspace/archive", async (req2, res) => {
22810
22843
  try {
22811
22844
  const { archivePath: archivePath2 } = await Promise.resolve().then(() => (init_parser2(), parser_exports));
22812
- const { resolve: resolve47 } = await import("path");
22845
+ const { resolve: resolve48 } = await import("path");
22813
22846
  const { readFile: readFile32 } = await import("fs/promises");
22814
22847
  const { writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
22815
22848
  const workspace = getWorkspaceParam(req2.params.workspace);
@@ -22826,7 +22859,7 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
22826
22859
  (e) => e.itemIds.every((id) => completedIds.has(id))
22827
22860
  );
22828
22861
  const archFile = archivePath2(todosDir2, workspace, checklist.archiveInterval);
22829
- await ensureDir(resolve47(todosDir2, "archive"));
22862
+ await ensureDir(resolve48(todosDir2, "archive"));
22830
22863
  let archContent = "";
22831
22864
  if (await fileExists(archFile)) {
22832
22865
  archContent = await readFile32(archFile, "utf-8");
@@ -25305,7 +25338,7 @@ async function runCcusage(opts = {}) {
25305
25338
  };
25306
25339
  }
25307
25340
  function runOnce(binary, args, env, timeoutMs, maxOutputBytes) {
25308
- return new Promise((resolve47) => {
25341
+ return new Promise((resolve48) => {
25309
25342
  const child = spawn4(binary, args, {
25310
25343
  env: env ?? process.env,
25311
25344
  stdio: ["ignore", "pipe", "pipe"]
@@ -25319,7 +25352,7 @@ function runOnce(binary, args, env, timeoutMs, maxOutputBytes) {
25319
25352
  if (settled) return;
25320
25353
  settled = true;
25321
25354
  clearTimeout(timer3);
25322
- resolve47(result);
25355
+ resolve48(result);
25323
25356
  };
25324
25357
  const timer3 = setTimeout(() => {
25325
25358
  timedOut = true;
@@ -25723,7 +25756,7 @@ function createDashboardServer(options) {
25723
25756
  (async () => {
25724
25757
  try {
25725
25758
  const configResult = await migrateLegacyConfig(
25726
- resolve46(syntaurRoot(), "config.md")
25759
+ resolve47(syntaurRoot(), "config.md")
25727
25760
  );
25728
25761
  const projectResult = await migrateLegacyProjectFiles(projectsDir);
25729
25762
  const summary = summarizeMigration(projectResult, configResult);
@@ -26246,14 +26279,14 @@ function createDashboardServer(options) {
26246
26279
  app.use("/api/backup", createBackupRouter());
26247
26280
  if (serveStaticUi && dashboardDistPath) {
26248
26281
  const sendOpts = { dotfiles: "allow" };
26249
- app.use("/assets", express.static(resolve46(dashboardDistPath, "assets"), sendOpts));
26282
+ app.use("/assets", express.static(resolve47(dashboardDistPath, "assets"), sendOpts));
26250
26283
  app.use(express.static(dashboardDistPath, { ...sendOpts, index: false, fallthrough: true }));
26251
26284
  app.get("{*path}", async (req2, res) => {
26252
26285
  if (req2.path.startsWith("/api") || req2.path === "/ws" || req2.path.startsWith("/assets")) {
26253
26286
  res.status(404).json({ error: "Not Found" });
26254
26287
  return;
26255
26288
  }
26256
- const indexPath = resolve46(dashboardDistPath, "index.html");
26289
+ const indexPath = resolve47(dashboardDistPath, "index.html");
26257
26290
  if (!await fileExists(indexPath)) {
26258
26291
  res.status(503).send(
26259
26292
  'Dashboard not built. Run "npm run build:dashboard" first.'
@@ -26289,8 +26322,8 @@ function createDashboardServer(options) {
26289
26322
  if (!await migrationGate()) return;
26290
26323
  try {
26291
26324
  const context = await resolveDeriveContext2();
26292
- const projectDir = projectSlug ? resolve46(projectsDir, projectSlug) : null;
26293
- const path = projectDir ? resolve46(projectDir, "assignments", assignmentSlug, "assignment.md") : resolve46(assignmentsDir2, assignmentSlug, "assignment.md");
26325
+ const projectDir = projectSlug ? resolve47(projectsDir, projectSlug) : null;
26326
+ const path = projectDir ? resolve47(projectDir, "assignments", assignmentSlug, "assignment.md") : resolve47(assignmentsDir2, assignmentSlug, "assignment.md");
26294
26327
  if (!await fileExists(path)) return;
26295
26328
  const result = await recomputeAndWrite2(path, {
26296
26329
  cause: "derive",
@@ -26343,8 +26376,8 @@ function createDashboardServer(options) {
26343
26376
  serversDir: serversDir2,
26344
26377
  playbooksDir: playbooksDir2,
26345
26378
  todosDir: todosDir2,
26346
- dbPath: resolve46(syntaurRoot(), "syntaur.db"),
26347
- configPath: resolve46(syntaurRoot(), "config.md"),
26379
+ dbPath: resolve47(syntaurRoot(), "syntaur.db"),
26380
+ configPath: resolve47(syntaurRoot(), "config.md"),
26348
26381
  onMessage: broadcast,
26349
26382
  onAssignmentChanged: (projectSlug, assignmentSlug) => {
26350
26383
  void recomputeOne(projectSlug, assignmentSlug);
@@ -26412,7 +26445,7 @@ function createDashboardServer(options) {
26412
26445
  }
26413
26446
  });
26414
26447
  server.listen(port, () => {
26415
- const portFile = resolve46(syntaurRoot(), "dashboard-port");
26448
+ const portFile = resolve47(syntaurRoot(), "dashboard-port");
26416
26449
  writeFile8(portFile, String(port), "utf-8").catch(() => {
26417
26450
  });
26418
26451
  resolvePromise();
@@ -26436,7 +26469,7 @@ function createDashboardServer(options) {
26436
26469
  client.terminate();
26437
26470
  }
26438
26471
  clients.clear();
26439
- const portFile = resolve46(syntaurRoot(), "dashboard-port");
26472
+ const portFile = resolve47(syntaurRoot(), "dashboard-port");
26440
26473
  await unlink9(portFile).catch(() => {
26441
26474
  });
26442
26475
  server.closeAllConnections?.();