sentinelayer-cli 0.46.5 → 0.48.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/README.md +47 -3
- package/package.json +12 -7
- package/skills/omar-gate-setup/SKILL.md +103 -0
- package/skills/senti-agent-identity/SKILL.md +80 -0
- package/skills/senti-judge/SKILL.md +139 -0
- package/skills/senti-room-notifications/SKILL.md +73 -0
- package/skills/senti-room-notifications/listen-wrapper.cjs +150 -0
- package/skills/senti-room-notifications/package.json +1 -0
- package/skills/senti-room-notifications/test-classifier.js +109 -0
- package/skills/senti-room-notifications/test-receipt.js +54 -0
- package/skills/sentinelayer-cli/SKILL.md +222 -0
- package/src/agents/devtestbot/runner.js +22 -11
- package/src/agents/devtestbot/tool.js +24 -11
- package/src/agents/jules/fix-cycle.js +7 -1
- package/src/agents/jules/tools/runtime-audit.js +17 -12
- package/src/agents/shared-tools/dispatch-core.js +10 -5
- package/src/ai/aidenid.js +4 -2
- package/src/ai/domain-target-store.js +11 -5
- package/src/ai/identity-store.js +11 -5
- package/src/ai/site-store.js +8 -4
- package/src/audit/orchestrator.js +21 -14
- package/src/audit/package.js +6 -4
- package/src/audit/persona-loop.js +7 -0
- package/src/audit/replay.js +4 -2
- package/src/auth/gate.js +32 -1
- package/src/auth/session-store.js +37 -0
- package/src/board/evidence-resolvers.js +3 -4
- package/src/cli.js +23 -3
- package/src/commands/ai/identity-lifecycle.js +30 -26
- package/src/commands/ai/provision-governance.js +32 -24
- package/src/commands/ai/shared.js +6 -4
- package/src/commands/audit.js +14 -12
- package/src/commands/board.js +2 -2
- package/src/commands/chat.js +8 -6
- package/src/commands/judge.js +125 -0
- package/src/commands/legacy-args.js +1 -0
- package/src/commands/mcp.js +45 -52
- package/src/commands/omargate.js +2 -1
- package/src/commands/plugin.js +11 -3
- package/src/commands/review.js +9 -4
- package/src/commands/scan.js +7 -7
- package/src/commands/session.js +516 -30
- package/src/commands/skills.js +45 -0
- package/src/commands/watch.js +9 -7
- package/src/config/output-filesystem.js +156 -0
- package/src/config/schema.js +4 -2
- package/src/config/service.js +40 -4
- package/src/cost/history.js +15 -9
- package/src/daemon/artifact-lineage.js +15 -12
- package/src/daemon/assignment-ledger.js +31 -25
- package/src/daemon/ast-drift.js +7 -3
- package/src/daemon/budget-governor.js +26 -20
- package/src/daemon/error-worker.js +28 -22
- package/src/daemon/fix-cycle.js +5 -4
- package/src/daemon/hybrid-mapper.js +19 -16
- package/src/daemon/ingest-refresh.js +4 -0
- package/src/daemon/jira-lifecycle.js +21 -17
- package/src/daemon/operator-control.js +18 -15
- package/src/daemon/reliability-lane.js +18 -15
- package/src/daemon/scope-engine.js +12 -8
- package/src/daemon/watchdog.js +15 -12
- package/src/engram/build-state.js +24 -3
- package/src/engram/capability-index.js +367 -0
- package/src/engram/compile-graph.js +1063 -0
- package/src/engram/contracts/action-receipt.js +82 -0
- package/src/engram/contracts/capability-index.js +259 -0
- package/src/engram/contracts/capability.js +377 -0
- package/src/engram/contracts/content-hash.js +8 -0
- package/src/engram/contracts/evidence.js +258 -0
- package/src/engram/contracts/graph.js +594 -0
- package/src/engram/contracts/identity.js +8 -0
- package/src/engram/contracts/index.js +149 -0
- package/src/engram/contracts/media-asset.js +85 -0
- package/src/engram/contracts/money.js +249 -0
- package/src/engram/contracts/source-snapshot.js +57 -0
- package/src/engram/contracts/task-context-pack.js +114 -0
- package/src/engram/contracts/temporal-segment.js +72 -0
- package/src/engram/contracts/versions.js +96 -0
- package/src/engram/extract-html.js +692 -0
- package/src/engram/graph-store.js +877 -0
- package/src/engram/index.js +98 -14
- package/src/engram/ingest-site.js +132 -0
- package/src/engram/live-state.js +1180 -0
- package/src/engram/namespace.js +34 -1
- package/src/engram/retrieve-site.js +753 -0
- package/src/engram/safe-reader.js +597 -0
- package/src/engram/source-intake.js +251 -0
- package/src/engram/store.js +227 -9
- package/src/ingest/engine.js +21 -28
- package/src/judge/verdict.js +305 -0
- package/src/legacy-cli.js +96 -19
- package/src/mcp/bridge-inputs.js +25 -4
- package/src/mcp/cli-command-tools.js +70 -0
- package/src/mcp/registry.js +17 -13
- package/src/mcp/session-stdio-server.js +297 -19
- package/src/memory/blackboard.js +5 -4
- package/src/net/gh-api-path.js +35 -9
- package/src/net/test-egress-guard.js +16 -4
- package/src/plugin/manifest.js +12 -8
- package/src/review/ai-review.js +110 -10
- package/src/review/investor-dd-devtestbot.js +17 -6
- package/src/review/investor-dd-orchestrator.js +21 -13
- package/src/review/local-review.js +26 -16
- package/src/review/omargate-cache.js +5 -5
- package/src/review/omargate-orchestrator.js +89 -17
- package/src/review/replay.js +9 -4
- package/src/review/report.js +22 -13
- package/src/review/typesafe-severity.js +201 -0
- package/src/session/admission-auth.js +34 -7
- package/src/session/admission.js +110 -15
- package/src/session/attachment-summary.js +77 -0
- package/src/session/edit-summary.js +19 -0
- package/src/session/invitations.js +65 -1
- package/src/session/mentions.js +17 -9
- package/src/session/message-edits.js +5 -4
- package/src/session/preview.js +10 -2
- package/src/session/recap.js +3 -2
- package/src/session/room-files.js +519 -0
- package/src/session/route-classes.js +22 -8
- package/src/session/setup-guides.js +4 -0
- package/src/session/standup.js +192 -0
- package/src/session/sync.js +229 -10
- package/src/session/tickets.js +41 -0
- package/src/session/transcript.js +2 -1
- package/src/session/wake/resolve-target.js +7 -3
- package/src/skills/listener-runtime.cjs +92 -0
- package/src/skills/service.js +207 -0
- package/src/swarm/factory.js +5 -5
- package/src/swarm/pentest.js +9 -9
- package/src/swarm/report.js +4 -4
- package/src/swarm/runtime.js +15 -12
- package/src/swarm/scenario-dsl.js +7 -5
- package/src/telemetry/ledger.js +16 -7
package/src/session/tickets.js
CHANGED
|
@@ -207,6 +207,47 @@ export async function ticketEvents(
|
|
|
207
207
|
});
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
/** Create with an explicit acting agent claim; the API authorizes it, never a createdBy body. */
|
|
211
|
+
export async function createTicket(sessionId, {
|
|
212
|
+
title, body = "", agentId = "", assignee = "", sourceSequenceId = null,
|
|
213
|
+
idempotencyKey = `ticket-create-${randomUUID()}`, targetPath = process.cwd(),
|
|
214
|
+
resolveAuthSession = resolveActiveAuthSession, requestMutation = requestJsonMutation,
|
|
215
|
+
} = {}) {
|
|
216
|
+
const cleanTitle = normalizeString(title), cleanAgent = normalizeString(agentId);
|
|
217
|
+
if (!cleanTitle || [...cleanTitle].length > 200 || typeof body !== "string" || [...body].length > 8000) throw new Error("Ticket title/body exceeds its limits.");
|
|
218
|
+
if (cleanAgent && !/^[a-zA-Z0-9._:-]{1,64}$/.test(cleanAgent)) throw new Error("Invalid agent identity.");
|
|
219
|
+
if (sourceSequenceId !== null && (!Number.isSafeInteger(sourceSequenceId) || sourceSequenceId < 1)) throw new Error("Invalid source sequence.");
|
|
220
|
+
if (typeof idempotencyKey !== "string" || !idempotencyKey.trim() || idempotencyKey.length > 128) throw new Error("Invalid idempotency key.");
|
|
221
|
+
const { credential, apiUrl } = await auth(targetPath, resolveAuthSession);
|
|
222
|
+
try {
|
|
223
|
+
const ticket = await mutate(ticketsUrl(apiUrl, sessionId), { credential, key: idempotencyKey, operation: "create", requestMutation,
|
|
224
|
+
body: { title: cleanTitle, body, ...(cleanAgent ? { agentId: cleanAgent } : {}),
|
|
225
|
+
...(normalizeString(assignee) ? { assignee: normalizeString(assignee) } : {}),
|
|
226
|
+
...(sourceSequenceId !== null ? { sourceSequenceId } : {}) },
|
|
227
|
+
});
|
|
228
|
+
if (typeof ticket?.id !== "string" || !ticket.id.trim()) {
|
|
229
|
+
throw new Error("The API returned no usable ticket identity; creation may have committed.");
|
|
230
|
+
}
|
|
231
|
+
// The public API projects server-authored creator/sourceMessage. Legacy
|
|
232
|
+
// createdBy and a top-level sourceSequenceId do not establish that provenance.
|
|
233
|
+
// Keep the returned ticket and retry key even when confirmation is incomplete.
|
|
234
|
+
const creator = ticket?.creator;
|
|
235
|
+
const source = ticket?.sourceMessage;
|
|
236
|
+
const provenance = {
|
|
237
|
+
agentRequested: cleanAgent || null,
|
|
238
|
+
agentRecorded: Boolean(cleanAgent) && creator?.kind === "agent" && creator.id === cleanAgent,
|
|
239
|
+
sourceRequested: sourceSequenceId,
|
|
240
|
+
sourceRecorded: sourceSequenceId !== null && source?.sequenceId === sourceSequenceId
|
|
241
|
+
&& typeof source.id === "string" && /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(source.id)
|
|
242
|
+
&& Number.isSafeInteger(source.revision) && source.revision >= 1,
|
|
243
|
+
};
|
|
244
|
+
return { ticket, idempotencyKey, provenance };
|
|
245
|
+
} catch (error) {
|
|
246
|
+
// Retain the logical write's retry key even if the server committed but the response was lost.
|
|
247
|
+
throw new Error(`${error.message || "Ticket creation failed"} (retry with --idempotency-key ${idempotencyKey})`, { cause: error });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
210
251
|
async function mutate(url, { credential, key, operation, body, requestMutation }) {
|
|
211
252
|
return checkedTransport(requestMutation)(url, {
|
|
212
253
|
method: "POST",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
import { PERSONA_VISUALS, ORCHESTRATOR_VISUALS } from "../agents/persona-visuals.js";
|
|
22
22
|
import { buildSessionUsageLedger } from "./pricing-ledger.js";
|
|
23
|
+
import { appendAttachmentSummary } from "./attachment-summary.js";
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* Avatar map for client families (the OUTSIDE-the-persona-set agents
|
|
@@ -720,7 +721,7 @@ export function buildTranscriptMarkdown({
|
|
|
720
721
|
profile,
|
|
721
722
|
});
|
|
722
723
|
const ts = eventTimestamp(event);
|
|
723
|
-
const body = eventBody(event);
|
|
724
|
+
const body = appendAttachmentSummary(eventBody(event), { sessionId, ...event });
|
|
724
725
|
|
|
725
726
|
if (SYSTEM_EVENT_KINDS.has(kind)) {
|
|
726
727
|
const hint = body || kind.replace(/_/g, " ");
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
// here is small enough to own.
|
|
18
18
|
|
|
19
19
|
import { messageRevision, sessionMessageEditSnapshot, sessionMessageRoutingEvent } from "../message-edits.js";
|
|
20
|
+
import { formatAttachmentSummary } from "../attachment-summary.js";
|
|
20
21
|
|
|
21
22
|
const BROADCAST_RECIPIENTS = new Set(["*", "all", "broadcast", "everyone", "anyone", "agents", "all-agents"]);
|
|
22
23
|
const DEFAULT_WAKE_EVENT_TYPES = new Set(["session_message", "help_request", "session_message_edited"]);
|
|
@@ -100,9 +101,12 @@ function defaultFormatMessage(event, agentId) {
|
|
|
100
101
|
const author = agentIdOf(event) || "unknown";
|
|
101
102
|
const text = event?.payload?.message ?? event?.message ?? "";
|
|
102
103
|
const body = typeof text === "string" ? text : "";
|
|
103
|
-
const head = `Senti wake for ${agentId}: ${event.event === "session_message_edited" ? "edited message" : "new message"} from ${author}
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
const head = `Senti wake for ${agentId}: ${event.event === "session_message_edited" ? "edited message" : "new message"} from ${author}.`
|
|
105
|
+
.slice(0, MAX_WAKE_MESSAGE_CHARS);
|
|
106
|
+
const attachmentBudget = Math.max(0, MAX_WAKE_MESSAGE_CHARS - head.length - 2);
|
|
107
|
+
const attachments = formatAttachmentSummary(event).slice(0, attachmentBudget);
|
|
108
|
+
const bodyBudget = Math.max(0, MAX_WAKE_MESSAGE_CHARS - head.length - 2 - (attachments ? attachments.length + 2 : 0));
|
|
109
|
+
return [head, body.slice(0, bodyBudget), attachments].filter(Boolean).join("\n\n");
|
|
106
110
|
}
|
|
107
111
|
|
|
108
112
|
/**
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const { spawn, execFile } = require("node:child_process");
|
|
2
|
+
const readline = require("node:readline");
|
|
3
|
+
|
|
4
|
+
// Process lifecycle for the bundled notification wrapper. Commands use argv, never a shell.
|
|
5
|
+
function runListener({ me: ME, sessionId: SID, cliPath: CLI, cwd: CWD, pollCwd: POLL_CWD, wake: WAKE = null, classifier }) {
|
|
6
|
+
const { deliver, observe, rememberMine, newerThanDelivered, flushBroadcasts } = classifier;
|
|
7
|
+
const { hadReceiptAtBoot } = classifier.state();
|
|
8
|
+
let restarts = 0;
|
|
9
|
+
let startedAt = 0;
|
|
10
|
+
|
|
11
|
+
function wanted(who) {
|
|
12
|
+
if (who === ME) return false;
|
|
13
|
+
return WAKE === null || WAKE.has(who);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function handle(line) {
|
|
17
|
+
let e;
|
|
18
|
+
try { e = JSON.parse(line); } catch {
|
|
19
|
+
if (/error|Error|refused|403|401|stopped|wedged/i.test(line)) console.log("[raw] " + line.slice(0, 300));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const ev = e.event || "";
|
|
23
|
+
const who = (e.agent && e.agent.id) || e.agentId || "?";
|
|
24
|
+
if (who !== ME && (ev === "session_message" || ev === "session_reply")) observe(e.ts);
|
|
25
|
+
if (who === ME) rememberMine(e.sequenceId);
|
|
26
|
+
if (!wanted(who)) return;
|
|
27
|
+
const p = e.payload || {};
|
|
28
|
+
if (ev === "session_message" || ev === "session_reply") {
|
|
29
|
+
const text = (p.message || p.note || "").replace(/\s+/g, " ");
|
|
30
|
+
deliver(ev, who, e.ts, e.sequenceId, p.replyToSequenceId || p.targetSequenceId, text);
|
|
31
|
+
} else if (ev === "daemon_alert" && p.alert && /human|directive|mention|assign|lock|stuck/i.test(p.alert) && (!p.targetAgentId || p.targetAgentId === ME)) {
|
|
32
|
+
console.log(`[alert] ${p.alert} ${p.targetAgentId || ""} ${(p.message || "").slice(0, 200)}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function start() {
|
|
37
|
+
startedAt = Date.now();
|
|
38
|
+
const p = spawn(process.execPath, [CLI, "session", "listen", "--session", SID, "--agent", ME,
|
|
39
|
+
"--active-interval", "15", "--active-window", "600", "--interval", "15", "--presence-interval", "60",
|
|
40
|
+
"--display-name", ME, "--force", "--from-now"],
|
|
41
|
+
{ cwd: CWD, env: process.env, stdio: ["pipe", "pipe", "pipe"] });
|
|
42
|
+
readline.createInterface({ input: p.stdout }).on("line", handle);
|
|
43
|
+
readline.createInterface({ input: p.stderr }).on("line", (l) => {
|
|
44
|
+
if (/error|Error|refused|403|401|wedged|circuit/i.test(l)) console.log("[listener-stderr] " + l.slice(0, 300));
|
|
45
|
+
});
|
|
46
|
+
p.on("exit", (code, sig) => {
|
|
47
|
+
const wasHealthy = Date.now() - startedAt > 60000;
|
|
48
|
+
if (wasHealthy) restarts = 0;
|
|
49
|
+
restarts++;
|
|
50
|
+
if (!wasHealthy && (restarts === 2 || restarts % 25 === 0)) console.log(`[listener-flapping] code=${code} sig=${sig} rapid-restarts=${restarts}`);
|
|
51
|
+
if (restarts > 500) { console.log("[listener-giving-up] too many rapid restarts"); process.exit(1); }
|
|
52
|
+
setTimeout(start, 5000);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
start();
|
|
56
|
+
|
|
57
|
+
const seenReplies = new Set();
|
|
58
|
+
let repliesPrimed = false;
|
|
59
|
+
function pollReplies() {
|
|
60
|
+
execFile(process.execPath, [CLI, "session", "read", SID, "--remote", "--agent", ME, "--tail", "40", "--json"],
|
|
61
|
+
{ cwd: POLL_CWD, env: process.env, maxBuffer: 16 * 1024 * 1024 }, (err, stdout) => {
|
|
62
|
+
if (err) { console.log("[reply-poll-error] " + String(err.message).slice(0, 200)); return; }
|
|
63
|
+
let j; try { j = JSON.parse(stdout); } catch { return; }
|
|
64
|
+
const items = [];
|
|
65
|
+
for (const e of j.events || []) if (e.event === "session_reply" || e.event === "session_message")
|
|
66
|
+
items.push({ key: (e.ts || "") + "|" + ((e.payload && (e.payload.targetSequenceId || e.payload.replyToSequenceId)) || ""), who: (e.agent && e.agent.id) || "?", ts: e.ts, target: e.payload && (e.payload.targetSequenceId || e.payload.replyToSequenceId), text: (e.payload && (e.payload.message || e.payload.note)) || "" });
|
|
67
|
+
for (const a of j.recentHumanActivity || [])
|
|
68
|
+
items.push({ key: (a.createdAt || "") + "|" + (a.targetSequenceId || ""), who: a.actorId || "human", ts: a.createdAt, target: a.targetSequenceId, text: a.note || "" });
|
|
69
|
+
for (const it of items) {
|
|
70
|
+
if (seenReplies.has(it.key)) continue;
|
|
71
|
+
seenReplies.add(it.key);
|
|
72
|
+
if (it.who !== ME) observe(it.ts);
|
|
73
|
+
if (!repliesPrimed && !(hadReceiptAtBoot && newerThanDelivered(it.ts))) continue;
|
|
74
|
+
if (!wanted(it.who)) continue;
|
|
75
|
+
deliver("session_reply", it.who, it.ts, null, it.target, String(it.text).replace(/\s+/g, " "));
|
|
76
|
+
}
|
|
77
|
+
repliesPrimed = true;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
pollReplies();
|
|
81
|
+
setInterval(pollReplies, 20000);
|
|
82
|
+
console.log(`[listener-started] ${ME} listening on ${SID.slice(0, 8)} (presence heartbeats on; reply-poll every 20s)`);
|
|
83
|
+
|
|
84
|
+
function flushAndExit(sig) {
|
|
85
|
+
try { flushBroadcasts(); } catch {}
|
|
86
|
+
process.exit(sig === "SIGINT" ? 130 : 0);
|
|
87
|
+
}
|
|
88
|
+
process.on("SIGTERM", () => flushAndExit("SIGTERM"));
|
|
89
|
+
process.on("SIGINT", () => flushAndExit("SIGINT"));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = { runListener };
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
8
|
+
import { parse } from "yaml";
|
|
9
|
+
|
|
10
|
+
const BUNDLE_ROOT = fileURLToPath(new URL("../../skills/", import.meta.url));
|
|
11
|
+
const SKILL_NAMES = ["senti-agent-identity", "senti-room-notifications", "sentinelayer-cli", "senti-judge", "omar-gate-setup"];
|
|
12
|
+
|
|
13
|
+
function sha256(value) {
|
|
14
|
+
return createHash("sha256").update(value).digest("hex");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function statOrNull(filePath) {
|
|
18
|
+
try {
|
|
19
|
+
return await fs.lstat(filePath);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (error.code === "ENOENT") return null;
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function unsafePath(filePath) {
|
|
27
|
+
return Object.assign(new Error(`Expected a regular file or directory, not a link: ${filePath}`), {
|
|
28
|
+
code: "SKILLS_UNSAFE_PATH",
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function readTree(directory) {
|
|
33
|
+
const rootStat = await statOrNull(directory);
|
|
34
|
+
if (!rootStat) return null;
|
|
35
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) throw unsafePath(directory);
|
|
36
|
+
const files = [];
|
|
37
|
+
async function visit(relative) {
|
|
38
|
+
const entries = await fs.readdir(path.join(directory, relative), { withFileTypes: true });
|
|
39
|
+
entries.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
const name = relative ? `${relative}/${entry.name}` : entry.name;
|
|
42
|
+
const fullPath = path.join(directory, name);
|
|
43
|
+
if (entry.isDirectory()) await visit(name);
|
|
44
|
+
else if (entry.isFile()) files.push({ name, content: await fs.readFile(fullPath) });
|
|
45
|
+
else throw unsafePath(fullPath);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
await visit("");
|
|
49
|
+
return files;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function describe(files) {
|
|
53
|
+
const document = files.find((file) => file.name === "SKILL.md")?.content.toString("utf8") || "";
|
|
54
|
+
const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(document);
|
|
55
|
+
let version = null;
|
|
56
|
+
try {
|
|
57
|
+
const value = frontmatter ? parse(frontmatter[1])?.metadata?.version : null;
|
|
58
|
+
if (typeof value === "string") version = value;
|
|
59
|
+
} catch {
|
|
60
|
+
// Locally edited frontmatter can be invalid; its content hash still reports the change.
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
version,
|
|
64
|
+
hash: sha256(JSON.stringify(files.map((file) => [file.name, sha256(file.content)]))),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function resolveDirectory(dir) {
|
|
69
|
+
if (dir === undefined) return path.join(os.homedir(), ".claude", "skills");
|
|
70
|
+
if (typeof dir !== "string" || !dir.trim()) throw new Error("--dir must be a nonempty path.");
|
|
71
|
+
return path.resolve(dir);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check every component below the chosen destination, including dangling symlinks.
|
|
75
|
+
async function inspectPath(root, destination) {
|
|
76
|
+
const relative = path.relative(root, destination);
|
|
77
|
+
if (relative.startsWith(`..${path.sep}`) || relative === ".." || path.isAbsolute(relative)) {
|
|
78
|
+
throw unsafePath(destination);
|
|
79
|
+
}
|
|
80
|
+
const components = relative ? relative.split(path.sep) : [];
|
|
81
|
+
let current = root;
|
|
82
|
+
for (let index = 0; index <= components.length; index += 1) {
|
|
83
|
+
const stat = await statOrNull(current);
|
|
84
|
+
if (stat && (stat.isSymbolicLink() || (index < components.length && !stat.isDirectory()))) {
|
|
85
|
+
throw unsafePath(current);
|
|
86
|
+
}
|
|
87
|
+
if (index === components.length) return stat;
|
|
88
|
+
current = path.join(current, components[index]);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function selectNames(names) {
|
|
93
|
+
if (names === undefined || names === null || (Array.isArray(names) && names.length === 0)) return SKILL_NAMES;
|
|
94
|
+
const wanted = (Array.isArray(names) ? names : [names]).map((name) => String(name).trim());
|
|
95
|
+
const unknown = wanted.filter((name) => !SKILL_NAMES.includes(name));
|
|
96
|
+
if (unknown.length) {
|
|
97
|
+
throw Object.assign(new Error(`Unknown skill: ${unknown.join(", ")}. Bundled skills: ${SKILL_NAMES.join(", ")}`), {
|
|
98
|
+
code: "SKILLS_UNKNOWN",
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
// Bundle order, each once.
|
|
102
|
+
return SKILL_NAMES.filter((name) => wanted.includes(name));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function loadBundle(names = SKILL_NAMES) {
|
|
106
|
+
return Promise.all(names.map(async (name) => {
|
|
107
|
+
const files = await readTree(path.join(BUNDLE_ROOT, name));
|
|
108
|
+
if (!files?.some((file) => file.name === "SKILL.md")) {
|
|
109
|
+
throw new Error(`Bundled skill is missing SKILL.md: ${name}`);
|
|
110
|
+
}
|
|
111
|
+
const metadata = describe(files);
|
|
112
|
+
if (!metadata.version) throw new Error(`Bundled skill is missing a version: ${name}`);
|
|
113
|
+
return { name, files, ...metadata };
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function replaceFile(source, destination) {
|
|
118
|
+
const deadline = Date.now() + 1000;
|
|
119
|
+
for (;;) {
|
|
120
|
+
try {
|
|
121
|
+
await fs.rename(source, destination);
|
|
122
|
+
return;
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (!["EPERM", "EBUSY"].includes(error.code) || Date.now() >= deadline) throw error;
|
|
125
|
+
await delay(25);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function listSkills({ dir } = {}) {
|
|
131
|
+
const directory = resolveDirectory(dir);
|
|
132
|
+
const skills = [];
|
|
133
|
+
for (const bundle of await loadBundle()) {
|
|
134
|
+
const destination = path.join(directory, bundle.name);
|
|
135
|
+
let installed = null;
|
|
136
|
+
let error = null;
|
|
137
|
+
try {
|
|
138
|
+
await inspectPath(directory, destination);
|
|
139
|
+
const files = await readTree(destination);
|
|
140
|
+
if (files) installed = describe(files);
|
|
141
|
+
} catch (caught) {
|
|
142
|
+
error = caught.message;
|
|
143
|
+
}
|
|
144
|
+
skills.push({
|
|
145
|
+
name: bundle.name,
|
|
146
|
+
bundled: { version: bundle.version, hash: bundle.hash },
|
|
147
|
+
installed,
|
|
148
|
+
status: error ? "invalid" : !installed ? "not-installed" :
|
|
149
|
+
installed.hash === bundle.hash ? "installed" : "modified",
|
|
150
|
+
...(error ? { error } : {}),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return { directory, skills };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function installSkills({ dir, force = false, names } = {}) {
|
|
157
|
+
const directory = resolveDirectory(dir);
|
|
158
|
+
// Named skills only: a local edit to one skill never blocks installing another.
|
|
159
|
+
const bundles = await loadBundle(selectNames(names));
|
|
160
|
+
const plan = [];
|
|
161
|
+
const conflicts = [];
|
|
162
|
+
// Preflight the entire bundle so a conflict in the last skill cannot leave a partial install.
|
|
163
|
+
for (const bundle of bundles) {
|
|
164
|
+
for (const file of bundle.files) {
|
|
165
|
+
const destination = path.join(directory, bundle.name, file.name);
|
|
166
|
+
const stat = await inspectPath(directory, destination);
|
|
167
|
+
if (stat && !stat.isFile()) throw unsafePath(destination);
|
|
168
|
+
const unchanged = stat ? (await fs.readFile(destination)).equals(file.content) : false;
|
|
169
|
+
if (stat && !unchanged && !force) conflicts.push(destination);
|
|
170
|
+
plan.push({ bundle, file, destination, unchanged, exists: Boolean(stat) });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (conflicts.length) {
|
|
174
|
+
throw Object.assign(new Error(`Local skill files differ; use --force to replace them:\n${conflicts.join("\n")}`), {
|
|
175
|
+
code: "SKILLS_CONFLICT", files: conflicts,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
for (const item of plan) {
|
|
179
|
+
if (item.unchanged) continue;
|
|
180
|
+
await fs.mkdir(path.dirname(item.destination), { recursive: true });
|
|
181
|
+
await inspectPath(directory, item.destination);
|
|
182
|
+
if (!item.exists) {
|
|
183
|
+
// Exclusive creation also protects a file created after preflight.
|
|
184
|
+
await fs.copyFile(path.join(BUNDLE_ROOT, item.bundle.name, item.file.name), item.destination, constants.COPYFILE_EXCL);
|
|
185
|
+
} else {
|
|
186
|
+
const temporary = path.join(path.dirname(item.destination), `.skill-${randomUUID()}.tmp`);
|
|
187
|
+
try {
|
|
188
|
+
await fs.writeFile(temporary, item.file.content, { flag: "wx" });
|
|
189
|
+
// Replace the directory entry rather than modifying another file through a hard link.
|
|
190
|
+
await replaceFile(temporary, item.destination);
|
|
191
|
+
} finally {
|
|
192
|
+
await fs.rm(temporary, { force: true });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
directory,
|
|
198
|
+
skills: bundles.map((bundle) => {
|
|
199
|
+
const entries = plan.filter((item) => item.bundle === bundle);
|
|
200
|
+
return {
|
|
201
|
+
name: bundle.name, version: bundle.version, hash: bundle.hash,
|
|
202
|
+
written: entries.filter((item) => !item.unchanged).length,
|
|
203
|
+
unchanged: entries.filter((item) => item.unchanged).length,
|
|
204
|
+
};
|
|
205
|
+
}),
|
|
206
|
+
};
|
|
207
|
+
}
|
package/src/swarm/factory.js
CHANGED
|
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import fsp from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { resolveOutputContext } from "../config/service.js";
|
|
6
6
|
|
|
7
7
|
const SCHEMA_VERSION = 1;
|
|
8
8
|
const DEFAULT_GLOBAL_BUDGET = Object.freeze({
|
|
@@ -341,7 +341,7 @@ export async function writeSwarmPlanArtifacts({
|
|
|
341
341
|
throw new Error("plan is required.");
|
|
342
342
|
}
|
|
343
343
|
|
|
344
|
-
const outputRoot = await
|
|
344
|
+
const { outputRoot, outputFs } = await resolveOutputContext({
|
|
345
345
|
cwd: path.resolve(String(plan.targetPath || ".")),
|
|
346
346
|
outputDirOverride: outputDir,
|
|
347
347
|
env,
|
|
@@ -350,9 +350,9 @@ export async function writeSwarmPlanArtifacts({
|
|
|
350
350
|
const runDirectory = path.join(outputRoot, "swarms", plan.runId);
|
|
351
351
|
const planJsonPath = path.join(runDirectory, "SWARM_PLAN.json");
|
|
352
352
|
const planMarkdownPath = path.join(runDirectory, "SWARM_PLAN.md");
|
|
353
|
-
await
|
|
354
|
-
await
|
|
355
|
-
await
|
|
353
|
+
await outputFs.mkdir(runDirectory, { recursive: true });
|
|
354
|
+
await outputFs.writeFile(planJsonPath, `${JSON.stringify(plan, null, 2)}\n`, "utf-8");
|
|
355
|
+
await outputFs.writeFile(planMarkdownPath, `${renderSwarmPlanMarkdown(plan).trim()}\n`, "utf-8");
|
|
356
356
|
|
|
357
357
|
return {
|
|
358
358
|
outputRoot,
|
package/src/swarm/pentest.js
CHANGED
|
@@ -3,7 +3,7 @@ import fsp from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
5
|
import { getTargetById } from "../ai/domain-target-store.js";
|
|
6
|
-
import {
|
|
6
|
+
import { resolveOutputContext } from "../config/service.js";
|
|
7
7
|
|
|
8
8
|
const PENTEST_SCHEMA_VERSION = 1;
|
|
9
9
|
|
|
@@ -679,7 +679,7 @@ export async function runSwarmPentest({
|
|
|
679
679
|
}
|
|
680
680
|
const parsedTargetUrl = parseTargetUrl(targetUrl);
|
|
681
681
|
|
|
682
|
-
const outputRoot = await
|
|
682
|
+
const { outputRoot, outputFs } = await resolveOutputContext({
|
|
683
683
|
cwd: normalizedTargetPath,
|
|
684
684
|
outputDirOverride: outputDir,
|
|
685
685
|
env,
|
|
@@ -747,8 +747,8 @@ export async function runSwarmPentest({
|
|
|
747
747
|
const cleanupContractPath = path.join(pentestDirectory, "CLEANUP_CONTRACT.json");
|
|
748
748
|
const reportJsonPath = path.join(pentestDirectory, "PENTEST_REPORT.json");
|
|
749
749
|
const reportMarkdownPath = path.join(pentestDirectory, "PENTEST_REPORT.md");
|
|
750
|
-
await
|
|
751
|
-
await
|
|
750
|
+
await outputFs.mkdir(pentestDirectory, { recursive: true });
|
|
751
|
+
await outputFs.writeFile(
|
|
752
752
|
requestPlanPath,
|
|
753
753
|
`${JSON.stringify(
|
|
754
754
|
{
|
|
@@ -826,7 +826,7 @@ export async function runSwarmPentest({
|
|
|
826
826
|
};
|
|
827
827
|
|
|
828
828
|
await runCheckAtIndex(0);
|
|
829
|
-
await
|
|
829
|
+
await outputFs.writeFile(
|
|
830
830
|
auditLogPath,
|
|
831
831
|
auditEntries.length > 0 ? `${auditEntries.map((entry) => JSON.stringify(entry)).join("\n")}\n` : "",
|
|
832
832
|
"utf-8"
|
|
@@ -856,7 +856,7 @@ export async function runSwarmPentest({
|
|
|
856
856
|
},
|
|
857
857
|
],
|
|
858
858
|
};
|
|
859
|
-
await
|
|
859
|
+
await outputFs.writeFile(identityIsolationPath, `${JSON.stringify(identityIsolation, null, 2)}\n`, "utf-8");
|
|
860
860
|
|
|
861
861
|
const cleanupContract = {
|
|
862
862
|
schemaVersion: 1,
|
|
@@ -868,7 +868,7 @@ export async function runSwarmPentest({
|
|
|
868
868
|
cleanupRequested: true,
|
|
869
869
|
cleanupTaskId: `cleanup-${runId}`,
|
|
870
870
|
};
|
|
871
|
-
await
|
|
871
|
+
await outputFs.writeFile(cleanupContractPath, `${JSON.stringify(cleanupContract, null, 2)}\n`, "utf-8");
|
|
872
872
|
|
|
873
873
|
const findings = [];
|
|
874
874
|
for (const entry of results) {
|
|
@@ -922,8 +922,8 @@ export async function runSwarmPentest({
|
|
|
922
922
|
reportMarkdownPath,
|
|
923
923
|
};
|
|
924
924
|
|
|
925
|
-
await
|
|
926
|
-
await
|
|
925
|
+
await outputFs.writeFile(reportJsonPath, `${JSON.stringify(report, null, 2)}\n`, "utf-8");
|
|
926
|
+
await outputFs.writeFile(reportMarkdownPath, `${renderPentestReportMarkdown(report).trim()}\n`, "utf-8");
|
|
927
927
|
|
|
928
928
|
return {
|
|
929
929
|
...report,
|
package/src/swarm/report.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fsp from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { resolveOutputContext } from "../config/service.js";
|
|
5
5
|
import { loadSwarmDashboardSnapshot, resolveSwarmRuntimeFiles } from "./dashboard.js";
|
|
6
6
|
|
|
7
7
|
function normalizeString(value) {
|
|
@@ -109,7 +109,7 @@ export async function buildSwarmExecutionReport({
|
|
|
109
109
|
homeDir,
|
|
110
110
|
});
|
|
111
111
|
|
|
112
|
-
const outputRoot = await
|
|
112
|
+
const { outputRoot, outputFs } = await resolveOutputContext({
|
|
113
113
|
cwd: path.resolve(String(targetPath || ".")),
|
|
114
114
|
outputDirOverride: outputDir,
|
|
115
115
|
env,
|
|
@@ -152,7 +152,7 @@ export async function buildSwarmExecutionReport({
|
|
|
152
152
|
reportMarkdownPath,
|
|
153
153
|
};
|
|
154
154
|
|
|
155
|
-
await
|
|
156
|
-
await
|
|
155
|
+
await outputFs.writeFile(reportJsonPath, `${JSON.stringify(report, null, 2)}\n`, "utf-8");
|
|
156
|
+
await outputFs.writeFile(reportMarkdownPath, `${buildReportMarkdown(report).trim()}\n`, "utf-8");
|
|
157
157
|
return report;
|
|
158
158
|
}
|
package/src/swarm/runtime.js
CHANGED
|
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import fsp from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { resolveOutputContext } from "../config/service.js";
|
|
6
6
|
import { evaluateBudget } from "../cost/budget.js";
|
|
7
7
|
import { estimateTokens } from "../cost/tokenizer.js";
|
|
8
8
|
|
|
@@ -121,7 +121,7 @@ function createEvent({
|
|
|
121
121
|
};
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
async function executePlaywrightAction({ page, action, runDirectory, usage, runId, step }) {
|
|
124
|
+
async function executePlaywrightAction({ page, action, runDirectory, usage, runId, step, outputFs }) {
|
|
125
125
|
const type = action.type;
|
|
126
126
|
const metadata = {
|
|
127
127
|
action: type,
|
|
@@ -195,14 +195,15 @@ async function executePlaywrightAction({ page, action, runDirectory, usage, runI
|
|
|
195
195
|
});
|
|
196
196
|
}
|
|
197
197
|
if (type === "screenshot") {
|
|
198
|
-
const
|
|
198
|
+
const requestedOutputPath = action.path
|
|
199
199
|
? path.resolve(runDirectory, action.path)
|
|
200
200
|
: path.join(runDirectory, "runtime", `step-${String(step).padStart(3, "0")}.png`);
|
|
201
|
-
|
|
202
|
-
await
|
|
203
|
-
|
|
201
|
+
const outputPath = await outputFs.assertFilePath(requestedOutputPath);
|
|
202
|
+
await outputFs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
203
|
+
const screenshot = await page.screenshot({
|
|
204
204
|
fullPage: true,
|
|
205
205
|
});
|
|
206
|
+
await outputFs.writeFile(outputPath, screenshot);
|
|
206
207
|
metadata.path = toPosixPath(path.relative(runDirectory, outputPath));
|
|
207
208
|
usage.toolCalls += 1;
|
|
208
209
|
usage.outputTokens += estimateTokens(`screenshot:${metadata.path}`);
|
|
@@ -268,7 +269,7 @@ async function writeRuntimeArtifacts({
|
|
|
268
269
|
env,
|
|
269
270
|
homeDir,
|
|
270
271
|
} = {}) {
|
|
271
|
-
const outputRoot = await
|
|
272
|
+
const { outputRoot, outputFs } = await resolveOutputContext({
|
|
272
273
|
cwd: path.resolve(String(summary.targetPath || ".")),
|
|
273
274
|
outputDirOverride: outputDir,
|
|
274
275
|
env,
|
|
@@ -279,8 +280,8 @@ async function writeRuntimeArtifacts({
|
|
|
279
280
|
const runtimeJsonPath = path.join(runtimeDirectory, "SWARM_RUNTIME.json");
|
|
280
281
|
const runtimeMarkdownPath = path.join(runtimeDirectory, "SWARM_RUNTIME.md");
|
|
281
282
|
const runtimeEventsPath = path.join(runtimeDirectory, "events.ndjson");
|
|
282
|
-
await
|
|
283
|
-
await
|
|
283
|
+
await outputFs.mkdir(runtimeDirectory, { recursive: true });
|
|
284
|
+
await outputFs.writeFile(runtimeEventsPath, `${events.map((event) => JSON.stringify(event)).join("\n")}\n`, "utf-8");
|
|
284
285
|
|
|
285
286
|
const fullSummary = {
|
|
286
287
|
...summary,
|
|
@@ -292,8 +293,8 @@ async function writeRuntimeArtifacts({
|
|
|
292
293
|
runtimeEventsPath,
|
|
293
294
|
};
|
|
294
295
|
|
|
295
|
-
await
|
|
296
|
-
await
|
|
296
|
+
await outputFs.writeFile(runtimeJsonPath, `${JSON.stringify(fullSummary, null, 2)}\n`, "utf-8");
|
|
297
|
+
await outputFs.writeFile(runtimeMarkdownPath, `${buildRuntimeMarkdown(fullSummary).trim()}\n`, "utf-8");
|
|
297
298
|
|
|
298
299
|
return fullSummary;
|
|
299
300
|
}
|
|
@@ -323,7 +324,7 @@ export async function runSwarmRuntime({
|
|
|
323
324
|
const normalizedTargetPath = path.resolve(String(targetPath || plan.targetPath || "."));
|
|
324
325
|
const normalizedMaxSteps = normalizeMaxSteps(maxSteps);
|
|
325
326
|
const runId = createRuntimeRunId();
|
|
326
|
-
const resolvedOutputRoot = await
|
|
327
|
+
const { outputRoot: resolvedOutputRoot, outputFs } = await resolveOutputContext({
|
|
327
328
|
cwd: normalizedTargetPath,
|
|
328
329
|
outputDirOverride: outputDir,
|
|
329
330
|
env,
|
|
@@ -463,6 +464,7 @@ export async function runSwarmRuntime({
|
|
|
463
464
|
const result = await runner(toolInput, {
|
|
464
465
|
targetPath: normalizedTargetPath,
|
|
465
466
|
outputRoot: resolvedOutputRoot,
|
|
467
|
+
outputFs,
|
|
466
468
|
runId: toolInput.runId,
|
|
467
469
|
execute: Boolean(execute),
|
|
468
470
|
env,
|
|
@@ -583,6 +585,7 @@ export async function runSwarmRuntime({
|
|
|
583
585
|
page,
|
|
584
586
|
action,
|
|
585
587
|
runDirectory: runtimeRunDirectory,
|
|
588
|
+
outputFs,
|
|
586
589
|
usage,
|
|
587
590
|
runId,
|
|
588
591
|
step,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fsp from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { resolveOutputContext } from "../config/service.js";
|
|
3
4
|
|
|
4
5
|
const SUPPORTED_ACTIONS = new Set(["goto", "click", "fill", "wait", "screenshot"]);
|
|
5
6
|
|
|
@@ -256,15 +257,16 @@ export async function writeScenarioTemplate({
|
|
|
256
257
|
startUrl = "https://example.com",
|
|
257
258
|
} = {}) {
|
|
258
259
|
const normalizedTargetPath = path.resolve(String(targetPath || "."));
|
|
259
|
-
const resolvedOutputFile =
|
|
260
|
-
|
|
261
|
-
: path.join(
|
|
260
|
+
const { outputRoot: resolvedOutputFile, outputFs } = await resolveOutputContext({
|
|
261
|
+
cwd: normalizedTargetPath,
|
|
262
|
+
outputDirOverride: normalizeString(outputFile) || path.join(".sentinelayer", "scenarios", `${scenarioId}.sls`),
|
|
263
|
+
});
|
|
262
264
|
const content = renderScenarioTemplate({
|
|
263
265
|
scenarioId,
|
|
264
266
|
startUrl,
|
|
265
267
|
});
|
|
266
|
-
await
|
|
267
|
-
await
|
|
268
|
+
await outputFs.mkdir(path.dirname(resolvedOutputFile), { recursive: true });
|
|
269
|
+
await outputFs.writeFile(resolvedOutputFile, `${content.trim()}\n`, "utf-8");
|
|
268
270
|
return {
|
|
269
271
|
filePath: resolvedOutputFile,
|
|
270
272
|
content,
|