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.
- package/dist/index.js +146 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/platforms/SESSION-ID-RESOLUTION.md +117 -0
- package/platforms/claude-code/agents/syntaur-expert.md +1 -1
- package/platforms/claude-code/commands/track-session/track-session.md +5 -4
- package/platforms/claude-code/hooks/hooks.json +1 -1
- package/platforms/claude-code/hooks/session-cleanup.sh +16 -8
- package/platforms/claude-code/skills/clear-assignment/SKILL.md +1 -1
- package/platforms/claude-code/skills/complete-assignment/SKILL.md +1 -1
- package/platforms/claude-code/skills/grab-assignment/SKILL.md +5 -4
- package/platforms/claude-code/skills/save-session-summary/SKILL.md +15 -6
- package/platforms/claude-code/skills/track-session/SKILL.md +5 -4
- package/platforms/codex/agents/syntaur-operator.md +1 -1
- package/platforms/codex/commands/save-session-summary.md +1 -1
- package/platforms/codex/scripts/session-cleanup.sh +63 -6
- package/platforms/codex/skills/clear-assignment/SKILL.md +1 -1
- package/platforms/codex/skills/complete-assignment/SKILL.md +1 -1
- package/platforms/codex/skills/grab-assignment/SKILL.md +5 -4
- package/platforms/codex/skills/save-session-summary/SKILL.md +15 -6
- package/platforms/codex/skills/track-session/SKILL.md +5 -4
- package/platforms/cursor/hooks/README.md +49 -0
- package/platforms/hermes/plugins/syntaur/__pycache__/__init__.cpython-312.pyc +0 -0
- package/platforms/hermes/plugins/syntaur/__pycache__/boundary.cpython-312.pyc +0 -0
- package/platforms/opencode/plugin/syntaur-session-env.js +30 -0
- package/platforms/pi/README.md +50 -0
- package/skills/clear-assignment/SKILL.md +1 -1
- package/skills/complete-assignment/SKILL.md +1 -1
- package/skills/grab-assignment/SKILL.md +5 -4
- package/skills/save-session-summary/SKILL.md +15 -6
- 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
|
|
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:
|
|
26006
|
-
await mkdir12(
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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();
|