create-egregore 0.21.0-runtime-mvp.11 → 0.21.0-runtime-mvp.12
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/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 +3 -0
- package/runtime/claude/bin/lib/greeting.sh +6 -0
- package/runtime/claude/bin/lib/settings-drift.sh +80 -0
- package/runtime/claude/bin/settings.sh +36 -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 +3 -0
- package/runtime/codex/bin/lib/greeting.sh +6 -0
- package/runtime/codex/bin/lib/settings-drift.sh +80 -0
- package/runtime/codex/bin/settings.sh +36 -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 +3 -0
- package/runtime/pi/bin/lib/greeting.sh +6 -0
- package/runtime/pi/bin/lib/settings-drift.sh +80 -0
- package/runtime/pi/bin/settings.sh +36 -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 +3 -0
- package/runtime/prime/bin/lib/greeting.sh +6 -0
- package/runtime/prime/bin/lib/settings-drift.sh +80 -0
- package/runtime/prime/bin/settings.sh +36 -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.12
|
|
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.12";
|
|
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/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.12",
|
|
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"
|
|
@@ -335,6 +335,9 @@ setup_develop() {
|
|
|
335
335
|
settings_drift_heal
|
|
336
336
|
git merge --ff-only "origin/$BASE_BRANCH" --quiet 2>/dev/null || true
|
|
337
337
|
settings_drift_reapply
|
|
338
|
+
# Connected mode: reconcile with the control plane (org settings row) —
|
|
339
|
+
# pull a teammate's confirmed write down, or re-deliver a failed push.
|
|
340
|
+
settings_drift_pull
|
|
338
341
|
settings_drift_report
|
|
339
342
|
fi
|
|
340
343
|
|
|
@@ -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
|
|
@@ -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 ;;
|
|
@@ -70,6 +70,19 @@ Preserve supplied wording, order, headings, lists, code, and links. For
|
|
|
70
70
|
generated content, use only the useful subset of Briefing, Current State, Open
|
|
71
71
|
Threads, and Next Steps. Keep it operational and concise.
|
|
72
72
|
|
|
73
|
+
## Staircase (staging)
|
|
74
|
+
|
|
75
|
+
A second face for addressed handoffs exists: the staircase, a
|
|
76
|
+
progressive-disclosure page the receiver walks one step at a time and closes
|
|
77
|
+
with a readback (docs/specs/handoff-staircase.md). It is in staging and does
|
|
78
|
+
not change this skill. When the user asks for a staircase and this instance
|
|
79
|
+
has the staging command (.claude/skills/handoff-staircase/SKILL.md, Codex: the
|
|
80
|
+
handoff-staircase adapter; internal to Curve Labs while in staging), route to
|
|
81
|
+
it: it reuses this preview and approval bridge with a composed spec and adds
|
|
82
|
+
one feedback question at the end. Without it, say the staircase is not
|
|
83
|
+
installed here and continue with this skill. When its feedback
|
|
84
|
+
promotes it, the staircase becomes the default here and that command retires.
|
|
85
|
+
|
|
73
86
|
## One-command preview
|
|
74
87
|
|
|
75
88
|
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"
|
|
@@ -335,6 +335,9 @@ setup_develop() {
|
|
|
335
335
|
settings_drift_heal
|
|
336
336
|
git merge --ff-only "origin/$BASE_BRANCH" --quiet 2>/dev/null || true
|
|
337
337
|
settings_drift_reapply
|
|
338
|
+
# Connected mode: reconcile with the control plane (org settings row) —
|
|
339
|
+
# pull a teammate's confirmed write down, or re-deliver a failed push.
|
|
340
|
+
settings_drift_pull
|
|
338
341
|
settings_drift_report
|
|
339
342
|
fi
|
|
340
343
|
|
|
@@ -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
|