@sema-agent/core 2.13.0 → 4.0.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/CHANGELOG.md +2345 -0
- package/dist/agents/cascade.d.ts +6 -1
- package/dist/agents/cascade.js +76 -32
- package/dist/agents/cumulative-stats.d.ts +29 -0
- package/dist/agents/cumulative-stats.js +29 -0
- package/dist/agents/observer.d.ts +4 -0
- package/dist/agents/observer.js +11 -10
- package/dist/agents/repair-loop.d.ts +2 -0
- package/dist/agents/repair-loop.js +110 -3
- package/dist/agents/retain-ledger.d.ts +20 -2
- package/dist/agents/retain-ledger.js +38 -8
- package/dist/agents/roster-store.d.ts +4 -0
- package/dist/agents/roster-store.js +20 -1
- package/dist/agents/subagent.js +66 -64
- package/dist/agents/teacher.d.ts +1 -9
- package/dist/agents/teacher.js +23 -24
- package/dist/agents/team.js +7 -4
- package/dist/agents/verify.d.ts +15 -2
- package/dist/agents/verify.js +39 -3
- package/dist/bin/sema-tb.js +2 -2
- package/dist/brain/anthropic.js +24 -21
- package/dist/brain/circuit-breaker.d.ts +1 -0
- package/dist/brain/circuit-breaker.js +26 -9
- package/dist/brain/degrading.js +13 -1
- package/dist/brain/failover.js +2 -0
- package/dist/brain/openai.js +24 -21
- package/dist/brain/repetition.js +10 -1
- package/dist/brain/routing.js +5 -1
- package/dist/brain/status-sink.js +5 -1
- package/dist/brain/stream-engine.js +5 -2
- package/dist/brain/stream-shared.d.ts +1 -0
- package/dist/brain/stream-shared.js +14 -0
- package/dist/brain/terminal-cause.d.ts +4 -0
- package/dist/brain/terminal-cause.js +12 -0
- package/dist/brain/tool-call-id.d.ts +1 -0
- package/dist/brain/tool-call-id.js +3 -0
- package/dist/brain/tool-call-repair.js +34 -15
- package/dist/core/auto-compaction.js +33 -32
- package/dist/core/background-agent-store.d.ts +22 -22
- package/dist/core/background-agent-store.js +19 -23
- package/dist/core/checkpoint-store.d.ts +7 -13
- package/dist/core/checkpoint-store.js +39 -15
- package/dist/core/consolidate-scope.js +4 -3
- package/dist/core/context-edit.js +3 -0
- package/dist/core/context-guard.js +3 -0
- package/dist/core/file-snapshot-store.d.ts +8 -0
- package/dist/core/file-snapshot-store.js +4 -3
- package/dist/core/git-worktree-env.d.ts +6 -1
- package/dist/core/git-worktree-env.js +12 -1
- package/dist/core/hooks.d.ts +4 -1
- package/dist/core/hooks.js +35 -7
- package/dist/core/lsp-diagnostics.js +13 -7
- package/dist/core/lsp-protocol.d.ts +1 -1
- package/dist/core/lsp-protocol.js +20 -8
- package/dist/core/lsp-session.d.ts +12 -2
- package/dist/core/lsp-session.js +114 -49
- package/dist/core/lsp.d.ts +13 -1
- package/dist/core/lsp.js +65 -9
- package/dist/core/mailbox-store.d.ts +7 -10
- package/dist/core/mcp.d.ts +7 -10
- package/dist/core/mcp.js +7 -3
- package/dist/core/memory-engine/data-plane.js +4 -0
- package/dist/core/memory-engine/engine.d.ts +2 -2
- package/dist/core/memory-engine/engine.js +96 -31
- package/dist/core/memory-engine/file-backend.d.ts +2 -2
- package/dist/core/memory-engine/file-backend.js +38 -39
- package/dist/core/memory-engine/layout.d.ts +8 -0
- package/dist/core/memory-engine/layout.js +71 -4
- package/dist/core/memory-engine/scope-contract.js +9 -3
- package/dist/core/memory-engine/types.d.ts +1 -1
- package/dist/core/memory.js +3 -0
- package/dist/core/permission-rules.d.ts +2 -1
- package/dist/core/permission-rules.js +24 -3
- package/dist/core/runner/active-skill-scope.d.ts +0 -1
- package/dist/core/runner/active-skill-scope.js +1 -15
- package/dist/core/runner/assemble-result.d.ts +3 -8
- package/dist/core/runner/assemble-result.js +3 -5
- package/dist/core/runner/compaction-call-options.d.ts +7 -0
- package/dist/core/runner/compaction-call-options.js +30 -0
- package/dist/core/runner/prepare-task.d.ts +6 -23
- package/dist/core/runner/prepare-task.js +31 -53
- package/dist/core/runner/runtask.d.ts +1 -0
- package/dist/core/runner/runtask.js +86 -178
- package/dist/core/runner/session-rule-policy.js +4 -1
- package/dist/core/runner/teardown-bounded.d.ts +7 -0
- package/dist/core/runner/teardown-bounded.js +36 -0
- package/dist/core/runner/tool-disclosure.d.ts +2 -5
- package/dist/core/runner/turn-attachments.d.ts +5 -15
- package/dist/core/runner/turn-attachments.js +1 -1
- package/dist/core/runner/usage-accounting.d.ts +6 -0
- package/dist/core/runner/usage-accounting.js +9 -8
- package/dist/core/safe-notify.d.ts +17 -0
- package/dist/core/safe-notify.js +59 -0
- package/dist/core/secret-env.js +5 -1
- package/dist/core/sensitive-path-policy.js +11 -4
- package/dist/core/store-contracts/checkpoint-store-contract.js +1 -1
- package/dist/core/store-contracts/session-repo-contract.js +40 -0
- package/dist/core/stub-env.d.ts +2 -6
- package/dist/core/surrogate-safe-slice.d.ts +4 -0
- package/dist/core/surrogate-safe-slice.js +18 -0
- package/dist/core/task-registry-monitor.js +15 -14
- package/dist/core/task-registry-shared.d.ts +22 -0
- package/dist/core/task-registry-shared.js +16 -0
- package/dist/core/task-registry.d.ts +1 -0
- package/dist/core/task-registry.js +40 -57
- package/dist/core/tool-errors.js +5 -4
- package/dist/core/tool-name-aliases.js +2 -1
- package/dist/core/tool-policy.js +72 -19
- package/dist/core/tool-result-store.d.ts +1 -0
- package/dist/core/tool-result-store.js +17 -1
- package/dist/core/trace.d.ts +3 -9
- package/dist/core/types.d.ts +16 -12
- package/dist/core/untrusted-egress.js +7 -5
- package/dist/core/workflow-journal-store.d.ts +9 -20
- package/dist/core/workflow-run-store.d.ts +11 -10
- package/dist/core/workflow-run-store.js +22 -16
- package/dist/engine/compaction/compaction.d.ts +1 -0
- package/dist/engine/compaction/compaction.js +14 -5
- package/dist/engine/compaction/utils.js +4 -2
- package/dist/engine/execution-env/node-execution-env.d.ts +2 -24
- package/dist/engine/harness/agent-harness.d.ts +1 -0
- package/dist/engine/harness/agent-harness.js +13 -33
- package/dist/engine/harness/types.d.ts +29 -67
- package/dist/engine/llm/types.d.ts +1 -1
- package/dist/engine/loop/agent-loop.js +11 -4
- package/dist/engine/loop/types.d.ts +11 -9
- package/dist/engine/lsp/frame-decoder.js +6 -3
- package/dist/engine/lsp/node-lsp-manager.d.ts +2 -0
- package/dist/engine/lsp/node-lsp-manager.js +103 -26
- package/dist/engine/lsp/stdio-lsp-transport.js +16 -7
- package/dist/engine/session/memory-repo.d.ts +2 -6
- package/dist/engine/session/session.d.ts +5 -30
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -1
- package/dist/internal/harness-types.d.ts +3 -1
- package/dist/internal/harness.d.ts +1 -1
- package/dist/internal/harness.js +1 -1
- package/dist/orchestration/run-spec.js +7 -4
- package/dist/orchestration/run-workflow-tool.js +3 -35
- package/dist/orchestration/workflow-governance.js +43 -9
- package/dist/orchestration/workflow-sandbox-conformance.js +85 -51
- package/dist/orchestration/workflow-script-store.js +34 -4
- package/dist/orchestration/workflow.d.ts +3 -11
- package/dist/orchestration/workflow.js +164 -204
- package/dist/prompt-assembly/artifact-store.d.ts +2 -0
- package/dist/prompt-assembly/artifact-store.js +39 -24
- package/dist/prompt-assembly/assemble.js +38 -7
- package/dist/prompt-assembly/epoch.js +19 -2
- package/dist/prompt-assembly/event-registry.js +3 -3
- package/dist/prompt-assembly/explain.js +1 -1
- package/dist/prompt-assembly/tool-catalog.d.ts +10 -9
- package/dist/prompt-assembly/tool-catalog.js +16 -1
- package/dist/prompt-assembly/turn-snapshot.d.ts +2 -5
- package/dist/prompt-assembly/turn-snapshot.js +4 -1
- package/dist/stores/file/background-agent-store.d.ts +3 -12
- package/dist/stores/file/background-agent-store.js +3 -41
- package/dist/stores/file/checkpoint-store.js +6 -6
- package/dist/stores/file/fs-atomic.d.ts +3 -0
- package/dist/stores/file/fs-atomic.js +53 -8
- package/dist/stores/file/index.d.ts +1 -0
- package/dist/stores/file/index.js +7 -0
- package/dist/stores/file/mailbox-store.d.ts +2 -6
- package/dist/stores/file/session-policy-store.js +15 -1
- package/dist/stores/file/session-store.d.ts +4 -6
- package/dist/stores/file/session-store.js +28 -1
- package/dist/stores/file/workflow-journal-store.d.ts +3 -11
- package/dist/stores/file/workflow-run-store.d.ts +3 -7
- package/dist/stores/file/workflow-run-store.js +4 -17
- package/dist/tools/fs/bash-readonly-classifier.d.ts +2 -0
- package/dist/tools/fs/bash-readonly-classifier.js +73 -21
- package/dist/tools/fs/fs-bash.d.ts +1 -1
- package/dist/tools/fs/fs-bash.js +407 -91
- package/dist/tools/fs/fs-pdf.d.ts +2 -12
- package/dist/tools/fs/fs-shared.js +2 -2
- package/dist/tools/fs/notebook.d.ts +2 -5
- package/dist/tools/fs/safety.d.ts +3 -0
- package/dist/tools/fs/safety.js +43 -14
- package/dist/tools/fs/search.js +5 -5
- package/dist/tools/loop-tick.js +1 -1
- package/dist/tools/monitor.d.ts +1 -0
- package/dist/tools/monitor.js +57 -9
- package/dist/tools/worktree.d.ts +4 -7
- package/dist/tools/worktree.js +235 -143
- package/package.json +9 -2
package/dist/tools/worktree.js
CHANGED
|
@@ -47,7 +47,9 @@ const EXIT_DESCRIPTION = "Exit a worktree session created by EnterWorktree and r
|
|
|
47
47
|
"- The directory you're in if EnterWorktree was never called\n" +
|
|
48
48
|
"\n" +
|
|
49
49
|
"If called outside an EnterWorktree session, the tool is a no-op: it reports that no worktree session is " +
|
|
50
|
-
"active and takes no action. Filesystem state is unchanged
|
|
50
|
+
"active and takes no action. Filesystem state is unchanged. The one exception is contradictory arguments " +
|
|
51
|
+
"(`keep: true` together with `discardChanges: true`), which are rejected before anything else, with or " +
|
|
52
|
+
"without an active session — that path changes nothing on disk either.\n" +
|
|
51
53
|
"\n" +
|
|
52
54
|
"## Behavior\n" +
|
|
53
55
|
"\n" +
|
|
@@ -63,6 +65,16 @@ const EXIT_DESCRIPTION = "Exit a worktree session created by EnterWorktree and r
|
|
|
63
65
|
"or the user asked to discard it); it cannot be combined with `keep: true`.\n" +
|
|
64
66
|
"- Once exited, EnterWorktree can be called again to create a fresh worktree.";
|
|
65
67
|
const shq = (s) => `'${s.replace(/'/g, "'\\''")}'`;
|
|
68
|
+
function reasonOf(e) {
|
|
69
|
+
if (e instanceof Error)
|
|
70
|
+
return e.message;
|
|
71
|
+
if (typeof e === "object" && e !== null) {
|
|
72
|
+
const m = e.message;
|
|
73
|
+
if (typeof m === "string" && m !== "")
|
|
74
|
+
return m;
|
|
75
|
+
}
|
|
76
|
+
return String(e);
|
|
77
|
+
}
|
|
66
78
|
async function resolveBaseCommit(env, repoRoot, baseRef) {
|
|
67
79
|
const rev = async (ref) => {
|
|
68
80
|
const r = await env.exec(`git rev-parse --verify --quiet ${shq(`${ref}^{commit}`)}`, { cwd: repoRoot });
|
|
@@ -123,89 +135,105 @@ export function createWorktreeTools(env, opts) {
|
|
|
123
135
|
"Use ExitWorktree to leave it before creating a new worktree, or pass `path` to switch into an " +
|
|
124
136
|
"existing worktree of this repository.");
|
|
125
137
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
return errorResult(`Error (EnterWorktree): the working directory ${repoRoot} is not a git repository` +
|
|
130
|
-
`${detail ? ` (${detail})` : ""}. This deployment has no VCS-agnostic worktree extension point, ` +
|
|
131
|
-
"so worktree isolation is unavailable here.");
|
|
138
|
+
if (session.entering === true) {
|
|
139
|
+
return errorResult("Error (EnterWorktree): another EnterWorktree call on this session is already in progress — wait for it to " +
|
|
140
|
+
"finish and then act on its result. Two concurrent entries would leave one worktree untracked and unremovable.");
|
|
132
141
|
}
|
|
133
|
-
if (
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
return errorResult(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
.split("\n")
|
|
146
|
-
.filter((l) => l.startsWith("worktree "))
|
|
147
|
-
.map((l) => l.slice("worktree ".length).trim());
|
|
148
|
-
if (!registered.includes(target)) {
|
|
149
|
-
return errorResult(`Error (EnterWorktree): ${target} is not a registered worktree of this repository ` +
|
|
150
|
-
"(it does not appear in `git worktree list`).");
|
|
142
|
+
if (session.exiting === true) {
|
|
143
|
+
return errorResult("Error (EnterWorktree): an ExitWorktree call on this session is already in progress — wait for it to finish " +
|
|
144
|
+
"before entering another worktree.");
|
|
145
|
+
}
|
|
146
|
+
session.entering = true;
|
|
147
|
+
try {
|
|
148
|
+
const probe = await env.exec("git rev-parse --is-inside-work-tree", { cwd: repoRoot });
|
|
149
|
+
if (!probe.ok || probe.value.exitCode !== 0) {
|
|
150
|
+
const detail = probe.ok ? (probe.value.stderr || probe.value.stdout).trim().split("\n")[0] : String(probe.error);
|
|
151
|
+
return errorResult(`Error (EnterWorktree): the working directory ${repoRoot} is not a git repository` +
|
|
152
|
+
`${detail ? ` (${detail})` : ""}. This deployment has no VCS-agnostic worktree extension point, ` +
|
|
153
|
+
"so worktree isolation is unavailable here.");
|
|
151
154
|
}
|
|
152
|
-
if (
|
|
153
|
-
|
|
155
|
+
if (path !== undefined) {
|
|
156
|
+
const candidate = resolve(cwdRef.current, path);
|
|
157
|
+
const top = await env.exec("git rev-parse --show-toplevel", { cwd: candidate });
|
|
158
|
+
if (!top.ok || top.value.exitCode !== 0) {
|
|
159
|
+
return errorResult(`Error (EnterWorktree): ${candidate} is not an accessible git worktree (cannot resolve its top-level directory).`);
|
|
160
|
+
}
|
|
161
|
+
const target = top.value.stdout.trim();
|
|
162
|
+
const list = await env.exec("git worktree list --porcelain", { cwd: repoRoot });
|
|
163
|
+
if (!list.ok || list.value.exitCode !== 0) {
|
|
164
|
+
return errorResult("Error (EnterWorktree): could not read `git worktree list` for this repository.");
|
|
165
|
+
}
|
|
166
|
+
const registered = list.value.stdout
|
|
167
|
+
.split("\n")
|
|
168
|
+
.filter((l) => l.startsWith("worktree "))
|
|
169
|
+
.map((l) => l.slice("worktree ".length).trim());
|
|
170
|
+
if (!registered.includes(target)) {
|
|
171
|
+
return errorResult(`Error (EnterWorktree): ${target} is not a registered worktree of this repository ` +
|
|
172
|
+
"(it does not appear in `git worktree list`).");
|
|
173
|
+
}
|
|
174
|
+
if (target === registered[0]) {
|
|
175
|
+
return errorResult(`Error (EnterWorktree): ${target} is the main working tree, not a linked worktree — nothing to enter.`);
|
|
176
|
+
}
|
|
177
|
+
const rootCanon = await env.canonicalPath(repoRoot);
|
|
178
|
+
const fence = rootCanon.ok ? rootCanon.value : repoRoot;
|
|
179
|
+
if (target !== fence && !target.startsWith(fence.endsWith("/") ? fence : `${fence}/`)) {
|
|
180
|
+
return errorResult(`Error (EnterWorktree): the worktree ${target} lives outside the task root ${repoRoot}; ` +
|
|
181
|
+
"file containment stays anchored at the task root, so it cannot be entered from this session.");
|
|
182
|
+
}
|
|
183
|
+
const head = await env.exec("git rev-parse HEAD", { cwd: target });
|
|
184
|
+
if (!head.ok || head.value.exitCode !== 0) {
|
|
185
|
+
return errorResult(`Error (EnterWorktree): cannot resolve HEAD of the worktree ${target}.`);
|
|
186
|
+
}
|
|
187
|
+
const previous = session.current;
|
|
188
|
+
const originalCwd = previous ? previous.originalCwd : cwdRef.current;
|
|
189
|
+
session.current = { worktreeDir: target, originalCwd, baseSha: head.value.stdout.trim(), entered: true };
|
|
190
|
+
cwdRef.current = target;
|
|
191
|
+
return {
|
|
192
|
+
content: `Switched into the existing worktree at ${target}${previous ? ` (the previous worktree at ${previous.worktreeDir} was left on disk, untouched)` : ""}. ` +
|
|
193
|
+
"The session's working directory is now this worktree — subsequent commands and relative paths " +
|
|
194
|
+
"operate there. Note this is a cwd switch, not a sandbox: file containment stays anchored at the " +
|
|
195
|
+
"task root. ExitWorktree will restore the original directory but will NOT remove a worktree " +
|
|
196
|
+
"entered via `path`.",
|
|
197
|
+
details: { type: "worktree", action: "enter", worktreeDir: target },
|
|
198
|
+
};
|
|
154
199
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
200
|
+
const base = await resolveBaseCommit(env, repoRoot, baseRef ?? "fresh");
|
|
201
|
+
if ("error" in base)
|
|
202
|
+
return errorResult(`Error (EnterWorktree): ${base.error}.`);
|
|
203
|
+
let createdDir;
|
|
204
|
+
try {
|
|
205
|
+
const { worktreeDir } = await addWorktree(env, {
|
|
206
|
+
repoRoot,
|
|
207
|
+
sessionId: name ?? "worktree",
|
|
208
|
+
commit: base.sha,
|
|
209
|
+
rootEnvAt: (dir) => Object.create(env, {
|
|
210
|
+
cwd: { value: dir, configurable: true },
|
|
211
|
+
destroy: { value: undefined, writable: true, configurable: true },
|
|
212
|
+
}),
|
|
213
|
+
});
|
|
214
|
+
createdDir = worktreeDir;
|
|
158
215
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
return errorResult(`Error (EnterWorktree): cannot resolve HEAD of the worktree ${target}.`);
|
|
216
|
+
catch (e) {
|
|
217
|
+
return errorResult(`Error (EnterWorktree): ${e instanceof Error ? e.message : String(e)}`);
|
|
162
218
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
219
|
+
session.current = {
|
|
220
|
+
worktreeDir: createdDir,
|
|
221
|
+
originalCwd: cwdRef.current,
|
|
222
|
+
baseSha: base.sha,
|
|
223
|
+
};
|
|
224
|
+
cwdRef.current = createdDir;
|
|
167
225
|
return {
|
|
168
|
-
content: `
|
|
169
|
-
"The session's working directory is now
|
|
226
|
+
content: `Created worktree at ${createdDir} (base: ${base.label}${base.note ?? ""}). ` +
|
|
227
|
+
"The session's working directory is now the worktree — subsequent commands and relative paths " +
|
|
170
228
|
"operate there. Note this is a cwd switch, not a sandbox: file containment stays anchored at the " +
|
|
171
|
-
"task root
|
|
172
|
-
"
|
|
173
|
-
details: { type: "worktree", action: "
|
|
229
|
+
"task root, so the rest of the repository remains reachable. " +
|
|
230
|
+
"Use ExitWorktree to leave (an unchanged worktree is removed; a changed one is kept).",
|
|
231
|
+
details: { type: "worktree", action: "create", worktreeDir: createdDir, base: base.label },
|
|
174
232
|
};
|
|
175
233
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
return errorResult(`Error (EnterWorktree): ${base.error}.`);
|
|
179
|
-
let createdDir;
|
|
180
|
-
try {
|
|
181
|
-
const { worktreeDir } = await addWorktree(env, {
|
|
182
|
-
repoRoot,
|
|
183
|
-
sessionId: name ?? "worktree",
|
|
184
|
-
commit: base.sha,
|
|
185
|
-
rootEnvAt: (dir) => Object.create(env, {
|
|
186
|
-
cwd: { value: dir, configurable: true },
|
|
187
|
-
destroy: { value: undefined, writable: true, configurable: true },
|
|
188
|
-
}),
|
|
189
|
-
});
|
|
190
|
-
createdDir = worktreeDir;
|
|
191
|
-
}
|
|
192
|
-
catch (e) {
|
|
193
|
-
return errorResult(`Error (EnterWorktree): ${e instanceof Error ? e.message : String(e)}`);
|
|
234
|
+
finally {
|
|
235
|
+
session.entering = false;
|
|
194
236
|
}
|
|
195
|
-
session.current = {
|
|
196
|
-
worktreeDir: createdDir,
|
|
197
|
-
originalCwd: cwdRef.current,
|
|
198
|
-
baseSha: base.sha,
|
|
199
|
-
};
|
|
200
|
-
cwdRef.current = createdDir;
|
|
201
|
-
return {
|
|
202
|
-
content: `Created worktree at ${createdDir} (base: ${base.label}${base.note ?? ""}). ` +
|
|
203
|
-
"The session's working directory is now the worktree — subsequent commands and relative paths " +
|
|
204
|
-
"operate there. Note this is a cwd switch, not a sandbox: file containment stays anchored at the " +
|
|
205
|
-
"task root, so the rest of the repository remains reachable. " +
|
|
206
|
-
"Use ExitWorktree to leave (an unchanged worktree is removed; a changed one is kept).",
|
|
207
|
-
details: { type: "worktree", action: "create", worktreeDir: createdDir, base: base.label },
|
|
208
|
-
};
|
|
209
237
|
},
|
|
210
238
|
});
|
|
211
239
|
const exit = defineTool({
|
|
@@ -239,86 +267,150 @@ export function createWorktreeTools(env, opts) {
|
|
|
239
267
|
return ("No-op: there is no active EnterWorktree session to exit. This tool only operates on the " +
|
|
240
268
|
"worktree created by EnterWorktree in this session; no filesystem changes were made.");
|
|
241
269
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
let restoreNote = "";
|
|
246
|
-
const origThere = await env.exists(active.originalCwd);
|
|
247
|
-
if (origThere.ok && !origThere.value) {
|
|
248
|
-
restoredTo = repoRoot;
|
|
249
|
-
restoreNote = ` (the original directory ${active.originalCwd} no longer exists — fell back to the repository root)`;
|
|
270
|
+
if (session.exiting === true) {
|
|
271
|
+
return errorResult("Error (ExitWorktree): an exit of this worktree session is already in progress — wait for it to finish. " +
|
|
272
|
+
"Running a second exit now would race the same removal.");
|
|
250
273
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
`was left on disk, untouched${discardNote}.`,
|
|
258
|
-
details: { type: "worktree", action: "exit", worktreeDir: active.worktreeDir, kept: true },
|
|
259
|
-
};
|
|
274
|
+
const active = session.current;
|
|
275
|
+
session.exiting = true;
|
|
276
|
+
try {
|
|
277
|
+
const outcome = await runExit(active, { keep, discardChanges, wantsRemove });
|
|
278
|
+
session.current = undefined;
|
|
279
|
+
return outcome;
|
|
260
280
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
return {
|
|
265
|
-
content: `Exited the worktree session; working directory restored to ${restoredTo}${restoreNote}. ` +
|
|
266
|
-
`The worktree at ${active.worktreeDir} was already gone from disk (removed externally, not by this ` +
|
|
267
|
-
"session — any uncommitted work in it is lost); stale git worktree metadata was pruned.",
|
|
268
|
-
details: { type: "worktree", action: "exit", worktreeDir: active.worktreeDir, kept: false },
|
|
269
|
-
};
|
|
281
|
+
catch (e) {
|
|
282
|
+
return errorResult(`Error (ExitWorktree): the exit failed before it could complete (${reasonOf(e)}). The worktree session at ` +
|
|
283
|
+
`${active.worktreeDir} is still active and nothing was removed — retry ExitWorktree.`);
|
|
270
284
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const status = await env.exec("git status --porcelain", { cwd: active.worktreeDir });
|
|
274
|
-
if (status.ok && status.value.exitCode === 0) {
|
|
275
|
-
const dirtyFiles = status.value.stdout.split("\n").filter((l) => l.trim() !== "").length;
|
|
276
|
-
const ahead = await env.exec(`git rev-list --count ${shq(`${active.baseSha}..HEAD`)}`, { cwd: active.worktreeDir });
|
|
277
|
-
const commits = ahead.ok && ahead.value.exitCode === 0 ? parseInt(ahead.value.stdout.trim(), 10) || 0 : -1;
|
|
278
|
-
if (commits >= 0) {
|
|
279
|
-
changed = dirtyFiles > 0 || commits > 0;
|
|
280
|
-
const parts = [];
|
|
281
|
-
if (dirtyFiles > 0)
|
|
282
|
-
parts.push(`${dirtyFiles} uncommitted ${dirtyFiles === 1 ? "file" : "files"}`);
|
|
283
|
-
if (commits > 0)
|
|
284
|
-
parts.push(`${commits} new ${commits === 1 ? "commit" : "commits"}`);
|
|
285
|
-
changeDetail = parts.join(" and ");
|
|
286
|
-
}
|
|
285
|
+
finally {
|
|
286
|
+
session.exiting = false;
|
|
287
287
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
async function runExit(active, opts2) {
|
|
291
|
+
const { keep, discardChanges, wantsRemove } = opts2;
|
|
292
|
+
let restoredTo = active.originalCwd;
|
|
293
|
+
let restoreNote = "";
|
|
294
|
+
const origThere = await env.exists(active.originalCwd);
|
|
295
|
+
if (origThere.ok && !origThere.value) {
|
|
296
|
+
restoredTo = repoRoot;
|
|
297
|
+
restoreNote = ` (the original directory ${active.originalCwd} no longer exists — fell back to the repository root)`;
|
|
298
|
+
}
|
|
299
|
+
else if (!origThere.ok) {
|
|
300
|
+
restoreNote = ` (whether ${active.originalCwd} still exists could not be verified: ${reasonOf(origThere.error)} — the directory was restored as-is)`;
|
|
301
|
+
}
|
|
302
|
+
cwdRef.current = restoredTo;
|
|
303
|
+
if (active.entered) {
|
|
304
|
+
const discardNote = discardChanges === true ? " (`discardChanges` does not apply to a worktree this session did not create)" : "";
|
|
305
|
+
const enteredThere = await env.exists(active.worktreeDir);
|
|
306
|
+
if (enteredThere.ok && !enteredThere.value) {
|
|
293
307
|
return {
|
|
294
308
|
content: `Exited the worktree session; working directory restored to ${restoredTo}${restoreNote}. ` +
|
|
295
|
-
`The worktree
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
details: { type: "worktree", action: "exit", worktreeDir: active.worktreeDir, kept: true },
|
|
309
|
+
`The worktree at ${active.worktreeDir} was entered via \`path\` (not created by this session), so this ` +
|
|
310
|
+
`session never touched it — but it is no longer on disk (removed by something else while it was entered)${discardNote}.`,
|
|
311
|
+
details: { type: "worktree", action: "exit", worktreeDir: active.worktreeDir, kept: false },
|
|
299
312
|
};
|
|
300
313
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
:
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
314
|
+
const enteredUnverified = enteredThere.ok ? "" : ` (whether it is still on disk could not be verified: ${reasonOf(enteredThere.error)})`;
|
|
315
|
+
return {
|
|
316
|
+
content: `Exited the worktree session; working directory restored to ${restoredTo}${restoreNote}. ` +
|
|
317
|
+
`The worktree at ${active.worktreeDir} was entered via \`path\` (not created by this session) and ` +
|
|
318
|
+
`was left on disk, untouched${enteredUnverified}${discardNote}.`,
|
|
319
|
+
details: {
|
|
320
|
+
type: "worktree",
|
|
321
|
+
action: "exit",
|
|
322
|
+
worktreeDir: active.worktreeDir,
|
|
323
|
+
...(enteredThere.ok ? { kept: true } : {}),
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
const wtThere = await env.exists(active.worktreeDir);
|
|
328
|
+
if (wtThere.ok && !wtThere.value) {
|
|
329
|
+
const pruned = await pruneWorktrees(env, repoRoot);
|
|
330
|
+
return {
|
|
331
|
+
content: `Exited the worktree session; working directory restored to ${restoredTo}${restoreNote}. ` +
|
|
332
|
+
`The worktree at ${active.worktreeDir} was already gone from disk (removed externally, not by this ` +
|
|
333
|
+
"session — any uncommitted work in it is lost); " +
|
|
334
|
+
(pruned.ok
|
|
335
|
+
? "stale git worktree metadata was pruned."
|
|
336
|
+
: `stale git worktree metadata could NOT be pruned (${pruned.detail}) — run \`git worktree prune\` in ${repoRoot} to clear the leftover registration.`),
|
|
337
|
+
details: {
|
|
338
|
+
type: "worktree",
|
|
339
|
+
action: "exit",
|
|
340
|
+
worktreeDir: active.worktreeDir,
|
|
341
|
+
kept: false,
|
|
342
|
+
...(pruned.ok ? {} : { metadataPruned: false }),
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
let changed = true;
|
|
347
|
+
let changeDetail = "could not verify the worktree state — keeping it to be safe";
|
|
348
|
+
const status = await env.exec("git status --porcelain", { cwd: active.worktreeDir });
|
|
349
|
+
if (status.ok && status.value.exitCode === 0) {
|
|
350
|
+
const dirtyFiles = status.value.stdout.split("\n").filter((l) => l.trim() !== "").length;
|
|
351
|
+
const ahead = await env.exec(`git rev-list --count ${shq(`${active.baseSha}..HEAD`)}`, { cwd: active.worktreeDir });
|
|
352
|
+
const commits = ahead.ok && ahead.value.exitCode === 0 ? parseInt(ahead.value.stdout.trim(), 10) || 0 : -1;
|
|
353
|
+
if (commits >= 0) {
|
|
354
|
+
changed = dirtyFiles > 0 || commits > 0;
|
|
355
|
+
const parts = [];
|
|
356
|
+
if (dirtyFiles > 0)
|
|
357
|
+
parts.push(`${dirtyFiles} uncommitted ${dirtyFiles === 1 ? "file" : "files"}`);
|
|
358
|
+
if (commits > 0)
|
|
359
|
+
parts.push(`${commits} new ${commits === 1 ? "commit" : "commits"}`);
|
|
360
|
+
changeDetail = parts.join(" and ");
|
|
315
361
|
}
|
|
362
|
+
}
|
|
363
|
+
if (keep === true || (changed && discardChanges !== true)) {
|
|
364
|
+
const why = keep === true ? "keep was requested" : `it has ${changeDetail}`;
|
|
365
|
+
const removeHint = wantsRemove && keep !== true
|
|
366
|
+
? " To remove it anyway and permanently discard these changes, re-invoke with discard_changes: true."
|
|
367
|
+
: "";
|
|
316
368
|
return {
|
|
317
369
|
content: `Exited the worktree session; working directory restored to ${restoredTo}${restoreNote}. ` +
|
|
318
|
-
`The worktree at ${active.worktreeDir} ${
|
|
319
|
-
|
|
370
|
+
`The worktree was KEPT at ${active.worktreeDir} (${why}) — inspect or merge it there, and remove it ` +
|
|
371
|
+
"later with `git worktree remove` if no longer needed." +
|
|
372
|
+
removeHint,
|
|
373
|
+
details: { type: "worktree", action: "exit", worktreeDir: active.worktreeDir, kept: true },
|
|
320
374
|
};
|
|
321
|
-
}
|
|
322
|
-
|
|
375
|
+
}
|
|
376
|
+
const removeFailure = await env
|
|
377
|
+
.exec(`git worktree remove --force ${shq(active.worktreeDir)}`, { cwd: repoRoot })
|
|
378
|
+
.then((r) => r.ok && r.value.exitCode === 0
|
|
379
|
+
? undefined
|
|
380
|
+
: r.ok
|
|
381
|
+
? (r.value.stderr || r.value.stdout || `exit ${r.value.exitCode}`).trim()
|
|
382
|
+
: reasonOf(r.error), (e) => reasonOf(e));
|
|
383
|
+
const pruned = await pruneWorktrees(env, repoRoot);
|
|
384
|
+
const stillThere = await env.exists(active.worktreeDir);
|
|
385
|
+
const removalNote = removeFailure !== undefined ? ` (the removal command reported: ${removeFailure})` : "";
|
|
386
|
+
const pruneNote = pruned.ok ? "" : ` (stale git worktree metadata could NOT be pruned: ${pruned.detail} — run \`git worktree prune\` in ${repoRoot})`;
|
|
387
|
+
const pruneDetail = pruned.ok ? {} : { metadataPruned: false };
|
|
388
|
+
const detailForDiscard = changeDetail.startsWith("could not verify")
|
|
389
|
+
? "changes that could not be enumerated"
|
|
390
|
+
: changeDetail;
|
|
391
|
+
const stateClause = changed ? `had ${detailForDiscard} (discarded — discardChanges was set)` : "had no changes";
|
|
392
|
+
if (stillThere.ok && stillThere.value) {
|
|
393
|
+
return {
|
|
394
|
+
content: `Exited the worktree session; working directory restored to ${restoredTo}${restoreNote}. ` +
|
|
395
|
+
`The worktree at ${active.worktreeDir} ${changed ? "was marked for discard" : "had no changes"} but could NOT be removed — it is still on disk${removalNote}${pruneNote}; ` +
|
|
396
|
+
"remove it manually with `git worktree remove --force` if no longer needed.",
|
|
397
|
+
details: { type: "worktree", action: "exit", worktreeDir: active.worktreeDir, kept: true, ...pruneDetail },
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
if (!stillThere.ok) {
|
|
401
|
+
return {
|
|
402
|
+
content: `Exited the worktree session; working directory restored to ${restoredTo}${restoreNote}. ` +
|
|
403
|
+
`The worktree at ${active.worktreeDir} ${stateClause}, and its removal was attempted but could NOT be verified ` +
|
|
404
|
+
`(${reasonOf(stillThere.error)})${removalNote}${pruneNote} — check ${active.worktreeDir} and remove it manually with ` +
|
|
405
|
+
"`git worktree remove --force` if it is still there.",
|
|
406
|
+
details: { type: "worktree", action: "exit", worktreeDir: active.worktreeDir, ...pruneDetail },
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
return {
|
|
410
|
+
content: `Exited the worktree session; working directory restored to ${restoredTo}${restoreNote}. ` +
|
|
411
|
+
`The worktree at ${active.worktreeDir} ${stateClause} and was removed.${removalNote}${pruneNote}`,
|
|
412
|
+
details: { type: "worktree", action: "exit", worktreeDir: active.worktreeDir, kept: false, ...pruneDetail },
|
|
413
|
+
};
|
|
414
|
+
}
|
|
323
415
|
return [enter, exit];
|
|
324
416
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "Stateless, task-oriented AI agent core",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BUSL-1.1",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"dist",
|
|
42
42
|
"README.md",
|
|
43
43
|
"NOTICE.md",
|
|
44
|
+
"CHANGELOG.md",
|
|
44
45
|
"LICENSE"
|
|
45
46
|
],
|
|
46
47
|
"engines": {
|
|
@@ -57,7 +58,13 @@
|
|
|
57
58
|
"eval": "tsx eval/run.ts",
|
|
58
59
|
"prepare": "npm run build",
|
|
59
60
|
"prepublishOnly": "npm run build && npm run test",
|
|
60
|
-
"gate:blackbox": "node scripts/run-blackbox-gate.mjs"
|
|
61
|
+
"gate:blackbox": "node scripts/run-blackbox-gate.mjs",
|
|
62
|
+
"gate:ledger": "node scripts/verify-hardening-ledger.mjs",
|
|
63
|
+
"gate:compensation": "node scripts/verify-compensation-registry.mjs",
|
|
64
|
+
"gate:message-branching": "node scripts/verify-no-message-branching.mjs",
|
|
65
|
+
"gate:failloud": "node scripts/verify-failloud.mjs",
|
|
66
|
+
"gate:domain-lexicon": "node scripts/verify-domain-lexicon.mjs",
|
|
67
|
+
"gate:single-mint": "node scripts/verify-single-mint.mjs"
|
|
61
68
|
},
|
|
62
69
|
"dependencies": {
|
|
63
70
|
"@modelcontextprotocol/sdk": "1.29.0",
|