jeo-code 0.12.9 → 0.12.11
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 +15 -0
- package/README.ja.md +17 -12
- package/README.ko.md +86 -27
- package/README.md +76 -20
- package/README.zh.md +17 -12
- package/package.json +2 -1
- package/src/agent/ast-edit-tool.ts +3 -1
- package/src/agent/engine.ts +114 -16
- package/src/agent/goal-tool.ts +7 -1
- package/src/agent/goal-verifier.ts +95 -16
- package/src/agent/loop-guards.ts +10 -9
- package/src/agent/lsp-rename-tool.ts +3 -1
- package/src/agent/notify/discord-daemon.ts +3 -1
- package/src/agent/notify/paths.ts +8 -0
- package/src/agent/notify/session-endpoint.ts +23 -4
- package/src/agent/notify/slack-daemon.ts +2 -2
- package/src/agent/notify/telegram-daemon.ts +223 -89
- package/src/agent/notify/topic-registry.ts +118 -15
- package/src/agent/skill-router.ts +31 -5
- package/src/agent/subagent-registry.ts +87 -5
- package/src/agent/subagent-tool.ts +133 -18
- package/src/agent/subagents.ts +7 -5
- package/src/agent/task-tool.ts +85 -14
- package/src/agent/tools.ts +14 -4
- package/src/cli.ts +1 -0
- package/src/commands/launch/flags.ts +2 -1
- package/src/commands/launch.ts +163 -16
- package/src/commands/messenger.ts +7 -1
- package/src/commands/notify.ts +5 -6
- package/src/commands/skills.ts +9 -0
- package/src/commands/team.ts +72 -53
- package/src/messenger/config.ts +8 -3
- package/src/messenger/discord-setup.ts +3 -1
- package/src/messenger/discord.ts +5 -1
- package/src/messenger/session-pool.ts +99 -13
- package/src/messenger/types.ts +2 -0
- package/src/skills/catalog.ts +19 -2
package/src/commands/launch.ts
CHANGED
|
@@ -62,7 +62,7 @@ import { rememberModelPatch, recentModelsForDisplay } from "../agent/model-recen
|
|
|
62
62
|
import { describeModel, describeAllProviders, describeProvider, resolveProvider, thinkingMaxTokens, resolveMaxOutputTokens, thinkingToReasoningEffort, discoverModels, flattenModels, resolveSelection, catalogMetadata, catalogByProvider, liveProviderCatalogModels, resolveRoleModel, CODEX_MODELS, qualifyModelId, modelServableWithConfig, isLocalProviderReachable } from "../ai";
|
|
63
63
|
import { rehydrateLiveModels, writeModelCache } from "../ai/model-cache";
|
|
64
64
|
import type { ProviderModelsResult, PickEntry, ProviderName, ModelRole, ThinkLevel } from "../ai";
|
|
65
|
-
import { readGoalState, writeGoalState, clearGoalState, verifyGoal, applyEvidenceGate } from "../agent/goal-verifier";
|
|
65
|
+
import { GoalStateCorruptError, readGoalState, writeGoalState, clearGoalState, verifyGoal, applyEvidenceGate, applyPendingEvidence, type GoalState } from "../agent/goal-verifier";
|
|
66
66
|
import { routePrompt, deriveCacheSessionKey, warnOnce, tierModelPool, selectFromPool, PROMPT_TIERS, withRoutingTierSetting, inferTierForModel, credentialScopeFor, resolveVerifierModel, type PromptTier, type RouteDecision, type RoutingConfig } from "../agent/prompt-router";
|
|
67
67
|
import { RouteHistory } from "../agent/route-history";
|
|
68
68
|
|
|
@@ -122,6 +122,7 @@ import { bashTool } from "../agent/tools";
|
|
|
122
122
|
import { loadProjectContext, withProjectContext } from "../agent/context-files";
|
|
123
123
|
import { maybeCompact, historyTokens, buildHandoffDocument } from "../agent/compaction";
|
|
124
124
|
import * as path from "node:path";
|
|
125
|
+
import { tmpdir } from "node:os";
|
|
125
126
|
import * as fs from "node:fs";
|
|
126
127
|
import { listThemes, resolveTheme, themeGradient, accentPaint, accentShadowPaint } from "../tui/components/themes";
|
|
127
128
|
import {
|
|
@@ -739,7 +740,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
739
740
|
// never persisted to config.
|
|
740
741
|
const routeHistory = new RouteHistory();
|
|
741
742
|
// Computer-use session override: undefined = follow config.computer.enabled, true/false = /computer on|off wins this session.
|
|
742
|
-
let sessionComputerOverride: boolean | undefined;
|
|
743
|
+
let sessionComputerOverride: boolean | undefined = flags.literalWorker && process.env.JEO_MESSENGER_COMPUTER_ENABLED === "1" ? true : undefined;
|
|
743
744
|
|
|
744
745
|
// Cache of live, credential-validated models per provider (refreshed by live pickers).
|
|
745
746
|
// Every successful discovery is also persisted (`writeModelCache`), so the NEXT launch
|
|
@@ -971,6 +972,15 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
971
972
|
// Session-scoped registries for DETACHED subagents, background jobs, and monitors (gjc parity).
|
|
972
973
|
// Created ONCE for the whole launch session lifetime (not per-turn) so detached
|
|
973
974
|
// runs persist across turns and remain controllable/inspectable.
|
|
975
|
+
let pendingMutationVerification = false;
|
|
976
|
+
try {
|
|
977
|
+
pendingMutationVerification = (await readGoalState(cwd))?.pendingMutationVerification === true;
|
|
978
|
+
} catch (error) {
|
|
979
|
+
if (!(error instanceof GoalStateCorruptError)) throw error;
|
|
980
|
+
pendingMutationVerification = true;
|
|
981
|
+
console.error(`${error.message}. Run /goal clear to remove it; fresh verification is still required.`);
|
|
982
|
+
}
|
|
983
|
+
|
|
974
984
|
const subagentRegistry = new SubagentRegistry();
|
|
975
985
|
const jobRegistry = new JobRegistry();
|
|
976
986
|
let activeTui: LaunchTui | null = null;
|
|
@@ -1025,8 +1035,27 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1025
1035
|
// rolling base — the engine's onBudget event keeps the denominator honest as it grows.
|
|
1026
1036
|
const initialStepLimit = flags.maxSteps > 0 ? flags.maxSteps : initialDynamicStepLimit();
|
|
1027
1037
|
// Plain (non-TTY / --no-tui) progress sink — the cmd-mode equivalent of the live TUI.
|
|
1028
|
-
const streamEvents = flags.literalPrompt
|
|
1029
|
-
? {
|
|
1038
|
+
const streamEvents = (flags.literalPrompt || flags.literalWorker)
|
|
1039
|
+
? {
|
|
1040
|
+
onStep: () => {
|
|
1041
|
+
process.stderr.write("JEO_MESSENGER_STEP\n");
|
|
1042
|
+
},
|
|
1043
|
+
onAssistant: (_raw: string, invocation: any) => {
|
|
1044
|
+
if (invocation?.tool) {
|
|
1045
|
+
const summary = summarizeForgeInvocation(invocation.tool, invocation.arguments);
|
|
1046
|
+
process.stderr.write(`JEO_MESSENGER_ACTIVITY ${summary.title}\n`);
|
|
1047
|
+
}
|
|
1048
|
+
},
|
|
1049
|
+
onToolResult: (tool: string) => {
|
|
1050
|
+
process.stderr.write(`JEO_MESSENGER_ACTIVITY Finished ${tool}\n`);
|
|
1051
|
+
},
|
|
1052
|
+
onNotice: (msg: string) => {
|
|
1053
|
+
process.stderr.write(`JEO_MESSENGER_ACTIVITY ${msg}\n`);
|
|
1054
|
+
},
|
|
1055
|
+
onReasoningStart: () => {
|
|
1056
|
+
process.stderr.write("JEO_MESSENGER_ACTIVITY Thinking...\n");
|
|
1057
|
+
},
|
|
1058
|
+
}
|
|
1030
1059
|
: createStreamEvents(initialStepLimit);
|
|
1031
1060
|
let queueBusyInput: ((chunk: string) => boolean) | undefined;
|
|
1032
1061
|
let queueBusyPasteActive: (() => boolean) | undefined;
|
|
@@ -1716,10 +1745,61 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1716
1745
|
// Todos checklist used to end a finished turn stuck at "✓0 ◐1 ·4 / 5"
|
|
1717
1746
|
// because nothing ever forced the model to update item statuses).
|
|
1718
1747
|
let turnTodos: { title: string; status: string }[] = [];
|
|
1748
|
+
const onEvidence = async (
|
|
1749
|
+
evidence: { sawMutation: boolean; sawVerification: boolean; verificationStale: boolean },
|
|
1750
|
+
): Promise<void> => {
|
|
1751
|
+
if (evidence.sawVerification && !evidence.verificationStale) {
|
|
1752
|
+
pendingMutationVerification = false;
|
|
1753
|
+
} else if (evidence.sawMutation) {
|
|
1754
|
+
pendingMutationVerification = true;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
let goalState: GoalState | null;
|
|
1758
|
+
try {
|
|
1759
|
+
goalState = await readGoalState(cwd);
|
|
1760
|
+
} catch (error) {
|
|
1761
|
+
if (error instanceof GoalStateCorruptError) return;
|
|
1762
|
+
throw error;
|
|
1763
|
+
}
|
|
1764
|
+
if (!goalState) return;
|
|
1765
|
+
const updatedState = applyPendingEvidence(
|
|
1766
|
+
{ ...goalState, pendingMutationVerification },
|
|
1767
|
+
evidence,
|
|
1768
|
+
);
|
|
1769
|
+
pendingMutationVerification = updatedState.pendingMutationVerification === true;
|
|
1770
|
+
await writeGoalState(updatedState, cwd);
|
|
1771
|
+
};
|
|
1772
|
+
|
|
1719
1773
|
const onBeforeDone = async (
|
|
1720
1774
|
reason: string,
|
|
1721
1775
|
evidence: { sawMutation: boolean; sawVerification: boolean; verificationStale: boolean },
|
|
1722
1776
|
): Promise<string | null> => {
|
|
1777
|
+
const runningJobs = jobRegistry.running();
|
|
1778
|
+
if (runningJobs.length > 0) {
|
|
1779
|
+
const jobs = runningJobs.map(record => `${record.id} (${record.command})`).join(", ");
|
|
1780
|
+
return (
|
|
1781
|
+
`Cannot finish while background job(s) are still running: ${jobs}. ` +
|
|
1782
|
+
`Wait for them to exit or cancel them, then call done again.`
|
|
1783
|
+
);
|
|
1784
|
+
}
|
|
1785
|
+
const runningMonitors = monitorRegistry.running();
|
|
1786
|
+
if (runningMonitors.length > 0) {
|
|
1787
|
+
const monitors = runningMonitors.map(record => `${record.id} (${record.command})`).join(", ");
|
|
1788
|
+
return (
|
|
1789
|
+
`Cannot finish while background monitor(s) are still running: ${monitors}. ` +
|
|
1790
|
+
`Wait for them to exit or cancel them, then call done again.`
|
|
1791
|
+
);
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
const inFlightMutators = subagentRegistry.inFlightMutators();
|
|
1795
|
+
if (inFlightMutators.length > 0) {
|
|
1796
|
+
const agents = inFlightMutators.map(record => `${record.id} (${record.role})`).join(", ");
|
|
1797
|
+
return (
|
|
1798
|
+
`Cannot finish while mutating subagent(s) are still in flight: ${agents}. ` +
|
|
1799
|
+
`Wait for them to settle after completion or cancellation, then call done again.`
|
|
1800
|
+
);
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1723
1803
|
const unfinished = turnTodos.filter(t => t.status !== "done");
|
|
1724
1804
|
if (turnTodos.length > 0 && unfinished.length > 0) {
|
|
1725
1805
|
return (
|
|
@@ -1728,8 +1808,22 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1728
1808
|
`(drop items that no longer apply), then call done again.`
|
|
1729
1809
|
);
|
|
1730
1810
|
}
|
|
1811
|
+
let goalState: GoalState | null;
|
|
1812
|
+
try {
|
|
1813
|
+
goalState = await readGoalState(cwd);
|
|
1814
|
+
} catch (error) {
|
|
1815
|
+
if (error instanceof GoalStateCorruptError) {
|
|
1816
|
+
return `Cannot finish: persisted goal state is corrupt. Run /goal clear, then verify any workspace mutations before calling done again.`;
|
|
1817
|
+
}
|
|
1818
|
+
throw error;
|
|
1819
|
+
}
|
|
1820
|
+
if ((pendingMutationVerification || evidence.sawMutation) && !(evidence.sawVerification && !evidence.verificationStale)) {
|
|
1821
|
+
return (
|
|
1822
|
+
`Cannot finish: a workspace mutation from this session still lacks a successful, non-stale verification. ` +
|
|
1823
|
+
`Run a relevant test/build/typecheck/lint after the last change, then call done again.`
|
|
1824
|
+
);
|
|
1825
|
+
}
|
|
1731
1826
|
|
|
1732
|
-
const goalState = await readGoalState(cwd);
|
|
1733
1827
|
if (goalState && goalState.condition) {
|
|
1734
1828
|
if (tui) tui.events().onNotice?.("[Goal Verifier] Running goal verification...");
|
|
1735
1829
|
const llmVerdict = await verifyGoal(goalState.condition, history, resolveVerifierModel(turnConfig));
|
|
@@ -1737,7 +1831,11 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1737
1831
|
// judge still needs mutation/verification evidence to back it up —
|
|
1738
1832
|
// see applyEvidenceGate's own docs (goal-verifier.ts). NOT_MET/
|
|
1739
1833
|
// IMPOSSIBLE pass through unchanged; this only ever tightens MET.
|
|
1740
|
-
const
|
|
1834
|
+
const effectiveEvidence = {
|
|
1835
|
+
...evidence,
|
|
1836
|
+
pendingMutation: goalState.pendingMutationVerification === true,
|
|
1837
|
+
};
|
|
1838
|
+
const verdict = applyEvidenceGate(llmVerdict, effectiveEvidence);
|
|
1741
1839
|
|
|
1742
1840
|
goalState.verdicts.push({
|
|
1743
1841
|
at: Date.now(),
|
|
@@ -1895,7 +1993,8 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1895
1993
|
// with the reroute loop below, which `routingEnabled` also gates.
|
|
1896
1994
|
...(routingEnabled ? { rateLimitFallbackAvailable, safetyFallbackAvailable } : {}),
|
|
1897
1995
|
|
|
1898
|
-
events: wrapEvents(withStepPersistence({ ...withToolDetailCapture(tui ? tui.events() : streamEvents), onBeforeDone }, persistTurnTail), opik),
|
|
1996
|
+
events: wrapEvents(withStepPersistence({ ...withToolDetailCapture(tui ? tui.events() : streamEvents), onBeforeDone, onEvidence }, persistTurnTail), opik),
|
|
1997
|
+
|
|
1899
1998
|
});
|
|
1900
1999
|
const routeFailureReason = (doneReason: unknown): string | null => {
|
|
1901
2000
|
const message = String(doneReason ?? "");
|
|
@@ -2236,6 +2335,8 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2236
2335
|
console.log("JEO_MESSENGER_ERROR");
|
|
2237
2336
|
break;
|
|
2238
2337
|
}
|
|
2338
|
+
|
|
2339
|
+
|
|
2239
2340
|
}
|
|
2240
2341
|
} finally { input.close(); }
|
|
2241
2342
|
return;
|
|
@@ -3152,6 +3253,9 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
3152
3253
|
let typedLine = ""; // the user-typed line (restored after readline's history nav)
|
|
3153
3254
|
let pendingSelection: string | undefined; // command chosen via arrows, applied on Enter
|
|
3154
3255
|
let pendingImages: ImageAttachment[] = []; // clipboard images attached to the next message (ctrl+v)
|
|
3256
|
+
// Remote image paths are already ingested before prompt injection. Count the
|
|
3257
|
+
// associated text so the general typed-path scanner does not read it again.
|
|
3258
|
+
const remoteAttachmentTextCounts = new Map<string, number>();
|
|
3155
3259
|
let pasteInFlight = false; // guard concurrent ctrl+v clipboard reads
|
|
3156
3260
|
let imagePasteInFlight = false;
|
|
3157
3261
|
let idleDirtyCount: number | undefined; // git dirty count refreshed once per prompt
|
|
@@ -4508,13 +4612,31 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
4508
4612
|
// ingestion mechanism, not a parallel one. Bare paths never contain
|
|
4509
4613
|
// whitespace here (daemon-generated temp names), so simple whitespace
|
|
4510
4614
|
// joining is a safe token boundary for the path-token scanner.
|
|
4511
|
-
const
|
|
4512
|
-
|
|
4513
|
-
|
|
4615
|
+
const remoteImagePaths = msg.imagePaths ?? [];
|
|
4616
|
+
// Never scan remote text for local paths: a Telegram user must not be
|
|
4617
|
+
// able to make the REPL read arbitrary files by mentioning one. Only
|
|
4618
|
+
// daemon-owned temp attachments are eligible for image ingestion.
|
|
4619
|
+
const tempRoot = path.resolve(tmpdir());
|
|
4620
|
+
const validImagePaths = remoteImagePaths.filter((filePath) => {
|
|
4621
|
+
if (!path.isAbsolute(filePath)) return false;
|
|
4622
|
+
const relative = path.relative(tempRoot, path.resolve(filePath));
|
|
4623
|
+
const parts = relative.split(path.sep);
|
|
4624
|
+
return parts.length === 2 && parts[0]!.startsWith("jeo-telegram-") && parts[1] !== "" && parts[1] !== "." && parts[1] !== "..";
|
|
4625
|
+
});
|
|
4626
|
+
if (validImagePaths.length !== remoteImagePaths.length) return false;
|
|
4627
|
+
const attached = validImagePaths.length
|
|
4628
|
+
? await attachImagePaths(validImagePaths.join(" "), pendingImages.length + 1)
|
|
4629
|
+
: { text: msg.text, images: [] };
|
|
4630
|
+
// Attachment parsing receives paths only; preserve the remote message
|
|
4631
|
+
// text verbatim so an image never replaces the user's prompt.
|
|
4632
|
+
const cleanedText = msg.text;
|
|
4514
4633
|
// Attachment normalization must not expose a remote slash command that
|
|
4515
4634
|
// bypasses the local approval prompt or changes local execution policy.
|
|
4516
|
-
if (cleanedText.trimStart().startsWith("/")) return false;
|
|
4517
|
-
if (attached.images.length)
|
|
4635
|
+
if (cleanedText.trimStart().startsWith("/") || cleanedText.trimStart().startsWith("!")) return false;
|
|
4636
|
+
if (attached.images.length) {
|
|
4637
|
+
pendingImages.push(...attached.images);
|
|
4638
|
+
remoteAttachmentTextCounts.set(cleanedText, (remoteAttachmentTextCounts.get(cleanedText) ?? 0) + 1);
|
|
4639
|
+
}
|
|
4518
4640
|
|
|
4519
4641
|
if (interactiveTurnActive && currentTurnSteer) {
|
|
4520
4642
|
// Busy: steer the running turn exactly like a local mid-turn Enter.
|
|
@@ -4537,7 +4659,10 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
4537
4659
|
};
|
|
4538
4660
|
sessionNotifyEndpoint.onConfigCommand = (cmd): void => {
|
|
4539
4661
|
if (cmd.verbosity !== undefined) notifyVerbosity = cmd.verbosity;
|
|
4540
|
-
if (cmd.redact !== undefined)
|
|
4662
|
+
if (cmd.redact !== undefined) {
|
|
4663
|
+
notifyRedact = cmd.redact;
|
|
4664
|
+
sessionNotifyEndpoint.setRedact(cmd.redact);
|
|
4665
|
+
}
|
|
4541
4666
|
};
|
|
4542
4667
|
}
|
|
4543
4668
|
|
|
@@ -4660,7 +4785,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
4660
4785
|
// /login→/provider login, /settings→/config, /subagent(s)→/agents.
|
|
4661
4786
|
input = normalizeSlashAlias(input);
|
|
4662
4787
|
pendingSelection = undefined;
|
|
4663
|
-
|
|
4788
|
+
navMatches = [];
|
|
4664
4789
|
navIdx = -1;
|
|
4665
4790
|
if (input === "/discord" || input.startsWith("/discord ")) {
|
|
4666
4791
|
const parts = input.split(/\s+/);
|
|
@@ -4711,11 +4836,17 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
4711
4836
|
}
|
|
4712
4837
|
continue;
|
|
4713
4838
|
}
|
|
4839
|
+
|
|
4840
|
+
const remoteAttachmentCount = remoteAttachmentTextCounts.get(input) ?? 0;
|
|
4841
|
+
if (remoteAttachmentCount > 0) {
|
|
4842
|
+
if (remoteAttachmentCount === 1) remoteAttachmentTextCounts.delete(input);
|
|
4843
|
+
else remoteAttachmentTextCounts.set(input, remoteAttachmentCount - 1);
|
|
4844
|
+
}
|
|
4845
|
+
if (remoteAttachmentCount === 0 && input && !input.startsWith("/") && !input.startsWith("!")) {
|
|
4714
4846
|
// File attachment: a dragged-and-dropped image file arrives as its path in
|
|
4715
4847
|
// the typed text. Read any image path(s), attach them, and swap the path for
|
|
4716
4848
|
// an [image #N] tag (continuing the clipboard-image numbering) — the same
|
|
4717
4849
|
// scheme Ctrl+V uses. Skipped for slash commands and `!` shell escapes.
|
|
4718
|
-
if (input && !input.startsWith("/") && !input.startsWith("!")) {
|
|
4719
4850
|
const dropped = await attachImagePaths(input, pendingImages.length + 1);
|
|
4720
4851
|
if (dropped.images.length > 0) {
|
|
4721
4852
|
pendingImages.push(...dropped.images);
|
|
@@ -4956,7 +5087,14 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
4956
5087
|
if (input === "/goal" || input.startsWith("/goal ")) {
|
|
4957
5088
|
const arg = input.substring(5).trim();
|
|
4958
5089
|
if (!arg) {
|
|
4959
|
-
|
|
5090
|
+
let current: GoalState | null;
|
|
5091
|
+
try {
|
|
5092
|
+
current = await readGoalState(cwd);
|
|
5093
|
+
} catch (error) {
|
|
5094
|
+
if (!(error instanceof GoalStateCorruptError)) throw error;
|
|
5095
|
+
console.error(`${error.message}. Run /goal clear to remove it.`);
|
|
5096
|
+
continue;
|
|
5097
|
+
}
|
|
4960
5098
|
if (current) {
|
|
4961
5099
|
console.log(`Current goal: "${current.condition}" (set at ${new Date(current.setAt).toLocaleTimeString()})`);
|
|
4962
5100
|
} else {
|
|
@@ -4970,10 +5108,19 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
4970
5108
|
console.log("Goal cleared.");
|
|
4971
5109
|
continue;
|
|
4972
5110
|
}
|
|
5111
|
+
let previous: GoalState | null;
|
|
5112
|
+
try {
|
|
5113
|
+
previous = await readGoalState(cwd);
|
|
5114
|
+
} catch (error) {
|
|
5115
|
+
if (!(error instanceof GoalStateCorruptError)) throw error;
|
|
5116
|
+
console.error(`${error.message}. Run /goal clear before setting a new goal.`);
|
|
5117
|
+
continue;
|
|
5118
|
+
}
|
|
4973
5119
|
const state = {
|
|
4974
5120
|
condition: arg,
|
|
4975
5121
|
setAt: Date.now(),
|
|
4976
5122
|
verdicts: [],
|
|
5123
|
+
...(previous?.pendingMutationVerification ? { pendingMutationVerification: true } : {}),
|
|
4977
5124
|
};
|
|
4978
5125
|
await writeGoalState(state, cwd);
|
|
4979
5126
|
console.log(`Goal set to: "${arg}"`);
|
|
@@ -7,7 +7,7 @@ import { loadMessengerConfig, validateMessengerEnvironment } from "../messenger/
|
|
|
7
7
|
import { createDiscordAdapter } from "../messenger/discord";
|
|
8
8
|
import { setupDiscordMessenger } from "../messenger/discord-setup";
|
|
9
9
|
import { messengerArtifactPrompt } from "../messenger/artifacts";
|
|
10
|
-
import { createMessengerGateway, messengerSessionRoute } from "../messenger/gateway";
|
|
10
|
+
import { createMessengerGateway, messengerSessionRoute, messengerConversationKey } from "../messenger/gateway";
|
|
11
11
|
import { createMessengerSessionPool } from "../messenger/session-pool";
|
|
12
12
|
import { createSlackAdapter } from "../messenger/slack";
|
|
13
13
|
import { createTelegramAdapter } from "../messenger/telegram";
|
|
@@ -57,6 +57,10 @@ function terminalIO() {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
function routeUuid(config: MessengerConfig, message: Parameters<MessengerReply>[0]): string {
|
|
60
|
+
if (config.independentSessions) {
|
|
61
|
+
const hex = new Bun.CryptoHasher("sha256").update(JSON.stringify([config.cwd, messengerConversationKey(message), message.messageId])).digest("hex");
|
|
62
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-a${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
|
|
63
|
+
}
|
|
60
64
|
const hex = messengerSessionRoute(config.cwd, message);
|
|
61
65
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-a${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
|
|
62
66
|
}
|
|
@@ -114,6 +118,8 @@ export function createMessengerCliReply(config: MessengerConfig, spawn: typeof B
|
|
|
114
118
|
const env = Object.fromEntries(Object.entries(process.env).filter(([name, value]) => value !== undefined && !secretNames.has(name))) as Record<string, string>;
|
|
115
119
|
for (const name of secretNames) env[name] = "";
|
|
116
120
|
env.JEO_SESSION_DIR = config.sessionDir;
|
|
121
|
+
// The explicit tool allowlist is the opt-in for this worker only.
|
|
122
|
+
env.JEO_MESSENGER_COMPUTER_ENABLED = tools.includes("computer") ? "1" : "0";
|
|
117
123
|
// Never inherit another platform's fallback policy from the gateway environment.
|
|
118
124
|
delete env.JEO_MESSENGER_USAGE_FALLBACK_MODEL;
|
|
119
125
|
if (platformConfig?.usageFallbackModel) env.JEO_MESSENGER_USAGE_FALLBACK_MODEL = platformConfig.usageFallbackModel;
|
package/src/commands/notify.ts
CHANGED
|
@@ -15,7 +15,6 @@ interface NotifyOptions {
|
|
|
15
15
|
}
|
|
16
16
|
interface Flags {
|
|
17
17
|
provider: Provider;
|
|
18
|
-
token?: string;
|
|
19
18
|
tokenEnv?: string;
|
|
20
19
|
appTokenEnv?: string;
|
|
21
20
|
chatId?: string;
|
|
@@ -27,13 +26,13 @@ function parseFlags(args: string[]): Flags {
|
|
|
27
26
|
const out: Flags = { provider: "telegram" };
|
|
28
27
|
for (let i = 0; i < args.length; i++) {
|
|
29
28
|
const flag = args[i];
|
|
29
|
+
if (flag === "--token") throw new Error("Raw --token credentials are not supported; use --token-env NAME");
|
|
30
30
|
const value = args[++i];
|
|
31
31
|
if (!value || value.startsWith("--")) throw new Error("Missing value for notify option (use --token-env NAME for credentials)");
|
|
32
32
|
if (flag === "--provider" || flag === "--platform") {
|
|
33
33
|
if (value !== "telegram" && value !== "discord" && value !== "slack") throw new Error("Provider must be telegram, discord or slack");
|
|
34
34
|
out.provider = value;
|
|
35
|
-
} else if (flag === "--token") out.
|
|
36
|
-
else if (flag === "--token-env") out.tokenEnv = value;
|
|
35
|
+
} else if (flag === "--token-env") out.tokenEnv = value;
|
|
37
36
|
else if (flag === "--app-token-env") out.appTokenEnv = value;
|
|
38
37
|
else if (flag === "--chat-id") out.chatId = value;
|
|
39
38
|
else if (flag === "--channel-id") out.channelId = value;
|
|
@@ -73,12 +72,12 @@ async function verifySlack(api: SlackApi, channelId: string): Promise<void> {
|
|
|
73
72
|
|
|
74
73
|
async function runSetup(flags: Flags, opts: NotifyOptions): Promise<void> {
|
|
75
74
|
const envName = flags.tokenEnv ?? `JEO_${flags.provider.toUpperCase()}_BOT_TOKEN`;
|
|
76
|
-
let token =
|
|
75
|
+
let token = process.env[envName]?.trim();
|
|
77
76
|
if (!token) {
|
|
78
77
|
const name = await question(`Set the bot token in an environment variable, then enter its NAME (default ${envName}): `, opts);
|
|
79
78
|
token = process.env[name || envName]?.trim();
|
|
80
79
|
}
|
|
81
|
-
if (!token) throw new Error(
|
|
80
|
+
if (!token) throw new Error("Bot token environment variable is empty; use --token-env NAME");
|
|
82
81
|
const existing = (await readGlobalConfig()).notifications;
|
|
83
82
|
let allowedUserIds = flags.allowedUserIds;
|
|
84
83
|
if (flags.provider === "slack") {
|
|
@@ -237,7 +236,7 @@ export async function runNotifyCommand(args: string[], opts: NotifyOptions = {})
|
|
|
237
236
|
} catch (error) {
|
|
238
237
|
// Only emit our known configuration/API errors, never fetch URLs or token-bearing bodies.
|
|
239
238
|
const message = error instanceof Error ? error.message : "request failed";
|
|
240
|
-
const safe = /^(Missing |Unknown notify|Provider |Allowed user|Telegram |Discord |Slack |Bot token|Missing setup|A daemon|Notification lock recovery|Usage:)/.test(message) && !message.includes("https:") && !message.includes("http:");
|
|
239
|
+
const safe = /^(Missing |Unknown notify|Raw --token|Provider |Allowed user|Telegram |Discord |Slack |Bot token|Missing setup|A daemon|Notification lock recovery|Usage:)/.test(message) && !message.includes("https:") && !message.includes("http:");
|
|
241
240
|
console.error(`Notification ${sub} failed: ${safe ? message : "API or storage request failed; check credentials, connectivity and permissions"}${(error instanceof DiscordApiError || error instanceof SlackApiError) && error.status ? ` (HTTP ${error.status})` : ""}`);
|
|
242
241
|
process.exitCode = 1;
|
|
243
242
|
}
|
package/src/commands/skills.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { SKILLS, getSkillFrom, formatSkill, loadSkills, skillDirs, bundledSkillFileContent, userSkillsDir } from "../skills/catalog";
|
|
4
|
+
import { inventorySkillConflicts } from "../agent/skill-router";
|
|
4
5
|
import { getLocalJeoDir } from "../agent/state";
|
|
5
6
|
import { appendSkillLesson, evalSkillLessons, type SkillLesson } from "../agent/skill-lessons";
|
|
6
7
|
|
|
@@ -221,6 +222,14 @@ export async function runSkillsCommand(args: string[] = []): Promise<void> {
|
|
|
221
222
|
console.log("Install/inspect bundled defaults: 'jeo skills sync' (preserve local) · '--check' (drift report) · '--force' (overwrite local)");
|
|
222
223
|
console.log("Discovery dirs (later wins on name clash; JEO_SKILLS_DIR adds more):");
|
|
223
224
|
for (const d of skillDirs(cwd)) console.log(` ${d}`);
|
|
225
|
+
const conflicts = inventorySkillConflicts(skills);
|
|
226
|
+
if (conflicts.length > 0) {
|
|
227
|
+
console.log("\nECC Single-Harness duplicate identity warnings:");
|
|
228
|
+
for (const c of conflicts) {
|
|
229
|
+
console.log(` ! Duplicate skill '${c.normalized}' installed across multiple distinct paths:`);
|
|
230
|
+
for (const p of c.paths) console.log(` ${p}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
224
233
|
console.log("");
|
|
225
234
|
}
|
|
226
235
|
return;
|