sentinelayer-cli 0.16.0 → 0.17.1
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/README.md +16 -6
- package/package.json +3 -2
- package/src/commands/legacy-args.js +1 -0
- package/src/commands/omargate.js +1 -0
- package/src/commands/session.js +322 -25
- package/src/events/schema.js +21 -0
- package/src/legacy-cli.js +16 -0
- package/src/review/investor-dd-devtestbot.js +83 -8
- package/src/review/investor-dd-file-loop.js +83 -6
- package/src/review/investor-dd-orchestrator.js +42 -1
- package/src/review/investor-dd-progress.js +351 -0
- package/src/review/investor-dd-usage.js +227 -0
- package/src/session/daemon.js +341 -2
- package/src/session/recap.js +288 -69
- package/src/session/sync.js +1 -4
package/README.md
CHANGED
|
@@ -111,6 +111,9 @@ Sentinelayer includes a deterministic session coordination surface for multi-age
|
|
|
111
111
|
|
|
112
112
|
Read the full guide: [docs/sessions.md](docs/sessions.md)
|
|
113
113
|
|
|
114
|
+
For new engineers or agents joining an active SentinelLayer loop, start with
|
|
115
|
+
[docs/ENGINEERING_ONBOARDING.md](docs/ENGINEERING_ONBOARDING.md).
|
|
116
|
+
|
|
114
117
|
For strategy context, see the long-form blog draft: [docs/blog/slack-for-ai-coding-agents.md](docs/blog/slack-for-ai-coding-agents.md)
|
|
115
118
|
|
|
116
119
|
## Advanced options
|
|
@@ -914,13 +917,20 @@ Prerequisites:
|
|
|
914
917
|
- repository secret `NPM_TOKEN` with publish access, or
|
|
915
918
|
- npm trusted publishing for this repository/tag workflow
|
|
916
919
|
|
|
917
|
-
Release
|
|
920
|
+
Release flow:
|
|
921
|
+
|
|
922
|
+
1. Merge to `main` and let `Release Please` open/update the release PR.
|
|
923
|
+
2. Merge the release PR after Omar, Quality Gates, and Attestation are green.
|
|
924
|
+
3. From a clean checkout of the release commit, run the guarded release command:
|
|
925
|
+
```bash
|
|
926
|
+
git fetch origin main --tags
|
|
927
|
+
git checkout main && git pull --ff-only
|
|
928
|
+
npm run release:publish -- --tag v0.1.1 --notes-file release-notes.md
|
|
929
|
+
```
|
|
930
|
+
4. The guarded command creates the tag with `git tag -s`, verifies it with `git tag -v`, pushes it, confirms the remote GitHub tag object is annotated and cryptographically verified by an allowlisted signer, then creates the GitHub release with `gh release create --verify-tag`.
|
|
931
|
+
5. The tag push triggers `Release`, which re-verifies upstream checks, release artifact attestations, rollback readiness, protected `package-release` approval, and npm trusted publishing OIDC before publishing.
|
|
918
932
|
|
|
919
|
-
|
|
920
|
-
2. Push a tag like `v0.1.1` to publish automatically (or via release-please tag creation).
|
|
921
|
-
3. Run `Release` manually (`workflow_dispatch`) to validate gates and rollback readiness without publishing.
|
|
922
|
-
4. Tag-triggered publish resolves auth mode at runtime (`NPM_TOKEN` first, otherwise trusted publishing OIDC).
|
|
923
|
-
5. If neither auth mode is available, publish fails closed with an explicit workflow error.
|
|
933
|
+
Do not run `gh release create v0.1.1` before pushing the signed tag. GitHub release creation creates a lightweight tag when the tag does not already exist, and the Release workflow intentionally fails closed for lightweight tags. If a remote tag already exists as a lightweight tag, `npm run release:publish` refuses to create the GitHub release.
|
|
924
934
|
|
|
925
935
|
Release publish now enforces tarball checksum-manifest validation and attestation verification bound to `.github/workflows/release.yml` before `npm publish`.
|
|
926
936
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sentinelayer-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.1",
|
|
4
4
|
"description": "Scaffold Sentinelayer spec/prompt/guide artifacts with secure browser auth and token bootstrap.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
"test:unit": "node --import ./tests/setup-env.mjs --test tests/unit*.test.mjs",
|
|
12
12
|
"test:e2e": "node --import ./tests/setup-env.mjs --test tests/e2e.test.mjs",
|
|
13
13
|
"test:coverage": "c8 node --import ./tests/setup-env.mjs --test tests/unit*.test.mjs",
|
|
14
|
-
"verify": "npm run check && npm run docs:build && npm run test:e2e && npm run test:coverage && npm pack --dry-run"
|
|
14
|
+
"verify": "npm run check && npm run docs:build && npm run test:e2e && npm run test:coverage && npm pack --dry-run",
|
|
15
|
+
"release:publish": "node scripts/release-publish.mjs"
|
|
15
16
|
},
|
|
16
17
|
"bin": {
|
|
17
18
|
"sentinelayer-cli": "bin/sentinelayer-cli.js",
|
|
@@ -61,6 +61,7 @@ export function buildLegacyArgs(baseArgs, { commandOptions = {}, command } = {})
|
|
|
61
61
|
appendPassthroughFlag(args, "--notify-email", commandOptions.notifyEmail);
|
|
62
62
|
appendPassthroughFlag(args, "--email-on-complete", commandOptions.emailOnComplete);
|
|
63
63
|
appendPassthroughFlag(args, "--notify-session", commandOptions.notifySession);
|
|
64
|
+
appendBooleanFlag(args, "--require-usage-ledger", commandOptions.requireUsageLedger);
|
|
64
65
|
appendPassthroughFlag(args, "--devtestbot-base-url", commandOptions.devtestbotBaseUrl);
|
|
65
66
|
appendPassthroughFlag(args, "--devtestbot-scope", commandOptions.devtestbotScope);
|
|
66
67
|
appendNegatedBooleanFlag(args, "--no-devtestbot", commandOptions.devtestbot);
|
package/src/commands/omargate.js
CHANGED
|
@@ -50,6 +50,7 @@ export function registerOmarGateCommand(program, invokeLegacy) {
|
|
|
50
50
|
.option("--notify-email <addr>", "Send final report to this email (default: account email)")
|
|
51
51
|
.option("--email-on-complete <addr>", "Trigger the API-side DD report email after the run completes")
|
|
52
52
|
.option("--notify-session <session-id>", "Stream progress into this Senti session (default: auto-start)")
|
|
53
|
+
.option("--require-usage-ledger", "Fail if Investor-DD LLM planner calls cannot write session_usage")
|
|
53
54
|
.option("--no-email", "Skip email dispatch")
|
|
54
55
|
.option("--no-dashboard", "Skip dashboard card persistence")
|
|
55
56
|
.option("--devtestbot-base-url <url>", "Approved absolute URL for devTestBot browser lanes")
|
package/src/commands/session.js
CHANGED
|
@@ -43,6 +43,7 @@ import { listSessionTasks } from "../session/tasks.js";
|
|
|
43
43
|
import {
|
|
44
44
|
createSession,
|
|
45
45
|
DEFAULT_TTL_SECONDS,
|
|
46
|
+
expireSession,
|
|
46
47
|
getSession,
|
|
47
48
|
isSessionCacheExpired,
|
|
48
49
|
listActiveSessions,
|
|
@@ -86,7 +87,7 @@ import {
|
|
|
86
87
|
generateSessionCheckpoint,
|
|
87
88
|
listSessionCheckpoints,
|
|
88
89
|
} from "../session/checkpoints.js";
|
|
89
|
-
import { authLoginHint } from "../ui/command-hints.js";
|
|
90
|
+
import { authLoginHint, preferredCliCommand } from "../ui/command-hints.js";
|
|
90
91
|
import { parseCsvTokens } from "./ai/shared.js";
|
|
91
92
|
|
|
92
93
|
function shouldEmitJson(options, command) {
|
|
@@ -425,6 +426,63 @@ function sentiAutostartDisabled() {
|
|
|
425
426
|
return String(process.env.SENTINELAYER_SKIP_SENTI_AUTOSTART || "").trim() === "1";
|
|
426
427
|
}
|
|
427
428
|
|
|
429
|
+
function buildResumeContext(candidate, { reuseWindowSeconds = 3600 } = {}) {
|
|
430
|
+
if (!candidate) return null;
|
|
431
|
+
const source = normalizeString(candidate._source) || "unknown";
|
|
432
|
+
const lastActivityAt =
|
|
433
|
+
normalizeString(candidate.lastInteractionAt) ||
|
|
434
|
+
normalizeString(candidate.lastActivityAt) ||
|
|
435
|
+
normalizeString(candidate.updatedAt) ||
|
|
436
|
+
normalizeString(candidate.createdAt) ||
|
|
437
|
+
null;
|
|
438
|
+
return {
|
|
439
|
+
source,
|
|
440
|
+
reuseWindowSeconds,
|
|
441
|
+
lastActivityAt,
|
|
442
|
+
reason: `recent_${source}_session`,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async function resolveSessionRemoteSyncState({ dashboardUrl } = {}) {
|
|
447
|
+
if (remoteSessionLookupDisabled()) {
|
|
448
|
+
return {
|
|
449
|
+
status: "disabled",
|
|
450
|
+
attempted: false,
|
|
451
|
+
reason: "remote_sync_disabled",
|
|
452
|
+
dashboardUrl,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
let storedSession = null;
|
|
457
|
+
try {
|
|
458
|
+
storedSession = await readStoredSession();
|
|
459
|
+
} catch {
|
|
460
|
+
storedSession = null;
|
|
461
|
+
}
|
|
462
|
+
const apiUrl =
|
|
463
|
+
normalizeString(storedSession?.apiUrl) ||
|
|
464
|
+
normalizeString(process.env.SENTINELAYER_API_URL) ||
|
|
465
|
+
"https://api.sentinelayer.com";
|
|
466
|
+
const hasToken = Boolean(
|
|
467
|
+
normalizeString(storedSession?.token) || normalizeString(process.env.SENTINELAYER_TOKEN),
|
|
468
|
+
);
|
|
469
|
+
if (!hasToken) {
|
|
470
|
+
return {
|
|
471
|
+
status: "auth_required",
|
|
472
|
+
attempted: false,
|
|
473
|
+
reason: "not_authenticated",
|
|
474
|
+
apiUrl,
|
|
475
|
+
dashboardUrl,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
return {
|
|
479
|
+
status: "background_sync_queued",
|
|
480
|
+
attempted: true,
|
|
481
|
+
apiUrl,
|
|
482
|
+
dashboardUrl,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
428
486
|
function mergeResumeCandidate(existing, incoming) {
|
|
429
487
|
if (!existing) return incoming;
|
|
430
488
|
const existingActivity = Number(existing._activityMs || 0);
|
|
@@ -614,6 +672,69 @@ async function verifyRemoteSession(sessionId, { targetPath } = {}) {
|
|
|
614
672
|
return { ok: false, reason: lastReason };
|
|
615
673
|
}
|
|
616
674
|
|
|
675
|
+
function normalizeRemoteResumeStatus(session = {}) {
|
|
676
|
+
return normalizeString(session?.archiveStatus || session?.status).toLowerCase();
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function remoteStatusAllowsResume(status) {
|
|
680
|
+
if (!status) return true;
|
|
681
|
+
return status === "active" || status === "pending";
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
async function reconcileLocalResumeCandidate(candidate, { targetPath } = {}) {
|
|
685
|
+
if (!candidate || candidate._source !== "local" || remoteSessionLookupDisabled()) {
|
|
686
|
+
return { candidate, staleResume: null };
|
|
687
|
+
}
|
|
688
|
+
const verification = await verifyRemoteSession(candidate.sessionId, { targetPath }).catch((error) => ({
|
|
689
|
+
ok: false,
|
|
690
|
+
reason: normalizeString(error?.message) || "probe_failed",
|
|
691
|
+
}));
|
|
692
|
+
|
|
693
|
+
if (verification.ok) {
|
|
694
|
+
const remoteStatus = normalizeRemoteResumeStatus(verification.session);
|
|
695
|
+
if (remoteStatusAllowsResume(remoteStatus)) {
|
|
696
|
+
return { candidate, staleResume: null };
|
|
697
|
+
}
|
|
698
|
+
await expireSession(candidate.sessionId, { targetPath }).catch(() => null);
|
|
699
|
+
return {
|
|
700
|
+
candidate: null,
|
|
701
|
+
staleResume: {
|
|
702
|
+
sessionId: candidate.sessionId,
|
|
703
|
+
source: "remote",
|
|
704
|
+
reason: "remote_not_active",
|
|
705
|
+
remoteStatus,
|
|
706
|
+
action: "expired_local_and_created_new",
|
|
707
|
+
},
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
if (verification.reason === "not_found" || verification.reason === "forbidden") {
|
|
712
|
+
await expireSession(candidate.sessionId, { targetPath }).catch(() => null);
|
|
713
|
+
return {
|
|
714
|
+
candidate: null,
|
|
715
|
+
staleResume: {
|
|
716
|
+
sessionId: candidate.sessionId,
|
|
717
|
+
source: "remote",
|
|
718
|
+
reason: verification.reason,
|
|
719
|
+
remoteStatus: verification.reason,
|
|
720
|
+
status: verification.status || null,
|
|
721
|
+
action: "expired_local_and_created_new",
|
|
722
|
+
},
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
return {
|
|
727
|
+
candidate,
|
|
728
|
+
staleResume: {
|
|
729
|
+
sessionId: candidate.sessionId,
|
|
730
|
+
source: "remote",
|
|
731
|
+
reason: verification.reason || "probe_failed",
|
|
732
|
+
status: verification.status || null,
|
|
733
|
+
action: "kept_local_resume",
|
|
734
|
+
},
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
|
|
617
738
|
// Render an absolute ISO timestamp as a coarse "Nm ago" / "Nh ago" / "Nd ago"
|
|
618
739
|
// label for human-readable join output. Returns `"never"` for missing input
|
|
619
740
|
// and `"just now"` for sub-minute deltas.
|
|
@@ -737,12 +858,15 @@ async function ensureWorkspaceSession({
|
|
|
737
858
|
const titleArg = normalizeString(title);
|
|
738
859
|
const fallbackTitle = deriveSessionTitle(targetPath);
|
|
739
860
|
const startedAt = Date.now();
|
|
740
|
-
|
|
861
|
+
let resumedCandidate = await findReusableSessionCandidate({
|
|
741
862
|
targetPath,
|
|
742
863
|
reuseWindowSeconds,
|
|
743
864
|
resume,
|
|
744
865
|
forceNew,
|
|
745
866
|
});
|
|
867
|
+
const reconciliation = await reconcileLocalResumeCandidate(resumedCandidate, { targetPath });
|
|
868
|
+
resumedCandidate = reconciliation.candidate;
|
|
869
|
+
const staleResume = reconciliation.staleResume;
|
|
746
870
|
let created;
|
|
747
871
|
const resumeTitle =
|
|
748
872
|
titleArg || normalizeString(resumedCandidate?.title) || fallbackTitle;
|
|
@@ -825,6 +949,7 @@ async function ensureWorkspaceSession({
|
|
|
825
949
|
title: effectiveTitle || null,
|
|
826
950
|
titleAuto,
|
|
827
951
|
titleSync,
|
|
952
|
+
staleResume,
|
|
828
953
|
};
|
|
829
954
|
}
|
|
830
955
|
|
|
@@ -848,6 +973,42 @@ async function defaultAgentId(value, _targetPath) {
|
|
|
848
973
|
return resolveSessionSayAgentId(value);
|
|
849
974
|
}
|
|
850
975
|
|
|
976
|
+
async function resolveSessionAgentEnvelope(
|
|
977
|
+
sessionId,
|
|
978
|
+
agentId,
|
|
979
|
+
{
|
|
980
|
+
targetPath = process.cwd(),
|
|
981
|
+
model = "",
|
|
982
|
+
role = "",
|
|
983
|
+
displayName = "",
|
|
984
|
+
clientKind = "cli",
|
|
985
|
+
} = {}
|
|
986
|
+
) {
|
|
987
|
+
const normalizedAgentId = normalizeAgentId(agentId, "cli-user");
|
|
988
|
+
let registeredAgent = null;
|
|
989
|
+
try {
|
|
990
|
+
const agents = await listAgents(sessionId, { targetPath, includeInactive: true });
|
|
991
|
+
registeredAgent = agents.find(
|
|
992
|
+
(agent) => normalizeString(agent.agentId).toLowerCase() === normalizedAgentId.toLowerCase(),
|
|
993
|
+
);
|
|
994
|
+
} catch {
|
|
995
|
+
registeredAgent = null;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
const resolvedModel = normalizeString(model) || normalizeString(registeredAgent?.model);
|
|
999
|
+
const resolvedRole = normalizeString(role) || normalizeString(registeredAgent?.role);
|
|
1000
|
+
const resolvedDisplayName =
|
|
1001
|
+
normalizeString(displayName) || normalizeString(registeredAgent?.displayName);
|
|
1002
|
+
const envelope = {
|
|
1003
|
+
id: normalizedAgentId,
|
|
1004
|
+
model: resolvedModel || undefined,
|
|
1005
|
+
role: resolvedRole || undefined,
|
|
1006
|
+
displayName: resolvedDisplayName || undefined,
|
|
1007
|
+
clientKind: normalizeString(clientKind) || undefined,
|
|
1008
|
+
};
|
|
1009
|
+
return Object.fromEntries(Object.entries(envelope).filter(([, value]) => value !== undefined));
|
|
1010
|
+
}
|
|
1011
|
+
|
|
851
1012
|
async function runWithConcurrency(items = [], concurrency = 1, worker = async () => null) {
|
|
852
1013
|
const normalizedItems = Array.isArray(items) ? items : [];
|
|
853
1014
|
const normalizedConcurrency = Math.max(
|
|
@@ -913,13 +1074,93 @@ function checkpointSequenceRange(checkpoint = {}) {
|
|
|
913
1074
|
return "anchor pending";
|
|
914
1075
|
}
|
|
915
1076
|
|
|
916
|
-
function
|
|
1077
|
+
function checkpointGradeLabel(checkpoint = {}) {
|
|
1078
|
+
const grade = normalizeString(checkpoint.grade || checkpoint.gradeLetter || checkpoint.grade_letter).toUpperCase();
|
|
1079
|
+
if (!["A", "B", "C", "D", "F"].includes(grade)) {
|
|
1080
|
+
return "";
|
|
1081
|
+
}
|
|
1082
|
+
const score = Number(checkpoint.gradeScore ?? checkpoint.grade_score);
|
|
1083
|
+
const scoreLabel = Number.isFinite(score) ? ` ${Math.max(0, Math.min(100, Math.floor(score)))}/100` : "";
|
|
1084
|
+
const rawReasons = Array.isArray(checkpoint.gradeReasons)
|
|
1085
|
+
? checkpoint.gradeReasons
|
|
1086
|
+
: Array.isArray(checkpoint.grade_reasons)
|
|
1087
|
+
? checkpoint.grade_reasons
|
|
1088
|
+
: [];
|
|
1089
|
+
const reasons = rawReasons
|
|
1090
|
+
.map((reason) => normalizeString(reason?.message || reason?.code || reason))
|
|
1091
|
+
.filter(Boolean)
|
|
1092
|
+
.slice(0, 2);
|
|
1093
|
+
const reasonLabel = reasons.length ? `: ${reasons.join("; ")}` : "";
|
|
1094
|
+
return ` completeness ${grade}${scoreLabel}${reasonLabel}`;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
function checkpointSummarySections(checkpoint = {}) {
|
|
1098
|
+
const sections = checkpoint.summarySections || checkpoint.summary_sections;
|
|
1099
|
+
return sections && typeof sections === "object" && !Array.isArray(sections) ? sections : null;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
function normalizeCheckpointTextItems(value, limit = 3) {
|
|
1103
|
+
if (!Array.isArray(value)) {
|
|
1104
|
+
return [];
|
|
1105
|
+
}
|
|
1106
|
+
return value
|
|
1107
|
+
.map((item) => normalizeString(item))
|
|
1108
|
+
.filter(Boolean)
|
|
1109
|
+
.slice(0, limit);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
function formatCheckpointSectionPreview(checkpoint = {}) {
|
|
1113
|
+
const sections = checkpointSummarySections(checkpoint);
|
|
1114
|
+
if (!sections) {
|
|
1115
|
+
return "";
|
|
1116
|
+
}
|
|
1117
|
+
const parts = [];
|
|
1118
|
+
const work = normalizeCheckpointTextItems(sections.workCompleted || sections.work_completed, 1)[0];
|
|
1119
|
+
if (work) {
|
|
1120
|
+
parts.push(`work: ${work}`);
|
|
1121
|
+
}
|
|
1122
|
+
const agents = Array.isArray(sections.agentContributions)
|
|
1123
|
+
? sections.agentContributions
|
|
1124
|
+
: Array.isArray(sections.agent_contributions)
|
|
1125
|
+
? sections.agent_contributions
|
|
1126
|
+
: [];
|
|
1127
|
+
const agentLabels = agents
|
|
1128
|
+
.map((item) => {
|
|
1129
|
+
const agentId = normalizeString(item?.agentId || item?.agent_id);
|
|
1130
|
+
const summary = normalizeString(item?.summary);
|
|
1131
|
+
return agentId && summary ? `${agentId}: ${summary}` : "";
|
|
1132
|
+
})
|
|
1133
|
+
.filter(Boolean)
|
|
1134
|
+
.slice(0, 2);
|
|
1135
|
+
if (agentLabels.length) {
|
|
1136
|
+
parts.push(`agents: ${agentLabels.join("; ")}`);
|
|
1137
|
+
}
|
|
1138
|
+
const evidence = Array.isArray(sections.evidence) ? sections.evidence : [];
|
|
1139
|
+
const evidenceLabels = evidence
|
|
1140
|
+
.map((item) => normalizeString(item?.label || item?.value))
|
|
1141
|
+
.filter(Boolean)
|
|
1142
|
+
.slice(0, 3);
|
|
1143
|
+
if (evidenceLabels.length) {
|
|
1144
|
+
parts.push(`evidence: ${evidenceLabels.join(", ")}`);
|
|
1145
|
+
}
|
|
1146
|
+
const risks = normalizeCheckpointTextItems(sections.risks, 1);
|
|
1147
|
+
if (risks.length) {
|
|
1148
|
+
parts.push(`risks: ${risks.join("; ")}`);
|
|
1149
|
+
}
|
|
1150
|
+
const nextSteps = normalizeCheckpointTextItems(sections.nextSteps || sections.next_steps, 1);
|
|
1151
|
+
if (nextSteps.length) {
|
|
1152
|
+
parts.push(`next: ${nextSteps.join("; ")}`);
|
|
1153
|
+
}
|
|
1154
|
+
return parts.length ? ` | ${parts.join(" | ")}` : "";
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
export function formatCheckpointLine(checkpoint = {}) {
|
|
917
1158
|
const id = normalizeString(checkpoint.checkpointId) || "checkpoint";
|
|
918
1159
|
const kind = normalizeString(checkpoint.kind) || "summary";
|
|
919
1160
|
const title = normalizeString(checkpoint.title) || "Untitled checkpoint";
|
|
920
1161
|
const byline = normalizeString(checkpoint.createdByAgentId || checkpoint.createdBy);
|
|
921
1162
|
const by = byline ? ` by ${byline}` : "";
|
|
922
|
-
return `${checkpointSequenceRange(checkpoint)} ${id} [${kind}] ${title}${by}`;
|
|
1163
|
+
return `${checkpointSequenceRange(checkpoint)} ${id} [${kind}] ${title}${by}${checkpointGradeLabel(checkpoint)}${formatCheckpointSectionPreview(checkpoint)}`;
|
|
923
1164
|
}
|
|
924
1165
|
|
|
925
1166
|
async function readCheckpointSummaryOption(options = {}, { targetPath } = {}) {
|
|
@@ -1224,6 +1465,9 @@ export function registerSessionCommand(program) {
|
|
|
1224
1465
|
const launchPlan = template ? buildTemplateLaunchPlan(created.sessionId, template) : [];
|
|
1225
1466
|
const dashboardUrl = buildDashboardUrl(created.sessionId);
|
|
1226
1467
|
const effectiveTitle = ensured.title;
|
|
1468
|
+
const cliCommand = preferredCliCommand();
|
|
1469
|
+
const resumeContext = buildResumeContext(ensured.resumedCandidate, { reuseWindowSeconds });
|
|
1470
|
+
const remoteSync = await resolveSessionRemoteSyncState({ dashboardUrl });
|
|
1227
1471
|
|
|
1228
1472
|
const payload = {
|
|
1229
1473
|
command: "session start",
|
|
@@ -1245,23 +1489,29 @@ export function registerSessionCommand(program) {
|
|
|
1245
1489
|
launchPlan,
|
|
1246
1490
|
dashboardUrl,
|
|
1247
1491
|
resumed,
|
|
1492
|
+
resumeSource: resumeContext?.source || null,
|
|
1493
|
+
resumeContext: resumeContext || undefined,
|
|
1494
|
+
staleResume: ensured.staleResume || undefined,
|
|
1495
|
+
remoteSync,
|
|
1248
1496
|
title: effectiveTitle || null,
|
|
1249
1497
|
titleAuto: Boolean(ensured.titleAuto),
|
|
1250
1498
|
titleSync: ensured.titleSync || undefined,
|
|
1251
1499
|
};
|
|
1252
1500
|
|
|
1253
1501
|
// Best-effort admin visibility sync. Session creation remains local-first.
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1502
|
+
if (remoteSync.attempted) {
|
|
1503
|
+
void syncSessionMetadataToApi(created.sessionId, {
|
|
1504
|
+
targetPath,
|
|
1505
|
+
sessionId: created.sessionId,
|
|
1506
|
+
status: created.status,
|
|
1507
|
+
createdAt: created.createdAt,
|
|
1508
|
+
expiresAt: created.expiresAt,
|
|
1509
|
+
title: effectiveTitle || null,
|
|
1510
|
+
ttlSeconds,
|
|
1511
|
+
template: created.template,
|
|
1512
|
+
codebaseContext: created.codebaseContext,
|
|
1513
|
+
}).catch(() => {});
|
|
1514
|
+
}
|
|
1265
1515
|
|
|
1266
1516
|
// Auto-start the Senti orchestrator daemon. Without this, every
|
|
1267
1517
|
// session ran with `Senti actions: 1` (just the welcome alert)
|
|
@@ -1303,15 +1553,38 @@ export function registerSessionCommand(program) {
|
|
|
1303
1553
|
console.log(pc.bold(resumed ? "Session resumed" : "Session created"));
|
|
1304
1554
|
console.log(pc.gray(`Session: ${created.sessionId}`));
|
|
1305
1555
|
if (titleArg) console.log(pc.gray(`Title: ${titleArg}`));
|
|
1556
|
+
if (resumeContext) {
|
|
1557
|
+
console.log(
|
|
1558
|
+
pc.gray(
|
|
1559
|
+
`Resume source: ${resumeContext.source} (last activity ${resumeContext.lastActivityAt || "unknown"}; window ${reuseWindowSeconds}s)`,
|
|
1560
|
+
),
|
|
1561
|
+
);
|
|
1562
|
+
}
|
|
1563
|
+
if (ensured.staleResume?.action === "expired_local_and_created_new") {
|
|
1564
|
+
console.log(
|
|
1565
|
+
pc.yellow(
|
|
1566
|
+
`Skipped stale local session ${ensured.staleResume.sessionId}: remote status is ${ensured.staleResume.remoteStatus || ensured.staleResume.reason}.`,
|
|
1567
|
+
),
|
|
1568
|
+
);
|
|
1569
|
+
}
|
|
1306
1570
|
if (created.streamPath) console.log(pc.gray(`Stream: ${created.streamPath}`));
|
|
1307
1571
|
console.log(pc.gray(`${resumed ? "Resumed" : "Created"} in ${durationMs}ms`));
|
|
1308
1572
|
console.log(
|
|
1309
1573
|
`status=${created.status} created_at=${created.createdAt} expires_at=${created.expiresAt} ttl_seconds=${ttlSeconds}`,
|
|
1310
1574
|
);
|
|
1575
|
+
if (remoteSync.status === "auth_required") {
|
|
1576
|
+
console.log(
|
|
1577
|
+
pc.yellow(
|
|
1578
|
+
`Dashboard sync pending: run \`${authLoginHint()}\`, then rerun \`${cliCommand} session start\` in this workspace to publish local metadata.`,
|
|
1579
|
+
),
|
|
1580
|
+
);
|
|
1581
|
+
} else if (remoteSync.status === "disabled") {
|
|
1582
|
+
console.log(pc.gray("Dashboard sync disabled by SENTINELAYER_SKIP_REMOTE_SYNC=1."));
|
|
1583
|
+
}
|
|
1311
1584
|
if (!resumed) {
|
|
1312
1585
|
console.log(
|
|
1313
1586
|
pc.gray(
|
|
1314
|
-
|
|
1587
|
+
`Tip: subsequent \`${cliCommand} session start\` in this workspace within an hour will resume this session. Pass --force-new to override.`,
|
|
1315
1588
|
),
|
|
1316
1589
|
);
|
|
1317
1590
|
}
|
|
@@ -1431,13 +1704,20 @@ export function registerSessionCommand(program) {
|
|
|
1431
1704
|
forceNew: Boolean(options.forceNew),
|
|
1432
1705
|
reuseWindowSeconds,
|
|
1433
1706
|
});
|
|
1707
|
+
const dashboardUrl = buildDashboardUrl(ensured.created.sessionId);
|
|
1708
|
+
const resumeContext = buildResumeContext(ensured.resumedCandidate, { reuseWindowSeconds });
|
|
1709
|
+
const remoteSync = await resolveSessionRemoteSyncState({ dashboardUrl });
|
|
1434
1710
|
const payload = {
|
|
1435
1711
|
command: "session ensure",
|
|
1436
1712
|
targetPath,
|
|
1437
1713
|
sessionId: ensured.created.sessionId,
|
|
1438
1714
|
title: ensured.title || null,
|
|
1439
1715
|
resumed: Boolean(ensured.resumedCandidate),
|
|
1440
|
-
|
|
1716
|
+
resumeSource: resumeContext?.source || null,
|
|
1717
|
+
resumeContext: resumeContext || undefined,
|
|
1718
|
+
staleResume: ensured.staleResume || undefined,
|
|
1719
|
+
dashboardUrl,
|
|
1720
|
+
remoteSync,
|
|
1441
1721
|
titleSync: ensured.titleSync || undefined,
|
|
1442
1722
|
};
|
|
1443
1723
|
console.log(JSON.stringify(payload, null, 2));
|
|
@@ -1701,6 +1981,21 @@ export function registerSessionCommand(program) {
|
|
|
1701
1981
|
.command("say <sessionId> <message>")
|
|
1702
1982
|
.description("Send a message to the session")
|
|
1703
1983
|
.option("--agent <id>", "Agent id to emit from", "cli-user")
|
|
1984
|
+
.option(
|
|
1985
|
+
"--model <model>",
|
|
1986
|
+
"Agent model/provider hint; defaults to local joined agent metadata or SENTINELAYER_AGENT_MODEL",
|
|
1987
|
+
process.env.SENTINELAYER_AGENT_MODEL || "",
|
|
1988
|
+
)
|
|
1989
|
+
.option(
|
|
1990
|
+
"--display-name <name>",
|
|
1991
|
+
"Human-readable agent display name",
|
|
1992
|
+
process.env.SENTINELAYER_AGENT_DISPLAY_NAME || process.env.SENTINELAYER_AGENT_NAME || "",
|
|
1993
|
+
)
|
|
1994
|
+
.option(
|
|
1995
|
+
"--role <role>",
|
|
1996
|
+
"Agent role metadata; defaults to local joined agent metadata or SENTINELAYER_AGENT_ROLE",
|
|
1997
|
+
process.env.SENTINELAYER_AGENT_ROLE || "",
|
|
1998
|
+
)
|
|
1704
1999
|
.option("--to <agent>", "Direct the message to a specific agent id")
|
|
1705
2000
|
.option("--reply-to <sequence>", "Mark this message as a reply to a target sequence id")
|
|
1706
2001
|
.option("--reply-cursor <cursor>", "Mark this message as a reply to a target event cursor")
|
|
@@ -1737,9 +2032,15 @@ export function registerSessionCommand(program) {
|
|
|
1737
2032
|
eventPayload.replyToCursor = replyToCursor;
|
|
1738
2033
|
}
|
|
1739
2034
|
const clientMessageId = `cli-${randomUUID()}`;
|
|
2035
|
+
const agent = await resolveSessionAgentEnvelope(normalizedSessionId, agentId, {
|
|
2036
|
+
targetPath,
|
|
2037
|
+
model: options.model,
|
|
2038
|
+
role: options.role,
|
|
2039
|
+
displayName: options.displayName,
|
|
2040
|
+
});
|
|
1740
2041
|
const event = createAgentEvent({
|
|
1741
2042
|
event: "session_message",
|
|
1742
|
-
|
|
2043
|
+
agent,
|
|
1743
2044
|
sessionId: normalizedSessionId,
|
|
1744
2045
|
payload: eventPayload,
|
|
1745
2046
|
});
|
|
@@ -1834,13 +2135,9 @@ export function registerSessionCommand(program) {
|
|
|
1834
2135
|
event.eventId = clientMessageId;
|
|
1835
2136
|
event.idempotencyToken = clientMessageId;
|
|
1836
2137
|
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
targetPath,
|
|
1841
|
-
});
|
|
1842
|
-
if (remoteSync?.synced) break;
|
|
1843
|
-
}
|
|
2138
|
+
const remoteSync = await syncSessionEventToApi(normalizedSessionId, event, {
|
|
2139
|
+
targetPath,
|
|
2140
|
+
});
|
|
1844
2141
|
if (!remoteSync?.synced) {
|
|
1845
2142
|
throw new Error(
|
|
1846
2143
|
`Agent post failed (${remoteSync?.reason || "unknown"}). Ensure this user has an active grant for '${agentId}'.`,
|
package/src/events/schema.js
CHANGED
|
@@ -54,6 +54,14 @@ function normalizeTimestamp(value, fallbackTimestamp) {
|
|
|
54
54
|
return new Date(epoch).toISOString();
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
function normalizePositiveInteger(value) {
|
|
58
|
+
const normalized = Number(value);
|
|
59
|
+
if (!Number.isFinite(normalized) || normalized <= 0) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
return Math.floor(normalized);
|
|
63
|
+
}
|
|
64
|
+
|
|
57
65
|
function stripUndefinedEntries(record) {
|
|
58
66
|
const cleaned = {};
|
|
59
67
|
for (const [key, value] of Object.entries(record || {})) {
|
|
@@ -112,6 +120,11 @@ export function createAgentEvent({
|
|
|
112
120
|
agent,
|
|
113
121
|
ts,
|
|
114
122
|
timestamp,
|
|
123
|
+
eventId,
|
|
124
|
+
idempotencyToken,
|
|
125
|
+
cursor,
|
|
126
|
+
sequenceId,
|
|
127
|
+
sequence_id,
|
|
115
128
|
} = {}) {
|
|
116
129
|
const normalizedEvent = normalizeNonEmptyString(event);
|
|
117
130
|
const normalizedAgent = normalizeAgentShape({
|
|
@@ -135,6 +148,10 @@ export function createAgentEvent({
|
|
|
135
148
|
runId: normalizeOptionalString(runId),
|
|
136
149
|
workItemId: normalizeOptionalString(workItemId),
|
|
137
150
|
requestId: normalizeOptionalString(requestId),
|
|
151
|
+
eventId: normalizeOptionalString(eventId),
|
|
152
|
+
idempotencyToken: normalizeOptionalString(idempotencyToken),
|
|
153
|
+
cursor: normalizeOptionalString(cursor),
|
|
154
|
+
sequenceId: normalizePositiveInteger(sequenceId ?? sequence_id),
|
|
138
155
|
ts: canonicalTs,
|
|
139
156
|
// Keep legacy timestamp key for existing consumers while PR0 migrates envelope usage.
|
|
140
157
|
timestamp: canonicalTs,
|
|
@@ -202,6 +219,10 @@ export function normalizeAgentEvent(evt, { allowLegacy = true } = {}) {
|
|
|
202
219
|
runId: evt.runId,
|
|
203
220
|
workItemId: evt.workItemId,
|
|
204
221
|
requestId: evt.requestId,
|
|
222
|
+
eventId: evt.eventId,
|
|
223
|
+
idempotencyToken: evt.idempotencyToken,
|
|
224
|
+
cursor: evt.cursor,
|
|
225
|
+
sequenceId: evt.sequenceId ?? evt.sequence_id,
|
|
205
226
|
ts: normalizedTs,
|
|
206
227
|
});
|
|
207
228
|
} catch {
|
package/src/legacy-cli.js
CHANGED
|
@@ -1135,6 +1135,10 @@ async function runLocalOmarGateCommand(args) {
|
|
|
1135
1135
|
const devTestBotBaseUrl = getCommandOptionValue(args, "--devtestbot-base-url") || "";
|
|
1136
1136
|
const devTestBotScope = getCommandOptionValue(args, "--devtestbot-scope") || "";
|
|
1137
1137
|
const emailOnComplete = getCommandOptionValue(args, "--email-on-complete") || "";
|
|
1138
|
+
const notifySession = getCommandOptionValue(args, "--notify-session") || "";
|
|
1139
|
+
const requireUsageLedger = hasCommandOption(args, "--require-usage-ledger");
|
|
1140
|
+
const model = getCommandOptionValue(args, "--model") || "gpt-5.3-codex";
|
|
1141
|
+
const provider = getCommandOptionValue(args, "--provider") || "sentinelayer";
|
|
1138
1142
|
|
|
1139
1143
|
const targetPath = path.resolve(process.cwd(), pathArg);
|
|
1140
1144
|
if (!fs.existsSync(targetPath) || !fs.statSync(targetPath).isDirectory()) {
|
|
@@ -1163,6 +1167,18 @@ async function runLocalOmarGateCommand(args) {
|
|
|
1163
1167
|
baseUrl: devTestBotBaseUrl,
|
|
1164
1168
|
scope: devTestBotScope,
|
|
1165
1169
|
},
|
|
1170
|
+
sessionUsage: notifySession || requireUsageLedger
|
|
1171
|
+
? {
|
|
1172
|
+
sessionId: notifySession,
|
|
1173
|
+
agentId: "investor-dd",
|
|
1174
|
+
model,
|
|
1175
|
+
provider,
|
|
1176
|
+
targetPath,
|
|
1177
|
+
billingTier: "internal",
|
|
1178
|
+
sourceCommand: "omargate investor-dd",
|
|
1179
|
+
required: requireUsageLedger,
|
|
1180
|
+
}
|
|
1181
|
+
: null,
|
|
1166
1182
|
reportEmail: emailOnComplete
|
|
1167
1183
|
? {
|
|
1168
1184
|
to: emailOnComplete,
|