create-egregore 0.21.0-runtime-mvp.11 → 0.21.0-runtime-mvp.13
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/assets/egregore-launcher.js +2 -2
- package/bin/cli.js +13 -72
- package/lib/setup.js +18 -46
- package/package.json +1 -1
- package/runtime/claude/.claude/skills/handoff/SKILL.md +13 -0
- package/runtime/claude/bin/codex-sync-skills.sh +1 -0
- package/runtime/claude/bin/handoff-preview.sh +71 -7
- package/runtime/claude/bin/lib/git-sync.sh +15 -0
- package/runtime/claude/bin/lib/greeting.sh +6 -0
- package/runtime/claude/bin/lib/runtime-owned.sh +30 -0
- package/runtime/claude/bin/lib/settings-drift.sh +80 -0
- package/runtime/claude/bin/settings.sh +36 -0
- package/runtime/claude/egregore_runtime/harness_cli.py +3 -0
- package/runtime/claude/egregore_runtime/sync.py +74 -11
- package/runtime/claude/egregore_runtime/upgrade.py +200 -58
- package/runtime/claude/manifest.json +1 -0
- package/runtime/codex/.codex/skills/handoff/SKILL.md +13 -0
- package/runtime/codex/bin/codex-sync-skills.sh +1 -0
- package/runtime/codex/bin/handoff-preview.sh +71 -7
- package/runtime/codex/bin/lib/git-sync.sh +15 -0
- package/runtime/codex/bin/lib/greeting.sh +6 -0
- package/runtime/codex/bin/lib/runtime-owned.sh +30 -0
- package/runtime/codex/bin/lib/settings-drift.sh +80 -0
- package/runtime/codex/bin/settings.sh +36 -0
- package/runtime/codex/egregore_runtime/harness_cli.py +3 -0
- package/runtime/codex/egregore_runtime/sync.py +74 -11
- package/runtime/codex/egregore_runtime/upgrade.py +200 -58
- package/runtime/codex/manifest.json +1 -0
- package/runtime/pi/.codex/skills/handoff/SKILL.md +13 -0
- package/runtime/pi/bin/codex-sync-skills.sh +1 -0
- package/runtime/pi/bin/handoff-preview.sh +71 -7
- package/runtime/pi/bin/lib/git-sync.sh +15 -0
- package/runtime/pi/bin/lib/greeting.sh +6 -0
- package/runtime/pi/bin/lib/runtime-owned.sh +30 -0
- package/runtime/pi/bin/lib/settings-drift.sh +80 -0
- package/runtime/pi/bin/settings.sh +36 -0
- package/runtime/pi/egregore_runtime/harness_cli.py +3 -0
- package/runtime/pi/egregore_runtime/sync.py +74 -11
- package/runtime/pi/egregore_runtime/upgrade.py +200 -58
- package/runtime/pi/manifest.json +1 -0
- package/runtime/prime/.codex/skills/handoff/SKILL.md +13 -0
- package/runtime/prime/bin/codex-sync-skills.sh +1 -0
- package/runtime/prime/bin/handoff-preview.sh +71 -7
- package/runtime/prime/bin/lib/git-sync.sh +15 -0
- package/runtime/prime/bin/lib/greeting.sh +6 -0
- package/runtime/prime/bin/lib/runtime-owned.sh +30 -0
- package/runtime/prime/bin/lib/settings-drift.sh +80 -0
- package/runtime/prime/bin/settings.sh +36 -0
- package/runtime/prime/egregore_runtime/harness_cli.py +3 -0
- package/runtime/prime/egregore_runtime/sync.py +74 -11
- package/runtime/prime/egregore_runtime/upgrade.py +200 -58
- package/runtime/prime/manifest.json +1 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// egregore-launcher-version: 0.21.0-runtime-mvp.
|
|
2
|
+
// egregore-launcher-version: 0.21.0-runtime-mvp.13
|
|
3
3
|
//
|
|
4
4
|
// egregore — launch and browse Egregore instances from your terminal.
|
|
5
5
|
// Installed by create-egregore. Reads ~/.egregore/instances.json.
|
|
@@ -35,7 +35,7 @@ const crypto = require("node:crypto");
|
|
|
35
35
|
const { spawnSync, spawn } = require("node:child_process");
|
|
36
36
|
const readline = require("node:readline");
|
|
37
37
|
|
|
38
|
-
const VERSION = "0.21.0-runtime-mvp.
|
|
38
|
+
const VERSION = "0.21.0-runtime-mvp.13";
|
|
39
39
|
const REGISTRY = path.join(os.homedir(), ".egregore", "instances.json");
|
|
40
40
|
const DEFAULT_FILE = path.join(os.homedir(), ".egregore", "default");
|
|
41
41
|
const UPDATE_CACHE_FILE = path.join(os.homedir(), ".egregore", "update-check.json");
|
package/bin/cli.js
CHANGED
|
@@ -25,32 +25,10 @@ const {
|
|
|
25
25
|
ghCliExists,
|
|
26
26
|
tryGhAuthOrLogin,
|
|
27
27
|
} = require("../lib/auth");
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
} = require("../lib/codex-runtime");
|
|
33
|
-
const {
|
|
34
|
-
hasClaudeRuntimeSupport,
|
|
35
|
-
installClaudeRuntime,
|
|
36
|
-
reportClaudeRuntimeSummary,
|
|
37
|
-
} = require("../lib/claude-runtime");
|
|
38
|
-
const {
|
|
39
|
-
hasPiRuntimeSupport,
|
|
40
|
-
installPiRuntime,
|
|
41
|
-
reportPiRuntimeSummary,
|
|
42
|
-
} = require("../lib/pi-runtime");
|
|
43
|
-
const {
|
|
44
|
-
hasPrimeRuntimeSupport,
|
|
45
|
-
installPrimeRuntime,
|
|
46
|
-
reportPrimeRuntimeSummary,
|
|
47
|
-
} = require("../lib/prime-runtime");
|
|
48
|
-
const {
|
|
49
|
-
connectEligibility,
|
|
50
|
-
fetchConnectRuntimePack,
|
|
51
|
-
installConnectRuntime,
|
|
52
|
-
reportConnectRuntimeSummary,
|
|
53
|
-
} = require("../lib/connect-runtime");
|
|
28
|
+
// Runtime adapter installers are deliberately NOT imported here: the global
|
|
29
|
+
// CLI never writes framework/Runtime files into registered instances. Fresh
|
|
30
|
+
// installs get their files in lib/setup.js and lib/local.js; existing
|
|
31
|
+
// instances upgrade only through their own Settings prepare → activate flow.
|
|
54
32
|
const {
|
|
55
33
|
install,
|
|
56
34
|
ghApi,
|
|
@@ -125,50 +103,15 @@ function assistantLabel(runtime) {
|
|
|
125
103
|
return "Claude Code";
|
|
126
104
|
}
|
|
127
105
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
? await fetchConnectRuntimePack(inst.path)
|
|
138
|
-
: null;
|
|
139
|
-
const claudeSummary = installClaudeRuntime(inst.path);
|
|
140
|
-
const claudeReady = hasClaudeRuntimeSupport(inst.path);
|
|
141
|
-
reportClaudeRuntimeSummary(claudeSummary, ui, {
|
|
142
|
-
label: `Claude Code runtime for ${inst.slug || inst.name || path.basename(inst.path)}`,
|
|
143
|
-
includeConflicts: !claudeReady,
|
|
144
|
-
});
|
|
145
|
-
const summary = installCodexRuntime(inst.path);
|
|
146
|
-
const ready = hasCodexRuntimeSupport(inst.path);
|
|
147
|
-
reportCodexRuntimeSummary(summary, ui, {
|
|
148
|
-
label: `Codex runtime for ${inst.slug || inst.name || path.basename(inst.path)}`,
|
|
149
|
-
includeConflicts: !ready,
|
|
150
|
-
});
|
|
151
|
-
const piSummary = installPiRuntime(inst.path);
|
|
152
|
-
const piReady = hasPiRuntimeSupport(inst.path);
|
|
153
|
-
reportPiRuntimeSummary(piSummary, ui, {
|
|
154
|
-
label: `Pi meta-harness for ${inst.slug || inst.name || path.basename(inst.path)}`,
|
|
155
|
-
includeConflicts: !piReady,
|
|
156
|
-
});
|
|
157
|
-
const primeSummary = installPrimeRuntime(inst.path);
|
|
158
|
-
const primeReady = hasPrimeRuntimeSupport(inst.path);
|
|
159
|
-
reportPrimeRuntimeSummary(primeSummary, ui, {
|
|
160
|
-
label: `Prime Agent runtime for ${inst.slug || inst.name || path.basename(inst.path)}`,
|
|
161
|
-
includeConflicts: !primeReady,
|
|
162
|
-
});
|
|
163
|
-
const connectSummary = await installConnectRuntime(inst.path, { validatedPack: connectPack });
|
|
164
|
-
reportConnectRuntimeSummary(connectSummary, ui, {
|
|
165
|
-
label: `Connect features for ${inst.slug || inst.name || path.basename(inst.path)}`,
|
|
166
|
-
});
|
|
167
|
-
} catch (err) {
|
|
168
|
-
ui.warn(`Could not refresh shell-runtime support for ${inst.path}: ${err.message}`);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
106
|
+
// Runtime ownership contract: refreshing the global CLI updates ONLY the
|
|
107
|
+
// global CLI. Registered instances are never installed into, updated,
|
|
108
|
+
// removed from, or reconciled here — an existing instance crosses into the
|
|
109
|
+
// new Runtime exclusively through its own Settings flow
|
|
110
|
+
// (prepare → confirm → activate), which applies engine and all harness
|
|
111
|
+
// adapters transactionally from one verified candidate. A CLI refresh that
|
|
112
|
+
// sprayed adapters into every registered instance is exactly how a legacy
|
|
113
|
+
// instance became an incoherent mixture of old framework and new hooks
|
|
114
|
+
// (its own upstream sync then reverted the tracked half).
|
|
172
115
|
|
|
173
116
|
async function chooseRuntime(args) {
|
|
174
117
|
if (args.runtime) {
|
|
@@ -210,7 +153,6 @@ async function chooseRuntime(args) {
|
|
|
210
153
|
}
|
|
211
154
|
|
|
212
155
|
async function upgradeLauncherFromExisting(instances) {
|
|
213
|
-
await upgradePortableRuntimesForExisting(instances);
|
|
214
156
|
const inst = instances.find((entry) => entry && entry.path && fs.existsSync(entry.path));
|
|
215
157
|
if (!inst) return null;
|
|
216
158
|
try {
|
|
@@ -524,7 +466,6 @@ async function main() {
|
|
|
524
466
|
ui.error("No installed Egregore instance was found. Run create-egregore without --refresh-launcher.");
|
|
525
467
|
process.exit(1);
|
|
526
468
|
}
|
|
527
|
-
await upgradePortableRuntimesForExisting(instances);
|
|
528
469
|
const alias = await installLauncherBinary(instances[0].path, ui);
|
|
529
470
|
if (!alias || !alias.binaryPath) {
|
|
530
471
|
ui.error("Could not refresh the Egregore CLI.");
|
package/lib/setup.js
CHANGED
|
@@ -577,14 +577,20 @@ async function install(data, ui, targetDir, opts = {}) {
|
|
|
577
577
|
try { ensurePersonOnboardingWitness(memoryDir, state); } catch {}
|
|
578
578
|
|
|
579
579
|
// 4c. Overlay the selected harness adapter from the installed npm artifact.
|
|
580
|
-
//
|
|
581
|
-
//
|
|
580
|
+
// This is the one-time explicit setup write; launches never install
|
|
581
|
+
// framework files (see the ownership contract at the launch path), and
|
|
582
|
+
// existing instances change only through their Settings
|
|
583
|
+
// prepare → confirm → activate flow — which applies ALL FOUR harness
|
|
584
|
+
// trees from one verified candidate.
|
|
582
585
|
if (preferredRuntime === "claude-code") {
|
|
583
586
|
const claudeRuntime = installClaudeRuntime(egregoreDir);
|
|
584
587
|
reportClaudeRuntimeSummary(claudeRuntime, ui);
|
|
585
588
|
} else if (preferredRuntime === "codex") {
|
|
586
589
|
const codexRuntime = installCodexRuntime(egregoreDir);
|
|
587
590
|
reportCodexRuntimeSummary(codexRuntime, ui);
|
|
591
|
+
// Codex-native skills are framework code, so they render at setup —
|
|
592
|
+
// previously they were sprayed at every launch.
|
|
593
|
+
try { installCodexSkills(egregoreDir); } catch {}
|
|
588
594
|
} else if (preferredRuntime === "pi") {
|
|
589
595
|
const piRuntime = installPiRuntime(egregoreDir);
|
|
590
596
|
reportPiRuntimeSummary(piRuntime, ui);
|
|
@@ -1251,50 +1257,16 @@ async function launchOrInstruct(egregoreDir, alias, ui, ctx = {}) {
|
|
|
1251
1257
|
ui.info(`Next time, ${launcherNextStep(ui, aliasName)}`);
|
|
1252
1258
|
console.log("");
|
|
1253
1259
|
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
const codexRuntime = installCodexRuntime(egregoreDir);
|
|
1265
|
-
reportCodexRuntimeSummary(codexRuntime, ui);
|
|
1266
|
-
if (!hasCodexCoreSupport(egregoreDir)) {
|
|
1267
|
-
ui.warn(codexSupportMessage(egregoreDir));
|
|
1268
|
-
ui.info(`Run ${ui.bold(aliasName)} after updating the checkout.`);
|
|
1269
|
-
console.log("");
|
|
1270
|
-
return;
|
|
1271
|
-
}
|
|
1272
|
-
const skillsInstalled = installCodexSkills(egregoreDir);
|
|
1273
|
-
const cardRendered = await renderCodexStartupCard(egregoreDir);
|
|
1274
|
-
if (!skillsInstalled || !cardRendered) {
|
|
1275
|
-
ui.warn(codexSupportMessage(egregoreDir));
|
|
1276
|
-
ui.info(`Run ${ui.bold(aliasName)} after updating the checkout.`);
|
|
1277
|
-
console.log("");
|
|
1278
|
-
return;
|
|
1279
|
-
}
|
|
1280
|
-
} else if (runtime === "pi") {
|
|
1281
|
-
const piRuntime = installPiRuntime(egregoreDir);
|
|
1282
|
-
reportPiRuntimeSummary(piRuntime, ui);
|
|
1283
|
-
if (!hasPiRuntimeSupport(egregoreDir)) {
|
|
1284
|
-
ui.warn(piSupportMessage(egregoreDir));
|
|
1285
|
-
ui.info(`Run ${ui.bold(aliasName)} after updating the checkout.`);
|
|
1286
|
-
console.log("");
|
|
1287
|
-
return;
|
|
1288
|
-
}
|
|
1289
|
-
} else if (runtime === "prime") {
|
|
1290
|
-
const primeRuntime = installPrimeRuntime(egregoreDir);
|
|
1291
|
-
reportPrimeRuntimeSummary(primeRuntime, ui);
|
|
1292
|
-
if (!hasPrimeRuntimeSupport(egregoreDir)) {
|
|
1293
|
-
ui.warn(primeSupportMessage(egregoreDir));
|
|
1294
|
-
ui.info(`Run ${ui.bold(aliasName)} after updating the checkout.`);
|
|
1295
|
-
console.log("");
|
|
1296
|
-
return;
|
|
1297
|
-
}
|
|
1260
|
+
// Runtime ownership contract: launching NEVER adds, replaces, or removes
|
|
1261
|
+
// framework/Runtime code. Fresh installs received their files during
|
|
1262
|
+
// explicit setup; existing instances change only through their own
|
|
1263
|
+
// Settings prepare → confirm → activate flow. A legacy instance opens
|
|
1264
|
+
// exactly as it is — old framework, old graph retrieval, no commentary.
|
|
1265
|
+
// The one launch-time write kept below is the Codex startup card, which is
|
|
1266
|
+
// rendered presentation state, not framework code; a legacy instance
|
|
1267
|
+
// without the renderer skips it silently.
|
|
1268
|
+
if (runtime === "codex") {
|
|
1269
|
+
try { await renderCodexStartupCard(egregoreDir); } catch {}
|
|
1298
1270
|
}
|
|
1299
1271
|
|
|
1300
1272
|
const result = spawnSync(binary, runtimeLaunchArgs(runtime, egregoreDir), {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-egregore",
|
|
3
|
-
"version": "0.21.0-runtime-mvp.
|
|
3
|
+
"version": "0.21.0-runtime-mvp.13",
|
|
4
4
|
"description": "Set up Egregore for your team in one command — installs the egregore launcher and your choice of Claude Code, Codex, or Pi.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -74,6 +74,19 @@ Preserve supplied wording, order, headings, lists, code, and links. For
|
|
|
74
74
|
generated content, use only the useful subset of Briefing, Current State, Open
|
|
75
75
|
Threads, and Next Steps. Keep it operational and concise.
|
|
76
76
|
|
|
77
|
+
## Staircase (staging)
|
|
78
|
+
|
|
79
|
+
A second face for addressed handoffs exists: the staircase, a
|
|
80
|
+
progressive-disclosure page the receiver walks one step at a time and closes
|
|
81
|
+
with a readback (docs/specs/handoff-staircase.md). It is in staging and does
|
|
82
|
+
not change this skill. When the user asks for a staircase and this instance
|
|
83
|
+
has the staging command (.claude/skills/handoff-staircase/SKILL.md, Codex: the
|
|
84
|
+
handoff-staircase adapter; internal to Curve Labs while in staging), route to
|
|
85
|
+
it: it reuses this preview and approval bridge with a composed spec and adds
|
|
86
|
+
one feedback question at the end. Without it, say the staircase is not
|
|
87
|
+
installed here and continue with this skill. When its feedback
|
|
88
|
+
promotes it, the staircase becomes the default here and that command retires.
|
|
89
|
+
|
|
77
90
|
## One-command preview
|
|
78
91
|
|
|
79
92
|
Pipe the exact canonical Markdown once to the private preview bridge. It stores
|
|
@@ -13,7 +13,7 @@ usage() {
|
|
|
13
13
|
usage:
|
|
14
14
|
bash bin/handoff-preview.sh preview --author HANDLE --recipient HANDLE \
|
|
15
15
|
--topic TOPIC --intent action|feedback|fyi \
|
|
16
|
-
--content-mode supplied|generated < handoff.md
|
|
16
|
+
--content-mode supplied|generated [--composed SPEC.json] < handoff.md
|
|
17
17
|
bash bin/handoff-preview.sh approve TOKEN
|
|
18
18
|
EOF
|
|
19
19
|
}
|
|
@@ -44,7 +44,7 @@ validate_token() {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
preview() {
|
|
47
|
-
local author="" recipient="" topic="" intent="" content_mode=""
|
|
47
|
+
local author="" recipient="" topic="" intent="" content_mode="" composed=""
|
|
48
48
|
while [ $# -gt 0 ]; do
|
|
49
49
|
case "$1" in
|
|
50
50
|
--author) author="${2:-}"; shift 2 ;;
|
|
@@ -52,6 +52,7 @@ preview() {
|
|
|
52
52
|
--topic) topic="${2:-}"; shift 2 ;;
|
|
53
53
|
--intent) intent="${2:-}"; shift 2 ;;
|
|
54
54
|
--content-mode) content_mode="${2:-}"; shift 2 ;;
|
|
55
|
+
--composed) composed="${2:-}"; shift 2 ;;
|
|
55
56
|
*) echo "unknown preview option: $1" >&2; usage; exit 2 ;;
|
|
56
57
|
esac
|
|
57
58
|
done
|
|
@@ -60,16 +61,25 @@ preview() {
|
|
|
60
61
|
[ -n "$topic" ] || { echo "handoff preview requires --topic" >&2; exit 2; }
|
|
61
62
|
case "$intent" in action|feedback|fyi) ;; *) echo "invalid handoff intent" >&2; exit 2 ;; esac
|
|
62
63
|
case "$content_mode" in supplied|generated) ;; *) echo "invalid handoff content mode" >&2; exit 2 ;; esac
|
|
64
|
+
if [ -n "$composed" ]; then
|
|
65
|
+
# A composed spec (for example a staircase) is a presentation map over the
|
|
66
|
+
# canonical Markdown. It rides the same private record and is judged by the
|
|
67
|
+
# same fidelity gate; it never replaces the Markdown as the record.
|
|
68
|
+
[ "$content_mode" = "generated" ] || { echo "--composed requires --content-mode generated" >&2; exit 2; }
|
|
69
|
+
[ -s "$composed" ] || { echo "handoff preview --composed file is missing or empty" >&2; exit 2; }
|
|
70
|
+
jq -e . "$composed" >/dev/null 2>&1 || { echo "handoff preview --composed file is not JSON" >&2; exit 2; }
|
|
71
|
+
fi
|
|
63
72
|
|
|
64
73
|
umask 077
|
|
65
74
|
mkdir -p "$STORE_ROOT"
|
|
66
75
|
chmod 700 "$STORE_ROOT"
|
|
67
76
|
|
|
68
|
-
local token content_file metadata_file digest
|
|
77
|
+
local token content_file metadata_file spec_file digest spec_digest
|
|
69
78
|
token="$(new_token)"
|
|
70
79
|
validate_token "$token"
|
|
71
80
|
content_file="$STORE_ROOT/$token.md"
|
|
72
81
|
metadata_file="$STORE_ROOT/$token.json"
|
|
82
|
+
spec_file="$STORE_ROOT/$token.spec.json"
|
|
73
83
|
cat > "$content_file"
|
|
74
84
|
[ -s "$content_file" ] || {
|
|
75
85
|
rm -f "$content_file"
|
|
@@ -77,6 +87,11 @@ preview() {
|
|
|
77
87
|
exit 2
|
|
78
88
|
}
|
|
79
89
|
digest="$(sha256_file "$content_file")"
|
|
90
|
+
spec_digest=""
|
|
91
|
+
if [ -n "$composed" ]; then
|
|
92
|
+
cp "$composed" "$spec_file"
|
|
93
|
+
spec_digest="$(sha256_file "$spec_file")"
|
|
94
|
+
fi
|
|
80
95
|
jq -n \
|
|
81
96
|
--arg author "$author" \
|
|
82
97
|
--arg recipient "$recipient" \
|
|
@@ -84,14 +99,25 @@ preview() {
|
|
|
84
99
|
--arg intent "$intent" \
|
|
85
100
|
--arg contentMode "$content_mode" \
|
|
86
101
|
--arg digest "$digest" \
|
|
102
|
+
--arg specDigest "$spec_digest" \
|
|
87
103
|
--argjson createdAt "$(date +%s)" \
|
|
88
|
-
'{author:$author,recipient:$recipient,topic:$topic,intent:$intent,contentMode:$contentMode,digest:$digest,createdAt:$createdAt}' \
|
|
104
|
+
'{author:$author,recipient:$recipient,topic:$topic,intent:$intent,contentMode:$contentMode,digest:$digest,specDigest:$specDigest,createdAt:$createdAt}' \
|
|
89
105
|
> "$metadata_file"
|
|
90
106
|
|
|
91
|
-
|
|
107
|
+
local render_ok=0
|
|
108
|
+
if [ -n "$composed" ]; then
|
|
109
|
+
if bash "$ROOT/bin/node-run.sh" \
|
|
110
|
+
"$ROOT/packages/egregore-artifacts/bin/cli.js" composed "$spec_file" \
|
|
111
|
+
--source "$content_file" --verify-fidelity; then
|
|
112
|
+
render_ok=1
|
|
113
|
+
fi
|
|
114
|
+
elif bash "$ROOT/bin/node-run.sh" \
|
|
92
115
|
"$ROOT/packages/egregore-artifacts/bin/cli.js" handoff "$content_file" \
|
|
93
116
|
--verify-fidelity; then
|
|
94
|
-
|
|
117
|
+
render_ok=1
|
|
118
|
+
fi
|
|
119
|
+
if [ "$render_ok" != "1" ]; then
|
|
120
|
+
rm -f "$content_file" "$metadata_file" "$spec_file"
|
|
95
121
|
exit 1
|
|
96
122
|
fi
|
|
97
123
|
|
|
@@ -132,12 +158,26 @@ approve() {
|
|
|
132
158
|
exit 1
|
|
133
159
|
fi
|
|
134
160
|
|
|
135
|
-
local author recipient topic intent content_mode
|
|
161
|
+
local author recipient topic intent content_mode spec_digest
|
|
136
162
|
author="$(jq -er '.author' "$metadata_file")"
|
|
137
163
|
recipient="$(jq -er '.recipient' "$metadata_file")"
|
|
138
164
|
topic="$(jq -er '.topic' "$metadata_file")"
|
|
139
165
|
intent="$(jq -er '.intent' "$metadata_file")"
|
|
140
166
|
content_mode="$(jq -er '.contentMode' "$metadata_file")"
|
|
167
|
+
spec_digest="$(jq -r '.specDigest // ""' "$metadata_file")"
|
|
168
|
+
|
|
169
|
+
# A previewed composed spec is bound to the same token and the same digest
|
|
170
|
+
# discipline as the Markdown: approval sends exactly the previewed bytes.
|
|
171
|
+
local spec_file="$STORE_ROOT/$token.spec.json"
|
|
172
|
+
if [ -n "$spec_digest" ]; then
|
|
173
|
+
if [ ! -f "$spec_file" ] || [ "$(sha256_file "$spec_file")" != "$spec_digest" ]; then
|
|
174
|
+
rmdir "$lock_dir" 2>/dev/null || true
|
|
175
|
+
echo "handoff preview composed spec is missing or changed after approval was requested" >&2
|
|
176
|
+
exit 1
|
|
177
|
+
fi
|
|
178
|
+
else
|
|
179
|
+
spec_file=""
|
|
180
|
+
fi
|
|
141
181
|
|
|
142
182
|
local result_tmp
|
|
143
183
|
result_tmp="$(mktemp -d -t egregore-handoff-result-XXXXXX)"
|
|
@@ -146,6 +186,7 @@ approve() {
|
|
|
146
186
|
--intent "$intent" --content-mode "$content_mode"
|
|
147
187
|
)
|
|
148
188
|
[ -z "$recipient" ] || args+=(--recipient "$recipient")
|
|
189
|
+
[ -z "$spec_file" ] || args+=(--composed "$spec_file")
|
|
149
190
|
|
|
150
191
|
if ! TMPDIR="$result_tmp" bash "$ROOT/bin/artifact-writeback.sh" "${args[@]}" \
|
|
151
192
|
< "$content_file"; then
|
|
@@ -153,10 +194,33 @@ approve() {
|
|
|
153
194
|
exit 1
|
|
154
195
|
fi
|
|
155
196
|
|
|
197
|
+
# Persist the composed spec beside the canonical record so the face can be
|
|
198
|
+
# re-rendered or shared later from memory alone. The record is already
|
|
199
|
+
# canonical at this point; a spec persistence failure is reported, not
|
|
200
|
+
# retried, so the approval can never write the record twice.
|
|
201
|
+
if [ -n "$spec_file" ]; then
|
|
202
|
+
local record_abs record_rel spec_rel
|
|
203
|
+
record_abs="$(jq -r '.absFile // empty' "$result_tmp/capture-run-result.json" 2>/dev/null || true)"
|
|
204
|
+
record_rel="${record_abs#"$ROOT/"}"
|
|
205
|
+
case "$record_rel" in
|
|
206
|
+
memory/*.md)
|
|
207
|
+
spec_rel="${record_rel%.md}.staircase.json"
|
|
208
|
+
if ! bash "$ROOT/bin/artifact-writeback.sh" create \
|
|
209
|
+
--path "$spec_rel" --input "$spec_file" >/dev/null; then
|
|
210
|
+
echo "warning: the canonical record was written but its composed spec could not be persisted at $spec_rel" >&2
|
|
211
|
+
fi
|
|
212
|
+
;;
|
|
213
|
+
*)
|
|
214
|
+
echo "warning: the canonical record path is not under memory/; the composed spec was not persisted" >&2
|
|
215
|
+
;;
|
|
216
|
+
esac
|
|
217
|
+
fi
|
|
218
|
+
|
|
156
219
|
# Consume the approved token before rendering. A renderer failure must not
|
|
157
220
|
# make a successful canonical write eligible for accidental replay.
|
|
158
221
|
mv "$content_file" "$result_tmp/approved-handoff.md"
|
|
159
222
|
mv "$metadata_file" "$result_tmp/approved-handoff.json"
|
|
223
|
+
[ -z "$spec_file" ] || mv "$spec_file" "$result_tmp/approved-handoff.staircase.json"
|
|
160
224
|
rmdir "$lock_dir" 2>/dev/null || true
|
|
161
225
|
bash "$ROOT/bin/render-card.sh" \
|
|
162
226
|
--result "$result_tmp/capture-run-result.json"
|
|
@@ -28,6 +28,10 @@ SYNC_RECEIPT=""
|
|
|
28
28
|
# shellcheck source=bin/lib/settings-drift.sh
|
|
29
29
|
source "$SCRIPT_DIR/bin/lib/settings-drift.sh"
|
|
30
30
|
|
|
31
|
+
# Runtime framework ownership (activation record gate for legacy sync).
|
|
32
|
+
# shellcheck source=bin/lib/runtime-owned.sh
|
|
33
|
+
source "$SCRIPT_DIR/bin/lib/runtime-owned.sh"
|
|
34
|
+
|
|
31
35
|
# The core repo's integration branch: "develop" unless egregore.json sets
|
|
32
36
|
# base_branch. session-start.sh resolves this before sourcing; resolve it here
|
|
33
37
|
# too so the lib stands alone for other callers.
|
|
@@ -181,6 +185,14 @@ _apply_framework_update() {
|
|
|
181
185
|
if [ "$_UPSTREAM_URL" = "none" ] || [ "$_AUTO_UPDATE" != "true" ]; then
|
|
182
186
|
return 0
|
|
183
187
|
fi
|
|
188
|
+
# Runtime ownership: once this exact instance activated the new Runtime
|
|
189
|
+
# (activation record with retrieval runtime-qmd), the legacy upstream sync
|
|
190
|
+
# must never overwrite Runtime-owned framework paths — that fight is how a
|
|
191
|
+
# launched-then-synced instance became a broken old/new mixture. Rollback
|
|
192
|
+
# flips the record off runtime-qmd and legacy ownership resumes here.
|
|
193
|
+
if runtime_owns_framework 2>/dev/null; then
|
|
194
|
+
return 0
|
|
195
|
+
fi
|
|
184
196
|
if ! git show-ref --verify --quiet refs/remotes/upstream/main 2>/dev/null; then
|
|
185
197
|
return 0
|
|
186
198
|
fi
|
|
@@ -335,6 +347,9 @@ setup_develop() {
|
|
|
335
347
|
settings_drift_heal
|
|
336
348
|
git merge --ff-only "origin/$BASE_BRANCH" --quiet 2>/dev/null || true
|
|
337
349
|
settings_drift_reapply
|
|
350
|
+
# Connected mode: reconcile with the control plane (org settings row) —
|
|
351
|
+
# pull a teammate's confirmed write down, or re-deliver a failed push.
|
|
352
|
+
settings_drift_pull
|
|
338
353
|
settings_drift_report
|
|
339
354
|
fi
|
|
340
355
|
|
|
@@ -99,6 +99,12 @@ fi
|
|
|
99
99
|
if [ "${SETTINGS_ADAPTER_RESTORED:-false}" = "true" ]; then
|
|
100
100
|
echo " ◇ restored org .claude/settings.json — a generated adapter had replaced it locally"
|
|
101
101
|
fi
|
|
102
|
+
if [ -n "${SETTINGS_SYNCED_FROM_ORG:-}" ]; then
|
|
103
|
+
echo " ◇ org settings synced from the control plane (${SETTINGS_SYNCED_FROM_ORG})"
|
|
104
|
+
fi
|
|
105
|
+
if [ -n "${SETTINGS_POSTURE_HELD:-}" ]; then
|
|
106
|
+
echo " ◇ boundary posture was set to ${SETTINGS_POSTURE_HELD} elsewhere — relaxing access needs your confirm: bash bin/settings.sh posture ${SETTINGS_POSTURE_HELD}"
|
|
107
|
+
fi
|
|
102
108
|
if [ -n "${UNSHARED_SETTINGS_KEYS:-}" ]; then
|
|
103
109
|
echo " ◇ org settings changed on this machine (${UNSHARED_SETTINGS_KEYS}) — say \"share settings\" to make them org-wide"
|
|
104
110
|
fi
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# runtime-owned.sh — does the Runtime own this instance's framework paths?
|
|
3
|
+
#
|
|
4
|
+
# True exactly when THIS instance has an activation record whose retrieval
|
|
5
|
+
# mode is runtime-qmd. Uses the same stable identity as the Python upgrade
|
|
6
|
+
# store and the graph cutover gate: sha256(org_id | main-checkout path),
|
|
7
|
+
# first 16 hex chars, under EGREGORE_UPGRADE_ROOT (default
|
|
8
|
+
# ~/.egregore/runtime/upgrade). No global flag, no pathname guess, no slug,
|
|
9
|
+
# no branch name, no hardcoded organization or version.
|
|
10
|
+
#
|
|
11
|
+
# After a rollback the record's retrieval flips to previous-runtime, so this
|
|
12
|
+
# returns false again and legacy framework ownership resumes automatically.
|
|
13
|
+
#
|
|
14
|
+
# Contract: sourced with SCRIPT_DIR set to the instance root.
|
|
15
|
+
|
|
16
|
+
runtime_owns_framework() {
|
|
17
|
+
local org main gd key rec
|
|
18
|
+
org=$(jq -r '.org_id // empty' "$SCRIPT_DIR/egregore.json" 2>/dev/null)
|
|
19
|
+
[ -n "$org" ] || return 1
|
|
20
|
+
main="$SCRIPT_DIR"
|
|
21
|
+
if [ -f "$SCRIPT_DIR/.git" ]; then
|
|
22
|
+
gd=$(sed -n 's/^gitdir: //p' "$SCRIPT_DIR/.git" 2>/dev/null)
|
|
23
|
+
case "$gd" in */.git/worktrees/*) main="${gd%/.git/worktrees/*}" ;; esac
|
|
24
|
+
fi
|
|
25
|
+
main=$(cd "$main" 2>/dev/null && pwd -P || printf '%s' "$SCRIPT_DIR")
|
|
26
|
+
key=$(printf '%s|%s' "$org" "$main" | shasum -a 256 2>/dev/null | cut -c1-16)
|
|
27
|
+
[ -n "$key" ] || return 1
|
|
28
|
+
rec="${EGREGORE_UPGRADE_ROOT:-$HOME/.egregore/runtime/upgrade}/${key}/active.json"
|
|
29
|
+
[ -f "$rec" ] && [ "$(jq -r '.retrieval // empty' "$rec" 2>/dev/null)" = "runtime-qmd" ]
|
|
30
|
+
}
|
|
@@ -98,8 +98,88 @@ settings_drift_reapply() {
|
|
|
98
98
|
_SETTINGS_DRIFT_BASE=""
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
# ── control-plane sync (Connected mode) ─────────────────────────────────
|
|
102
|
+
# The control plane is authoritative for org settings in Connected mode: a
|
|
103
|
+
# teammate's confirmed CLI write lands in the orgs row immediately, and every
|
|
104
|
+
# session start pulls it down here. One trust rule: an incoming posture that
|
|
105
|
+
# RELAXES local file access is never auto-applied — it is held and surfaced
|
|
106
|
+
# for local confirm; tightening applies immediately. If the server has not
|
|
107
|
+
# moved but the local settings-owned keys have (an offline write whose push
|
|
108
|
+
# failed), the local state is pushed instead.
|
|
109
|
+
_settings_posture_rank() {
|
|
110
|
+
case "$1" in strict) echo 0 ;; open) echo 2 ;; *) echo 1 ;; esac
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export SETTINGS_POSTURE_HELD=""
|
|
114
|
+
export SETTINGS_SYNCED_FROM_ORG=""
|
|
115
|
+
# true once the control plane and the local settings-owned keys agree — the
|
|
116
|
+
# greeting then never asks the user to share what is already org-wide.
|
|
117
|
+
export SETTINGS_CP_SYNCED="false"
|
|
118
|
+
|
|
119
|
+
settings_drift_pull() {
|
|
120
|
+
local api_url api_key resp revision applied local_subset server_settings
|
|
121
|
+
api_url=$(jq -r '.api_url // empty' egregore.json 2>/dev/null)
|
|
122
|
+
[ -n "$api_url" ] || return 0
|
|
123
|
+
api_key=$(grep '^EGREGORE_API_KEY=' .env 2>/dev/null | cut -d'=' -f2-)
|
|
124
|
+
[ -n "$api_key" ] || return 0
|
|
125
|
+
resp=$(curl -s --max-time 8 -H "Authorization: Bearer $api_key" "$api_url/api/org/settings" 2>/dev/null) || return 0
|
|
126
|
+
printf '%s' "$resp" | jq -e '.settings' >/dev/null 2>&1 || return 0
|
|
127
|
+
revision=$(printf '%s' "$resp" | jq -r '.settings_revision // ""')
|
|
128
|
+
server_settings=$(printf '%s' "$resp" | jq -c '.settings')
|
|
129
|
+
applied=$(jq -r '.org_settings_revision // ""' .egregore-state.json 2>/dev/null)
|
|
130
|
+
local_subset=$(jq -c --argjson keys "$_SETTINGS_OWNED_KEYS" \
|
|
131
|
+
'with_entries(select(.key as $k | $keys | index($k)))' egregore.json 2>/dev/null)
|
|
132
|
+
|
|
133
|
+
if [ -n "$revision" ] && [ "$revision" != "$applied" ] && [ "$server_settings" != "{}" ]; then
|
|
134
|
+
# Server moved: apply it, holding a posture relaxation for local confirm.
|
|
135
|
+
local incoming_posture local_posture hold="false"
|
|
136
|
+
incoming_posture=$(printf '%s' "$resp" | jq -r '.settings.boundary.posture // empty')
|
|
137
|
+
local_posture=$(jq -r '.boundary.posture // "standard"' egregore.json 2>/dev/null)
|
|
138
|
+
if [ -n "$incoming_posture" ] \
|
|
139
|
+
&& [ "$(_settings_posture_rank "$incoming_posture")" -gt "$(_settings_posture_rank "$local_posture")" ]; then
|
|
140
|
+
hold="true"
|
|
141
|
+
SETTINGS_POSTURE_HELD="$incoming_posture"
|
|
142
|
+
export SETTINGS_POSTURE_HELD
|
|
143
|
+
fi
|
|
144
|
+
local merged tmp
|
|
145
|
+
merged=$(printf '%s' "$server_settings" | jq -c \
|
|
146
|
+
--argjson keys "$_SETTINGS_OWNED_KEYS" --argjson hold "$hold" --slurpfile cfg egregore.json '
|
|
147
|
+
. as $s | $cfg[0] as $local |
|
|
148
|
+
reduce $keys[] as $k ($local;
|
|
149
|
+
if ($s | has($k)) then
|
|
150
|
+
if $k == "boundary" and $hold then
|
|
151
|
+
.boundary = (($s.boundary // {}) + {posture: ($local.boundary.posture // "standard")})
|
|
152
|
+
else .[$k] = $s[$k] end
|
|
153
|
+
else del(.[$k]) end)
|
|
154
|
+
' 2>/dev/null || true)
|
|
155
|
+
if [ -n "$merged" ] && printf '%s' "$merged" | jq -e . >/dev/null 2>&1; then
|
|
156
|
+
tmp=$(mktemp "egregore.json.XXXXXX") || return 0
|
|
157
|
+
printf '%s\n' "$merged" | jq . > "$tmp" 2>/dev/null && [ -s "$tmp" ] && mv "$tmp" egregore.json || { rm -f "$tmp"; return 0; }
|
|
158
|
+
SETTINGS_SYNCED_FROM_ORG=$(printf '%s' "$resp" | jq -r '.settings | keys | join(", ")')
|
|
159
|
+
export SETTINGS_SYNCED_FROM_ORG
|
|
160
|
+
# A held posture keeps the revision unapplied so the next start retries.
|
|
161
|
+
if [ "$hold" = "false" ] && [ -f .egregore-state.json ]; then
|
|
162
|
+
tmp=$(mktemp ".egregore-state.json.XXXXXX") || return 0
|
|
163
|
+
jq --arg rev "$revision" '.org_settings_revision = $rev' .egregore-state.json > "$tmp" 2>/dev/null \
|
|
164
|
+
&& [ -s "$tmp" ] && mv "$tmp" .egregore-state.json || rm -f "$tmp"
|
|
165
|
+
SETTINGS_CP_SYNCED="true"; export SETTINGS_CP_SYNCED
|
|
166
|
+
fi
|
|
167
|
+
fi
|
|
168
|
+
elif [ -n "$revision" ] && [ "$revision" = "$applied" ] && [ "$local_subset" != "$server_settings" ]; then
|
|
169
|
+
# Server unchanged but local settings moved (offline write, failed push):
|
|
170
|
+
# deliver the local state now. bin/settings.sh owns the push mechanics.
|
|
171
|
+
if bash bin/settings.sh push >/dev/null 2>&1; then
|
|
172
|
+
SETTINGS_CP_SYNCED="true"; export SETTINGS_CP_SYNCED
|
|
173
|
+
fi
|
|
174
|
+
elif [ "$local_subset" = "$server_settings" ]; then
|
|
175
|
+
SETTINGS_CP_SYNCED="true"; export SETTINGS_CP_SYNCED
|
|
176
|
+
fi
|
|
177
|
+
}
|
|
178
|
+
|
|
101
179
|
settings_drift_report() {
|
|
102
180
|
UNSHARED_SETTINGS_KEYS=""
|
|
181
|
+
# Control plane already carries these settings — nothing to ask the user.
|
|
182
|
+
[ "${SETTINGS_CP_SYNCED:-false}" = "true" ] && return 0
|
|
103
183
|
git diff --quiet -- egregore.json 2>/dev/null && return 0
|
|
104
184
|
local tmp_head
|
|
105
185
|
tmp_head=$(mktemp) || return 0
|
|
@@ -40,12 +40,47 @@ _save() {
|
|
|
40
40
|
tmp="$(mktemp "$CONFIG.XXXXXX")" || _die "cannot create temp file" 1
|
|
41
41
|
if jq "$@" "$filter" "$CONFIG" > "$tmp" 2>/dev/null && [ -s "$tmp" ]; then
|
|
42
42
|
mv "$tmp" "$CONFIG"
|
|
43
|
+
_control_plane_push
|
|
43
44
|
else
|
|
44
45
|
rm -f "$tmp"
|
|
45
46
|
_die "failed to update $CONFIG (invalid jq filter or unwritable file)" 1
|
|
46
47
|
fi
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
# ── control-plane push (Connected mode) ──────────────────────────────────
|
|
51
|
+
# A confirmed settings write reaches the control plane immediately — no
|
|
52
|
+
# harness session, no git ceremony in the loop. Fail-soft: the file write
|
|
53
|
+
# already landed, and a missed push is reconciled by the next session
|
|
54
|
+
# start's settings sync (bin/lib/settings-drift.sh).
|
|
55
|
+
_control_plane_push() {
|
|
56
|
+
local api_url api_key payload actor response revision
|
|
57
|
+
api_url=$(jq -r '.api_url // empty' "$CONFIG" 2>/dev/null)
|
|
58
|
+
[ -n "$api_url" ] || return 0
|
|
59
|
+
api_key=$(grep '^EGREGORE_API_KEY=' "$SCRIPT_DIR/.env" 2>/dev/null | cut -d'=' -f2-)
|
|
60
|
+
[ -n "$api_key" ] || return 0
|
|
61
|
+
actor=$(jq -r '.github_username // empty' "$SCRIPT_DIR/.egregore-state.json" 2>/dev/null)
|
|
62
|
+
payload=$(jq -c --arg by "$actor" '{
|
|
63
|
+
settings: ({boundary, base_branch, people_removed, features, repos, admins, people}
|
|
64
|
+
| with_entries(select(.value != null))),
|
|
65
|
+
updated_by: (if $by == "" then null else $by end)
|
|
66
|
+
}' "$CONFIG" 2>/dev/null)
|
|
67
|
+
[ -n "$payload" ] || return 0
|
|
68
|
+
response=$(curl -s --max-time 10 -X PUT "$api_url/api/org/settings" \
|
|
69
|
+
-H "Authorization: Bearer $api_key" -H "Content-Type: application/json" \
|
|
70
|
+
--data-binary "$payload" 2>/dev/null) || return 0
|
|
71
|
+
revision=$(printf '%s' "$response" | jq -r '.settings_revision // empty' 2>/dev/null)
|
|
72
|
+
if [ -n "$revision" ] && [ -f "$SCRIPT_DIR/.egregore-state.json" ]; then
|
|
73
|
+
local st
|
|
74
|
+
st="$(mktemp "$SCRIPT_DIR/.egregore-state.json.XXXXXX")" || return 0
|
|
75
|
+
if jq --arg rev "$revision" '.org_settings_revision = $rev' "$SCRIPT_DIR/.egregore-state.json" > "$st" 2>/dev/null && [ -s "$st" ]; then
|
|
76
|
+
mv "$st" "$SCRIPT_DIR/.egregore-state.json"
|
|
77
|
+
else
|
|
78
|
+
rm -f "$st"
|
|
79
|
+
fi
|
|
80
|
+
fi
|
|
81
|
+
return 0
|
|
82
|
+
}
|
|
83
|
+
|
|
49
84
|
_has_repo() { jq -e --arg n "$1" 'any(.repos[]?; (if type=="object" then .name else . end) == $n)' "$CONFIG" >/dev/null; }
|
|
50
85
|
_has_admin() { jq -e --arg h "$1" 'any(.admins[]?; . == $h)' "$CONFIG" >/dev/null; }
|
|
51
86
|
|
|
@@ -559,6 +594,7 @@ case "$DOMAIN" in
|
|
|
559
594
|
people) cmd_people "$@" ;;
|
|
560
595
|
privacy) cmd_privacy ;;
|
|
561
596
|
posture) cmd_posture "$@" ;;
|
|
597
|
+
push) _control_plane_push ;;
|
|
562
598
|
boundary) cmd_boundary "$@" ;;
|
|
563
599
|
dump) cmd_dump ;;
|
|
564
600
|
""|-h|--help|help) usage 0 ;;
|
|
@@ -897,6 +897,9 @@ def _upgrade(args: argparse.Namespace) -> int:
|
|
|
897
897
|
state = upgrade_module.describe(store.load())
|
|
898
898
|
state["active_version"] = _active_runtime_version(root, store)
|
|
899
899
|
state["instance_hash"] = upgrade_module.instance_key(root)
|
|
900
|
+
# A live apply journal means a previous candidate application died
|
|
901
|
+
# mid-transaction; the next activate recovers deterministically.
|
|
902
|
+
state["interrupted_apply"] = store.load_apply_journal() is not None
|
|
900
903
|
memory_root = (root / "memory").resolve()
|
|
901
904
|
if memory_root.is_dir():
|
|
902
905
|
state["team_sync"] = team_sync(memory_root, fetch=bool(args.fetch)).to_dict()
|