@rubytech/create-maxy-code 0.1.330 → 0.1.332
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/dist/index.js +19 -2
- package/package.json +1 -1
- package/payload/platform/config/brand.json +1 -0
- package/payload/platform/lib/account-enumeration/dist/index.d.ts +33 -0
- package/payload/platform/lib/account-enumeration/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/account-enumeration/dist/index.js +76 -0
- package/payload/platform/lib/account-enumeration/dist/index.js.map +1 -1
- package/payload/platform/lib/account-enumeration/src/index.ts +78 -0
- package/payload/platform/lib/mcp-spawn-tee/dist/index.d.ts +24 -24
- package/payload/platform/lib/mcp-spawn-tee/dist/index.js +109 -75
- package/payload/platform/lib/mcp-spawn-tee/dist/index.js.map +1 -1
- package/payload/platform/lib/mcp-spawn-tee/src/__tests__/spawn-tee.test.ts +56 -19
- package/payload/platform/lib/mcp-spawn-tee/src/index.ts +112 -77
- package/payload/platform/neo4j/schema.cypher +58 -4
- package/payload/platform/plugins/admin/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/admin/PLUGIN.md +13 -1
- package/payload/platform/plugins/admin/lib/mcp-spawn-tee/index.js +109 -75
- package/payload/platform/plugins/admin/mcp/dist/index.js +102 -0
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/admin/mcp/dist/tools/__tests__/account-lifecycle.test.d.ts +2 -0
- package/payload/platform/plugins/admin/mcp/dist/tools/__tests__/account-lifecycle.test.d.ts.map +1 -0
- package/payload/platform/plugins/admin/mcp/dist/tools/__tests__/account-lifecycle.test.js +57 -0
- package/payload/platform/plugins/admin/mcp/dist/tools/__tests__/account-lifecycle.test.js.map +1 -0
- package/payload/platform/plugins/admin/mcp/dist/tools/account-lifecycle.d.ts +34 -0
- package/payload/platform/plugins/admin/mcp/dist/tools/account-lifecycle.d.ts.map +1 -0
- package/payload/platform/plugins/admin/mcp/dist/tools/account-lifecycle.js +78 -0
- package/payload/platform/plugins/admin/mcp/dist/tools/account-lifecycle.js.map +1 -0
- package/payload/platform/plugins/admin/skills/plainly/SKILL.md +7 -0
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +2 -2
- package/payload/platform/plugins/aeo/lib/mcp-spawn-tee/index.js +109 -75
- package/payload/platform/plugins/browser/lib/mcp-spawn-tee/index.js +109 -75
- package/payload/platform/plugins/contacts/lib/mcp-spawn-tee/index.js +109 -75
- package/payload/platform/plugins/contacts/mcp/dist/index.js +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/__tests__/contact-lookup.test.d.ts +2 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/__tests__/contact-lookup.test.d.ts.map +1 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/__tests__/contact-lookup.test.js +71 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/__tests__/contact-lookup.test.js.map +1 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/__tests__/group-create.test.d.ts +2 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/__tests__/group-create.test.d.ts.map +1 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/__tests__/group-create.test.js +112 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/__tests__/group-create.test.js.map +1 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/__tests__/group-manage.test.d.ts +2 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/__tests__/group-manage.test.d.ts.map +1 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/__tests__/group-manage.test.js +102 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/__tests__/group-manage.test.js.map +1 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-lookup.d.ts +1 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-lookup.d.ts.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-lookup.js +10 -3
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-lookup.js.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/group-create.d.ts.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/group-create.js +7 -2
- package/payload/platform/plugins/contacts/mcp/dist/tools/group-create.js.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/group-manage.d.ts.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/group-manage.js +5 -4
- package/payload/platform/plugins/contacts/mcp/dist/tools/group-manage.js.map +1 -1
- package/payload/platform/plugins/contacts/mcp/package.json +4 -2
- package/payload/platform/plugins/docs/references/admin-ui.md +1 -1
- package/payload/platform/plugins/email/lib/mcp-spawn-tee/index.js +109 -75
- package/payload/platform/plugins/memory/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/memory/PLUGIN.md +1 -1
- package/payload/platform/plugins/memory/lib/mcp-spawn-tee/index.js +109 -75
- package/payload/platform/plugins/memory/mcp/dist/index.js +3 -1
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-lookup-by-name.test.js +39 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-lookup-by-name.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-lookup-by-name.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-lookup-by-name.js +13 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-lookup-by-name.js.map +1 -1
- package/payload/platform/plugins/memory/references/schema-estate-agent.md +17 -5
- package/payload/platform/plugins/outlook/lib/mcp-spawn-tee/index.js +109 -75
- package/payload/platform/plugins/quickbooks/lib/mcp-spawn-tee/index.js +109 -75
- package/payload/platform/plugins/replicate/lib/mcp-spawn-tee/index.js +109 -75
- package/payload/platform/plugins/scheduling/lib/mcp-spawn-tee/index.js +109 -75
- package/payload/platform/plugins/telegram/lib/mcp-spawn-tee/index.js +109 -75
- package/payload/platform/plugins/url-get/lib/mcp-spawn-tee/index.js +109 -75
- package/payload/platform/plugins/whatsapp/lib/mcp-spawn-tee/index.js +109 -75
- package/payload/platform/plugins/work/lib/mcp-spawn-tee/index.js +109 -75
- package/payload/platform/plugins/workflows/lib/mcp-spawn-tee/index.js +109 -75
- package/payload/platform/scripts/__tests__/provision-role-stamp.test.sh +40 -0
- package/payload/platform/scripts/__tests__/sweep-durability.test.sh +103 -0
- package/payload/platform/scripts/lib/provision-account-dir.sh +319 -0
- package/payload/platform/scripts/lib/resolve-account-dir.sh +79 -132
- package/payload/platform/scripts/seed-neo4j.sh +18 -0
- package/payload/platform/scripts/setup-account.sh +28 -361
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +4 -0
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/config.d.ts +18 -0
- package/payload/platform/services/claude-session-manager/dist/config.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/config.js +63 -7
- package/payload/platform/services/claude-session-manager/dist/config.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +30 -2
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/index.js +16 -1
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.js +7 -0
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/session-sidecar.d.ts +5 -0
- package/payload/platform/services/claude-session-manager/dist/session-sidecar.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/session-sidecar.js +8 -0
- package/payload/platform/services/claude-session-manager/dist/session-sidecar.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/stuck-turn.d.ts +6 -0
- package/payload/platform/services/claude-session-manager/dist/stuck-turn.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/stuck-turn.js +17 -5
- package/payload/platform/services/claude-session-manager/dist/stuck-turn.js.map +1 -1
- package/payload/platform/services/webchat-channel/dist/instructions.d.ts.map +1 -1
- package/payload/platform/services/webchat-channel/dist/instructions.js +25 -1
- package/payload/platform/services/webchat-channel/dist/instructions.js.map +1 -1
- package/payload/platform/services/webchat-channel/dist/notification.d.ts +17 -0
- package/payload/platform/services/webchat-channel/dist/notification.d.ts.map +1 -1
- package/payload/platform/services/webchat-channel/dist/notification.js +14 -0
- package/payload/platform/services/webchat-channel/dist/notification.js.map +1 -1
- package/payload/platform/services/webchat-channel/dist/server.d.ts.map +1 -1
- package/payload/platform/services/webchat-channel/dist/server.js +66 -2
- package/payload/platform/services/webchat-channel/dist/server.js.map +1 -1
- package/payload/platform/services/webchat-channel/dist/turn-follow.d.ts +28 -0
- package/payload/platform/services/webchat-channel/dist/turn-follow.d.ts.map +1 -0
- package/payload/platform/services/webchat-channel/dist/turn-follow.js +188 -0
- package/payload/platform/services/webchat-channel/dist/turn-follow.js.map +1 -0
- package/payload/platform/services/whatsapp-channel/dist/notification.d.ts.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/notification.js +24 -1
- package/payload/platform/services/whatsapp-channel/dist/notification.js.map +1 -1
- package/payload/platform/templates/account.json +2 -0
- package/payload/premium-plugins/writer-craft/lib/mcp-spawn-tee/index.js +109 -75
- package/payload/server/public/assets/AdminLoginScreens-DexSvtjD.js +1 -0
- package/payload/server/public/assets/AdminShell-Bf9tndNg.js +1 -0
- package/payload/server/public/assets/{Checkbox-1flpB9H0.js → Checkbox-D58GsKoQ.js} +1 -1
- package/payload/server/public/assets/{OperatorConversations-DrPqB9Ym.css → OperatorConversations-CDdp2nVn.css} +1 -1
- package/payload/server/public/assets/OperatorConversations-RmqANYz8.js +9 -0
- package/payload/server/public/assets/{admin-BWJTvagN.js → admin-CH5Vkkt4.js} +1 -1
- package/payload/server/public/assets/{browser-XM5e-fn1.js → browser-BwDtzSlr.js} +1 -1
- package/payload/server/public/assets/chat-3GY0LmYH.js +1 -0
- package/payload/server/public/assets/data-CttrzhfL.js +1 -0
- package/payload/server/public/assets/{graph-CIIz9Dmh.js → graph-CZSdF2jv.js} +3 -3
- package/payload/server/public/assets/{graph-labels-BRKvhE0e.js → graph-labels-Bi0fu8Ns.js} +1 -1
- package/payload/server/public/assets/operator-BEivxmRy.js +1 -0
- package/payload/server/public/assets/page-DUvT07SB.js +30 -0
- package/payload/server/public/assets/{public-BPlZnopr.js → public-B3eIlVbd.js} +1 -1
- package/payload/server/public/browser.html +4 -4
- package/payload/server/public/chat.html +5 -5
- package/payload/server/public/data.html +4 -4
- package/payload/server/public/graph.html +6 -6
- package/payload/server/public/index.html +6 -6
- package/payload/server/public/operator.html +7 -7
- package/payload/server/public/public.html +5 -5
- package/payload/server/server.js +605 -435
- package/payload/server/public/assets/AdminLoginScreens-BnuaXvxN.js +0 -1
- package/payload/server/public/assets/AdminShell-Br0DJQNu.js +0 -1
- package/payload/server/public/assets/OperatorConversations-C8sITz2_.js +0 -9
- package/payload/server/public/assets/chat-COFMxaoC.js +0 -1
- package/payload/server/public/assets/data-BHtJozT2.js +0 -1
- package/payload/server/public/assets/operator-9hbxrXKm.js +0 -1
- package/payload/server/public/assets/page-CXkBfOhG.js +0 -30
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# provision-account-dir.sh — per-account scaffolding, callable per account.
|
|
2
|
+
# Sourced by setup-account.sh (for the resolved house account) and by the
|
|
3
|
+
# admin-core `account_create` lifecycle tool (for a new client account). Defines
|
|
4
|
+
# `provision_account_dir <account_dir> <account_id> <role>`.
|
|
5
|
+
#
|
|
6
|
+
# The function is pure scaffolding: it creates the account dir layout, writes the
|
|
7
|
+
# Claude Code project settings (hooks only), installs agent identities and the
|
|
8
|
+
# specialist plugin, seeds account.json (stamping accountId + role), seeds
|
|
9
|
+
# enabledPlugins from brand defaults, and stamps the install owner into admins[].
|
|
10
|
+
# It does NO account resolution and NO Neo4j work.
|
|
11
|
+
#
|
|
12
|
+
# Required globals (set by the caller before invoking):
|
|
13
|
+
# TEMPLATES_DIR — <project>/templates
|
|
14
|
+
# PROJECT_DIR — the platform/ dir
|
|
15
|
+
# SCRIPT_DIR — the platform/scripts dir (for lib bootstraps)
|
|
16
|
+
# USERS_FILE — $HOME/<brand.configDir>/users.json (owner stamp; optional)
|
|
17
|
+
|
|
18
|
+
provision_account_dir() {
|
|
19
|
+
local ACCOUNT_DIR="$1"
|
|
20
|
+
local ACCOUNT_ID="$2"
|
|
21
|
+
local ROLE="$3"
|
|
22
|
+
|
|
23
|
+
mkdir -p "$ACCOUNT_DIR/agents/admin" "$ACCOUNT_DIR/.claude" "$ACCOUNT_DIR/specialists/.claude-plugin" "$ACCOUNT_DIR/specialists/agents"
|
|
24
|
+
|
|
25
|
+
# Claude Code discovers project-level .claude/ relative to the nearest .git
|
|
26
|
+
# root. Without a .git in the account dir it traverses up to ~/maxy/.git and
|
|
27
|
+
# misses accountDir/.claude/. Initialise a repo so the account dir IS the root.
|
|
28
|
+
[ -d "$ACCOUNT_DIR/.git" ] || git init -q "$ACCOUNT_DIR"
|
|
29
|
+
|
|
30
|
+
# --- project-level .claude/settings.json (hooks; no allowlist) -------------
|
|
31
|
+
local ACCOUNT_SETTINGS="$ACCOUNT_DIR/.claude/settings.json"
|
|
32
|
+
local HOOKS_PATH="\$PLATFORM_ROOT/plugins/admin/hooks"
|
|
33
|
+
cat > "$ACCOUNT_SETTINGS" << SETTINGS_EOF
|
|
34
|
+
{
|
|
35
|
+
"permissions": {
|
|
36
|
+
"defaultMode": "bypassPermissions",
|
|
37
|
+
"allow": [],
|
|
38
|
+
"deny": []
|
|
39
|
+
},
|
|
40
|
+
"hooks": {
|
|
41
|
+
"PreToolUse": [
|
|
42
|
+
{
|
|
43
|
+
"matcher": "Write",
|
|
44
|
+
"hooks": [
|
|
45
|
+
{ "type": "command", "command": "bash $HOOKS_PATH/archive-ingest-surface-gate.sh" }
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"matcher": "Edit",
|
|
50
|
+
"hooks": [
|
|
51
|
+
{ "type": "command", "command": "bash $HOOKS_PATH/archive-ingest-surface-gate.sh" }
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"matcher": "NotebookEdit",
|
|
56
|
+
"hooks": [
|
|
57
|
+
{ "type": "command", "command": "bash $HOOKS_PATH/archive-ingest-surface-gate.sh" }
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"matcher": "Bash",
|
|
62
|
+
"hooks": [
|
|
63
|
+
{ "type": "command", "command": "bash $HOOKS_PATH/archive-ingest-surface-gate.sh" }
|
|
64
|
+
]
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"hooks": [
|
|
68
|
+
{ "type": "command", "command": "bash $HOOKS_PATH/archive-ingest-surface-gate.sh" }
|
|
69
|
+
]
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"matcher": "WebFetch",
|
|
73
|
+
"hooks": [
|
|
74
|
+
{ "type": "command", "command": "node $HOOKS_PATH/webfetch-preflight.mjs" }
|
|
75
|
+
]
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"matcher": "AskUserQuestion",
|
|
79
|
+
"hooks": [
|
|
80
|
+
{ "type": "command", "command": "bash $HOOKS_PATH/askuserquestion-investigate-gate.sh" }
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
],
|
|
84
|
+
"PostToolUse": [
|
|
85
|
+
{
|
|
86
|
+
"matcher": "Agent",
|
|
87
|
+
"hooks": [
|
|
88
|
+
{ "type": "command", "command": "bash $HOOKS_PATH/post-tool-use-agent.sh" }
|
|
89
|
+
]
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"matcher": "mcp__.*__.*-(export|import)-parse$",
|
|
93
|
+
"hooks": [
|
|
94
|
+
{ "type": "command", "command": "bash $HOOKS_PATH/archive-ingest-surface-gate.sh" }
|
|
95
|
+
]
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"matcher": "mcp__.*",
|
|
99
|
+
"hooks": [
|
|
100
|
+
{ "type": "command", "command": "bash $HOOKS_PATH/mcp-tool-missing.sh" }
|
|
101
|
+
]
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"matcher": "Write",
|
|
105
|
+
"hooks": [
|
|
106
|
+
{ "type": "command", "command": "bash $HOOKS_PATH/admin-authoring-observer.sh" }
|
|
107
|
+
]
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"matcher": "Edit",
|
|
111
|
+
"hooks": [
|
|
112
|
+
{ "type": "command", "command": "bash $HOOKS_PATH/admin-authoring-observer.sh" }
|
|
113
|
+
]
|
|
114
|
+
}
|
|
115
|
+
],
|
|
116
|
+
"UserPromptSubmit": [
|
|
117
|
+
{
|
|
118
|
+
"hooks": [
|
|
119
|
+
{ "type": "command", "command": "bash $HOOKS_PATH/archive-ingest-surface-gate.sh" },
|
|
120
|
+
{ "type": "command", "command": "bash $HOOKS_PATH/prompt-optimiser-directive.sh" }
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
],
|
|
124
|
+
"Stop": [
|
|
125
|
+
{
|
|
126
|
+
"hooks": [
|
|
127
|
+
{ "type": "command", "command": "bash $HOOKS_PATH/prompt-optimiser-compliance.sh" }
|
|
128
|
+
]
|
|
129
|
+
}
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
SETTINGS_EOF
|
|
134
|
+
|
|
135
|
+
# --- agent identities + specialist plugin templates ------------------------
|
|
136
|
+
cp "$TEMPLATES_DIR/agents/admin/IDENTITY.md" "$ACCOUNT_DIR/agents/admin/IDENTITY.md"
|
|
137
|
+
|
|
138
|
+
# On re-provision, re-sync the Rubytech-controlled IDENTITY.md for every
|
|
139
|
+
# already-existing non-admin agent dir (security-load-bearing). Never create
|
|
140
|
+
# dirs; never write SOUL/KNOWLEDGE/config.
|
|
141
|
+
local _AGENT_DIR _AGENT_NAME
|
|
142
|
+
for _AGENT_DIR in "$ACCOUNT_DIR/agents"/*/; do
|
|
143
|
+
[ -d "$_AGENT_DIR" ] || continue
|
|
144
|
+
_AGENT_NAME="$(basename "$_AGENT_DIR")"
|
|
145
|
+
[ "$_AGENT_NAME" = "admin" ] && continue
|
|
146
|
+
[ -f "$_AGENT_DIR/IDENTITY.md" ] || continue
|
|
147
|
+
cp "$TEMPLATES_DIR/agents/public/IDENTITY.md" "$_AGENT_DIR/IDENTITY.md"
|
|
148
|
+
echo " agents/$_AGENT_NAME/IDENTITY.md re-synced"
|
|
149
|
+
done
|
|
150
|
+
|
|
151
|
+
cp "$TEMPLATES_DIR/specialists/.claude-plugin/plugin.json" "$ACCOUNT_DIR/specialists/.claude-plugin/plugin.json"
|
|
152
|
+
|
|
153
|
+
# Replace core specialist files (no '--'); preserve premium plugin agents.
|
|
154
|
+
local _existing
|
|
155
|
+
for _existing in "$ACCOUNT_DIR/specialists/agents/"*.md; do
|
|
156
|
+
[ -f "$_existing" ] || continue
|
|
157
|
+
case "$(basename "$_existing")" in
|
|
158
|
+
*--*) continue ;;
|
|
159
|
+
esac
|
|
160
|
+
rm -f "$_existing"
|
|
161
|
+
done
|
|
162
|
+
|
|
163
|
+
local specialist
|
|
164
|
+
for specialist in "$TEMPLATES_DIR/specialists/agents/"*.md; do
|
|
165
|
+
[ -f "$specialist" ] && cp "$specialist" "$ACCOUNT_DIR/specialists/agents/$(basename "$specialist")"
|
|
166
|
+
done
|
|
167
|
+
|
|
168
|
+
rm -f "$ACCOUNT_DIR/.claude/agents/"*.md 2>/dev/null || true
|
|
169
|
+
|
|
170
|
+
# SOUL.md — user-controlled. Only create if missing. Never overwrite.
|
|
171
|
+
if [ -f "$ACCOUNT_DIR/agents/admin/SOUL.md" ]; then
|
|
172
|
+
echo " agents/admin/SOUL.md preserved ($(wc -c < "$ACCOUNT_DIR/agents/admin/SOUL.md" | tr -d ' ') bytes)"
|
|
173
|
+
else
|
|
174
|
+
cp "$TEMPLATES_DIR/agents/admin/SOUL.md" "$ACCOUNT_DIR/agents/admin/SOUL.md"
|
|
175
|
+
echo " agents/admin/SOUL.md created ($(wc -c < "$ACCOUNT_DIR/agents/admin/SOUL.md" | tr -d ' ') bytes)"
|
|
176
|
+
fi
|
|
177
|
+
|
|
178
|
+
bash "$SCRIPT_DIR/lib/agents-md-bootstrap.sh" "$ACCOUNT_DIR"
|
|
179
|
+
bash "$SCRIPT_DIR/lib/admin-skills-bootstrap.sh" "$ACCOUNT_DIR" "$PROJECT_DIR/plugins"
|
|
180
|
+
|
|
181
|
+
# --- account.json: create from template, stamp accountId + role ------------
|
|
182
|
+
local TEMPLATE="$TEMPLATES_DIR/account.json"
|
|
183
|
+
if [ ! -f "$ACCOUNT_DIR/account.json" ]; then
|
|
184
|
+
if [ -f "$TEMPLATE" ]; then
|
|
185
|
+
sed "s/\"accountId\": *\"[^\"]*\"/\"accountId\":\"$ACCOUNT_ID\"/" "$TEMPLATE" \
|
|
186
|
+
| sed "s/\"tier\": *\"[^\"]*\"/\"tier\":\"solo\"/" \
|
|
187
|
+
> "$ACCOUNT_DIR/account.json"
|
|
188
|
+
else
|
|
189
|
+
echo "ERROR: account.json template not found at $TEMPLATE" >&2
|
|
190
|
+
return 1
|
|
191
|
+
fi
|
|
192
|
+
fi
|
|
193
|
+
|
|
194
|
+
# Stamp role on the account.json (idempotent migration + designation). A
|
|
195
|
+
# pre-existing legacy account.json with no role gets ROLE; an account already
|
|
196
|
+
# carrying a role keeps it unless ROLE differs (operator-driven via the
|
|
197
|
+
# lifecycle tools, never here).
|
|
198
|
+
ACCOUNT_ID="$ACCOUNT_ID" ROLE="$ROLE" python3 - "$ACCOUNT_DIR/account.json" <<'PY'
|
|
199
|
+
import json, os, sys
|
|
200
|
+
path = sys.argv[1]
|
|
201
|
+
role = os.environ["ROLE"]
|
|
202
|
+
acct_id = os.environ["ACCOUNT_ID"]
|
|
203
|
+
with open(path) as f:
|
|
204
|
+
cfg = json.load(f)
|
|
205
|
+
changed = False
|
|
206
|
+
if not cfg.get("accountId"):
|
|
207
|
+
cfg["accountId"] = acct_id; changed = True
|
|
208
|
+
if role and cfg.get("role") != role:
|
|
209
|
+
cfg["role"] = role; changed = True
|
|
210
|
+
if "managedService" not in cfg:
|
|
211
|
+
cfg["managedService"] = False; changed = True
|
|
212
|
+
if changed:
|
|
213
|
+
with open(path, "w") as f:
|
|
214
|
+
json.dump(cfg, f, indent=2); f.write("\n")
|
|
215
|
+
print(f" account.json role={cfg.get('role')} stamped")
|
|
216
|
+
PY
|
|
217
|
+
|
|
218
|
+
# --- enabledPlugins from brand.json plugins.defaultEnabled -----------------
|
|
219
|
+
local BRAND_JSON="$PROJECT_DIR/config/brand.json"
|
|
220
|
+
if [ -f "$BRAND_JSON" ] && [ -f "$ACCOUNT_DIR/account.json" ]; then
|
|
221
|
+
PROJECT_DIR="$PROJECT_DIR" BRAND_JSON="$BRAND_JSON" ACCOUNT_JSON="$ACCOUNT_DIR/account.json" python3 - <<'PY'
|
|
222
|
+
import json, os, sys
|
|
223
|
+
|
|
224
|
+
brand_path = os.environ['BRAND_JSON']
|
|
225
|
+
account_path = os.environ['ACCOUNT_JSON']
|
|
226
|
+
plugins_dir = os.path.join(os.environ['PROJECT_DIR'], 'plugins')
|
|
227
|
+
|
|
228
|
+
with open(brand_path) as f:
|
|
229
|
+
brand = json.load(f)
|
|
230
|
+
|
|
231
|
+
plugins_cfg = brand.get('plugins', {})
|
|
232
|
+
default_enabled = plugins_cfg.get('defaultEnabled', [])
|
|
233
|
+
core = set(plugins_cfg.get('core', []))
|
|
234
|
+
|
|
235
|
+
if not default_enabled:
|
|
236
|
+
print(' [setup] no defaultEnabled plugins in brand.json — skipping')
|
|
237
|
+
sys.exit(0)
|
|
238
|
+
|
|
239
|
+
satisfied = core | set(default_enabled)
|
|
240
|
+
validated = []
|
|
241
|
+
for plugin in default_enabled:
|
|
242
|
+
plugin_md = os.path.join(plugins_dir, plugin, 'PLUGIN.md')
|
|
243
|
+
requires = []
|
|
244
|
+
if os.path.isfile(plugin_md):
|
|
245
|
+
in_frontmatter = False
|
|
246
|
+
in_requires = False
|
|
247
|
+
with open(plugin_md) as f:
|
|
248
|
+
for line in f:
|
|
249
|
+
stripped = line.rstrip()
|
|
250
|
+
if stripped == '---':
|
|
251
|
+
if in_frontmatter:
|
|
252
|
+
break
|
|
253
|
+
in_frontmatter = True
|
|
254
|
+
continue
|
|
255
|
+
if not in_frontmatter:
|
|
256
|
+
continue
|
|
257
|
+
if stripped.startswith('requires:'):
|
|
258
|
+
inline = stripped.split(':', 1)[1].strip()
|
|
259
|
+
if inline.startswith('['):
|
|
260
|
+
requires = [x.strip().strip('"').strip("'") for x in inline.strip('[]').split(',') if x.strip()]
|
|
261
|
+
in_requires = False
|
|
262
|
+
else:
|
|
263
|
+
in_requires = True
|
|
264
|
+
continue
|
|
265
|
+
if in_requires:
|
|
266
|
+
if stripped.startswith(' - '):
|
|
267
|
+
requires.append(stripped.lstrip(' -').strip())
|
|
268
|
+
else:
|
|
269
|
+
in_requires = False
|
|
270
|
+
unmet = [r for r in requires if r not in satisfied]
|
|
271
|
+
if unmet:
|
|
272
|
+
print(f' [setup] WARNING: skipping defaultEnabled plugin "{plugin}" — unmet requires: {unmet}')
|
|
273
|
+
else:
|
|
274
|
+
validated.append(plugin)
|
|
275
|
+
|
|
276
|
+
with open(account_path) as f:
|
|
277
|
+
account = json.load(f)
|
|
278
|
+
|
|
279
|
+
existing = account.get('enabledPlugins', [])
|
|
280
|
+
existing_set = set(existing)
|
|
281
|
+
added = [p for p in validated if p not in existing_set]
|
|
282
|
+
account['enabledPlugins'] = existing + added
|
|
283
|
+
|
|
284
|
+
with open(account_path, 'w') as f:
|
|
285
|
+
json.dump(account, f, indent=2)
|
|
286
|
+
f.write('\n')
|
|
287
|
+
|
|
288
|
+
if not existing:
|
|
289
|
+
print(f' [setup] seeded enabledPlugins: {validated}')
|
|
290
|
+
elif added:
|
|
291
|
+
print(f' [setup] merged into enabledPlugins: +{added} (existing: {existing})')
|
|
292
|
+
else:
|
|
293
|
+
print(f' [setup] enabledPlugins already contains all defaultEnabled entries')
|
|
294
|
+
PY
|
|
295
|
+
fi
|
|
296
|
+
|
|
297
|
+
# --- stamp install owner into admins[] (idempotent) ------------------------
|
|
298
|
+
if [ -n "${USERS_FILE:-}" ] && [ -f "$USERS_FILE" ]; then
|
|
299
|
+
local USER_ID
|
|
300
|
+
USER_ID=$(python3 -c "import json; print(json.load(open('$USERS_FILE'))[0]['userId'])" 2>/dev/null || true)
|
|
301
|
+
if [ -n "$USER_ID" ] && [ -f "$ACCOUNT_DIR/account.json" ]; then
|
|
302
|
+
USER_ID="$USER_ID" python3 - "$ACCOUNT_DIR/account.json" <<'PY'
|
|
303
|
+
import json, os, sys
|
|
304
|
+
path = sys.argv[1]
|
|
305
|
+
uid = os.environ["USER_ID"]
|
|
306
|
+
with open(path) as f:
|
|
307
|
+
cfg = json.load(f)
|
|
308
|
+
cfg.setdefault("admins", [])
|
|
309
|
+
if not any(a.get("userId") == uid for a in cfg["admins"]):
|
|
310
|
+
cfg["admins"].append({"userId": uid, "role": "owner"})
|
|
311
|
+
with open(path, "w") as f:
|
|
312
|
+
json.dump(cfg, f, indent=2); f.write("\n")
|
|
313
|
+
print(f" Stamped userId={uid} as owner in account.json admins")
|
|
314
|
+
PY
|
|
315
|
+
fi
|
|
316
|
+
fi
|
|
317
|
+
|
|
318
|
+
echo " Account $ACCOUNT_ID at $ACCOUNT_DIR (role=$ROLE)"
|
|
319
|
+
}
|
|
@@ -1,184 +1,131 @@
|
|
|
1
|
-
# resolve-account-dir.sh —
|
|
1
|
+
# resolve-account-dir.sh — house-account resolver + registry-membership reporter
|
|
2
2
|
# Sourced by setup-account.sh and seed-neo4j.sh. Defines `resolve_and_sweep_account_dir`.
|
|
3
3
|
#
|
|
4
|
+
# Multi-account managed-service model (Task 983): an install may host N accounts
|
|
5
|
+
# under data/accounts/ — exactly one carries role:"house", the rest role:"client".
|
|
6
|
+
# The registry is the set of dirs holding a parseable account.json. EVERY
|
|
7
|
+
# registered account is durable: this resolver never trashes a dir with an
|
|
8
|
+
# account.json. It resolves ACCOUNT_DIR/ACCOUNT_ID to the single role:"house"
|
|
9
|
+
# account (or the sole account in the pre-migration window, before
|
|
10
|
+
# setup-account.sh stamps role:"house").
|
|
11
|
+
#
|
|
12
|
+
# Sweep is LOG-ONLY this sprint. The destructive `mv` to .trash/ was removed to
|
|
13
|
+
# eliminate any chance of deleting a registered account during the multi-account
|
|
14
|
+
# rollout; restoring a guarded destructive sweep is tracked as a follow-up task.
|
|
15
|
+
# Orphan dirs (no account.json) are reported, never moved.
|
|
16
|
+
#
|
|
4
17
|
# Invariant: the TypeScript `resolveAccount()` at platform/ui/app/lib/claude-agent.ts
|
|
5
|
-
# is READ-ONLY. This bash helper is the SOLE
|
|
6
|
-
# the only safe moment to mutate
|
|
7
|
-
# refactor must preserve that asymmetry.
|
|
18
|
+
# is READ-ONLY. This bash helper is the SOLE filesystem-mutation surface for the
|
|
19
|
+
# account layout, because seed-time is the only safe moment to mutate it.
|
|
8
20
|
#
|
|
9
21
|
# Contract (inputs via env vars):
|
|
10
22
|
# ACCOUNTS_DIR — {installDir}/data/accounts
|
|
11
|
-
# USERS_FILE — $HOME/<brand.configDir>/users.json
|
|
23
|
+
# USERS_FILE — $HOME/<brand.configDir>/users.json (read-tolerant; unused for sweep)
|
|
12
24
|
#
|
|
13
25
|
# Contract (outputs via env vars):
|
|
14
|
-
# ACCOUNT_ID — resolved uuid (existing kept, or freshly minted)
|
|
15
|
-
# ACCOUNT_DIR — full path to the live account dir
|
|
26
|
+
# ACCOUNT_ID — resolved house uuid (existing kept, or freshly minted on fresh install)
|
|
27
|
+
# ACCOUNT_DIR — full path to the live house account dir
|
|
16
28
|
#
|
|
17
29
|
# Exit:
|
|
18
|
-
# 0 on success; non-zero on abort (
|
|
30
|
+
# 0 on success; non-zero on abort (registry drift: zero-house with >1 candidate,
|
|
31
|
+
# or multiple houses).
|
|
19
32
|
|
|
20
|
-
# Helper:
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
|
|
24
|
-
# 0 = admins[] contains userId (match)
|
|
25
|
-
# 1 = account.json readable and parsed; userId absent from admins[] (no match)
|
|
26
|
-
# 2 = account.json missing, unreadable, or unparseable (undetermined — caller must abort)
|
|
27
|
-
_account_dir_matches() {
|
|
33
|
+
# Helper: print the `role` field of the account.json at $1, or empty string when
|
|
34
|
+
# absent/unparseable. Trichotomy is unnecessary here — a missing role just means
|
|
35
|
+
# "not a house candidate"; corruption discipline lives in the enumeration step.
|
|
36
|
+
_account_dir_role() {
|
|
28
37
|
local config="$1/account.json"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
python3 - "$config" "$uid" <<'PY'
|
|
38
|
+
[ -f "$config" ] || { echo ""; return 0; }
|
|
39
|
+
python3 - "$config" <<'PY' 2>/dev/null || echo ""
|
|
32
40
|
import json, sys
|
|
33
41
|
try:
|
|
34
42
|
cfg = json.load(open(sys.argv[1]))
|
|
35
43
|
except Exception:
|
|
36
|
-
sys.exit(
|
|
37
|
-
|
|
38
|
-
uid = sys.argv[2]
|
|
39
|
-
for a in admins:
|
|
40
|
-
if isinstance(a, dict) and a.get('userId') == uid:
|
|
41
|
-
sys.exit(0)
|
|
42
|
-
sys.exit(1)
|
|
44
|
+
print(""); sys.exit(0)
|
|
45
|
+
print(cfg.get("role") or "")
|
|
43
46
|
PY
|
|
44
47
|
}
|
|
45
48
|
|
|
46
|
-
#
|
|
47
|
-
#
|
|
49
|
+
# House-account resolver. Enumerate registered candidates (dirs with account.json),
|
|
50
|
+
# classify by role, resolve the house (or sole-unlabelled), report orphans, and
|
|
51
|
+
# emit the registry snapshot. Never mutates the filesystem layout.
|
|
48
52
|
resolve_and_sweep_account_dir() {
|
|
49
53
|
mkdir -p "$ACCOUNTS_DIR"
|
|
50
54
|
|
|
51
55
|
# Enumerate candidates: direct children with account.json, basenames not starting with '.'
|
|
52
|
-
local candidates=()
|
|
56
|
+
local candidates=() orphans=()
|
|
53
57
|
local dir base
|
|
54
58
|
for dir in "$ACCOUNTS_DIR"/*/; do
|
|
55
59
|
[ -d "$dir" ] || continue
|
|
56
60
|
base="$(basename "$dir")"
|
|
57
61
|
case "$base" in .*) continue ;; esac
|
|
58
|
-
[ -f "$dir/account.json" ]
|
|
59
|
-
|
|
62
|
+
if [ -f "$dir/account.json" ]; then
|
|
63
|
+
candidates+=("$base")
|
|
64
|
+
else
|
|
65
|
+
orphans+=("$base")
|
|
66
|
+
fi
|
|
60
67
|
done
|
|
61
68
|
|
|
62
|
-
# Read target userId from users.json (if present and valid)
|
|
63
|
-
local target_user_id=""
|
|
64
|
-
if [ -f "$USERS_FILE" ]; then
|
|
65
|
-
target_user_id=$(python3 - "$USERS_FILE" <<'PY' 2>/dev/null || true
|
|
66
|
-
import json, sys
|
|
67
|
-
try:
|
|
68
|
-
data = json.load(open(sys.argv[1]))
|
|
69
|
-
if isinstance(data, list) and data and isinstance(data[0], dict):
|
|
70
|
-
uid = data[0].get('userId')
|
|
71
|
-
if uid:
|
|
72
|
-
print(uid)
|
|
73
|
-
except Exception:
|
|
74
|
-
pass
|
|
75
|
-
PY
|
|
76
|
-
)
|
|
77
|
-
fi
|
|
78
|
-
|
|
79
69
|
local n=${#candidates[@]}
|
|
80
70
|
|
|
81
71
|
# Case 0: no candidates — fresh install, mint new UUID.
|
|
82
|
-
#
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
# then losing X/account.json (cron stamped X/logs/ first); the next
|
|
86
|
-
# install sees zero candidates and mints Y, orphaning X/logs/. The
|
|
87
|
-
# guard turns that silent regression into an operator-actionable abort.
|
|
72
|
+
# Atomic-mint discipline: refuse to mint if any stub dir (subdir present,
|
|
73
|
+
# account.json missing) exists — a half-provisioned account from a crashed
|
|
74
|
+
# prior install must be operator-repaired, not silently orphaned.
|
|
88
75
|
if [ "$n" -eq 0 ]; then
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
[ -d "$dir" ] || continue
|
|
92
|
-
base="$(basename "$dir")"
|
|
93
|
-
case "$base" in .*) continue ;; esac
|
|
94
|
-
[ -f "$dir/account.json" ] && continue
|
|
95
|
-
stub_dirs+=("$base")
|
|
96
|
-
done
|
|
97
|
-
if [ "${#stub_dirs[@]}" -gt 0 ]; then
|
|
98
|
-
echo "==> [seed] FAIL phase=mint reason=\"stub-account-dirs present (no account.json): ${stub_dirs[*]}\" — refusing to mint a fresh accountId; remove or repair the stub before re-running install" >&2
|
|
76
|
+
if [ "${#orphans[@]}" -gt 0 ]; then
|
|
77
|
+
echo "==> [seed] FAIL phase=mint reason=\"stub-account-dirs present (no account.json): ${orphans[*]}\" — refusing to mint a fresh accountId; remove or repair the stub before re-running install" >&2
|
|
99
78
|
return 1
|
|
100
79
|
fi
|
|
101
80
|
ACCOUNT_ID="$(cat /proc/sys/kernel/random/uuid 2>/dev/null || python3 -c 'import uuid; print(uuid.uuid4())')"
|
|
102
81
|
ACCOUNT_DIR="$ACCOUNTS_DIR/$ACCOUNT_ID"
|
|
82
|
+
echo "==> [seed] op=registry-snapshot houses=0 clients=0"
|
|
103
83
|
return 0
|
|
104
84
|
fi
|
|
105
85
|
|
|
106
|
-
#
|
|
107
|
-
|
|
108
|
-
# accept-with-flag rather than abort because the users.json regeneration on PIN
|
|
109
|
-
# change would otherwise brick legitimate installs).
|
|
110
|
-
if [ "$n" -eq 1 ]; then
|
|
111
|
-
ACCOUNT_ID="${candidates[0]}"
|
|
112
|
-
ACCOUNT_DIR="$ACCOUNTS_DIR/$ACCOUNT_ID"
|
|
113
|
-
if [ -n "$target_user_id" ]; then
|
|
114
|
-
local rc=0
|
|
115
|
-
_account_dir_matches "$ACCOUNT_DIR" "$target_user_id" || rc=$?
|
|
116
|
-
case "$rc" in
|
|
117
|
-
0) echo "==> [seed] identity-match: kept=${ACCOUNT_ID:0:8} via userId=${target_user_id:0:8}" ;;
|
|
118
|
-
1) echo "==> [seed] WARN: single account does not match users.json userId=${target_user_id:0:8} — accepting but flagging" >&2 ;;
|
|
119
|
-
*) echo "==> [seed] identity-match FAILED: sole candidate $ACCOUNT_ID has unreadable or unparseable account.json — aborting" >&2
|
|
120
|
-
return 1 ;;
|
|
121
|
-
esac
|
|
122
|
-
fi
|
|
123
|
-
return 0
|
|
124
|
-
fi
|
|
125
|
-
|
|
126
|
-
# Case 2+: multi-candidate — identity-match-or-abort.
|
|
127
|
-
# First classify everything, THEN decide and act.
|
|
128
|
-
if [ -z "$target_user_id" ]; then
|
|
129
|
-
echo "==> [seed] identity-match FAILED: users.json missing or unreadable, $n candidates — aborting" >&2
|
|
130
|
-
for uuid in "${candidates[@]}"; do echo " candidate: $uuid" >&2; done
|
|
131
|
-
return 1
|
|
132
|
-
fi
|
|
133
|
-
|
|
134
|
-
local matches=()
|
|
135
|
-
local orphans=()
|
|
136
|
-
local rc
|
|
86
|
+
# Classify candidates by role.
|
|
87
|
+
local houses=() clients=() uuid role
|
|
137
88
|
for uuid in "${candidates[@]}"; do
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
1) orphans+=("$uuid") ;;
|
|
143
|
-
*) echo "==> [seed] identity-match FAILED: candidate $uuid has unreadable or unparseable account.json — aborting" >&2
|
|
144
|
-
for u in "${candidates[@]}"; do echo " candidate: $u" >&2; done
|
|
145
|
-
return 1 ;;
|
|
89
|
+
role="$(_account_dir_role "$ACCOUNTS_DIR/$uuid")"
|
|
90
|
+
case "$role" in
|
|
91
|
+
house) houses+=("$uuid") ;;
|
|
92
|
+
*) clients+=("$uuid") ;; # client OR unlabelled (legacy)
|
|
146
93
|
esac
|
|
147
94
|
done
|
|
148
95
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
for uuid in "${candidates[@]}"; do echo " candidate: $uuid" >&2; done
|
|
152
|
-
return 1
|
|
153
|
-
fi
|
|
96
|
+
# Pre-action registry snapshot — the count the post-seed census must match.
|
|
97
|
+
echo "==> [seed] op=registry-snapshot houses=${#houses[@]} clients=${#clients[@]}"
|
|
154
98
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
99
|
+
# Report orphans (dirs with no account.json). Log-only — never moved this sprint.
|
|
100
|
+
# Guard the empty-array expansion: under `set -u` (setup-account.sh uses
|
|
101
|
+
# `set -euo pipefail`), bash 3.2 treats "${orphans[@]}" on an empty array as an
|
|
102
|
+
# unbound variable and aborts.
|
|
103
|
+
local ts; ts=$(date -u +%Y%m%dT%H%M%SZ 2>/dev/null || echo unknown)
|
|
104
|
+
if [ "${#orphans[@]}" -gt 0 ]; then
|
|
105
|
+
for uuid in "${orphans[@]}"; do
|
|
106
|
+
echo "==> [seed] op=sweep uuid=${uuid:0:8} registered=false action=skipped-guarded ts=$ts"
|
|
107
|
+
done
|
|
159
108
|
fi
|
|
160
109
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
110
|
+
# Resolve the house.
|
|
111
|
+
if [ "${#houses[@]}" -eq 1 ]; then
|
|
112
|
+
ACCOUNT_ID="${houses[0]}"
|
|
113
|
+
ACCOUNT_DIR="$ACCOUNTS_DIR/$ACCOUNT_ID"
|
|
114
|
+
echo "==> [seed] house-resolved: ${ACCOUNT_ID:0:8}"
|
|
115
|
+
return 0
|
|
116
|
+
fi
|
|
164
117
|
|
|
165
|
-
#
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
if ! mv "$ACCOUNTS_DIR/$uuid" "$dest"; then
|
|
173
|
-
echo "==> [seed] ERROR: failed to move $uuid → $dest — aborting" >&2
|
|
174
|
-
return 1
|
|
175
|
-
fi
|
|
176
|
-
echo "==> [seed] swept orphan: ${uuid:0:8} → .trash/${uuid:0:8}-${ts}"
|
|
177
|
-
moved=$((moved + 1))
|
|
178
|
-
done
|
|
179
|
-
if [ "$moved" -gt 0 ]; then
|
|
180
|
-
echo "==> [seed] orphan sweep: moved $moved → $ACCOUNTS_DIR/.trash/"
|
|
118
|
+
# Pre-migration tolerance: zero labelled house but exactly one account → it is
|
|
119
|
+
# the house (setup-account.sh stamps role:"house" on it).
|
|
120
|
+
if [ "${#houses[@]}" -eq 0 ] && [ "$n" -eq 1 ]; then
|
|
121
|
+
ACCOUNT_ID="${candidates[0]}"
|
|
122
|
+
ACCOUNT_DIR="$ACCOUNTS_DIR/$ACCOUNT_ID"
|
|
123
|
+
echo "==> [seed] house-resolved: ${ACCOUNT_ID:0:8} (sole account, pre-migration)"
|
|
124
|
+
return 0
|
|
181
125
|
fi
|
|
182
126
|
|
|
183
|
-
|
|
127
|
+
# Genuine drift — loud-fail.
|
|
128
|
+
echo "==> [seed] FAIL phase=resolve-house reason=\"houses=${#houses[@]} total=$n — expected exactly one role:house (or a single unlabelled account)\" — aborting" >&2
|
|
129
|
+
for uuid in "${candidates[@]}"; do echo " candidate: $uuid role=$(_account_dir_role "$ACCOUNTS_DIR/$uuid")" >&2; done
|
|
130
|
+
return 1
|
|
184
131
|
}
|
|
@@ -190,4 +190,22 @@ else
|
|
|
190
190
|
echo " No users.json found — skipping AdminUser seed (set-pin POST creates it)"
|
|
191
191
|
fi
|
|
192
192
|
|
|
193
|
+
# ------------------------------------------------------------------
|
|
194
|
+
# 5. Standing reconcile — Property-hub invariant (estate-agent vertical)
|
|
195
|
+
# ------------------------------------------------------------------
|
|
196
|
+
# :Property is the obligatory parent of every :Listing. The schema-apply step
|
|
197
|
+
# above runs the one-shot backfill that relinks any pre-inversion listing-only
|
|
198
|
+
# data; this per-deploy count proves it reached zero and catches a write path
|
|
199
|
+
# that later regresses the inversion. orphanListings=0 is healthy. A fresh
|
|
200
|
+
# non-estate install has zero :Listing nodes (orphanListings=0 total=0). This
|
|
201
|
+
# is a visibility signal only — never install-blocking, so failures here do not
|
|
202
|
+
# exit non-zero.
|
|
203
|
+
ESTATE_ORPHANS=$("$CYPHER_SHELL" -u "$NEO4J_USER" -p "$NEO4J_PASSWORD" -a "$NEO4J_URI" \
|
|
204
|
+
--format plain \
|
|
205
|
+
"MATCH (l:Listing {accountId: '$ACCOUNT_ID'}) WHERE NOT ( (:Property)-[:HAS_LISTING]->(l) ) RETURN count(l)" 2>/dev/null | tail -1 | tr -d ' \r')
|
|
206
|
+
ESTATE_TOTAL=$("$CYPHER_SHELL" -u "$NEO4J_USER" -p "$NEO4J_PASSWORD" -a "$NEO4J_URI" \
|
|
207
|
+
--format plain \
|
|
208
|
+
"MATCH (l:Listing {accountId: '$ACCOUNT_ID'}) RETURN count(l)" 2>/dev/null | tail -1 | tr -d ' \r')
|
|
209
|
+
echo " [estate-reconcile] accountId=${ACCOUNT_ID:0:8} orphanListings=${ESTATE_ORPHANS:-0} total=${ESTATE_TOTAL:-0}"
|
|
210
|
+
|
|
193
211
|
echo " Done."
|