jeo-code 0.12.10 → 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 +7 -0
- package/README.ja.md +16 -11
- package/README.ko.md +85 -26
- package/README.md +75 -19
- package/README.zh.md +16 -11
- 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.ts +162 -15
- package/src/commands/messenger.ts +5 -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 +6 -2
- 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/agent/subagents.ts
CHANGED
|
@@ -258,14 +258,16 @@ export function subagentSystemPrompt(role: SubagentRole): string {
|
|
|
258
258
|
|
|
259
259
|
/**
|
|
260
260
|
* True when EVERY shell segment of `command` starts with one of `prefixes`
|
|
261
|
-
* (after stripping leading env-assignments and a leading `sudo`).
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
261
|
+
* (after stripping leading env-assignments and a leading `sudo`). Only `&&`,
|
|
262
|
+
* `||`, `;`, and `|` chaining is understood; other shell operators, newlines,
|
|
263
|
+
* and substitutions are rejected rather than approximated. Empty allowlist =
|
|
264
|
+
* unconstrained.
|
|
265
265
|
*/
|
|
266
266
|
export function bashCommandAllowed(command: string, prefixes: string[]): boolean {
|
|
267
267
|
if (prefixes.length === 0) return true;
|
|
268
|
-
if (/[
|
|
268
|
+
if (/[$`]/.test(command)) return false; // un-vettable expansion/substitution
|
|
269
|
+
const unsupportedSyntax = command.replace(/&&|\|\||[;|]/g, "");
|
|
270
|
+
if (/[&<>(){}\\\r\n]/.test(unsupportedSyntax)) return false;
|
|
269
271
|
const matches = (seg: string, p: string) =>
|
|
270
272
|
seg === p || seg.startsWith(p + " ") || seg.startsWith(p + "\t");
|
|
271
273
|
for (const raw of command.split(/(?:&&|\|\||[;|])/)) {
|
package/src/agent/task-tool.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* repo). Subagents are spawned with `subagentToolset(role)`, which never includes
|
|
10
10
|
* `task` itself, so delegation cannot recurse infinitely.
|
|
11
11
|
*/
|
|
12
|
-
import { runAgentLoop, isSafetyFallbackBail, stripSafetyFallbackTag, type ToolHandler } from "./engine";
|
|
12
|
+
import { runAgentLoop, isSafetyFallbackBail, stripSafetyFallbackTag, type AgentLoopResult, type ToolHandler } from "./engine";
|
|
13
13
|
import { toolTarget } from "./step-budget";
|
|
14
14
|
|
|
15
15
|
import type { ToolResult } from "./tools";
|
|
@@ -279,8 +279,11 @@ export interface RunSubagentOptions {
|
|
|
279
279
|
modelOverride?: string;
|
|
280
280
|
/** Stable detached id (from SubagentRegistry), attached to all emitted TaskSubEvents. */
|
|
281
281
|
detachedId?: string;
|
|
282
|
+
/** SubagentRegistry to report live activity into (e.g. current tool or step). */
|
|
283
|
+
registry?: SubagentRegistry;
|
|
282
284
|
}
|
|
283
285
|
|
|
286
|
+
|
|
284
287
|
/** `runSubagentOnce`'s result — a superset of `ToolResult` exposing the raw
|
|
285
288
|
* (unwrapped, unfenced) done reason and mutation-audit counters so a caller with
|
|
286
289
|
* its OWN pass/fail policy on top (e.g. `jeo team`'s role-gate-verdict parsing and
|
|
@@ -288,6 +291,8 @@ export interface RunSubagentOptions {
|
|
|
288
291
|
* report text. `task`/`subagent` tool callers only ever need `success`/`output`/
|
|
289
292
|
* `error`, which stay structurally identical to plain `ToolResult`. */
|
|
290
293
|
export interface SubagentRunResult extends ToolResult {
|
|
294
|
+
/** True when delegated execution observed a successful or potentially partial mutation. */
|
|
295
|
+
mutationMayHaveOccurred?: boolean;
|
|
291
296
|
/** Raw subagent done reason, before contract validation or fencing. */
|
|
292
297
|
doneReason: string;
|
|
293
298
|
/** True when the subagent actually called `done` (vs. exhausting its step
|
|
@@ -297,7 +302,8 @@ export interface SubagentRunResult extends ToolResult {
|
|
|
297
302
|
contractOk: boolean;
|
|
298
303
|
/** Required markers missing from the done reason, when `contractOk` is false. */
|
|
299
304
|
missingMarkers?: string[];
|
|
300
|
-
/** Successful write/edit/mkdir/delete
|
|
305
|
+
/** Successful workspace mutation calls observed (write/edit/mkdir/delete,
|
|
306
|
+
* ast_edit, or lsp_rename). */
|
|
301
307
|
fileMutations: number;
|
|
302
308
|
/** Successful bash calls observed (tracked apart — bash CAN mutate but isn't proof). */
|
|
303
309
|
bashRuns: number;
|
|
@@ -329,7 +335,18 @@ export async function runSubagentOnce(
|
|
|
329
335
|
const { steer, slot, detachedId, projectContext: preloadedContext, signal } = opts;
|
|
330
336
|
// Tag every live event with its fan-out slot or detached ID so a parent monitor can tell
|
|
331
337
|
// task 1 from task 3 or detached tasks apart when several subagents stream concurrently.
|
|
332
|
-
const emit = (ev: TaskSubEvent) =>
|
|
338
|
+
const emit = (ev: TaskSubEvent) => {
|
|
339
|
+
if (detachedId && opts.registry) {
|
|
340
|
+
if (ev.kind === "tool") {
|
|
341
|
+
opts.registry.setActivity(detachedId, ev.detail ? `tool: ${ev.detail}` : "running tool");
|
|
342
|
+
} else if (ev.kind === "thinking") {
|
|
343
|
+
opts.registry.setActivity(detachedId, "thinking");
|
|
344
|
+
} else if (ev.kind === "step") {
|
|
345
|
+
opts.registry.setActivity(detachedId, ev.detail ? `step: ${ev.detail}` : "next step");
|
|
346
|
+
} else if (ev.kind === "done") {
|
|
347
|
+
opts.registry.setActivity(detachedId, undefined);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
333
350
|
opts.onEvent?.(
|
|
334
351
|
detachedId
|
|
335
352
|
? { ...ev, detachedId }
|
|
@@ -337,6 +354,8 @@ export async function runSubagentOnce(
|
|
|
337
354
|
? { ...ev, index: slot.index, total: slot.total }
|
|
338
355
|
: ev
|
|
339
356
|
);
|
|
357
|
+
};
|
|
358
|
+
|
|
340
359
|
const initialModel = opts.modelOverride || resolveSubagentModel(role.id, opts.config);
|
|
341
360
|
const maxSteps = resolveSubagentMaxSteps(role.id, opts.config);
|
|
342
361
|
// gjc parity: a role may pin its own reasoning budget; absent = inherit the
|
|
@@ -365,6 +384,9 @@ export async function runSubagentOnce(
|
|
|
365
384
|
let fileMutations = 0;
|
|
366
385
|
let bashRuns = 0;
|
|
367
386
|
let readOnlyEvidenceCalls = 0;
|
|
387
|
+
// The engine's evidence signal is cumulative across tool batches and includes
|
|
388
|
+
// failed mutators whose handlers report that state may have changed.
|
|
389
|
+
let mutationMayHaveOccurred = false;
|
|
368
390
|
// Per-run prompt-cache key: the subagent replays its own growing history each
|
|
369
391
|
// step, so a stable key (even across a model switch below — a stale cache key
|
|
370
392
|
// on a NEW provider is simply unused, never wrong) gets provider cache hits
|
|
@@ -454,6 +476,9 @@ export async function runSubagentOnce(
|
|
|
454
476
|
tools: subagentToolset(role),
|
|
455
477
|
...(failoverEnabled ? { rateLimitFallbackAvailable, safetyFallbackAvailable } : {}),
|
|
456
478
|
events: {
|
|
479
|
+
onEvidence: evidence => {
|
|
480
|
+
if (evidence.sawMutation) mutationMayHaveOccurred = true;
|
|
481
|
+
},
|
|
457
482
|
onStep: n => { currentStep = n; },
|
|
458
483
|
// Live reasoning preview (native extended-thinking models only — the JSON-protocol
|
|
459
484
|
// "reasoning" field the main turn also extracts from onModelStream is intentionally
|
|
@@ -475,7 +500,7 @@ export async function runSubagentOnce(
|
|
|
475
500
|
},
|
|
476
501
|
onToolResult: (tool, success, output) => {
|
|
477
502
|
if (success) {
|
|
478
|
-
if (tool === "write" || tool === "edit" || tool === "mkdir" || tool === "delete") fileMutations++;
|
|
503
|
+
if (tool === "write" || tool === "edit" || tool === "mkdir" || tool === "delete" || tool === "ast_edit" || tool === "lsp_rename") fileMutations++;
|
|
479
504
|
else if (tool === "bash") bashRuns++;
|
|
480
505
|
else if (tool === "read" || tool === "search" || tool === "find" || tool === "ast_grep" || tool === "lsp") readOnlyEvidenceCalls++;
|
|
481
506
|
}
|
|
@@ -496,12 +521,36 @@ export async function runSubagentOnce(
|
|
|
496
521
|
});
|
|
497
522
|
};
|
|
498
523
|
|
|
499
|
-
|
|
524
|
+
const failureResult = (error: unknown): SubagentRunResult => {
|
|
525
|
+
const explicitSignal = typeof error === "object" && error !== null && "mutationMayHaveOccurred" in error && error.mutationMayHaveOccurred === true;
|
|
526
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
527
|
+
return {
|
|
528
|
+
success: false,
|
|
529
|
+
output: "",
|
|
530
|
+
error: message,
|
|
531
|
+
doneReason: message,
|
|
532
|
+
done: false,
|
|
533
|
+
contractOk: false,
|
|
534
|
+
fileMutations,
|
|
535
|
+
bashRuns,
|
|
536
|
+
readOnlyEvidenceCalls,
|
|
537
|
+
mutationMayHaveOccurred: mutationMayHaveOccurred || explicitSignal || (!role.readOnly ? true : undefined),
|
|
538
|
+
};
|
|
539
|
+
};
|
|
540
|
+
const runSafely = async (): Promise<{ ok: true; result: AgentLoopResult } | { ok: false; error: unknown }> => {
|
|
541
|
+
try {
|
|
542
|
+
return { ok: true, result: await runOnce() };
|
|
543
|
+
} catch (error: unknown) {
|
|
544
|
+
return { ok: false, error };
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
let outcome = await runSafely();
|
|
548
|
+
if (!outcome.ok) return failureResult(outcome.error);
|
|
549
|
+
let result = outcome.result;
|
|
500
550
|
// `routing.enabled` off = the model is FIXED: no failover reroute at all (the
|
|
501
551
|
// engine bail predicates above are withheld too, so a 429 rides its normal
|
|
502
552
|
// backoff ladder instead of bailing early with nowhere to go).
|
|
503
|
-
for (let attempt = 0; failoverEnabled && attempt < MAX_SUBAGENT_REROUTES; attempt++) {
|
|
504
|
-
const rateLimited = !result.done && isRateLimitError(new Error(result.doneReason ?? ""));
|
|
553
|
+
for (let attempt = 0; failoverEnabled && attempt < MAX_SUBAGENT_REROUTES; attempt++) { const rateLimited = !result.done && isRateLimitError(new Error(result.doneReason ?? ""));
|
|
505
554
|
const safetyBail = !result.done && !rateLimited && isSafetyFallbackBail(result.doneReason);
|
|
506
555
|
if (!rateLimited && !safetyBail) break;
|
|
507
556
|
const previousModel = activeModel;
|
|
@@ -526,7 +575,9 @@ export async function runSubagentOnce(
|
|
|
526
575
|
emit({ role: role.id, kind: "step", detail: `↳ refused on '${previousModel}' — switching to '${activeModel}' in case of a classifier false positive`, step: currentStep, maxSteps, model: activeModel });
|
|
527
576
|
}
|
|
528
577
|
attemptedModels.add(activeModel);
|
|
529
|
-
|
|
578
|
+
outcome = await runSafely();
|
|
579
|
+
if (!outcome.ok) return failureResult(outcome.error);
|
|
580
|
+
result = outcome.result;
|
|
530
581
|
}
|
|
531
582
|
|
|
532
583
|
const reason = stripSafetyFallbackTag(result.doneReason?.trim() || "") || `(subagent reached the ${result.steps}-step limit without signaling done)`;
|
|
@@ -543,8 +594,8 @@ export async function runSubagentOnce(
|
|
|
543
594
|
// "verify independently" instead of the stronger UNVERIFIED.
|
|
544
595
|
const audit = complete && !role.readOnly && fileMutations === 0
|
|
545
596
|
? bashRuns === 0
|
|
546
|
-
? `\n[parent audit] No successful
|
|
547
|
-
: `\n[parent audit] No successful
|
|
597
|
+
? `\n[parent audit] No successful workspace mutation or bash call was observed in this run — treat any "Changed Files:" claims above as UNVERIFIED.`
|
|
598
|
+
: `\n[parent audit] No successful workspace mutation was observed (only bash ran); bash may or may not have mutated files — verify any "Changed Files:" claims above independently.`
|
|
548
599
|
: "";
|
|
549
600
|
return {
|
|
550
601
|
success: complete,
|
|
@@ -556,6 +607,7 @@ export async function runSubagentOnce(
|
|
|
556
607
|
fileMutations,
|
|
557
608
|
bashRuns,
|
|
558
609
|
readOnlyEvidenceCalls,
|
|
610
|
+
mutationMayHaveOccurred,
|
|
559
611
|
};
|
|
560
612
|
}
|
|
561
613
|
|
|
@@ -585,8 +637,10 @@ export function createTaskTool(opts: TaskToolOptions): ToolHandler {
|
|
|
585
637
|
modelOverride?: string;
|
|
586
638
|
/** Stable detached id from SubagentRegistry. */
|
|
587
639
|
detachedId?: string;
|
|
640
|
+
/** SubagentRegistry to report live activity into. */
|
|
641
|
+
registry?: SubagentRegistry;
|
|
588
642
|
} = {},
|
|
589
|
-
): Promise<
|
|
643
|
+
): Promise<SubagentRunResult> =>
|
|
590
644
|
runSubagentOnce(role, taskText, context, cwd, {
|
|
591
645
|
config: opts.config,
|
|
592
646
|
signal: extra.signal ?? opts.signal,
|
|
@@ -597,8 +651,10 @@ export function createTaskTool(opts: TaskToolOptions): ToolHandler {
|
|
|
597
651
|
excludedCredentialScopes: extra.excludedCredentialScopes,
|
|
598
652
|
modelOverride: extra.modelOverride,
|
|
599
653
|
detachedId: extra.detachedId,
|
|
654
|
+
registry: extra.registry,
|
|
600
655
|
});
|
|
601
656
|
|
|
657
|
+
|
|
602
658
|
return async (args: Record<string, any>, cwd: string): Promise<ToolResult> => {
|
|
603
659
|
const roleArg = typeof args.role === "string" ? args.role.trim() : "";
|
|
604
660
|
const role = roleArg ? getSubagentRole(roleArg, opts.config) : defaultSubagentRole();
|
|
@@ -689,7 +745,7 @@ export function createTaskTool(opts: TaskToolOptions): ToolHandler {
|
|
|
689
745
|
// `executor`'s single resolved model), so the first worker to exclude a
|
|
690
746
|
// scope saves every sibling worker the same wasted round.
|
|
691
747
|
const batchExcludedScopes = new Set<string>();
|
|
692
|
-
const results:
|
|
748
|
+
const results: SubagentRunResult[] = new Array(items.length);
|
|
693
749
|
let next = 0;
|
|
694
750
|
// #7: broadcast steering hub — each concurrent worker sees every parent
|
|
695
751
|
// steer message exactly once (safe even for parallel read-only fan-out).
|
|
@@ -714,7 +770,11 @@ export function createTaskTool(opts: TaskToolOptions): ToolHandler {
|
|
|
714
770
|
const ok = results.filter(r => r.success).length;
|
|
715
771
|
const head = `[${role.title} fan-out] ${ok}/${items.length} completed (concurrency ${limit}).`;
|
|
716
772
|
const combined = results.map((r, i) => `### Task ${i + 1}/${items.length}\n${r.output}`).join("\n\n");
|
|
717
|
-
return {
|
|
773
|
+
return {
|
|
774
|
+
success: ok === items.length,
|
|
775
|
+
output: `${head}\n\n${combined}`,
|
|
776
|
+
mutationMayHaveOccurred: results.some(r => r.mutationMayHaveOccurred === true),
|
|
777
|
+
};
|
|
718
778
|
}
|
|
719
779
|
|
|
720
780
|
// Single-task form.
|
|
@@ -728,8 +788,15 @@ export function createTaskTool(opts: TaskToolOptions): ToolHandler {
|
|
|
728
788
|
// its own registry inbox (registry.steerDrainFor(id)) between its own steps.
|
|
729
789
|
if (args.detached === true && opts.registry) {
|
|
730
790
|
const rec = opts.registry.launch(role.id, taskText, (signal, id) =>
|
|
731
|
-
runOne(role, taskText, ctx(args.context), cwd, {
|
|
791
|
+
runOne(role, taskText, ctx(args.context), cwd, {
|
|
792
|
+
signal,
|
|
793
|
+
steer: opts.registry!.steerDrainFor(id),
|
|
794
|
+
detachedId: id,
|
|
795
|
+
registry: opts.registry,
|
|
796
|
+
}),
|
|
797
|
+
{ readOnly: role.readOnly },
|
|
732
798
|
);
|
|
799
|
+
|
|
733
800
|
// Remote subagent visibility/control over Telegram (gjc daemon parity, see
|
|
734
801
|
// `src/agent/notify/`): best-effort, no-op unless `notifications.enabled`.
|
|
735
802
|
ensureSessionNotifyEndpoint(opts.registry, cwd, opts.sessionEndpoint);
|
|
@@ -739,6 +806,10 @@ export function createTaskTool(opts: TaskToolOptions): ToolHandler {
|
|
|
739
806
|
output:
|
|
740
807
|
`[detached] launched ${role.title} subagent '${rec.id}'. It runs in the background — ` +
|
|
741
808
|
`keep working, then use the 'subagent' tool ({action:"await"|"list"|"inspect"|"cancel", ids?}) to collect its result.`,
|
|
809
|
+
// A mutating detached run may change the workspace after this tool returns;
|
|
810
|
+
// surface that possibility immediately so the parent cannot claim a clean
|
|
811
|
+
// workspace before collecting and verifying the settled result.
|
|
812
|
+
mutationMayHaveOccurred: role.readOnly ? undefined : true,
|
|
742
813
|
};
|
|
743
814
|
}
|
|
744
815
|
return runOne(role, taskText, ctx(args.context), cwd, { steer: opts.steer });
|
package/src/agent/tools.ts
CHANGED
|
@@ -26,6 +26,8 @@ export interface ToolResult {
|
|
|
26
26
|
success: boolean;
|
|
27
27
|
output: string;
|
|
28
28
|
error?: string;
|
|
29
|
+
/** Failed operations set this when an attempted filesystem write may have partially changed state. */
|
|
30
|
+
mutationMayHaveOccurred?: boolean;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
/**
|
|
@@ -315,18 +317,20 @@ export async function writeTool(
|
|
|
315
317
|
content: string,
|
|
316
318
|
cwd: string = process.cwd()
|
|
317
319
|
): Promise<ToolResult> {
|
|
320
|
+
let mutationMayHaveOccurred = false;
|
|
318
321
|
try {
|
|
319
322
|
if (typeof filePath !== "string" || filePath.trim() === "") {
|
|
320
323
|
return { success: false, output: "", error: 'write requires a non-empty "filePath".' };
|
|
321
324
|
}
|
|
322
325
|
await assertMutationAllowed(filePath, cwd);
|
|
323
326
|
const absPath = path.resolve(cwd, filePath);
|
|
327
|
+
mutationMayHaveOccurred = true;
|
|
324
328
|
await fs.mkdir(path.dirname(absPath), { recursive: true });
|
|
325
329
|
await fs.writeFile(absPath, content, "utf-8");
|
|
326
330
|
markRead(absPath); // own change counts as having seen the file
|
|
327
331
|
return { success: true, output: `Successfully wrote ${content.length} characters to ${filePath}` };
|
|
328
332
|
} catch (err: any) {
|
|
329
|
-
return { success: false, output: "", error: err.message };
|
|
333
|
+
return { success: false, output: "", error: err.message, mutationMayHaveOccurred };
|
|
330
334
|
}
|
|
331
335
|
}
|
|
332
336
|
|
|
@@ -363,6 +367,7 @@ export async function editTool(
|
|
|
363
367
|
editBlock: string,
|
|
364
368
|
cwd: string = process.cwd()
|
|
365
369
|
): Promise<ToolResult> {
|
|
370
|
+
let mutationMayHaveOccurred = false;
|
|
366
371
|
try {
|
|
367
372
|
if (typeof filePath !== "string" || filePath.trim() === "") {
|
|
368
373
|
return { success: false, output: "", error: 'edit requires a non-empty "filePath".' };
|
|
@@ -582,11 +587,12 @@ export async function editTool(
|
|
|
582
587
|
};
|
|
583
588
|
}
|
|
584
589
|
|
|
590
|
+
mutationMayHaveOccurred = true;
|
|
585
591
|
await fs.writeFile(absPath, content, "utf-8");
|
|
586
592
|
markRead(absPath); // own change counts as having seen the file
|
|
587
593
|
return { success: true, output: `Successfully updated ${filePath}` };
|
|
588
594
|
} catch (err: any) {
|
|
589
|
-
return { success: false, output: "", error: err.message };
|
|
595
|
+
return { success: false, output: "", error: err.message, mutationMayHaveOccurred };
|
|
590
596
|
}
|
|
591
597
|
}
|
|
592
598
|
|
|
@@ -1079,6 +1085,7 @@ export async function mkdirTool(
|
|
|
1079
1085
|
dirPath: string,
|
|
1080
1086
|
cwd: string = process.cwd()
|
|
1081
1087
|
): Promise<ToolResult> {
|
|
1088
|
+
let mutationMayHaveOccurred = false;
|
|
1082
1089
|
try {
|
|
1083
1090
|
if (typeof dirPath !== "string" || dirPath.trim() === "") {
|
|
1084
1091
|
return { success: false, output: "", error: 'mkdir requires a non-empty "dirPath".' };
|
|
@@ -1089,10 +1096,11 @@ export async function mkdirTool(
|
|
|
1089
1096
|
if (existing && !existing.isDirectory()) {
|
|
1090
1097
|
return { success: false, output: "", error: `Path exists and is not a directory: ${dirPath}` };
|
|
1091
1098
|
}
|
|
1099
|
+
mutationMayHaveOccurred = true;
|
|
1092
1100
|
await fs.mkdir(abs, { recursive: true });
|
|
1093
1101
|
return { success: true, output: `Directory ready: ${dirPath}` };
|
|
1094
1102
|
} catch (err: any) {
|
|
1095
|
-
return { success: false, output: "", error: err.message };
|
|
1103
|
+
return { success: false, output: "", error: err.message, mutationMayHaveOccurred };
|
|
1096
1104
|
}
|
|
1097
1105
|
}
|
|
1098
1106
|
|
|
@@ -1108,6 +1116,7 @@ export async function deleteTool(
|
|
|
1108
1116
|
cwd: string = process.cwd(),
|
|
1109
1117
|
recursive: boolean = false
|
|
1110
1118
|
): Promise<ToolResult> {
|
|
1119
|
+
let mutationMayHaveOccurred = false;
|
|
1111
1120
|
try {
|
|
1112
1121
|
if (typeof targetPath !== "string" || targetPath.trim() === "") {
|
|
1113
1122
|
return { success: false, output: "", error: 'delete requires a non-empty "path".' };
|
|
@@ -1124,11 +1133,12 @@ export async function deleteTool(
|
|
|
1124
1133
|
if (st.isDirectory() && !recursive) {
|
|
1125
1134
|
return { success: false, output: "", error: `${targetPath} is a directory — pass recursive:true to remove it and its contents.` };
|
|
1126
1135
|
}
|
|
1136
|
+
mutationMayHaveOccurred = true;
|
|
1127
1137
|
await fs.rm(abs, { recursive, force: false });
|
|
1128
1138
|
readThisSession.delete(abs); // a future edit against this path must read it again first
|
|
1129
1139
|
return { success: true, output: `Deleted ${st.isDirectory() ? "directory" : "file"}: ${targetPath}` };
|
|
1130
1140
|
} catch (err: any) {
|
|
1131
|
-
return { success: false, output: "", error: err.message };
|
|
1141
|
+
return { success: false, output: "", error: err.message, mutationMayHaveOccurred };
|
|
1132
1142
|
}
|
|
1133
1143
|
}
|
|
1134
1144
|
|
package/src/cli.ts
CHANGED
|
@@ -49,6 +49,7 @@ process.on("unhandledRejection", fatal);
|
|
|
49
49
|
process.stdout.on("error", fatal);
|
|
50
50
|
process.stderr.on("error", fatal);
|
|
51
51
|
|
|
52
|
+
|
|
52
53
|
try {
|
|
53
54
|
const code = await dispatch(process.argv.slice(2), { appName: APP_NAME, version: VERSION });
|
|
54
55
|
if (code !== 0) process.exit(code);
|
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 {
|
|
@@ -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}"`);
|