sentinelayer-cli 0.30.1 → 0.30.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sentinelayer-cli",
3
- "version": "0.30.1",
3
+ "version": "0.30.2",
4
4
  "description": "Scaffold Sentinelayer spec/prompt/guide artifacts with secure browser auth and token bootstrap.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -42,7 +42,9 @@ import {
42
42
  import { listRuntimeRuns } from "../session/runtime-bridge.js";
43
43
  import {
44
44
  listFileLocks,
45
+ lockFile,
45
46
  releaseFileLocksForAgent,
47
+ unlockFile,
46
48
  } from "../session/file-locks.js";
47
49
  import {
48
50
  injectSessionGuides,
@@ -2014,30 +2016,6 @@ export function buildSessionLockDirective(verb, file, intent = "") {
2014
2016
  return normalizedIntent ? `lock: ${normalizedFile} - ${normalizedIntent}` : `lock: ${normalizedFile}`;
2015
2017
  }
2016
2018
 
2017
- // Posts a coordination directive (e.g. "lock: <file> - <intent>") as a session
2018
- // message so the session daemon processes it into the authoritative file-lock
2019
- // registry. Used by `session lock`/`unlock` as ergonomic sugar over the
2020
- // say-convention; locks are advisory + daemon-enforced with TTL auto-release,
2021
- // so this is a best-effort post.
2022
- async function postSessionDirectiveMessage(sessionId, message, {
2023
- agentId,
2024
- targetPath = process.cwd(),
2025
- } = {}) {
2026
- const clientMessageId = `cli-${randomUUID()}`;
2027
- const agent = await resolveSessionAgentEnvelope(sessionId, agentId, { targetPath });
2028
- await ensureSessionSayAgentRegistered(sessionId, agent, { targetPath });
2029
- const event = createAgentEvent({
2030
- event: "session_message",
2031
- agent,
2032
- sessionId,
2033
- payload: { message, channel: "session", clientMessageId },
2034
- });
2035
- event.eventId = clientMessageId;
2036
- event.idempotencyToken = clientMessageId;
2037
- const result = await syncSessionEventToApi(sessionId, event, { targetPath });
2038
- return { event, result };
2039
- }
2040
-
2041
2019
  async function runWithConcurrency(items = [], concurrency = 1, worker = async () => null) {
2042
2020
  const normalizedItems = Array.isArray(items) ? items : [];
2043
2021
  const normalizedConcurrency = Math.max(
@@ -3672,7 +3650,7 @@ export function registerSessionCommand(program) {
3672
3650
  return payload;
3673
3651
  });
3674
3652
 
3675
- const runLockDirectiveCommand = async (verb, sessionId, files, options, command) => {
3653
+ const runFileLockCommand = async (verb, sessionId, files, options, command) => {
3676
3654
  const normalizedSessionId = normalizeString(sessionId);
3677
3655
  if (!normalizedSessionId) {
3678
3656
  throw new Error("session id is required.");
@@ -3698,20 +3676,68 @@ export function registerSessionCommand(program) {
3698
3676
  );
3699
3677
  }
3700
3678
  const intent = normalizeString(options.intent);
3679
+ const results = [];
3701
3680
  const processed = [];
3681
+ const skipped = [];
3682
+ const failed = [];
3702
3683
  for (const file of fileList) {
3703
- const directive = buildSessionLockDirective(verb, file, intent);
3704
- await postSessionDirectiveMessage(normalizedSessionId, directive, {
3705
- agentId: identity.agentId,
3684
+ if (verb === "lock") {
3685
+ const result = await lockFile(normalizedSessionId, identity.agentId, file, {
3686
+ intent,
3687
+ targetPath,
3688
+ awaitRemoteSync: true,
3689
+ });
3690
+ const record = {
3691
+ file: result.file || file,
3692
+ locked: Boolean(result.locked),
3693
+ reason: result.reason || (result.locked ? "locked" : "held_by_other_agent"),
3694
+ heldBy: result.heldBy || null,
3695
+ since: result.since || null,
3696
+ };
3697
+ results.push(record);
3698
+ if (result.locked) {
3699
+ processed.push(record.file);
3700
+ } else {
3701
+ failed.push(record);
3702
+ }
3703
+ continue;
3704
+ }
3705
+
3706
+ const result = await unlockFile(normalizedSessionId, identity.agentId, file, {
3707
+ reason: intent || "manual_release",
3706
3708
  targetPath,
3709
+ awaitRemoteSync: true,
3707
3710
  });
3708
- processed.push(file);
3711
+ const record = {
3712
+ file: result.file || file,
3713
+ unlocked: Boolean(result.unlocked),
3714
+ reason: result.reason || (result.unlocked ? "unlocked" : "not_locked"),
3715
+ heldBy: result.heldBy || null,
3716
+ since: result.since || null,
3717
+ };
3718
+ results.push(record);
3719
+ if (result.unlocked) {
3720
+ processed.push(record.file);
3721
+ } else if (record.reason === "not_locked") {
3722
+ skipped.push(record);
3723
+ } else {
3724
+ failed.push(record);
3725
+ }
3726
+ }
3727
+
3728
+ if (failed.length > 0) {
3729
+ const summary = failed
3730
+ .map((item) => `${item.file}${item.heldBy ? ` held by ${item.heldBy}` : ""}`)
3731
+ .join(", ");
3732
+ throw new Error(`session ${verb} failed for ${summary}`);
3709
3733
  }
3710
3734
  const payload = {
3711
3735
  command: `session ${verb}`,
3712
3736
  sessionId: normalizedSessionId,
3713
3737
  agentId: identity.agentId,
3714
3738
  files: processed,
3739
+ results,
3740
+ skipped,
3715
3741
  };
3716
3742
  if (shouldEmitJson(options, command)) {
3717
3743
  console.log(JSON.stringify(payload, null, 2));
@@ -3735,7 +3761,7 @@ export function registerSessionCommand(program) {
3735
3761
  .option("--path <path>", "Workspace path for the session", ".")
3736
3762
  .option("--json", "Emit machine-readable output")
3737
3763
  .action(async (sessionId, files, options, command) => {
3738
- await runLockDirectiveCommand("lock", sessionId, files, options, command);
3764
+ await runFileLockCommand("lock", sessionId, files, options, command);
3739
3765
  });
3740
3766
 
3741
3767
  session
@@ -3746,7 +3772,7 @@ export function registerSessionCommand(program) {
3746
3772
  .option("--path <path>", "Workspace path for the session", ".")
3747
3773
  .option("--json", "Emit machine-readable output")
3748
3774
  .action(async (sessionId, files, options, command) => {
3749
- await runLockDirectiveCommand("unlock", sessionId, files, options, command);
3775
+ await runFileLockCommand("unlock", sessionId, files, options, command);
3750
3776
  });
3751
3777
 
3752
3778
  session
@@ -260,6 +260,8 @@ async function appendLockEvent(
260
260
  {
261
261
  targetPath = process.cwd(),
262
262
  nowIso = new Date().toISOString(),
263
+ syncRemote = true,
264
+ awaitRemoteSync = false,
263
265
  } = {}
264
266
  ) {
265
267
  return appendToStream(
@@ -273,6 +275,8 @@ async function appendLockEvent(
273
275
  }),
274
276
  {
275
277
  targetPath,
278
+ syncRemote,
279
+ awaitRemoteSync,
276
280
  }
277
281
  );
278
282
  }
@@ -371,6 +375,8 @@ export async function lockFile(
371
375
  ttlSeconds = DEFAULT_FILE_LOCK_TTL_SECONDS,
372
376
  targetPath = process.cwd(),
373
377
  nowIso = new Date().toISOString(),
378
+ syncRemote = true,
379
+ awaitRemoteSync = false,
374
380
  } = {}
375
381
  ) {
376
382
  const normalizedAgentId = normalizeAgentId(agentId);
@@ -437,6 +443,8 @@ export async function lockFile(
437
443
  {
438
444
  targetPath,
439
445
  nowIso: normalizedNow,
446
+ syncRemote,
447
+ awaitRemoteSync,
440
448
  }
441
449
  );
442
450
  return {
@@ -462,6 +470,8 @@ export async function unlockFile(
462
470
  targetPath = process.cwd(),
463
471
  nowIso = new Date().toISOString(),
464
472
  actorAgentId = null,
473
+ syncRemote = true,
474
+ awaitRemoteSync = false,
465
475
  } = {}
466
476
  ) {
467
477
  const normalizedAgentId = normalizeAgentId(agentId);
@@ -527,6 +537,8 @@ export async function unlockFile(
527
537
  {
528
538
  targetPath,
529
539
  nowIso: normalizedNow,
540
+ syncRemote,
541
+ awaitRemoteSync,
530
542
  }
531
543
  );
532
544