@ouro.bot/cli 0.1.0-alpha.990 → 0.1.0-alpha.992
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.json +12 -0
- package/deploy/unraid/Dockerfile +15 -2
- package/deploy/unraid/README.txt +44 -9
- package/deploy/unraid/github_known_hosts +3 -0
- package/deploy/unraid/ouro-sync-ssh +98 -0
- package/deploy/unraid/ouro-sync.sudoers +3 -0
- package/deploy/unraid/sanctuary-butler-upgrade.mjs +26 -2
- package/deploy/unraid/sanctuary-replay-gate.mjs +361 -8
- package/deploy/unraid/sanctuary-sync-cutover.sh +1039 -0
- package/deploy/unraid/sanctuary.ouro/agent.json +2 -1
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.ouro/tool-profiles.json +3 -1
- package/deploy/unraid/sanctuary.xml +2 -1
- package/dist/heart/config.js +2 -1
- package/dist/heart/daemon/agent-discovery.js +5 -1
- package/dist/heart/daemon/agent-home.js +67 -31
- package/dist/heart/daemon/boot-sync-probe.js +36 -10
- package/dist/heart/daemon/cli-exec.js +79 -20
- package/dist/heart/daemon/cli-help.js +12 -0
- package/dist/heart/daemon/cli-parse.js +14 -0
- package/dist/heart/daemon/container-spec-auditor-main.js +22 -6
- package/dist/heart/daemon/container-spec-auditor.js +18 -2
- package/dist/heart/daemon/daemon-entry.js +22 -0
- package/dist/heart/daemon/sanctuary-acceptance-harness.js +1 -1
- package/dist/heart/daemon/sanctuary-authority-root-lifecycle.js +21 -5
- package/dist/heart/daemon/sanctuary-bundle-migration.js +97 -1
- package/dist/heart/daemon/sanctuary-package-management.js +10 -0
- package/dist/heart/sync-lock.js +431 -0
- package/dist/heart/sync.js +1252 -399
- package/dist/mind/prompt.js +92 -19
- package/dist/repertoire/guardrails.js +285 -3
- package/dist/repertoire/tools-bundle.js +107 -185
- package/dist/repertoire/tools.js +5 -1
- package/dist/senses/pipeline.js +37 -3
- package/dist/senses/private-runtime.js +1 -6
- package/dist/senses/voice/twilio-phone.js +16 -16
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -35,13 +35,14 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.bundleToolDefinitions = void 0;
|
|
37
37
|
exports.__getConfirmationTokenStore = __getConfirmationTokenStore;
|
|
38
|
-
exports.
|
|
38
|
+
exports.probeFirstPush = probeFirstPush;
|
|
39
39
|
/**
|
|
40
40
|
* Agent-callable bundle management tools.
|
|
41
41
|
*
|
|
42
42
|
* These tools let the agent fix its own git state — initializing the
|
|
43
|
-
* bundle as a repo, adding a remote, making the first commit,
|
|
44
|
-
*
|
|
43
|
+
* bundle as a repo, adding a remote, making the first commit, and syncing
|
|
44
|
+
* with the remote (bundle_push and bundle_pull_rebase both run the shared
|
|
45
|
+
* merge-only `syncNow`; neither ever leaves a rebase, a merge or a stash). The `bundleState` field in the
|
|
45
46
|
* start-of-turn packet (PR 5) points the agent at these tools whenever
|
|
46
47
|
* it detects an unresolved git issue.
|
|
47
48
|
*
|
|
@@ -57,8 +58,7 @@ exports.isFirstPushToRemote = isFirstPushToRemote;
|
|
|
57
58
|
* ## Destructive-op refusal pattern (Directive B)
|
|
58
59
|
*
|
|
59
60
|
* Tools that could lose work (bundle_init_git on an existing repo,
|
|
60
|
-
* bundle_add_remote on a configured remote,
|
|
61
|
-
* dirty tree, etc.) refuse by default and require an explicit `force`
|
|
61
|
+
* bundle_add_remote on a configured remote, etc.) refuse by default and require an explicit `force`
|
|
62
62
|
* flag — mirroring Claude Code's `ExitWorktreeTool` safety pattern. The
|
|
63
63
|
* refusal surface is the LLM's responsibility to bridge with the human:
|
|
64
64
|
* when a tool refuses, the agent should ask the user for permission
|
|
@@ -169,19 +169,16 @@ const checkSyncStatusHandler = () => {
|
|
|
169
169
|
.map((l) => l.trim())
|
|
170
170
|
.filter((l) => l.length > 0).length;
|
|
171
171
|
}
|
|
172
|
-
/* v8 ignore start -- upstream tracking ahead/behind requires a live remote, not practical to cover in unit tests @preserve */
|
|
173
172
|
if (hasRemote && firstCommitExists) {
|
|
174
|
-
//
|
|
175
|
-
const
|
|
173
|
+
// Against the last fetched sync branch, named explicitly; no upstream config is assumed.
|
|
174
|
+
const config = bundleSyncConfig(bundleRoot);
|
|
175
|
+
const counts = gitExec(bundleRoot, ["rev-list", "--left-right", "--count", `HEAD...refs/remotes/${config.remote}/${config.branch}`]);
|
|
176
176
|
if (counts.code === 0) {
|
|
177
177
|
const [a, b] = counts.stdout.trim().split("\t").map((n) => parseInt(n, 10));
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
behind = b;
|
|
181
|
-
}
|
|
178
|
+
ahead = a;
|
|
179
|
+
behind = b;
|
|
182
180
|
}
|
|
183
181
|
}
|
|
184
|
-
/* v8 ignore stop */
|
|
185
182
|
}
|
|
186
183
|
const pendingSyncExists = fs.existsSync(path.join(bundleRoot, "state", "pending-sync.json"));
|
|
187
184
|
const bundleStateIssues = (0, bundle_state_1.detectBundleState)(bundleRoot);
|
|
@@ -645,69 +642,78 @@ function makeFirstPushReviewHandler(deps = {}) {
|
|
|
645
642
|
};
|
|
646
643
|
}
|
|
647
644
|
const firstPushReviewHandler = makeFirstPushReviewHandler();
|
|
648
|
-
// ─── tool: bundle_push ─────────────────────────────────────────────────
|
|
649
|
-
/* v8 ignore start -- push error classification branches require mocking git stderr from each failure mode; covered by a single network-failure integration test in the suite @preserve */
|
|
650
|
-
function classifyPushError(stderr) {
|
|
651
|
-
const lower = stderr.toLowerCase();
|
|
652
|
-
if (lower.includes("rejected") || lower.includes("non-fast-forward") || lower.includes("fetch first"))
|
|
653
|
-
return "rejected";
|
|
654
|
-
if (lower.includes("could not resolve") || lower.includes("network") || lower.includes("connection") || lower.includes("timeout"))
|
|
655
|
-
return "network";
|
|
656
|
-
if (lower.includes("authentication") || lower.includes("permission denied") || lower.includes("unauthorized") || lower.includes("403"))
|
|
657
|
-
return "auth";
|
|
658
|
-
return "unknown";
|
|
659
|
-
}
|
|
660
|
-
/* v8 ignore stop */
|
|
661
645
|
/**
|
|
662
|
-
*
|
|
646
|
+
* Whether a push to `remote` would be the first push of `branch`: "first" when
|
|
647
|
+
* the remote has no such branch, "pushed" when it has. The remote is asked
|
|
648
|
+
* through sync's own ls-remote (the same flags and environment as sync's push,
|
|
649
|
+
* so in the container it goes through the ouro-sync ssh). A failed ls-remote
|
|
650
|
+
* is "unreachable", never taken for a first push or for a pushed one.
|
|
663
651
|
*
|
|
664
|
-
* Returns true if the remote branch does not yet exist (bundle has local
|
|
665
|
-
* commits but has never been pushed). Also returns true — conservatively —
|
|
666
|
-
* when git fails to probe the remote (network unreachable, git error).
|
|
667
|
-
* The token requirement is a security gate; if we can't verify the remote
|
|
668
|
-
* state, we assume the worst and force the agent to get confirmation. An
|
|
669
|
-
* unreachable remote should NOT be a bypass vector.
|
|
670
|
-
*/
|
|
671
|
-
/**
|
|
672
652
|
* Exported for direct unit testing in bundle-push-first-push.test.ts.
|
|
673
|
-
* The integration tests use unreachable remotes so they always hit the
|
|
674
|
-
* network-failure branch. The unit tests mock child_process to exercise
|
|
675
|
-
* the successful-probe branches (empty stdout = first push, non-empty
|
|
676
|
-
* = subsequent push, symbolic-ref failure = conservative true).
|
|
677
653
|
*/
|
|
678
|
-
function
|
|
679
|
-
const
|
|
680
|
-
if (
|
|
681
|
-
return
|
|
682
|
-
|
|
683
|
-
const lsRemote = gitExec(bundleRoot, ["ls-remote", "--heads", remote, branch], 10000);
|
|
684
|
-
if (lsRemote.code !== 0)
|
|
685
|
-
return true;
|
|
686
|
-
return lsRemote.stdout.trim().length === 0;
|
|
654
|
+
async function probeFirstPush(bundleRoot, remote, branch) {
|
|
655
|
+
const listed = await (0, sync_1.listRemoteRefs)(bundleRoot, remote, [`refs/heads/${branch}`]);
|
|
656
|
+
if (!listed.ok)
|
|
657
|
+
return { kind: "unreachable", error: listed.error };
|
|
658
|
+
return listed.refs.length === 0 ? { kind: "first" } : { kind: "pushed" };
|
|
687
659
|
}
|
|
688
|
-
|
|
689
|
-
|
|
660
|
+
/** How long a bundle tool waits for the bundle sync lock before reporting busy. */
|
|
661
|
+
const BUNDLE_TOOL_SYNC_WAIT_MS = 10_000;
|
|
662
|
+
/** The bundle's sync remote and branch from agent.json: the agent cannot name another remote. */
|
|
663
|
+
function bundleSyncConfig(bundleRoot) {
|
|
664
|
+
let remote = "origin";
|
|
665
|
+
let branch = "main";
|
|
666
|
+
try {
|
|
667
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(bundleRoot, "agent.json"), "utf-8"));
|
|
668
|
+
if (typeof parsed.sync?.remote === "string" && parsed.sync.remote.length > 0)
|
|
669
|
+
remote = parsed.sync.remote;
|
|
670
|
+
if (typeof parsed.sync?.branch === "string" && parsed.sync.branch.length > 0)
|
|
671
|
+
branch = parsed.sync.branch;
|
|
672
|
+
}
|
|
673
|
+
catch {
|
|
674
|
+
// No readable agent.json: the defaults.
|
|
675
|
+
}
|
|
676
|
+
return { enabled: true, remote, branch };
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Both tools sync the bundle through `syncNow`: commit, fetch, merge, push,
|
|
680
|
+
* under the bundle lock. A conflict is aborted and recorded by sync itself,
|
|
681
|
+
* so no tool call leaves a merge, a rebase or a stash behind.
|
|
682
|
+
*/
|
|
683
|
+
function makeSyncHandler(toolName, deps = {}) {
|
|
684
|
+
return async (args) => {
|
|
690
685
|
const bundleRoot = (0, identity_1.getAgentRoot)();
|
|
691
|
-
const remote = typeof args.remote === "string" && args.remote.length > 0 ? args.remote : "origin";
|
|
692
686
|
const confirmationToken = typeof args.confirmation_token === "string"
|
|
693
687
|
? args.confirmation_token
|
|
694
688
|
: undefined;
|
|
695
689
|
const now = deps.now ?? Date.now;
|
|
690
|
+
const config = bundleSyncConfig(bundleRoot);
|
|
691
|
+
const remote = config.remote;
|
|
696
692
|
(0, runtime_1.emitNervesEvent)({
|
|
697
693
|
component: "repertoire",
|
|
698
|
-
event: "repertoire.
|
|
699
|
-
message: "
|
|
700
|
-
meta: { bundleRoot, remote },
|
|
694
|
+
event: "repertoire.bundle_sync_tool_start",
|
|
695
|
+
message: "syncing bundle from a tool",
|
|
696
|
+
meta: { bundleRoot, tool: toolName, remote },
|
|
701
697
|
});
|
|
702
698
|
const finish = (ok, payload) => {
|
|
703
699
|
(0, runtime_1.emitNervesEvent)({
|
|
704
700
|
component: "repertoire",
|
|
705
|
-
event: "repertoire.
|
|
706
|
-
message: "
|
|
707
|
-
meta: { bundleRoot, ok, ...payload },
|
|
701
|
+
event: "repertoire.bundle_sync_tool_end",
|
|
702
|
+
message: "bundle sync tool finished",
|
|
703
|
+
meta: { bundleRoot, tool: toolName, ok, ...payload },
|
|
708
704
|
});
|
|
709
705
|
return json({ ok, ...payload });
|
|
710
706
|
};
|
|
707
|
+
const refuse = (message, error) => {
|
|
708
|
+
(0, runtime_1.emitNervesEvent)({
|
|
709
|
+
level: "warn",
|
|
710
|
+
component: "repertoire",
|
|
711
|
+
event: "repertoire.bundle_push_refused",
|
|
712
|
+
message,
|
|
713
|
+
meta: { bundleRoot, tool: toolName },
|
|
714
|
+
});
|
|
715
|
+
return finish(false, { error, kind: "confirmation_required" });
|
|
716
|
+
};
|
|
711
717
|
if (!isGitRepo(bundleRoot)) {
|
|
712
718
|
return finish(false, { error: "bundle is not a git repo" });
|
|
713
719
|
}
|
|
@@ -722,50 +728,22 @@ function makePushHandler(deps = {}) {
|
|
|
722
728
|
// previously called bundle_first_push_review and obtained a
|
|
723
729
|
// confirmation token. This forces a PII-review gate between
|
|
724
730
|
// "bundle built locally" and "bundle contents on the internet".
|
|
725
|
-
const
|
|
726
|
-
|
|
731
|
+
const probe = await probeFirstPush(bundleRoot, remote, config.branch);
|
|
732
|
+
if (probe.kind === "unreachable") {
|
|
733
|
+
return finish(false, { error: `I couldn't reach GitHub to check whether this would be my first push, so I didn't sync: ${probe.error}`, kind: "remote_unreachable" });
|
|
734
|
+
}
|
|
735
|
+
const isFirstPush = probe.kind === "first";
|
|
727
736
|
if (isFirstPush) {
|
|
728
737
|
if (!confirmationToken) {
|
|
729
|
-
(
|
|
730
|
-
level: "warn",
|
|
731
|
-
component: "repertoire",
|
|
732
|
-
event: "repertoire.bundle_push_refused",
|
|
733
|
-
message: "bundle_push refused: first push requires confirmation token",
|
|
734
|
-
meta: { bundleRoot },
|
|
735
|
-
});
|
|
736
|
-
return finish(false, {
|
|
737
|
-
error: "refused: first push requires a confirmation token — call bundle_first_push_review, show the warning to the user, and pass the returned confirmationToken to bundle_push",
|
|
738
|
-
kind: "confirmation_required",
|
|
739
|
-
});
|
|
738
|
+
return refuse(`${toolName} refused: first push requires confirmation token`, `refused: first push requires a confirmation token — call bundle_first_push_review, show the warning to the user, and pass the returned confirmationToken to ${toolName}`);
|
|
740
739
|
}
|
|
741
|
-
|
|
742
|
-
pruneExpiredTokens(currentTime);
|
|
740
|
+
pruneExpiredTokens(now());
|
|
743
741
|
const entry = _confirmationTokens.get(confirmationToken);
|
|
744
742
|
if (!entry) {
|
|
745
|
-
(
|
|
746
|
-
level: "warn",
|
|
747
|
-
component: "repertoire",
|
|
748
|
-
event: "repertoire.bundle_push_refused",
|
|
749
|
-
message: "bundle_push refused: confirmation token invalid or expired",
|
|
750
|
-
meta: { bundleRoot },
|
|
751
|
-
});
|
|
752
|
-
return finish(false, {
|
|
753
|
-
error: "refused: confirmation token invalid or expired — call bundle_first_push_review again",
|
|
754
|
-
kind: "confirmation_required",
|
|
755
|
-
});
|
|
743
|
+
return refuse(`${toolName} refused: confirmation token invalid or expired`, "refused: confirmation token invalid or expired — call bundle_first_push_review again");
|
|
756
744
|
}
|
|
757
745
|
if (entry.bundleRoot !== bundleRoot) {
|
|
758
|
-
(
|
|
759
|
-
level: "warn",
|
|
760
|
-
component: "repertoire",
|
|
761
|
-
event: "repertoire.bundle_push_refused",
|
|
762
|
-
message: "bundle_push refused: confirmation token bound to a different bundle",
|
|
763
|
-
meta: { bundleRoot, tokenBundleRoot: entry.bundleRoot },
|
|
764
|
-
});
|
|
765
|
-
return finish(false, {
|
|
766
|
-
error: "refused: confirmation token was issued for a different bundle",
|
|
767
|
-
kind: "confirmation_required",
|
|
768
|
-
});
|
|
746
|
+
return refuse(`${toolName} refused: confirmation token bound to a different bundle`, "refused: confirmation token was issued for a different bundle");
|
|
769
747
|
}
|
|
770
748
|
(0, runtime_1.emitNervesEvent)({
|
|
771
749
|
component: "repertoire",
|
|
@@ -773,95 +751,41 @@ function makePushHandler(deps = {}) {
|
|
|
773
751
|
message: "first push confirmed by valid token",
|
|
774
752
|
meta: { bundleRoot },
|
|
775
753
|
});
|
|
776
|
-
// One-shot: consume the token on successful validation.
|
|
777
|
-
_confirmationTokens.delete(confirmationToken);
|
|
778
754
|
}
|
|
779
|
-
const
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
755
|
+
const agentName = path.basename(bundleRoot).replace(/\.ouro$/, "");
|
|
756
|
+
// The valid token is the owner's recorded yes, which sync needs before a first push to an empty remote.
|
|
757
|
+
const result = await (0, sync_1.syncNow)(bundleRoot, config, agentName, BUNDLE_TOOL_SYNC_WAIT_MS, { firstPushApproved: isFirstPush });
|
|
758
|
+
const where = { remote, branch: config.branch, firstPush: isFirstPush };
|
|
759
|
+
if (result.paused)
|
|
760
|
+
return finish(false, { ...where, paused: true, pausedUntil: result.pausedUntil, error: `sync paused until ${result.pausedUntil}` });
|
|
761
|
+
if (result.busy)
|
|
762
|
+
return finish(false, { ...where, busy: true, holderPid: result.holderPid, error: result.error });
|
|
763
|
+
// One-shot: the token is spent once sync actually ran, not on a busy or paused refusal.
|
|
764
|
+
if (confirmationToken !== undefined && isFirstPush)
|
|
765
|
+
_confirmationTokens.delete(confirmationToken);
|
|
766
|
+
if (!result.ok) {
|
|
767
|
+
const failure = readPendingFailure(bundleRoot);
|
|
768
|
+
return finish(false, {
|
|
769
|
+
...where,
|
|
770
|
+
error: result.error,
|
|
771
|
+
...(failure?.classification ? { kind: failure.classification } : {}),
|
|
772
|
+
...(failure?.conflictFiles && failure.conflictFiles.length > 0 ? { conflictFiles: failure.conflictFiles } : {}),
|
|
773
|
+
});
|
|
784
774
|
}
|
|
785
|
-
|
|
786
|
-
const kind = classifyPushError(push.stderr);
|
|
787
|
-
return finish(false, { error: push.stderr.trim(), kind });
|
|
775
|
+
return finish(true, where);
|
|
788
776
|
};
|
|
789
777
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
const remote = typeof args.remote === "string" && args.remote.length > 0 ? args.remote : "origin";
|
|
795
|
-
const discardChanges = args.discard_changes === "true" || args.discard_changes === true;
|
|
796
|
-
(0, runtime_1.emitNervesEvent)({
|
|
797
|
-
component: "repertoire",
|
|
798
|
-
event: "repertoire.bundle_pull_rebase_start",
|
|
799
|
-
message: "pulling with rebase",
|
|
800
|
-
meta: { bundleRoot, remote, discardChanges },
|
|
801
|
-
});
|
|
802
|
-
const finish = (ok, payload) => {
|
|
803
|
-
(0, runtime_1.emitNervesEvent)({
|
|
804
|
-
component: "repertoire",
|
|
805
|
-
event: "repertoire.bundle_pull_rebase_end",
|
|
806
|
-
message: "bundle_pull_rebase finished",
|
|
807
|
-
meta: { bundleRoot, ok, ...payload },
|
|
808
|
-
});
|
|
809
|
-
return json({ ok, ...payload });
|
|
810
|
-
};
|
|
811
|
-
if (!isGitRepo(bundleRoot)) {
|
|
812
|
-
return finish(false, { error: "bundle is not a git repo" });
|
|
813
|
-
}
|
|
814
|
-
const remotes = listRemotes(bundleRoot);
|
|
815
|
-
if (!remotes.includes(remote)) {
|
|
816
|
-
return finish(false, { error: `remote "${remote}" not configured` });
|
|
817
|
-
}
|
|
818
|
-
// Uncommitted changes: refuse unless discard_changes was explicitly set.
|
|
819
|
-
// Even with discard_changes, we stash+rebase+pop rather than nuke — the
|
|
820
|
-
// user can manually `git stash drop` if they really want to discard.
|
|
821
|
-
const status = gitExec(bundleRoot, ["status", "--porcelain"]);
|
|
822
|
-
const isDirty = status.stdout.trim().length > 0;
|
|
823
|
-
const stashed = { value: false };
|
|
824
|
-
if (isDirty) {
|
|
825
|
-
if (!discardChanges) {
|
|
826
|
-
(0, runtime_1.emitNervesEvent)({
|
|
827
|
-
level: "warn",
|
|
828
|
-
component: "repertoire",
|
|
829
|
-
event: "repertoire.bundle_pull_rebase_refused",
|
|
830
|
-
message: "bundle_pull_rebase refused: uncommitted changes",
|
|
831
|
-
meta: { bundleRoot },
|
|
832
|
-
});
|
|
833
|
-
return finish(false, { error: "refused: bundle has uncommitted changes — commit them or pass discard_changes: true (stash+rebase+pop)" });
|
|
834
|
-
}
|
|
835
|
-
/* v8 ignore start -- discardChanges=true stash path exercised in a dirty-tree test below but full rebase+pop requires reachable remote @preserve */
|
|
836
|
-
const stash = gitExec(bundleRoot, ["stash", "push", "-u", "-m", "bundle_pull_rebase stash"]);
|
|
837
|
-
if (stash.code !== 0) {
|
|
838
|
-
return finish(false, { error: `git stash failed: ${stash.stderr}` });
|
|
839
|
-
}
|
|
840
|
-
stashed.value = true;
|
|
841
|
-
/* v8 ignore stop */
|
|
778
|
+
/** The pending-sync record sync wrote for this failure, when readable. */
|
|
779
|
+
function readPendingFailure(bundleRoot) {
|
|
780
|
+
try {
|
|
781
|
+
return JSON.parse(fs.readFileSync(path.join(bundleRoot, "state", "pending-sync.json"), "utf-8"));
|
|
842
782
|
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
return finish(false, { error: `git stash pop failed after successful rebase: ${pop.stderr}`, kind: "stash_conflict" });
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
return finish(true, { remote, stashed: stashed.value });
|
|
853
|
-
}
|
|
854
|
-
/* v8 ignore stop */
|
|
855
|
-
// Capture conflict files if the rebase left us mid-conflict.
|
|
856
|
-
const conflictStatus = gitExec(bundleRoot, ["status", "--porcelain=v1"]);
|
|
857
|
-
const conflictFiles = [];
|
|
858
|
-
for (const line of conflictStatus.stdout.split("\n")) {
|
|
859
|
-
/* v8 ignore next -- conflict marker extraction requires a real merge conflict, covered indirectly via the network-failure test which returns empty conflictStatus @preserve */
|
|
860
|
-
if (/^(UU|AA|DD|AU|UA|DU|UD) /.test(line))
|
|
861
|
-
conflictFiles.push(line.slice(3).trim());
|
|
862
|
-
}
|
|
863
|
-
return finish(false, { error: rebase.stderr.trim(), kind: "conflict", conflictFiles });
|
|
864
|
-
};
|
|
783
|
+
catch {
|
|
784
|
+
return null;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
const pushHandler = makeSyncHandler("bundle_push");
|
|
788
|
+
const pullRebaseHandler = makeSyncHandler("bundle_pull_rebase");
|
|
865
789
|
// ─── registry ──────────────────────────────────────────────────────────
|
|
866
790
|
exports.bundleToolDefinitions = [
|
|
867
791
|
{
|
|
@@ -946,7 +870,7 @@ exports.bundleToolDefinitions = [
|
|
|
946
870
|
type: "function",
|
|
947
871
|
function: {
|
|
948
872
|
name: "bundle_first_push_review",
|
|
949
|
-
description: "Review my bundle for PII exposure before the first push to a new remote. Enumerates PII-bearing directories (friends, Desk record, Arc, etc.) with per-directory counts, probes the remote URL for GitHub public/private visibility, and returns a first-person warning text I must show the human plus a confirmationToken I must pass to bundle_push on first push. Required before the first push to any new remote.",
|
|
873
|
+
description: "Review my bundle for PII exposure before the first push to a new remote. Enumerates PII-bearing directories (friends, Desk record, Arc, etc.) with per-directory counts, probes the remote URL for GitHub public/private visibility, and returns a first-person warning text I must show the human plus a confirmationToken I must pass to bundle_push or bundle_pull_rebase on first push. Required before the first push to any new remote.",
|
|
950
874
|
parameters: { type: "object", properties: {} },
|
|
951
875
|
},
|
|
952
876
|
},
|
|
@@ -957,11 +881,10 @@ exports.bundleToolDefinitions = [
|
|
|
957
881
|
type: "function",
|
|
958
882
|
function: {
|
|
959
883
|
name: "bundle_push",
|
|
960
|
-
description: "
|
|
884
|
+
description: "Sync my bundle with its remote now.",
|
|
961
885
|
parameters: {
|
|
962
886
|
type: "object",
|
|
963
887
|
properties: {
|
|
964
|
-
remote: { type: "string", description: "Remote name. Defaults to 'origin'." },
|
|
965
888
|
confirmation_token: { type: "string", description: "Opaque token from bundle_first_push_review. Required on the first push to a new remote; ignored on subsequent pushes." },
|
|
966
889
|
},
|
|
967
890
|
},
|
|
@@ -975,12 +898,11 @@ exports.bundleToolDefinitions = [
|
|
|
975
898
|
type: "function",
|
|
976
899
|
function: {
|
|
977
900
|
name: "bundle_pull_rebase",
|
|
978
|
-
description: "
|
|
901
|
+
description: "Sync my bundle with its remote now.",
|
|
979
902
|
parameters: {
|
|
980
903
|
type: "object",
|
|
981
904
|
properties: {
|
|
982
|
-
|
|
983
|
-
discard_changes: { type: "boolean", description: "Stash + rebase + pop even if the working tree is dirty. Default false." },
|
|
905
|
+
confirmation_token: { type: "string", description: "Opaque token from bundle_first_push_review. Required on the first push to a new remote; ignored on subsequent pushes." },
|
|
984
906
|
},
|
|
985
907
|
},
|
|
986
908
|
},
|
|
@@ -989,7 +911,7 @@ exports.bundleToolDefinitions = [
|
|
|
989
911
|
riskProfile: {
|
|
990
912
|
mutates: ["durable_state_write", "external_side_effect"],
|
|
991
913
|
risk: "high",
|
|
992
|
-
reason: "
|
|
914
|
+
reason: "syncs bundle state with a remote",
|
|
993
915
|
},
|
|
994
916
|
},
|
|
995
917
|
];
|
package/dist/repertoire/tools.js
CHANGED
|
@@ -86,6 +86,8 @@ exports.SANCTUARY_OWNER_ADDITIONS = new Set([
|
|
|
86
86
|
"web_search", "search_facts", "consult_diary", "consult_notes", "get_friend_note",
|
|
87
87
|
"session_summary", "query_session", "set_reasoning_effort", "restart_runtime", "revive_sense",
|
|
88
88
|
]);
|
|
89
|
+
/** The bundle sync tools, granted to the Sanctuary owner alone. */
|
|
90
|
+
const SANCTUARY_OWNER_SYNC_TOOLS = new Set(["bundle_push", "bundle_pull_rebase"]);
|
|
89
91
|
// Re-export types and constants used by the rest of the codebase
|
|
90
92
|
var tools_base_2 = require("./tools-base");
|
|
91
93
|
Object.defineProperty(exports, "tools", { enumerable: true, get: function () { return tools_base_2.tools; } });
|
|
@@ -158,7 +160,9 @@ function selectToolsForChannel(capabilities, toolPreferences, _context, provider
|
|
|
158
160
|
? [
|
|
159
161
|
...tools_base_1.baseToolDefinitions.filter((definition) => SANCTUARY_RELATIONSHIP_BASE_TOOLS.has(definition.tool.function.name)
|
|
160
162
|
|| (ownerLike && exports.SANCTUARY_OWNER_ADDITIONS.has(definition.tool.function.name))),
|
|
161
|
-
...tools_unraid_1.unraidToolDefinitions, tools_steward_policy_1.stewardPolicyToolDefinition, ...(ownerLike ? tools_house_care_1.houseCareToolDefinitions : []),
|
|
163
|
+
...tools_unraid_1.unraidToolDefinitions, tools_steward_policy_1.stewardPolicyToolDefinition, ...(ownerLike ? tools_house_care_1.houseCareToolDefinitions : []),
|
|
164
|
+
...(owner ? tools_bundle_1.bundleToolDefinitions.filter((definition) => SANCTUARY_OWNER_SYNC_TOOLS.has(definition.tool.function.name)) : []),
|
|
165
|
+
...(ownerLike ? mcp : []),
|
|
162
166
|
]
|
|
163
167
|
: [];
|
|
164
168
|
}
|
package/dist/senses/pipeline.js
CHANGED
|
@@ -40,6 +40,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
40
40
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
41
|
exports.emitObligationTransitionEpisodes = emitObligationTransitionEpisodes;
|
|
42
42
|
exports.selectCheckpointCurrentAsk = selectCheckpointCurrentAsk;
|
|
43
|
+
exports.startPostTurnSync = startPostTurnSync;
|
|
43
44
|
exports.handleInboundTurn = handleInboundTurn;
|
|
44
45
|
const fs = __importStar(require("fs"));
|
|
45
46
|
const path = __importStar(require("path"));
|
|
@@ -500,6 +501,37 @@ function prepareInboundRunLedger(input) {
|
|
|
500
501
|
return null;
|
|
501
502
|
}
|
|
502
503
|
}
|
|
504
|
+
/**
|
|
505
|
+
* Commit the turn's edits now, then let the push run without awaiting it, so
|
|
506
|
+
* the reply is not held for the network. A later edit is not in this commit;
|
|
507
|
+
* the next turn commits it whole.
|
|
508
|
+
*/
|
|
509
|
+
function startPostTurnSync(agentRoot, config) {
|
|
510
|
+
let result;
|
|
511
|
+
try {
|
|
512
|
+
result = (0, sync_1.postTurnPush)(agentRoot, config);
|
|
513
|
+
}
|
|
514
|
+
catch (err) {
|
|
515
|
+
// In the turn's `finally`: a throw here would replace the turn's result.
|
|
516
|
+
(0, runtime_1.emitNervesEvent)({
|
|
517
|
+
level: "error",
|
|
518
|
+
component: "senses",
|
|
519
|
+
event: "senses.post_turn_sync_error",
|
|
520
|
+
message: "post-turn sync threw",
|
|
521
|
+
meta: { agentRoot, error: err instanceof Error ? err.message : String(err) },
|
|
522
|
+
});
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
result.push?.catch((err) => {
|
|
526
|
+
(0, runtime_1.emitNervesEvent)({
|
|
527
|
+
level: "warn",
|
|
528
|
+
component: "senses",
|
|
529
|
+
event: "senses.post_turn_push_rejected",
|
|
530
|
+
message: "post-turn push rejected",
|
|
531
|
+
meta: { agentRoot, error: err instanceof Error ? err.message : String(err) },
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
}
|
|
503
535
|
async function handleInboundTurn(input) {
|
|
504
536
|
return (0, turn_execution_lease_1.withTurnExecutionLease)(() => handleInboundTurnExclusive(input));
|
|
505
537
|
}
|
|
@@ -773,7 +805,7 @@ async function handleInboundTurnExclusive(input) {
|
|
|
773
805
|
try {
|
|
774
806
|
/* v8 ignore start -- sync-enabled branches tested in sync.test.ts, pipeline tests mock at module boundary @preserve */
|
|
775
807
|
if (syncConfig.enabled && !liveLatencyMode) {
|
|
776
|
-
const pullResult = (0, sync_1.preTurnPull)((0, identity_1.getAgentRoot)(), syncConfig);
|
|
808
|
+
const pullResult = await (0, sync_1.preTurnPull)((0, identity_1.getAgentRoot)(), syncConfig);
|
|
777
809
|
if (!pullResult.ok) {
|
|
778
810
|
syncFailure = pullResult.error;
|
|
779
811
|
}
|
|
@@ -1253,9 +1285,11 @@ async function handleInboundTurnExclusive(input) {
|
|
|
1253
1285
|
};
|
|
1254
1286
|
}
|
|
1255
1287
|
finally {
|
|
1256
|
-
// Step 6b: Post-turn sync
|
|
1288
|
+
// Step 6b: Post-turn sync (opt-in). The commit is synchronous, so the
|
|
1289
|
+
// turn's edits are recorded before the reply goes out; the push runs on
|
|
1290
|
+
// without being awaited, holding the lock, and records its own failure.
|
|
1257
1291
|
if (syncConfig.enabled && !liveLatencyMode && !declinedForHome) {
|
|
1258
|
-
(
|
|
1292
|
+
startPostTurnSync((0, identity_1.getAgentRoot)(), syncConfig);
|
|
1259
1293
|
}
|
|
1260
1294
|
}
|
|
1261
1295
|
}
|
|
@@ -224,12 +224,7 @@ async function resolveRelationshipAwaitAuthority(agentRoot, store, registry, coo
|
|
|
224
224
|
return { authorization, friend };
|
|
225
225
|
}
|
|
226
226
|
function readAspirations(agentRoot) {
|
|
227
|
-
|
|
228
|
-
return fs.readFileSync(path.join(agentRoot, "psyche", "ASPIRATIONS.md"), "utf8").trim();
|
|
229
|
-
}
|
|
230
|
-
catch {
|
|
231
|
-
return "";
|
|
232
|
-
}
|
|
227
|
+
return (0, prompt_1.loadPsycheAt)(agentRoot).aspirations;
|
|
233
228
|
}
|
|
234
229
|
function loadPrivateRuntimeInstincts() {
|
|
235
230
|
return [...DEFAULT_PRIVATE_RUNTIME_INSTINCTS];
|
|
@@ -52,6 +52,7 @@ exports.computeOpenAIWebhookSignature = computeOpenAIWebhookSignature;
|
|
|
52
52
|
exports.validateOpenAIWebhookSignature = validateOpenAIWebhookSignature;
|
|
53
53
|
exports.tonesOnly = tonesOnly;
|
|
54
54
|
exports.parseToolArguments = parseToolArguments;
|
|
55
|
+
exports.voicePsycheExcerpts = voicePsycheExcerpts;
|
|
55
56
|
exports.computeTwilioSignature = computeTwilioSignature;
|
|
56
57
|
exports.validateTwilioSignature = validateTwilioSignature;
|
|
57
58
|
exports.twilioRecordingMediaUrl = twilioRecordingMediaUrl;
|
|
@@ -69,6 +70,7 @@ const http = __importStar(require("http"));
|
|
|
69
70
|
const path = __importStar(require("path"));
|
|
70
71
|
const ws_1 = require("ws");
|
|
71
72
|
const context_1 = require("../../mind/context");
|
|
73
|
+
const prompt_1 = require("../../mind/prompt");
|
|
72
74
|
const friends_1 = require("@ouro.bot/friends");
|
|
73
75
|
const identity_1 = require("../../heart/identity");
|
|
74
76
|
const mcp_manager_1 = require("../../repertoire/mcp-manager");
|
|
@@ -1601,24 +1603,22 @@ function looksLikeShortHumanPhoneGreeting(transcript) {
|
|
|
1601
1603
|
return false;
|
|
1602
1604
|
return /^(hi|hello|hey|yo|yes|yeah|yep)(\b|$)/.test(text);
|
|
1603
1605
|
}
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1606
|
+
function capVoiceText(text, maxChars) {
|
|
1607
|
+
if (text.length <= maxChars)
|
|
1608
|
+
return text;
|
|
1609
|
+
return `${text.slice(0, maxChars).trim()}\n[truncated for low-latency voice]`;
|
|
1610
|
+
}
|
|
1611
|
+
/** The psyche excerpts the live voice prompt carries, read through the shared psyche loader and capped for latency. */
|
|
1612
|
+
function voicePsycheExcerpts(agentRoot) {
|
|
1613
|
+
const psyche = (0, prompt_1.loadPsycheAt)(agentRoot);
|
|
1614
|
+
return {
|
|
1615
|
+
soul: capVoiceText(psyche.soul, 1_600),
|
|
1616
|
+
identity: capVoiceText(psyche.identity, 3_200),
|
|
1617
|
+
tacit: capVoiceText(psyche.tacitKnowledge, 1_400),
|
|
1618
|
+
};
|
|
1614
1619
|
}
|
|
1615
1620
|
async function buildRealtimeVoiceInstructions(options) {
|
|
1616
|
-
const
|
|
1617
|
-
const [soul, identity, tacit] = await Promise.all([
|
|
1618
|
-
readOptionalText(path.join(psycheDir, "SOUL.md"), 1_600),
|
|
1619
|
-
readOptionalText(path.join(psycheDir, "IDENTITY.md"), 3_200),
|
|
1620
|
-
readOptionalText(path.join(psycheDir, "TACIT.md"), 1_400),
|
|
1621
|
-
]);
|
|
1621
|
+
const { soul, identity, tacit } = voicePsycheExcerpts(options.agentRoot);
|
|
1622
1622
|
return [
|
|
1623
1623
|
`You are ${options.agentName} in the live Voice sense.`,
|
|
1624
1624
|
"This is the same agent identity as every other Ouro surface. Voice is not a reduced or alternate self.",
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ouro.bot/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.992",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@ouro.bot/cli",
|
|
9
|
-
"version": "0.1.0-alpha.
|
|
9
|
+
"version": "0.1.0-alpha.992",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
12
12
|
"@azure/identity": "^4.13.0",
|