@rallycry/conveyor-agent 10.13.70 → 10.13.71
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.
|
@@ -490,7 +490,108 @@ import {
|
|
|
490
490
|
symlinkSync,
|
|
491
491
|
writeFileSync as writeFileSync2
|
|
492
492
|
} from "fs";
|
|
493
|
-
import { dirname, join as
|
|
493
|
+
import { dirname, join as join3 } from "path";
|
|
494
|
+
|
|
495
|
+
// src/boot/memory-integrity.ts
|
|
496
|
+
import { readFile, readdir, stat, writeFile } from "fs/promises";
|
|
497
|
+
import { join as join2 } from "path";
|
|
498
|
+
var FRONTMATTER_OPEN = "---\n";
|
|
499
|
+
var FRONTMATTER_CLOSE = "\n---\n";
|
|
500
|
+
var MAX_MEMORY_FILE_BYTES = 512 * 1024;
|
|
501
|
+
var READ_CONCURRENCY = 16;
|
|
502
|
+
function readFrontmatterBlock(content) {
|
|
503
|
+
if (!content.startsWith(FRONTMATTER_OPEN)) return null;
|
|
504
|
+
const close = content.indexOf(FRONTMATTER_CLOSE, FRONTMATTER_OPEN.length - 1);
|
|
505
|
+
if (close === -1) return null;
|
|
506
|
+
return content.slice(0, close + FRONTMATTER_CLOSE.length);
|
|
507
|
+
}
|
|
508
|
+
function inspectMemoryFile(content) {
|
|
509
|
+
const header = readFrontmatterBlock(content);
|
|
510
|
+
if (!header) return { status: "clean" };
|
|
511
|
+
const repeat = content.indexOf(header, header.length);
|
|
512
|
+
if (repeat === -1) return { status: "clean" };
|
|
513
|
+
const half = content.length / 2;
|
|
514
|
+
if (repeat === half && content.slice(0, half) === content.slice(half)) {
|
|
515
|
+
return { status: "self-append", repaired: content.slice(0, half) };
|
|
516
|
+
}
|
|
517
|
+
return {
|
|
518
|
+
status: "suspicious",
|
|
519
|
+
detail: `frontmatter block repeats at offset ${repeat} of ${content.length} without identical halves`
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
async function candidateSize(path) {
|
|
523
|
+
try {
|
|
524
|
+
const info = await stat(path);
|
|
525
|
+
if (!info.isFile() || info.size === 0) return null;
|
|
526
|
+
if (info.size > MAX_MEMORY_FILE_BYTES || info.size % 2 !== 0) return null;
|
|
527
|
+
return info.size;
|
|
528
|
+
} catch {
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
async function repairOneFile(dir, name, log, out) {
|
|
533
|
+
const path = join2(dir, name);
|
|
534
|
+
const size = await candidateSize(path);
|
|
535
|
+
if (size === null) {
|
|
536
|
+
out.skipped += 1;
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
out.scanned += 1;
|
|
540
|
+
const verdict = inspectMemoryFile(await readFile(path, "utf8"));
|
|
541
|
+
if (verdict.status === "suspicious") {
|
|
542
|
+
out.suspicious.push(name);
|
|
543
|
+
log.warn(
|
|
544
|
+
`[boot] WARN shared memory ${name} looks doubled but was left alone: ${verdict.detail}`
|
|
545
|
+
);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
if (verdict.status !== "self-append" || verdict.repaired === void 0) return;
|
|
549
|
+
if (await candidateSize(path) !== size) {
|
|
550
|
+
log.warn(`[boot] WARN shared memory ${name} changed mid-check; left alone`);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
await writeFile(path, verdict.repaired);
|
|
554
|
+
out.repaired.push(name);
|
|
555
|
+
}
|
|
556
|
+
async function forEachLimited(items, limit, fn) {
|
|
557
|
+
let cursor = 0;
|
|
558
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
559
|
+
for (let i = cursor++; i < items.length; i = cursor++) {
|
|
560
|
+
const item = items[i];
|
|
561
|
+
if (item !== void 0) await fn(item);
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
await Promise.all(workers);
|
|
565
|
+
}
|
|
566
|
+
async function repairSharedMemory(dir, log) {
|
|
567
|
+
const out = { scanned: 0, repaired: [], suspicious: [], skipped: 0 };
|
|
568
|
+
let entries;
|
|
569
|
+
try {
|
|
570
|
+
entries = await readdir(dir);
|
|
571
|
+
} catch {
|
|
572
|
+
return out;
|
|
573
|
+
}
|
|
574
|
+
const names = entries.filter((name) => name.endsWith(".md"));
|
|
575
|
+
await forEachLimited(names, READ_CONCURRENCY, async (name) => {
|
|
576
|
+
try {
|
|
577
|
+
await repairOneFile(dir, name, log, out);
|
|
578
|
+
} catch (err) {
|
|
579
|
+
log.warn(
|
|
580
|
+
`[boot] WARN shared memory integrity check failed for ${name}: ${err instanceof Error ? err.message : String(err)}`
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
out.repaired.sort();
|
|
585
|
+
out.suspicious.sort();
|
|
586
|
+
if (out.repaired.length > 0) {
|
|
587
|
+
log.info(
|
|
588
|
+
`[boot] shared memory: repaired ${out.repaired.length} self-appended file(s): ${out.repaired.join(", ")}`
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
return out;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// src/boot/user-home.ts
|
|
494
595
|
var SEED_GATES = {
|
|
495
596
|
hasCompletedOnboarding: true,
|
|
496
597
|
bypassPermissionsModeAccepted: true,
|
|
@@ -566,8 +667,8 @@ function relink(target, link) {
|
|
|
566
667
|
}
|
|
567
668
|
function wireCredentials(opts) {
|
|
568
669
|
const { home, root, sharedDir, log } = opts;
|
|
569
|
-
const shared =
|
|
570
|
-
const podLocal = sharedDir ?
|
|
670
|
+
const shared = join3(root, ".claude", ".credentials.json");
|
|
671
|
+
const podLocal = sharedDir ? join3(sharedDir, "claude-credentials.pod.json") : join3(home, ".claude-credentials.pod.json");
|
|
571
672
|
safely(log, "carry legacy shared credentials", () => {
|
|
572
673
|
if (isRegularFile(shared)) copyFileSync(shared, podLocal);
|
|
573
674
|
});
|
|
@@ -583,66 +684,78 @@ function migrateMemories(from, to, log) {
|
|
|
583
684
|
return;
|
|
584
685
|
}
|
|
585
686
|
for (const entry of entries) {
|
|
586
|
-
const target =
|
|
687
|
+
const target = join3(to, entry);
|
|
587
688
|
if (existsSync(target)) continue;
|
|
588
|
-
safely(log, `migrate memory ${entry}`, () => copyFileSync(
|
|
689
|
+
safely(log, `migrate memory ${entry}`, () => copyFileSync(join3(from, entry), target));
|
|
589
690
|
}
|
|
590
691
|
}
|
|
692
|
+
function startSharedMemoryRepair(shared, log) {
|
|
693
|
+
return repairSharedMemory(shared, log).then(
|
|
694
|
+
() => void 0,
|
|
695
|
+
(err) => {
|
|
696
|
+
log.warn(
|
|
697
|
+
`[boot] WARN repair self-appended shared memories failed: ${err instanceof Error ? err.message : String(err)}`
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
);
|
|
701
|
+
}
|
|
591
702
|
function wireSharedMemory(root, userHomeMount, projectId, log) {
|
|
592
|
-
const shared =
|
|
593
|
-
const link =
|
|
703
|
+
const shared = join3(userHomeMount, "users", "_shared", "memory", projectId);
|
|
704
|
+
const link = join3(root, ".claude", "projects", AGENT_MEMORY_PROJECT_DIR, "memory");
|
|
594
705
|
safely(log, "mkdir shared memory dir", () => mkdirSync(shared, { recursive: true }));
|
|
595
706
|
safely(log, "migrate per-user memories", () => {
|
|
596
707
|
if (existsSync(link) && !lstatSync(link).isSymbolicLink()) migrateMemories(link, shared, log);
|
|
597
708
|
});
|
|
598
709
|
safely(log, "wire shared memory symlink", () => relink(shared, link));
|
|
710
|
+
return startSharedMemoryRepair(shared, log);
|
|
599
711
|
}
|
|
600
712
|
function wireMountedUserHome(opts) {
|
|
601
713
|
const { home, userHomeMount, userId, projectId, sharedDir, log } = opts;
|
|
602
|
-
const root =
|
|
714
|
+
const root = join3(userHomeMount, "users", userId, projectId);
|
|
603
715
|
safely(log, "mkdir user-home root", () => {
|
|
604
|
-
mkdirSync(
|
|
605
|
-
mkdirSync(
|
|
716
|
+
mkdirSync(join3(root, ".claude"), { recursive: true });
|
|
717
|
+
mkdirSync(join3(root, ".config", "claude"), { recursive: true });
|
|
606
718
|
});
|
|
607
|
-
safely(log, "wire ~/.claude symlink", () => relink(
|
|
719
|
+
safely(log, "wire ~/.claude symlink", () => relink(join3(root, ".claude"), join3(home, ".claude")));
|
|
608
720
|
safely(
|
|
609
721
|
log,
|
|
610
722
|
"wire ~/.config/claude symlink",
|
|
611
|
-
() => relink(
|
|
723
|
+
() => relink(join3(root, ".config", "claude"), join3(home, ".config", "claude"))
|
|
612
724
|
);
|
|
613
725
|
safely(log, "seed pod-local ~/.claude.json", () => {
|
|
614
|
-
rmSync(
|
|
615
|
-
seedClaudeJson(
|
|
726
|
+
rmSync(join3(home, ".claude.json"), { recursive: true, force: true });
|
|
727
|
+
seedClaudeJson(join3(home, ".claude.json"));
|
|
616
728
|
});
|
|
617
|
-
wireSharedMemory(root, userHomeMount, projectId, log);
|
|
729
|
+
const pendingRepair = wireSharedMemory(root, userHomeMount, projectId, log);
|
|
618
730
|
wireCredentials({ home, root, sharedDir, log });
|
|
619
731
|
safely(log, "mkdir opencode root", () => {
|
|
620
|
-
mkdirSync(
|
|
621
|
-
mkdirSync(
|
|
732
|
+
mkdirSync(join3(root, ".local", "share", "opencode"), { recursive: true });
|
|
733
|
+
mkdirSync(join3(root, ".config", "opencode"), { recursive: true });
|
|
622
734
|
});
|
|
623
735
|
safely(
|
|
624
736
|
log,
|
|
625
737
|
"wire opencode data symlink",
|
|
626
|
-
() => relink(
|
|
738
|
+
() => relink(join3(root, ".local", "share", "opencode"), join3(home, ".local", "share", "opencode"))
|
|
627
739
|
);
|
|
628
740
|
safely(
|
|
629
741
|
log,
|
|
630
742
|
"wire opencode config symlink",
|
|
631
|
-
() => relink(
|
|
743
|
+
() => relink(join3(root, ".config", "opencode"), join3(home, ".config", "opencode"))
|
|
632
744
|
);
|
|
745
|
+
return pendingRepair;
|
|
633
746
|
}
|
|
634
747
|
function wireNoMountUserHome(opts) {
|
|
635
748
|
const { home, sharedDir, log } = opts;
|
|
636
749
|
safely(
|
|
637
750
|
log,
|
|
638
751
|
"seed pod-local ~/.claude.json (no mount)",
|
|
639
|
-
() => seedClaudeJson(
|
|
752
|
+
() => seedClaudeJson(join3(home, ".claude.json"))
|
|
640
753
|
);
|
|
641
754
|
if (sharedDir) {
|
|
642
755
|
safely(log, "wire ~/.claude to shared emptyDir", () => {
|
|
643
|
-
const target =
|
|
756
|
+
const target = join3(sharedDir, "claude-home", ".claude");
|
|
644
757
|
mkdirSync(target, { recursive: true });
|
|
645
|
-
relink(target,
|
|
758
|
+
relink(target, join3(home, ".claude"));
|
|
646
759
|
});
|
|
647
760
|
}
|
|
648
761
|
}
|
|
@@ -654,14 +767,14 @@ function wireUserHome(opts) {
|
|
|
654
767
|
() => mkdirSync(opts.scratchDir ?? "/tmp/claude-cmd", { recursive: true })
|
|
655
768
|
);
|
|
656
769
|
if (userId && projectId && isDir(userHomeMount)) {
|
|
657
|
-
wireMountedUserHome({ home, userHomeMount, userId, projectId, sharedDir, log });
|
|
658
|
-
} else {
|
|
659
|
-
wireNoMountUserHome({ home, sharedDir, log });
|
|
770
|
+
return wireMountedUserHome({ home, userHomeMount, userId, projectId, sharedDir, log });
|
|
660
771
|
}
|
|
772
|
+
wireNoMountUserHome({ home, sharedDir, log });
|
|
773
|
+
return Promise.resolve();
|
|
661
774
|
}
|
|
662
775
|
var RECLAIM_FIND_TIMEOUT_MS = 1e4;
|
|
663
776
|
async function reclaimHomeOwnership(home, exec, log) {
|
|
664
|
-
for (const dir of [
|
|
777
|
+
for (const dir of [join3(home, ".config"), join3(home, ".local")]) {
|
|
665
778
|
if (!isDir(dir)) continue;
|
|
666
779
|
try {
|
|
667
780
|
await exec(
|
|
@@ -691,9 +804,9 @@ async function reclaimHomeOwnership(home, exec, log) {
|
|
|
691
804
|
|
|
692
805
|
// src/boot/sshd.ts
|
|
693
806
|
import { chmodSync as chmodSync2, closeSync, copyFileSync as copyFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, openSync } from "fs";
|
|
694
|
-
import { join as
|
|
807
|
+
import { join as join4 } from "path";
|
|
695
808
|
function persistedAuthorizedKeys() {
|
|
696
|
-
return
|
|
809
|
+
return join4(
|
|
697
810
|
process.env.CONVEYOR_SHARED_DIR || "/var/run/conveyor-workbench",
|
|
698
811
|
"ssh",
|
|
699
812
|
"authorized_keys"
|
|
@@ -701,10 +814,10 @@ function persistedAuthorizedKeys() {
|
|
|
701
814
|
}
|
|
702
815
|
async function startWorkspaceSshd(exec, log, port = process.env.CONVEYOR_WORKSPACE_SSH_PORT || "2222", home = "/home/conveyor") {
|
|
703
816
|
try {
|
|
704
|
-
const sshDir =
|
|
817
|
+
const sshDir = join4(home, ".ssh");
|
|
705
818
|
mkdirSync2(sshDir, { recursive: true });
|
|
706
819
|
chmodSync2(sshDir, 448);
|
|
707
|
-
const authorizedKeys =
|
|
820
|
+
const authorizedKeys = join4(sshDir, "authorized_keys");
|
|
708
821
|
const persisted = persistedAuthorizedKeys();
|
|
709
822
|
if (existsSync2(persisted)) {
|
|
710
823
|
copyFileSync2(persisted, authorizedKeys);
|
|
@@ -746,7 +859,7 @@ import {
|
|
|
746
859
|
statSync as statSync3,
|
|
747
860
|
symlinkSync as symlinkSync2
|
|
748
861
|
} from "fs";
|
|
749
|
-
import { join as
|
|
862
|
+
import { join as join5 } from "path";
|
|
750
863
|
function errText(err) {
|
|
751
864
|
return redactToken(err instanceof Error ? err.message : String(err));
|
|
752
865
|
}
|
|
@@ -782,7 +895,7 @@ function bindGraphifyBundle(workspace, graphifyEnv, log) {
|
|
|
782
895
|
log.info(`[boot] Graphify bundle not found for '${slug}' at ${sourceDir}`);
|
|
783
896
|
return;
|
|
784
897
|
}
|
|
785
|
-
const targetDir =
|
|
898
|
+
const targetDir = join5(workspace, "graphify-out");
|
|
786
899
|
try {
|
|
787
900
|
mkdirSync3(targetDir, { recursive: true });
|
|
788
901
|
} catch {
|
|
@@ -790,8 +903,8 @@ function bindGraphifyBundle(workspace, graphifyEnv, log) {
|
|
|
790
903
|
return;
|
|
791
904
|
}
|
|
792
905
|
for (const rel of GRAPHIFY_BUNDLE_FILES) {
|
|
793
|
-
const source =
|
|
794
|
-
const target =
|
|
906
|
+
const source = join5(sourceDir, rel);
|
|
907
|
+
const target = join5(targetDir, rel);
|
|
795
908
|
if (!existsSync3(source)) continue;
|
|
796
909
|
if (existsSync3(target) || isSymlink(target)) continue;
|
|
797
910
|
try {
|
|
@@ -803,7 +916,7 @@ function bindGraphifyBundle(workspace, graphifyEnv, log) {
|
|
|
803
916
|
}
|
|
804
917
|
async function ensureGrimoireSubmodule(workspace, githubToken, git, log) {
|
|
805
918
|
try {
|
|
806
|
-
if (!existsSync3(
|
|
919
|
+
if (!existsSync3(join5(workspace, ".gitmodules"))) return;
|
|
807
920
|
let paths = "";
|
|
808
921
|
try {
|
|
809
922
|
({ stdout: paths } = await git(
|
|
@@ -814,7 +927,7 @@ async function ensureGrimoireSubmodule(workspace, githubToken, git, log) {
|
|
|
814
927
|
return;
|
|
815
928
|
}
|
|
816
929
|
if (!paths.split("\n").some((line) => line.trim().endsWith(".claude/grimoire"))) return;
|
|
817
|
-
if (isDir2(
|
|
930
|
+
if (isDir2(join5(workspace, ".claude", "grimoire", "skills"))) return;
|
|
818
931
|
if (!githubToken) {
|
|
819
932
|
log.warn("[boot] WARN: grimoire submodule absent and no token to fetch it");
|
|
820
933
|
return;
|
|
@@ -843,11 +956,11 @@ async function ensureGrimoireSubmodule(workspace, githubToken, git, log) {
|
|
|
843
956
|
}
|
|
844
957
|
function excludeLinkedSkills(workspace, names, log) {
|
|
845
958
|
if (names.length === 0) return;
|
|
846
|
-
const infoDir =
|
|
847
|
-
if (!isDir2(
|
|
959
|
+
const infoDir = join5(workspace, ".git", "info");
|
|
960
|
+
if (!isDir2(join5(workspace, ".git"))) return;
|
|
848
961
|
try {
|
|
849
962
|
mkdirSync3(infoDir, { recursive: true });
|
|
850
|
-
const excludePath =
|
|
963
|
+
const excludePath = join5(infoDir, "exclude");
|
|
851
964
|
const existing = existsSync3(excludePath) ? readFileSync2(excludePath, "utf8") : "";
|
|
852
965
|
const missing = names.map((name) => `.claude/skills/${name}`).filter((line) => !existing.split("\n").includes(line));
|
|
853
966
|
if (missing.length === 0) return;
|
|
@@ -860,14 +973,14 @@ function excludeLinkedSkills(workspace, names, log) {
|
|
|
860
973
|
}
|
|
861
974
|
function symlinkTargetsGrimoire(path) {
|
|
862
975
|
try {
|
|
863
|
-
return readlinkSync(path).includes(
|
|
976
|
+
return readlinkSync(path).includes(join5("grimoire", "skills"));
|
|
864
977
|
} catch {
|
|
865
978
|
return false;
|
|
866
979
|
}
|
|
867
980
|
}
|
|
868
981
|
function linkGrimoireSkills(workspace, log) {
|
|
869
|
-
const sourceDir =
|
|
870
|
-
const targetDir =
|
|
982
|
+
const sourceDir = join5(workspace, ".claude", "grimoire", "skills");
|
|
983
|
+
const targetDir = join5(workspace, ".claude", "skills");
|
|
871
984
|
if (!isDir2(sourceDir)) return;
|
|
872
985
|
try {
|
|
873
986
|
mkdirSync3(targetDir, { recursive: true });
|
|
@@ -883,8 +996,8 @@ function linkGrimoireSkills(workspace, log) {
|
|
|
883
996
|
return;
|
|
884
997
|
}
|
|
885
998
|
for (const name of names) {
|
|
886
|
-
if (!isDir2(
|
|
887
|
-
const target =
|
|
999
|
+
if (!isDir2(join5(sourceDir, name))) continue;
|
|
1000
|
+
const target = join5(targetDir, name);
|
|
888
1001
|
if (existsSync3(target) && !isSymlink(target)) {
|
|
889
1002
|
log.warn(`[boot] WARN: ${target} exists and is not a symlink; skipping`);
|
|
890
1003
|
continue;
|
|
@@ -892,7 +1005,7 @@ function linkGrimoireSkills(workspace, log) {
|
|
|
892
1005
|
if (isSymlink(target) && !symlinkTargetsGrimoire(target)) continue;
|
|
893
1006
|
try {
|
|
894
1007
|
rmSync2(target, { force: true });
|
|
895
|
-
symlinkSync2(
|
|
1008
|
+
symlinkSync2(join5("..", "grimoire", "skills", name), target);
|
|
896
1009
|
linkedNames.push(name);
|
|
897
1010
|
} catch {
|
|
898
1011
|
}
|
|
@@ -901,7 +1014,7 @@ function linkGrimoireSkills(workspace, log) {
|
|
|
901
1014
|
log.info(`[boot] Linked ${linkedNames.length} grimoire skills into ${targetDir}`);
|
|
902
1015
|
}
|
|
903
1016
|
async function neutralizeGrimoirePointer(workspace, git, log) {
|
|
904
|
-
if (!existsSync3(
|
|
1017
|
+
if (!existsSync3(join5(workspace, ".claude", "grimoire"))) return;
|
|
905
1018
|
try {
|
|
906
1019
|
await git(["update-index", "--assume-unchanged", ".claude/grimoire"], {
|
|
907
1020
|
cwd: workspace,
|
|
@@ -953,8 +1066,8 @@ async function cloneReferenceRepos(refsJson, git, log, referencesDir = "/workspa
|
|
|
953
1066
|
return;
|
|
954
1067
|
}
|
|
955
1068
|
for (const ref of refs) {
|
|
956
|
-
const dest =
|
|
957
|
-
if (existsSync3(
|
|
1069
|
+
const dest = join5(referencesDir, ref.slug);
|
|
1070
|
+
if (existsSync3(join5(dest, ".git"))) continue;
|
|
958
1071
|
try {
|
|
959
1072
|
await git(
|
|
960
1073
|
[
|
|
@@ -1024,7 +1137,7 @@ function defaultWorkbenchBootDeps() {
|
|
|
1024
1137
|
ctx.log.info(`[boot] ensured workspace dir ${WORKSPACE_DIR}`);
|
|
1025
1138
|
},
|
|
1026
1139
|
wireHome: (ctx) => {
|
|
1027
|
-
wireUserHome({
|
|
1140
|
+
void wireUserHome({
|
|
1028
1141
|
home: HOME_DIR,
|
|
1029
1142
|
userHomeMount: USER_HOME_MOUNT2,
|
|
1030
1143
|
userId: ctx.childEnv?.CONVEYOR_USER_ID,
|
|
@@ -1183,7 +1296,7 @@ function agentBootSteps(deps = defaultBootDeps(), signal = new AbortController()
|
|
|
1183
1296
|
critical: true,
|
|
1184
1297
|
run: (ctx) => {
|
|
1185
1298
|
if (ctx.bundle) {
|
|
1186
|
-
wireUserHome({
|
|
1299
|
+
void wireUserHome({
|
|
1187
1300
|
home: HOME_DIR2,
|
|
1188
1301
|
userHomeMount: USER_HOME_MOUNT3,
|
|
1189
1302
|
userId: ctx.childEnv?.CONVEYOR_USER_ID,
|
|
@@ -2041,7 +2041,8 @@ var projectCheckpointSettingsSchema = z.object({
|
|
|
2041
2041
|
finalizeCommand: z.string().trim().min(1),
|
|
2042
2042
|
credentialEpoch: z.string().trim().min(1),
|
|
2043
2043
|
requiredSecretNames: uniqueSortedArray(secretNameSchema).optional(),
|
|
2044
|
-
optionalSecretNames: uniqueSortedArray(secretNameSchema).optional()
|
|
2044
|
+
optionalSecretNames: uniqueSortedArray(secretNameSchema).optional(),
|
|
2045
|
+
bakeWebAppBuild: z.boolean().optional()
|
|
2045
2046
|
}).superRefine((checkpoint, ctx) => {
|
|
2046
2047
|
const required = new Set(checkpoint.requiredSecretNames ?? []);
|
|
2047
2048
|
for (const name of checkpoint.optionalSecretNames ?? []) {
|
|
@@ -2291,7 +2292,15 @@ var PostToChatInputSchema = z4.object({
|
|
|
2291
2292
|
});
|
|
2292
2293
|
var GetTaskContextRequestSchema = z4.object({
|
|
2293
2294
|
sessionId: z4.string(),
|
|
2294
|
-
includeHistory: z4.boolean().optional().default(false)
|
|
2295
|
+
includeHistory: z4.boolean().optional().default(false),
|
|
2296
|
+
/**
|
|
2297
|
+
* Read the plan-revised marker WITHOUT consuming it. Bookkeeping fetches
|
|
2298
|
+
* (the session-identity check, the branch refresh) pass true so they cannot
|
|
2299
|
+
* race the boot fetch and swallow the notice before it reaches the prompt.
|
|
2300
|
+
* Defaults to false — consuming — so a pod running an older agent build still
|
|
2301
|
+
* clears the marker instead of showing the notice on every boot forever.
|
|
2302
|
+
*/
|
|
2303
|
+
peekPlanRevision: z4.boolean().optional().default(false)
|
|
2295
2304
|
});
|
|
2296
2305
|
var GetChatMessagesRequestSchema = z4.object({
|
|
2297
2306
|
sessionId: z4.string(),
|
|
@@ -3856,6 +3865,17 @@ var PRE_BUILD_TASK_STATUSES = /* @__PURE__ */ new Set(["Planning", "Open"]);
|
|
|
3856
3865
|
function hasTaskPlan(plan) {
|
|
3857
3866
|
return !!plan?.trim();
|
|
3858
3867
|
}
|
|
3868
|
+
var CARD_TYPE_SURFACE = {
|
|
3869
|
+
task: "board",
|
|
3870
|
+
chat: "board",
|
|
3871
|
+
incident: "report",
|
|
3872
|
+
suggestion: "report"
|
|
3873
|
+
};
|
|
3874
|
+
var surfaceTypes = (surface) => Object.keys(CARD_TYPE_SURFACE).filter(
|
|
3875
|
+
(type) => CARD_TYPE_SURFACE[type] === surface
|
|
3876
|
+
);
|
|
3877
|
+
var BOARD_CARD_TYPES = surfaceTypes("board");
|
|
3878
|
+
var REPORT_CARD_TYPES = surfaceTypes("report");
|
|
3859
3879
|
|
|
3860
3880
|
// src/runner/mode-controller.ts
|
|
3861
3881
|
var ModeController = class {
|
|
@@ -8615,6 +8635,36 @@ After addressing the feedback, resume your autonomous loop: call list_subtasks a
|
|
|
8615
8635
|
return parts;
|
|
8616
8636
|
}
|
|
8617
8637
|
|
|
8638
|
+
// src/execution/plan-revision-notice.ts
|
|
8639
|
+
var MINUTE_MS = 60 * 1e3;
|
|
8640
|
+
var HOUR_MS = 60 * MINUTE_MS;
|
|
8641
|
+
var DAY_MS = 24 * HOUR_MS;
|
|
8642
|
+
function describeAge(revisedAt, now) {
|
|
8643
|
+
const at = Date.parse(revisedAt);
|
|
8644
|
+
if (Number.isNaN(at)) return "";
|
|
8645
|
+
const ago = now - at;
|
|
8646
|
+
if (ago < MINUTE_MS) return "just now";
|
|
8647
|
+
if (ago < HOUR_MS) return `${Math.round(ago / MINUTE_MS)} minutes ago`;
|
|
8648
|
+
if (ago < DAY_MS) return `${Math.round(ago / HOUR_MS)} hours ago`;
|
|
8649
|
+
return `${Math.round(ago / DAY_MS)} days ago`;
|
|
8650
|
+
}
|
|
8651
|
+
function buildPlanRevisionNotice(context, now = Date.now()) {
|
|
8652
|
+
const revisedAt = context.planRevisedAt;
|
|
8653
|
+
if (!revisedAt) return [];
|
|
8654
|
+
const age = describeAge(revisedAt, now);
|
|
8655
|
+
const when = age ? `${age} (${revisedAt})` : revisedAt;
|
|
8656
|
+
return [
|
|
8657
|
+
`
|
|
8658
|
+
## \u26A0\uFE0F The plan changed since this build was dispatched`,
|
|
8659
|
+
`Someone revised this card's plan ${when}. It was NOT you \u2014 an agent is never told about its own plan edit.`,
|
|
8660
|
+
`Before your next Write or Edit:`,
|
|
8661
|
+
`1. Call \`get_current_plan\` and read the current plan in full.`,
|
|
8662
|
+
`2. Compare it against the plan you were launched with. Treat the current plan as the truth.`,
|
|
8663
|
+
`3. Drop or redo any work the revision supersedes. Do NOT keep building the old plan.`,
|
|
8664
|
+
`If the revision invalidates work you already committed, say so with post_to_chat before you continue.`
|
|
8665
|
+
];
|
|
8666
|
+
}
|
|
8667
|
+
|
|
8618
8668
|
// src/execution/prompt-formatters.ts
|
|
8619
8669
|
function baseDiffCommand(baseBranch, flags) {
|
|
8620
8670
|
const base = baseBranch ?? "dev";
|
|
@@ -9265,6 +9315,17 @@ function buildPlanDocumentationSection(context) {
|
|
|
9265
9315
|
`- Identification auto-fills title, story points, and icon with quick AI guesses. After exploring, refine the title, story points, and risk with update_task_properties whenever they no longer match what the work actually is \u2014 adjust in either direction. Icons are automatic \u2014 never set them.`
|
|
9266
9316
|
];
|
|
9267
9317
|
}
|
|
9318
|
+
function buildPlanRevisionSection() {
|
|
9319
|
+
return [
|
|
9320
|
+
``,
|
|
9321
|
+
`### The plan can change while you sleep`,
|
|
9322
|
+
`If a turn opens with "The plan changed since this build was dispatched", honor it BEFORE your next Write or Edit:`,
|
|
9323
|
+
`1. Call get_current_plan and read the current plan in full.`,
|
|
9324
|
+
`2. Compare it against the plan you were launched with \u2014 the current plan wins.`,
|
|
9325
|
+
`3. Drop or redo whatever the revision supersedes, and say so with post_to_chat if it invalidates work you already committed.`,
|
|
9326
|
+
`Building on a superseded plan is the most expensive mistake a resumed card can make.`
|
|
9327
|
+
];
|
|
9328
|
+
}
|
|
9268
9329
|
function buildNoPrWhenNoCodeSection(baseBranch) {
|
|
9269
9330
|
const diffCommand = baseDiffCommand(baseBranch);
|
|
9270
9331
|
return [
|
|
@@ -9464,6 +9525,7 @@ function buildAutoPrompt(context, runnerMode) {
|
|
|
9464
9525
|
...buildPrGuideSection(),
|
|
9465
9526
|
...buildNoPrWhenNoCodeSection(context?.baseBranch)
|
|
9466
9527
|
],
|
|
9528
|
+
...buildPlanRevisionSection(),
|
|
9467
9529
|
``,
|
|
9468
9530
|
`### Autonomous Guidelines:`,
|
|
9469
9531
|
`- Make decisions independently \u2014 do not ask the team for approval at each step`,
|
|
@@ -9493,7 +9555,8 @@ function buildBuildingPrompt(context) {
|
|
|
9493
9555
|
...buildPrGuideSection(),
|
|
9494
9556
|
...buildNoPrWhenNoCodeSection(context?.baseBranch),
|
|
9495
9557
|
...context?.isAuto || !context?.plan?.trim() ? buildPlanDocumentationSection(context) : []
|
|
9496
|
-
]
|
|
9558
|
+
],
|
|
9559
|
+
...buildPlanRevisionSection()
|
|
9497
9560
|
];
|
|
9498
9561
|
if (context) parts.push(...buildPropertyInstructions(context));
|
|
9499
9562
|
return parts.join("\n");
|
|
@@ -9939,6 +10002,7 @@ ${context.description}`);
|
|
|
9939
10002
|
## Plan
|
|
9940
10003
|
${truncatePlanForPrompt(context.plan)}`);
|
|
9941
10004
|
}
|
|
10005
|
+
parts.push(...buildPlanRevisionNotice(context));
|
|
9942
10006
|
if (context.files && context.files.length > 0) {
|
|
9943
10007
|
parts.push(`
|
|
9944
10008
|
## Attached Files`);
|
|
@@ -10209,7 +10273,9 @@ async function buildInitialPrompt(mode, context, isAuto, agentMode) {
|
|
|
10209
10273
|
const isPackRunner = mode === "pack" || mode === "pm" && !!isAuto && !!context.isParentTask;
|
|
10210
10274
|
if (!isPackRunner) {
|
|
10211
10275
|
const sessionRelaunch = buildRelaunchWithSession(mode, context, agentMode, isAuto);
|
|
10212
|
-
if (sessionRelaunch)
|
|
10276
|
+
if (sessionRelaunch) {
|
|
10277
|
+
return [...buildPlanRevisionNotice(context), sessionRelaunch].join("\n");
|
|
10278
|
+
}
|
|
10213
10279
|
}
|
|
10214
10280
|
const isPm = mode === "pm";
|
|
10215
10281
|
let scenario = detectRelaunchScenario(context, isPm);
|
|
@@ -11160,7 +11226,12 @@ function buildGetCurrentPlanTool(connection) {
|
|
|
11160
11226
|
const ctx = await connection.call("getTaskContext", {
|
|
11161
11227
|
sessionId: connection.sessionId
|
|
11162
11228
|
});
|
|
11163
|
-
|
|
11229
|
+
const plan = ctx.plan ?? "No plan available.";
|
|
11230
|
+
return textResult(
|
|
11231
|
+
ctx.planRevisedAt ? `\u26A0\uFE0F This plan was revised ${ctx.planRevisedAt}, after your build was dispatched. Treat it as the truth and drop any work it supersedes.
|
|
11232
|
+
|
|
11233
|
+
${plan}` : plan
|
|
11234
|
+
);
|
|
11164
11235
|
} catch {
|
|
11165
11236
|
return textResult("Could not fetch updated plan.");
|
|
11166
11237
|
}
|
|
@@ -16893,6 +16964,9 @@ var SessionRunner = class _SessionRunner {
|
|
|
16893
16964
|
title: ctx.title,
|
|
16894
16965
|
description: ctx.description,
|
|
16895
16966
|
plan: ctx.plan,
|
|
16967
|
+
// The server hands this to exactly one context fetch per revision, so it
|
|
16968
|
+
// must survive the mapping or the notice is lost.
|
|
16969
|
+
planRevisedAt: ctx.planRevisedAt ?? null,
|
|
16896
16970
|
status: ctx.status,
|
|
16897
16971
|
chatHistory,
|
|
16898
16972
|
agentId: ctx.agentId ?? null,
|
|
@@ -17065,7 +17139,10 @@ ${outcome.failures.join("\n")}
|
|
|
17065
17139
|
try {
|
|
17066
17140
|
const ctx = await this.connection.call("getTaskContext", {
|
|
17067
17141
|
sessionId: this.sessionId,
|
|
17068
|
-
includeHistory: false
|
|
17142
|
+
includeHistory: false,
|
|
17143
|
+
// Bookkeeping fetch — it must not swallow a plan-revised notice the
|
|
17144
|
+
// next prompt is about to carry.
|
|
17145
|
+
peekPlanRevision: true
|
|
17069
17146
|
});
|
|
17070
17147
|
if (ctx?.githubBranch && this.fullContext) {
|
|
17071
17148
|
this.fullContext.githubBranch = ctx.githubBranch;
|
package/dist/cli.js
CHANGED
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
sampleKeyUsage,
|
|
36
36
|
statWorkspacePath,
|
|
37
37
|
workspacePathExists
|
|
38
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-7MMECTTJ.js";
|
|
39
39
|
import {
|
|
40
40
|
reportBootMilestone
|
|
41
41
|
} from "./chunk-QU53HND5.js";
|
|
@@ -1715,7 +1715,7 @@ function hostsSpawnedChildren(mode) {
|
|
|
1715
1715
|
|
|
1716
1716
|
// src/cli.ts
|
|
1717
1717
|
if (process.argv[2] === "boot") {
|
|
1718
|
-
const { runBoot } = await import("./boot-
|
|
1718
|
+
const { runBoot } = await import("./boot-PKHZUDAC.js");
|
|
1719
1719
|
process.exit(await runBoot(process.argv.slice(3)));
|
|
1720
1720
|
}
|
|
1721
1721
|
if (isLegacyEntrypointLaunch(process.env)) {
|
|
@@ -2100,7 +2100,10 @@ void checkSessionTaskIdentity({
|
|
|
2100
2100
|
sessionId: process.env.CONVEYOR_SESSION_ID,
|
|
2101
2101
|
taskId: CONVEYOR_TASK_ID,
|
|
2102
2102
|
fetchSessionTaskId: async (sessionId) => {
|
|
2103
|
-
const ctx = await runner.connection.call("getTaskContext", {
|
|
2103
|
+
const ctx = await runner.connection.call("getTaskContext", {
|
|
2104
|
+
sessionId,
|
|
2105
|
+
peekPlanRevision: true
|
|
2106
|
+
});
|
|
2104
2107
|
return ctx.id;
|
|
2105
2108
|
},
|
|
2106
2109
|
logger: logger5
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rallycry/conveyor-agent",
|
|
3
|
-
"version": "10.13.
|
|
3
|
+
"version": "10.13.71",
|
|
4
4
|
"description": "Conveyor Agent Runner v10 - PTY harness for the task chat (SDK harness for audit/project-chat). Agent-as-User architecture with BaseService patterns. Works locally too.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|