sentinelayer-cli 0.30.0 → 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.0",
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
@@ -10,6 +10,7 @@
10
10
  import fsp from "node:fs/promises";
11
11
  import path from "node:path";
12
12
  import crypto from "node:crypto";
13
+ import { spawnSync } from "node:child_process";
13
14
 
14
15
  import { recordProvisionedIdentity } from "../ai/identity-store.js";
15
16
  import { runDevTestBotSession } from "../agents/devtestbot/tool.js";
@@ -67,6 +68,26 @@ function parsePlannerJson(text) {
67
68
  }
68
69
  }
69
70
 
71
+ // Derive a stable repo key for the managed-proxy DD entitlement gate (#94651):
72
+ // owner/repo from the git remote when present, else a non-identifying local key
73
+ // (hash of the absolute repo root — no raw path leak). Fail-safe to the local key.
74
+ function deriveRepoKey(rootPath) {
75
+ try {
76
+ const remote = spawnSync("git", ["-C", rootPath || ".", "remote", "get-url", "origin"], {
77
+ encoding: "utf-8",
78
+ });
79
+ if (remote.status === 0) {
80
+ const url = String(remote.stdout || "").trim();
81
+ const match = url.match(/[:/]([^/:]+\/[^/]+?)(?:\.git)?$/);
82
+ if (match && match[1]) return match[1].toLowerCase();
83
+ }
84
+ } catch {
85
+ // fall through to the local key
86
+ }
87
+ const abs = path.resolve(rootPath || ".");
88
+ return `local/${crypto.createHash("sha256").update(abs).digest("hex").slice(0, 12)}`;
89
+ }
90
+
70
91
  function buildPlannerPrompt({ rootPath, files = [], findings = [], budget = {} }) {
71
92
  const severityCounts = {};
72
93
  for (const finding of findings || []) {
@@ -86,9 +107,22 @@ function buildPlannerPrompt({ rootPath, files = [], findings = [], budget = {} }
86
107
  ].join("\n");
87
108
  }
88
109
 
89
- async function callPlannerClient({ plannerClient, rootPath, files, findings, budget, sessionUsage }) {
110
+ export async function callPlannerClient({ plannerClient, rootPath, files, findings, budget, sessionUsage }) {
90
111
  if (!plannerClient) return { planned: {}, usageLedger: null };
91
112
  const prompt = buildPlannerPrompt({ rootPath, files, findings, budget });
113
+ // DD entitlement gate metadata (#94651): only label proxy calls when there
114
+ // is a real session usage anchor. API-side DD actions are fail-closed and
115
+ // reject incomplete session context before provider spend.
116
+ const sessionId = String(sessionUsage?.sessionId || "").trim();
117
+ const ddProxyFields = sessionId
118
+ ? {
119
+ action: INVESTOR_DD_USAGE_ACTIONS.devTestBotPlanner,
120
+ sessionId,
121
+ agentId: "investor-dd-devtestbot-planner",
122
+ usageIdempotencyKey: `investor-dd-devtestbot-${crypto.randomUUID()}`,
123
+ metadata: { repoKey: deriveRepoKey(rootPath), phase: "devtestbot" },
124
+ }
125
+ : {};
92
126
  if (typeof plannerClient.decideDevTestBotPhase === "function") {
93
127
  return {
94
128
  planned: await plannerClient.decideDevTestBotPhase({ rootPath, files, findings, budget, prompt }),
@@ -105,7 +139,7 @@ async function callPlannerClient({ plannerClient, rootPath, files, findings, bud
105
139
  }
106
140
  const startedAtIso = new Date().toISOString();
107
141
  if (typeof plannerClient.invoke === "function") {
108
- const response = await plannerClient.invoke({ prompt, stream: false });
142
+ const response = await plannerClient.invoke({ prompt, stream: false, ...ddProxyFields });
109
143
  const usageLedger = sessionUsage
110
144
  ? await recordInvestorDdLlmUsage({
111
145
  usageContext: sessionUsage,
@@ -128,6 +162,7 @@ async function callPlannerClient({ plannerClient, rootPath, files, findings, bud
128
162
  if (typeof plannerClient.generatePlan === "function") {
129
163
  const response = await plannerClient.generatePlan([{ role: "user", content: prompt }], {
130
164
  phase: "devtestbot",
165
+ ...ddProxyFields,
131
166
  });
132
167
  const usageLedger = sessionUsage
133
168
  ? await recordInvestorDdLlmUsage({
@@ -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