syntaur 0.34.0 → 0.35.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.
Files changed (31) hide show
  1. package/dist/index.js +146 -10
  2. package/dist/index.js.map +1 -1
  3. package/package.json +1 -1
  4. package/platforms/SESSION-ID-RESOLUTION.md +117 -0
  5. package/platforms/claude-code/agents/syntaur-expert.md +1 -1
  6. package/platforms/claude-code/commands/track-session/track-session.md +5 -4
  7. package/platforms/claude-code/hooks/hooks.json +1 -1
  8. package/platforms/claude-code/hooks/session-cleanup.sh +16 -8
  9. package/platforms/claude-code/skills/clear-assignment/SKILL.md +1 -1
  10. package/platforms/claude-code/skills/complete-assignment/SKILL.md +1 -1
  11. package/platforms/claude-code/skills/grab-assignment/SKILL.md +5 -4
  12. package/platforms/claude-code/skills/save-session-summary/SKILL.md +15 -6
  13. package/platforms/claude-code/skills/track-session/SKILL.md +5 -4
  14. package/platforms/codex/agents/syntaur-operator.md +1 -1
  15. package/platforms/codex/commands/save-session-summary.md +1 -1
  16. package/platforms/codex/scripts/session-cleanup.sh +63 -6
  17. package/platforms/codex/skills/clear-assignment/SKILL.md +1 -1
  18. package/platforms/codex/skills/complete-assignment/SKILL.md +1 -1
  19. package/platforms/codex/skills/grab-assignment/SKILL.md +5 -4
  20. package/platforms/codex/skills/save-session-summary/SKILL.md +15 -6
  21. package/platforms/codex/skills/track-session/SKILL.md +5 -4
  22. package/platforms/cursor/hooks/README.md +49 -0
  23. package/platforms/hermes/plugins/syntaur/__pycache__/__init__.cpython-312.pyc +0 -0
  24. package/platforms/hermes/plugins/syntaur/__pycache__/boundary.cpython-312.pyc +0 -0
  25. package/platforms/opencode/plugin/syntaur-session-env.js +30 -0
  26. package/platforms/pi/README.md +50 -0
  27. package/skills/clear-assignment/SKILL.md +1 -1
  28. package/skills/complete-assignment/SKILL.md +1 -1
  29. package/skills/grab-assignment/SKILL.md +5 -4
  30. package/skills/save-session-summary/SKILL.md +15 -6
  31. package/skills/track-session/SKILL.md +5 -4
package/dist/index.js CHANGED
@@ -4234,7 +4234,7 @@ Before starting work, read these files in order:
4234
4234
  ## Context File
4235
4235
 
4236
4236
  - Treat \`.syntaur/context.json\` in the current working directory as the active assignment context when it exists.
4237
- - Use that file to resolve the workspace boundary, assignment path, project path, and active session ID.
4237
+ - Use that file to resolve the workspace boundary, assignment path, and project path (the active assignment binding). The active session id, however, is resolved from *your* running process -- prefer \`$CLAUDE_CODE_SESSION_ID\` (or the peer \`OPENCODE_SESSION_ID\` / \`PI_SESSION_ID\`), otherwise run \`syntaur session resolve-id\`; the \`sessionId\` scalar in context.json is only a clobberable legacy hint, not authoritative.
4238
4238
  - If there is no context file yet and you are supposed to work on an assignment, claim or set up the assignment before editing code.
4239
4239
 
4240
4240
  ## Directory Structure
@@ -26002,8 +26002,8 @@ async function moveTodo(id, options) {
26002
26002
  throw new Error(`Plan directory already exists at target: ${newPlanDir}; refusing to move.`);
26003
26003
  }
26004
26004
  const { rename: rename11, mkdir: mkdir12 } = await import("fs/promises");
26005
- const { dirname: dirname24 } = await import("path");
26006
- await mkdir12(dirname24(newPlanDir), { recursive: true });
26005
+ const { dirname: dirname25 } = await import("path");
26006
+ await mkdir12(dirname25(newPlanDir), { recursive: true });
26007
26007
  await rename11(item.planDir, newPlanDir);
26008
26008
  item.planDir = newPlanDir;
26009
26009
  }
@@ -27739,7 +27739,8 @@ function isBundleContext(ctx) {
27739
27739
  }
27740
27740
  function isStandaloneSession(ctx) {
27741
27741
  if (!ctx) return false;
27742
- return !hasAnyAssignmentField(ctx) && !hasAnyBundleField(ctx) && typeof ctx.sessionId === "string" && ctx.sessionId.length > 0;
27742
+ const hasSessionMeta = typeof ctx.sessionId === "string" && ctx.sessionId.length > 0 || typeof ctx.transcriptPath === "string" && ctx.transcriptPath.length > 0;
27743
+ return !hasAnyAssignmentField(ctx) && !hasAnyBundleField(ctx) && hasSessionMeta;
27743
27744
  }
27744
27745
  async function loadContext(ctx) {
27745
27746
  const path = resolve64(ctx.cwd, ".syntaur", "context.json");
@@ -29333,7 +29334,7 @@ function classifyContext(ctx) {
29333
29334
  const hasAssignment = Boolean(ctx.assignmentDir) || Boolean(ctx.assignmentSlug) || Boolean(ctx.projectSlug);
29334
29335
  if (hasAssignment) return "assignment";
29335
29336
  if (ctx.bundleId) return "bundle";
29336
- if (ctx.sessionId) return "standalone";
29337
+ if (ctx.sessionId || ctx.transcriptPath) return "standalone";
29337
29338
  return "empty";
29338
29339
  }
29339
29340
  async function readAssignmentFrontmatterId(assignmentDir) {
@@ -31649,7 +31650,8 @@ async function extractClaudeSessionMeta(jsonlPath) {
31649
31650
  sessionId,
31650
31651
  cwd,
31651
31652
  startTs,
31652
- endTs
31653
+ endTs,
31654
+ path: jsonlPath
31653
31655
  };
31654
31656
  }
31655
31657
  async function extractCodexSessionMeta(jsonlPath) {
@@ -31694,7 +31696,8 @@ async function extractCodexSessionMeta(jsonlPath) {
31694
31696
  sessionId: id,
31695
31697
  cwd,
31696
31698
  startTs: timestamp,
31697
- endTs
31699
+ endTs,
31700
+ path: jsonlPath
31698
31701
  };
31699
31702
  } finally {
31700
31703
  await handle.close().catch(() => {
@@ -31721,7 +31724,7 @@ async function* walkClaudeProjects(opts = {}) {
31721
31724
  const sessionId = f.name.replace(/\.jsonl$/, "");
31722
31725
  const startTs = await readFirstTimestamp(filePath);
31723
31726
  const endTs = await readLastTimestamp(filePath);
31724
- meta = { tool: "claude", sessionId, cwd: cachedCwd, startTs, endTs };
31727
+ meta = { tool: "claude", sessionId, cwd: cachedCwd, startTs, endTs, path: filePath };
31725
31728
  } else {
31726
31729
  meta = await extractClaudeSessionMeta(filePath);
31727
31730
  if (meta) cachedCwd = meta.cwd;
@@ -32472,6 +32475,123 @@ init_timestamp();
32472
32475
  import { Command as Command10 } from "commander";
32473
32476
  import { readFile as readFile55, readdir as readdir28, stat as stat12 } from "fs/promises";
32474
32477
  import { resolve as resolve78 } from "path";
32478
+
32479
+ // src/utils/session-id.ts
32480
+ import { execFileSync as execFileSync3 } from "child_process";
32481
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync2, statSync as statSync3, writeFileSync as writeFileSync2 } from "fs";
32482
+ import { homedir as homedir13 } from "os";
32483
+ import { dirname as dirname24, join as join20 } from "path";
32484
+ var SESSION_ID_ENV_VARS = [
32485
+ "CLAUDE_CODE_SESSION_ID",
32486
+ "OPENCODE_SESSION_ID",
32487
+ "PI_SESSION_ID"
32488
+ ];
32489
+ var SAFE_SESSION_ID = /^[A-Za-z0-9_-]+$/;
32490
+ function isSafeSessionId(value) {
32491
+ return typeof value === "string" && value.length > 0 && value.length <= 256 && SAFE_SESSION_ID.test(value);
32492
+ }
32493
+ function defaultReadPpid(pid) {
32494
+ if (!Number.isFinite(pid) || pid <= 1) return null;
32495
+ try {
32496
+ const out = execFileSync3("ps", ["-o", "ppid=", "-p", String(pid)], {
32497
+ encoding: "utf8",
32498
+ stdio: ["ignore", "pipe", "ignore"]
32499
+ });
32500
+ const parent = Number.parseInt(out.trim(), 10);
32501
+ return Number.isInteger(parent) && parent > 0 ? parent : null;
32502
+ } catch {
32503
+ return null;
32504
+ }
32505
+ }
32506
+ function defaultStatMtimeMs(path) {
32507
+ try {
32508
+ return statSync3(path).mtimeMs;
32509
+ } catch {
32510
+ return null;
32511
+ }
32512
+ }
32513
+ function readRuntimeMarker(pid, dir) {
32514
+ if (!Number.isInteger(pid) || pid <= 0) return null;
32515
+ const path = join20(dir, `${pid}.json`);
32516
+ try {
32517
+ const parsed = JSON.parse(readFileSync2(path, "utf8"));
32518
+ if (parsed && typeof parsed === "object" && typeof parsed.sessionId === "string" && parsed.sessionId.length > 0) {
32519
+ return parsed;
32520
+ }
32521
+ return null;
32522
+ } catch {
32523
+ return null;
32524
+ }
32525
+ }
32526
+ async function resolveSideChannelSessionId(_opts, _deps) {
32527
+ return void 0;
32528
+ }
32529
+ function resolveFromAncestorMarkers(startPid, claudeSessionsDir, runtimeSessionsDir, readPpid, pidStartedAt, maxDepth) {
32530
+ let pid = startPid;
32531
+ for (let depth = 0; depth < maxDepth; depth += 1) {
32532
+ if (!Number.isInteger(pid) || pid <= 1) break;
32533
+ for (const dir of [claudeSessionsDir, runtimeSessionsDir]) {
32534
+ const marker = readRuntimeMarker(pid, dir);
32535
+ if (!marker) continue;
32536
+ if (marker.procStart) {
32537
+ const actual = pidStartedAt(pid);
32538
+ if (!actual || actual !== marker.procStart) continue;
32539
+ }
32540
+ if (isSafeSessionId(marker.sessionId)) return marker.sessionId;
32541
+ }
32542
+ const parent = readPpid(pid);
32543
+ if (parent === null) break;
32544
+ pid = parent;
32545
+ }
32546
+ return void 0;
32547
+ }
32548
+ async function resolveFromCwdScan(cwd, statMtimeMs) {
32549
+ const candidates = [];
32550
+ for await (const meta of walkClaudeProjects()) {
32551
+ if (meta.cwd === cwd && isSafeSessionId(meta.sessionId)) {
32552
+ candidates.push({ sessionId: meta.sessionId, mtime: statMtimeMs(meta.path) ?? 0 });
32553
+ }
32554
+ }
32555
+ for await (const meta of walkCodexSessions()) {
32556
+ if (meta.cwd === cwd && isSafeSessionId(meta.sessionId)) {
32557
+ candidates.push({ sessionId: meta.sessionId, mtime: statMtimeMs(meta.path) ?? 0 });
32558
+ }
32559
+ }
32560
+ if (candidates.length === 0) return void 0;
32561
+ candidates.sort((a, b) => b.mtime - a.mtime || (a.sessionId < b.sessionId ? 1 : a.sessionId > b.sessionId ? -1 : 0));
32562
+ return candidates[0].sessionId;
32563
+ }
32564
+ async function resolveOwnSessionId(opts = {}, deps = {}) {
32565
+ if (isSafeSessionId(opts.sessionId)) return opts.sessionId;
32566
+ const env = deps.env ?? process.env;
32567
+ for (const key of SESSION_ID_ENV_VARS) {
32568
+ const value = env[key];
32569
+ if (isSafeSessionId(value)) return value;
32570
+ }
32571
+ const sideChannel = await resolveSideChannelSessionId(opts, deps);
32572
+ if (sideChannel) return sideChannel;
32573
+ const home2 = deps.homeDir ?? homedir13();
32574
+ const claudeSessionsDir = deps.claudeSessionsDir ?? join20(home2, ".claude", "sessions");
32575
+ const runtimeSessionsDir = deps.runtimeSessionsDir ?? join20(home2, ".syntaur", "runtime", "sessions");
32576
+ const startPid = deps.startPid ?? process.ppid;
32577
+ const fromMarker = resolveFromAncestorMarkers(
32578
+ startPid,
32579
+ claudeSessionsDir,
32580
+ runtimeSessionsDir,
32581
+ deps.readPpid ?? defaultReadPpid,
32582
+ deps.pidStartedAt ?? captureProcessStartedAt,
32583
+ deps.maxDepth ?? 12
32584
+ );
32585
+ if (fromMarker) return fromMarker;
32586
+ if (opts.cwd) {
32587
+ const fromScan = await resolveFromCwdScan(opts.cwd, deps.statMtimeMs ?? defaultStatMtimeMs);
32588
+ if (fromScan) return fromScan;
32589
+ }
32590
+ if (isSafeSessionId(opts.legacyHint)) return opts.legacyHint;
32591
+ return void 0;
32592
+ }
32593
+
32594
+ // src/commands/session.ts
32475
32595
  async function readContext(cwd) {
32476
32596
  const path = resolve78(cwd, ".syntaur", "context.json");
32477
32597
  if (!await fileExists(path)) return null;
@@ -32607,7 +32727,11 @@ async function resolveSaveTarget(options, cwd) {
32607
32727
  assignmentDir = ctx.assignmentDir;
32608
32728
  slug = ctx.assignmentSlug ?? "";
32609
32729
  }
32610
- const sessionId = options.sessionId ?? ctx?.sessionId;
32730
+ const sessionId = await resolveOwnSessionId({
32731
+ sessionId: options.sessionId,
32732
+ cwd,
32733
+ legacyHint: ctx?.sessionId
32734
+ });
32611
32735
  if (!sessionId) {
32612
32736
  throw new Error(
32613
32737
  "Session not tracked. Pass --session-id <id>, or run `syntaur track-session ...` first so context.json carries a real session id."
@@ -32693,7 +32817,7 @@ sessionCommand.command("resume").description(
32693
32817
  process.exit(1);
32694
32818
  }
32695
32819
  });
32696
- sessionCommand.command("save").description("Write the session's continuity summary to sessions/<sessionId>/summary.md").option("--session-id <id>", "Session id (defaults to .syntaur/context.json sessionId)").option("--from-file <path>", "Read the summary body from a file (else stdin; else a skeleton)").option("--assignment <slug>", "Assignment slug (UUID for standalone). Defaults to .syntaur/context.json").option("--project <slug>", "Project slug. Required with --assignment for a project-nested assignment").action(async (options) => {
32820
+ sessionCommand.command("save").description("Write the session's continuity summary to sessions/<sessionId>/summary.md").option("--session-id <id>", "Session id (defaults to the resolved session: env / process tree, falling back to the .syntaur/context.json hint)").option("--from-file <path>", "Read the summary body from a file (else stdin; else a skeleton)").option("--assignment <slug>", "Assignment slug (UUID for standalone). Defaults to .syntaur/context.json").option("--project <slug>", "Project slug. Required with --assignment for a project-nested assignment").action(async (options) => {
32697
32821
  try {
32698
32822
  const path = await runSessionSave(options);
32699
32823
  console.log(`Saved session summary to ${path}`);
@@ -32702,6 +32826,18 @@ sessionCommand.command("save").description("Write the session's continuity summa
32702
32826
  process.exit(1);
32703
32827
  }
32704
32828
  });
32829
+ sessionCommand.command("resolve-id").description(
32830
+ "Print the caller's own real session id, resolved from the process (env / process tree / transcript). Exits 1 if none can be resolved. Deliberately does NOT read the context.json scalar \u2014 for hooks that must attribute the exact ending session."
32831
+ ).option("--cwd <path>", "Working directory for the transcript-scan fallback", process.cwd()).action(async (options) => {
32832
+ try {
32833
+ const id = await resolveOwnSessionId({ cwd: options.cwd ?? process.cwd() });
32834
+ if (!id) process.exit(1);
32835
+ console.log(id);
32836
+ } catch (error) {
32837
+ console.error("Error:", error instanceof Error ? error.message : String(error));
32838
+ process.exit(1);
32839
+ }
32840
+ });
32705
32841
 
32706
32842
  // src/commands/worktree.ts
32707
32843
  init_git_worktree();