@rubytech/create-maxy 1.0.450 → 1.0.452
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/package.json +1 -1
- package/payload/maxy/server.js +139 -66
- package/payload/platform/plugins/admin/hooks/session-start.sh +21 -5
- package/payload/platform/plugins/admin/hooks/test-agent-creation-gate.sh +43 -10
- package/payload/platform/plugins/admin/skills/public-agent-manager/skill.md +1 -1
- package/payload/platform/templates/agents/admin/IDENTITY.md +2 -0
- package/payload/platform/templates/agents/admin/LEARNINGS.md +0 -18
- package/payload/platform/templates/agents/public/IDENTITY.md +4 -0
package/package.json
CHANGED
package/payload/maxy/server.js
CHANGED
|
@@ -9398,8 +9398,9 @@ async function safeSaveCreds(authDir, saveCreds) {
|
|
|
9398
9398
|
}
|
|
9399
9399
|
try {
|
|
9400
9400
|
await Promise.resolve(saveCreds());
|
|
9401
|
+
console.error(`${TAG2} creds saved successfully to ${authDir}`);
|
|
9401
9402
|
} catch (err) {
|
|
9402
|
-
console.error(`${TAG2} failed saving creds: ${String(err)}`);
|
|
9403
|
+
console.error(`${TAG2} failed saving creds to ${authDir}: ${String(err)}`);
|
|
9403
9404
|
}
|
|
9404
9405
|
}
|
|
9405
9406
|
function enqueueSaveCreds(authDir, saveCreds) {
|
|
@@ -9413,44 +9414,63 @@ async function createWaSocket(opts) {
|
|
|
9413
9414
|
maybeRestoreCredsFromBackup(authDir);
|
|
9414
9415
|
const { state, saveCreds } = await useMultiFileAuthState(authDir);
|
|
9415
9416
|
const { version: version2 } = await fetchLatestBaileysVersion();
|
|
9416
|
-
const
|
|
9417
|
-
|
|
9418
|
-
|
|
9419
|
-
|
|
9420
|
-
|
|
9421
|
-
|
|
9422
|
-
|
|
9423
|
-
|
|
9424
|
-
|
|
9425
|
-
|
|
9426
|
-
|
|
9427
|
-
|
|
9428
|
-
|
|
9429
|
-
|
|
9430
|
-
}
|
|
9431
|
-
|
|
9417
|
+
const BAILEYS_TAG = "[whatsapp:baileys]";
|
|
9418
|
+
function makeBaileysLogger() {
|
|
9419
|
+
const logger = {
|
|
9420
|
+
level: "warn",
|
|
9421
|
+
child: () => makeBaileysLogger(),
|
|
9422
|
+
trace: () => {
|
|
9423
|
+
},
|
|
9424
|
+
debug: () => {
|
|
9425
|
+
},
|
|
9426
|
+
info: () => {
|
|
9427
|
+
},
|
|
9428
|
+
warn: (...args) => console.error(BAILEYS_TAG, "WARN", ...args),
|
|
9429
|
+
error: (...args) => console.error(BAILEYS_TAG, "ERROR", ...args),
|
|
9430
|
+
fatal: (...args) => console.error(BAILEYS_TAG, "FATAL", ...args)
|
|
9431
|
+
};
|
|
9432
|
+
return logger;
|
|
9433
|
+
}
|
|
9434
|
+
const baileysLogger = makeBaileysLogger();
|
|
9432
9435
|
const sock = makeWASocket({
|
|
9433
9436
|
auth: {
|
|
9434
9437
|
creds: state.creds,
|
|
9435
|
-
keys: makeCacheableSignalKeyStore(state.keys,
|
|
9438
|
+
keys: makeCacheableSignalKeyStore(state.keys, baileysLogger)
|
|
9436
9439
|
},
|
|
9437
9440
|
version: version2,
|
|
9438
|
-
logger:
|
|
9441
|
+
logger: baileysLogger,
|
|
9439
9442
|
printQRInTerminal: false,
|
|
9440
9443
|
browser: Browsers.macOS("Chrome"),
|
|
9441
9444
|
syncFullHistory: false,
|
|
9442
9445
|
markOnlineOnConnect: false
|
|
9443
9446
|
});
|
|
9444
|
-
sock.ev.on("creds.update", () =>
|
|
9447
|
+
sock.ev.on("creds.update", () => {
|
|
9448
|
+
console.error(`${TAG2} creds.update received \u2014 saving to ${authDir}`);
|
|
9449
|
+
enqueueSaveCreds(authDir, saveCreds);
|
|
9450
|
+
});
|
|
9451
|
+
let qrSequence = 0;
|
|
9445
9452
|
sock.ev.on("connection.update", (update) => {
|
|
9446
9453
|
try {
|
|
9454
|
+
const parts = [];
|
|
9455
|
+
if (update.connection) parts.push(`connection=${update.connection}`);
|
|
9456
|
+
if (update.qr) parts.push(`qr=#${++qrSequence}`);
|
|
9457
|
+
if (update.isNewLogin !== void 0) parts.push(`isNewLogin=${update.isNewLogin}`);
|
|
9458
|
+
if (update.receivedPendingNotifications !== void 0) parts.push(`pendingNotifications=${update.receivedPendingNotifications}`);
|
|
9459
|
+
if (update.connection === "close" && update.lastDisconnect) {
|
|
9460
|
+
const status = getStatusCode(update.lastDisconnect.error);
|
|
9461
|
+
parts.push(`disconnectStatus=${status ?? "unknown"}`);
|
|
9462
|
+
parts.push(`disconnectReason=${formatError(update.lastDisconnect.error)}`);
|
|
9463
|
+
}
|
|
9464
|
+
if (!silent && parts.length > 0) {
|
|
9465
|
+
console.error(`${TAG2} connection.update ${parts.join(" ")}`);
|
|
9466
|
+
}
|
|
9447
9467
|
if (update.qr && onQr) {
|
|
9448
9468
|
onQr(update.qr);
|
|
9449
9469
|
}
|
|
9450
9470
|
if (update.connection === "close") {
|
|
9451
9471
|
const status = getStatusCode(update.lastDisconnect?.error);
|
|
9452
9472
|
if (status === DisconnectReason.loggedOut && !silent) {
|
|
9453
|
-
console.error(`${TAG2} session logged out \u2014 re-link required`);
|
|
9473
|
+
console.error(`${TAG2} session logged out (401) \u2014 re-link required`);
|
|
9454
9474
|
}
|
|
9455
9475
|
}
|
|
9456
9476
|
if (update.connection === "open" && !silent) {
|
|
@@ -9519,6 +9539,42 @@ function extractBoomDetails(err) {
|
|
|
9519
9539
|
return { statusCode, error: error48, message };
|
|
9520
9540
|
}
|
|
9521
9541
|
|
|
9542
|
+
// app/lib/whatsapp/reconnect.ts
|
|
9543
|
+
import { DisconnectReason as DisconnectReason2 } from "@whiskeysockets/baileys";
|
|
9544
|
+
function classifyDisconnect(err) {
|
|
9545
|
+
const statusCode = getStatusCode(err);
|
|
9546
|
+
const message = formatError(err);
|
|
9547
|
+
if (statusCode === DisconnectReason2.loggedOut) {
|
|
9548
|
+
return {
|
|
9549
|
+
kind: "loggedOut",
|
|
9550
|
+
statusCode,
|
|
9551
|
+
message: "Session logged out \u2014 re-link required",
|
|
9552
|
+
shouldRetry: false
|
|
9553
|
+
};
|
|
9554
|
+
}
|
|
9555
|
+
if (statusCode === 409 || statusCode === DisconnectReason2.connectionReplaced) {
|
|
9556
|
+
return {
|
|
9557
|
+
kind: "conflict",
|
|
9558
|
+
statusCode,
|
|
9559
|
+
message: "Connection replaced by another client",
|
|
9560
|
+
shouldRetry: false
|
|
9561
|
+
};
|
|
9562
|
+
}
|
|
9563
|
+
return {
|
|
9564
|
+
kind: "transient",
|
|
9565
|
+
statusCode,
|
|
9566
|
+
message,
|
|
9567
|
+
shouldRetry: true
|
|
9568
|
+
};
|
|
9569
|
+
}
|
|
9570
|
+
function computeBackoff(attempt, config2 = {}) {
|
|
9571
|
+
const base = config2.baseDelayMs ?? 2e3;
|
|
9572
|
+
const max = config2.maxDelayMs ?? 12e4;
|
|
9573
|
+
const exponential = Math.min(base * Math.pow(2, attempt), max);
|
|
9574
|
+
const jitter = exponential * (0.75 + Math.random() * 0.5);
|
|
9575
|
+
return Math.round(jitter);
|
|
9576
|
+
}
|
|
9577
|
+
|
|
9522
9578
|
// app/lib/whatsapp/login.ts
|
|
9523
9579
|
var TAG3 = "[whatsapp:login]";
|
|
9524
9580
|
var ACTIVE_LOGIN_TTL_MS = 3 * 6e4;
|
|
@@ -9539,6 +9595,56 @@ function resetActiveLogin(accountId) {
|
|
|
9539
9595
|
function isLoginFresh(login) {
|
|
9540
9596
|
return Date.now() - login.startedAt < ACTIVE_LOGIN_TTL_MS;
|
|
9541
9597
|
}
|
|
9598
|
+
var LOGIN_MAX_RECONNECTS = 3;
|
|
9599
|
+
var LOGIN_RECONNECT_DELAYS = [2e3, 4e3, 8e3];
|
|
9600
|
+
async function loginConnectionLoop(accountId, login) {
|
|
9601
|
+
let attempt = 0;
|
|
9602
|
+
while (true) {
|
|
9603
|
+
try {
|
|
9604
|
+
await waitForConnection(login.sock);
|
|
9605
|
+
const current = activeLogins.get(accountId);
|
|
9606
|
+
if (current?.id === login.id) {
|
|
9607
|
+
current.connected = true;
|
|
9608
|
+
}
|
|
9609
|
+
return;
|
|
9610
|
+
} catch (err) {
|
|
9611
|
+
const current = activeLogins.get(accountId);
|
|
9612
|
+
if (current?.id !== login.id) return;
|
|
9613
|
+
const classification = classifyDisconnect(err);
|
|
9614
|
+
if (!classification.shouldRetry || attempt >= LOGIN_MAX_RECONNECTS) {
|
|
9615
|
+
if (attempt >= LOGIN_MAX_RECONNECTS) {
|
|
9616
|
+
console.error(
|
|
9617
|
+
`${TAG3} login reconnect attempts exhausted (${attempt}/${LOGIN_MAX_RECONNECTS}) \u2014 surfacing error to agent`
|
|
9618
|
+
);
|
|
9619
|
+
current.error = `Login failed after ${attempt} reconnect attempts: ${formatError(err)}`;
|
|
9620
|
+
} else {
|
|
9621
|
+
current.error = formatError(err);
|
|
9622
|
+
}
|
|
9623
|
+
current.errorStatus = getStatusCode(err);
|
|
9624
|
+
return;
|
|
9625
|
+
}
|
|
9626
|
+
attempt++;
|
|
9627
|
+
const delay = LOGIN_RECONNECT_DELAYS[attempt - 1] ?? 8e3;
|
|
9628
|
+
console.error(
|
|
9629
|
+
`${TAG3} status=${classification.statusCode ?? "unknown"} restart required \u2014 reconnecting with saved creds (attempt ${attempt}/${LOGIN_MAX_RECONNECTS}) delay=${delay}ms`
|
|
9630
|
+
);
|
|
9631
|
+
closeSocket(current.sock);
|
|
9632
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
9633
|
+
const afterDelay = activeLogins.get(accountId);
|
|
9634
|
+
if (afterDelay?.id !== login.id) return;
|
|
9635
|
+
try {
|
|
9636
|
+
const newSock = await createWaSocket({ authDir: login.authDir });
|
|
9637
|
+
current.sock = newSock;
|
|
9638
|
+
} catch (sockErr) {
|
|
9639
|
+
console.error(
|
|
9640
|
+
`${TAG3} reconnect socket creation failed (attempt ${attempt}/${LOGIN_MAX_RECONNECTS}): ${String(sockErr)}`
|
|
9641
|
+
);
|
|
9642
|
+
current.error = `Reconnection failed: ${String(sockErr)}`;
|
|
9643
|
+
return;
|
|
9644
|
+
}
|
|
9645
|
+
}
|
|
9646
|
+
}
|
|
9647
|
+
}
|
|
9542
9648
|
async function startLogin(opts) {
|
|
9543
9649
|
const { accountId, authDir, force, timeoutMs = 3e4 } = opts;
|
|
9544
9650
|
const existing0 = activeLogins.get(accountId);
|
|
@@ -9580,16 +9686,21 @@ async function startLogin(opts) {
|
|
|
9580
9686
|
);
|
|
9581
9687
|
let sock;
|
|
9582
9688
|
let pendingQr = null;
|
|
9689
|
+
let loginQrCount = 0;
|
|
9583
9690
|
try {
|
|
9584
9691
|
sock = await createWaSocket({
|
|
9585
9692
|
authDir,
|
|
9586
9693
|
onQr: (qr2) => {
|
|
9587
|
-
|
|
9694
|
+
loginQrCount++;
|
|
9695
|
+
if (pendingQr) {
|
|
9696
|
+
console.error(`${TAG3} QR rotation #${loginQrCount} received for account=${accountId} \u2014 not forwarded (initial QR already captured)`);
|
|
9697
|
+
return;
|
|
9698
|
+
}
|
|
9588
9699
|
pendingQr = qr2;
|
|
9589
9700
|
const current = activeLogins.get(accountId);
|
|
9590
9701
|
if (current && !current.qr) current.qr = qr2;
|
|
9591
9702
|
clearTimeout(qrTimer);
|
|
9592
|
-
console.error(`${TAG3} QR received for account=${accountId}`);
|
|
9703
|
+
console.error(`${TAG3} QR #${loginQrCount} received for account=${accountId} \u2014 forwarding to caller`);
|
|
9593
9704
|
resolveQr?.(qr2);
|
|
9594
9705
|
}
|
|
9595
9706
|
});
|
|
@@ -9608,14 +9719,12 @@ async function startLogin(opts) {
|
|
|
9608
9719
|
};
|
|
9609
9720
|
activeLogins.set(accountId, login);
|
|
9610
9721
|
if (pendingQr && !login.qr) login.qr = pendingQr;
|
|
9611
|
-
|
|
9722
|
+
loginConnectionLoop(accountId, login).catch((err) => {
|
|
9723
|
+
console.error(`${TAG3} loginConnectionLoop unexpected error: ${String(err)}`);
|
|
9612
9724
|
const current = activeLogins.get(accountId);
|
|
9613
|
-
if (current?.id === login.id)
|
|
9614
|
-
|
|
9615
|
-
|
|
9616
|
-
if (current?.id !== login.id) return;
|
|
9617
|
-
current.error = formatError(err);
|
|
9618
|
-
current.errorStatus = getStatusCode(err);
|
|
9725
|
+
if (current?.id === login.id) {
|
|
9726
|
+
current.error = `Unexpected login error: ${String(err)}`;
|
|
9727
|
+
}
|
|
9619
9728
|
});
|
|
9620
9729
|
let qr;
|
|
9621
9730
|
try {
|
|
@@ -23523,42 +23632,6 @@ var WhatsAppConfigSchema = external_exports.object({
|
|
|
23523
23632
|
});
|
|
23524
23633
|
});
|
|
23525
23634
|
|
|
23526
|
-
// app/lib/whatsapp/reconnect.ts
|
|
23527
|
-
import { DisconnectReason as DisconnectReason2 } from "@whiskeysockets/baileys";
|
|
23528
|
-
function classifyDisconnect(err) {
|
|
23529
|
-
const statusCode = getStatusCode(err);
|
|
23530
|
-
const message = formatError(err);
|
|
23531
|
-
if (statusCode === DisconnectReason2.loggedOut) {
|
|
23532
|
-
return {
|
|
23533
|
-
kind: "loggedOut",
|
|
23534
|
-
statusCode,
|
|
23535
|
-
message: "Session logged out \u2014 re-link required",
|
|
23536
|
-
shouldRetry: false
|
|
23537
|
-
};
|
|
23538
|
-
}
|
|
23539
|
-
if (statusCode === 409 || statusCode === DisconnectReason2.connectionReplaced) {
|
|
23540
|
-
return {
|
|
23541
|
-
kind: "conflict",
|
|
23542
|
-
statusCode,
|
|
23543
|
-
message: "Connection replaced by another client",
|
|
23544
|
-
shouldRetry: false
|
|
23545
|
-
};
|
|
23546
|
-
}
|
|
23547
|
-
return {
|
|
23548
|
-
kind: "transient",
|
|
23549
|
-
statusCode,
|
|
23550
|
-
message,
|
|
23551
|
-
shouldRetry: true
|
|
23552
|
-
};
|
|
23553
|
-
}
|
|
23554
|
-
function computeBackoff(attempt, config2 = {}) {
|
|
23555
|
-
const base = config2.baseDelayMs ?? 2e3;
|
|
23556
|
-
const max = config2.maxDelayMs ?? 12e4;
|
|
23557
|
-
const exponential = Math.min(base * Math.pow(2, attempt), max);
|
|
23558
|
-
const jitter = exponential * (0.75 + Math.random() * 0.5);
|
|
23559
|
-
return Math.round(jitter);
|
|
23560
|
-
}
|
|
23561
|
-
|
|
23562
23635
|
// app/lib/whatsapp/normalize.ts
|
|
23563
23636
|
var WHATSAPP_USER_JID_RE = /^(\d+)(?::\d+)?@s\.whatsapp\.net$/i;
|
|
23564
23637
|
var WHATSAPP_LID_RE = /^(\d+)@lid$/i;
|
|
@@ -30,9 +30,13 @@ if [ ! -f "$DURABLE_STATE" ]; then
|
|
|
30
30
|
fi
|
|
31
31
|
|
|
32
32
|
# Staleness + completion check — reject durable state that is stale (>6h, missing
|
|
33
|
-
# timestamp) OR already complete (all gates true
|
|
34
|
-
# completed durable file would be recovered on
|
|
35
|
-
# misleading "resume creation" message.
|
|
33
|
+
# timestamp) OR already complete (all gates true AND all gated files exist on
|
|
34
|
+
# disk). Without this, a stale or completed durable file would be recovered on
|
|
35
|
+
# every new session, injecting a misleading "resume creation" message.
|
|
36
|
+
#
|
|
37
|
+
# "All gates true" alone is NOT completion — it means user approval is done but
|
|
38
|
+
# file writes may still be pending. Deleting state at that point causes the
|
|
39
|
+
# remaining writes to fail with "invoke the public-agent-manager skill first."
|
|
36
40
|
DURABLE_VALID=$(python3 -c "
|
|
37
41
|
import json, sys
|
|
38
42
|
from datetime import datetime, timezone
|
|
@@ -49,10 +53,22 @@ try:
|
|
|
49
53
|
if age_hours > 6 or age_hours < 0:
|
|
50
54
|
print('stale')
|
|
51
55
|
sys.exit(0)
|
|
52
|
-
# All gates true
|
|
56
|
+
# All gates true AND all files on disk = truly complete. All gates true
|
|
57
|
+
# alone means the user approved everything but writes may still be pending.
|
|
58
|
+
# Must match the PostToolUse completion check in agent-creation-post.sh.
|
|
53
59
|
gates = state.get('gates', {})
|
|
54
60
|
if isinstance(gates, dict) and set(gates.keys()) == {'soul', 'knowledge', 'config'} and all(v is True for v in gates.values()):
|
|
55
|
-
|
|
61
|
+
slug = state.get('slug', '')
|
|
62
|
+
if slug:
|
|
63
|
+
import os
|
|
64
|
+
agent_dir = os.path.join('$ACCOUNT_DIR', 'agents', slug)
|
|
65
|
+
gated_files = ['SOUL.md', 'KNOWLEDGE.md', 'config.json']
|
|
66
|
+
if all(os.path.isfile(os.path.join(agent_dir, f)) for f in gated_files):
|
|
67
|
+
print('complete')
|
|
68
|
+
else:
|
|
69
|
+
print('valid')
|
|
70
|
+
else:
|
|
71
|
+
print('valid')
|
|
56
72
|
else:
|
|
57
73
|
print('valid')
|
|
58
74
|
except Exception:
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
# - Allow Bash writes after gates pass
|
|
14
14
|
# - Allow all gated writes when all gates pass
|
|
15
15
|
# - Clean up state immediately when all files exist on disk (PostToolUse)
|
|
16
|
-
# -
|
|
16
|
+
# - Recover all-gates-true state on session start when files not yet written
|
|
17
|
+
# - Detect and clean up truly completed state on session start (all gates true + files on disk)
|
|
17
18
|
# - Ignore non-document-editor/form components
|
|
18
19
|
# - Ignore _componentDone for admin agent
|
|
19
20
|
# - Handle gate advancement idempotently
|
|
@@ -721,10 +722,11 @@ fi
|
|
|
721
722
|
rm -f "$STATE_FILE"
|
|
722
723
|
|
|
723
724
|
# ---------------------------------------------------------------------------
|
|
724
|
-
# Test 30: Session-start
|
|
725
|
+
# Test 30: Session-start RECOVERS all-gates-true state when files are missing
|
|
725
726
|
# ---------------------------------------------------------------------------
|
|
726
|
-
echo "Test 30: Session-start
|
|
727
|
+
echo "Test 30: Session-start recovers all-gates-true state when files are missing"
|
|
727
728
|
rm -f "$STATE_FILE"
|
|
729
|
+
rm -f "$ACCOUNT_DIR/agents/test-agent/SOUL.md" "$ACCOUNT_DIR/agents/test-agent/KNOWLEDGE.md" "$ACCOUNT_DIR/agents/test-agent/config.json" 2>/dev/null || true
|
|
728
730
|
DURABLE_FILE="$ACCOUNT_DIR/.claude/agent-create-state.json"
|
|
729
731
|
|
|
730
732
|
python3 -c "
|
|
@@ -741,16 +743,47 @@ with open('$DURABLE_FILE', 'w') as f:
|
|
|
741
743
|
|
|
742
744
|
bash "$SCRIPT_DIR/session-start.sh" "$ACCOUNT_DIR" admin 2>/dev/null || true
|
|
743
745
|
|
|
744
|
-
if [
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
746
|
+
if [ -f "$STATE_FILE" ] && [ -f "$DURABLE_FILE" ]; then
|
|
747
|
+
pass "All-gates-true state recovered (files not yet written — writes still pending)"
|
|
748
|
+
else
|
|
749
|
+
fail "All-gates-true state should be recovered when gated files are missing from disk"
|
|
750
|
+
fi
|
|
751
|
+
|
|
752
|
+
rm -f "$STATE_FILE"
|
|
753
|
+
|
|
754
|
+
# ---------------------------------------------------------------------------
|
|
755
|
+
# Test 30a: Session-start does NOT recover truly completed state (all gates true + all files on disk)
|
|
756
|
+
# ---------------------------------------------------------------------------
|
|
757
|
+
echo "Test 30a: Session-start does NOT recover truly completed state"
|
|
758
|
+
|
|
759
|
+
# Create all gated files on disk
|
|
760
|
+
touch "$ACCOUNT_DIR/agents/test-agent/SOUL.md"
|
|
761
|
+
touch "$ACCOUNT_DIR/agents/test-agent/KNOWLEDGE.md"
|
|
762
|
+
touch "$ACCOUNT_DIR/agents/test-agent/config.json"
|
|
763
|
+
|
|
764
|
+
python3 -c "
|
|
765
|
+
import json
|
|
766
|
+
from datetime import datetime, timezone
|
|
767
|
+
state = {
|
|
768
|
+
'slug': 'test-agent',
|
|
769
|
+
'started': datetime.now(timezone.utc).isoformat(),
|
|
770
|
+
'gates': {'soul': True, 'knowledge': True, 'config': True}
|
|
771
|
+
}
|
|
772
|
+
with open('$DURABLE_FILE', 'w') as f:
|
|
773
|
+
json.dump(state, f)
|
|
774
|
+
"
|
|
775
|
+
|
|
776
|
+
bash "$SCRIPT_DIR/session-start.sh" "$ACCOUNT_DIR" admin 2>/dev/null || true
|
|
777
|
+
|
|
778
|
+
if [ ! -f "$STATE_FILE" ] && [ ! -f "$DURABLE_FILE" ]; then
|
|
779
|
+
pass "Truly completed state (all gates true + all files on disk) deleted"
|
|
750
780
|
else
|
|
751
|
-
fail "
|
|
781
|
+
fail "Truly completed state should be deleted — both gates passed and files written"
|
|
752
782
|
fi
|
|
753
783
|
|
|
784
|
+
# Clean up gated files for subsequent tests
|
|
785
|
+
rm -f "$ACCOUNT_DIR/agents/test-agent/SOUL.md" "$ACCOUNT_DIR/agents/test-agent/KNOWLEDGE.md" "$ACCOUNT_DIR/agents/test-agent/config.json"
|
|
786
|
+
|
|
754
787
|
# ---------------------------------------------------------------------------
|
|
755
788
|
# Test 31: PostToolUse cleans up state after last gated file is written
|
|
756
789
|
# ---------------------------------------------------------------------------
|
|
@@ -94,7 +94,7 @@ SOUL.md is strictly personality. When the user provides content for SOUL.md, rou
|
|
|
94
94
|
- **Engagement strategy, qualification flows, component usage** → handled by selected plugins
|
|
95
95
|
- **Boundaries, tool rules, procedural instructions** → IDENTITY.md
|
|
96
96
|
|
|
97
|
-
SOUL.md answers "what does this agent feel like to talk to?" — tone, warmth, formality, humour, greeting personality. SOUL.md must not restate constraints from IDENTITY.md. When user-provided content could belong to either file, apply this test: "does this restrict what the agent does?" → IDENTITY.md. "Does this describe how the agent sounds?" → SOUL.md. If content does both (e.g. "always respond politely"), route to SOUL — the constraint is a consequence of tone, not an operational boundary. Present SOUL.md via `document-editor` for the user to review and approve.
|
|
97
|
+
SOUL.md answers "what does this agent feel like to talk to?" — tone, warmth, formality, humour, greeting personality. SOUL.md must not restate constraints from IDENTITY.md. When user-provided content could belong to either file, apply this test: "does this restrict what the agent does?" → IDENTITY.md. "Does this describe how the agent sounds?" → SOUL.md. If content does both (e.g. "always respond politely"), route to SOUL — the constraint is a consequence of tone, not an operational boundary. Present SOUL.md via `document-editor` for the user to review and approve. Follow the document-editor encoding constraint in IDENTITY.md when generating content.
|
|
98
98
|
|
|
99
99
|
## KNOWLEDGE.md Population
|
|
100
100
|
|
|
@@ -127,6 +127,8 @@ The user can also launch the browser independently from the header menu. Both pa
|
|
|
127
127
|
|
|
128
128
|
When the user asks to view, review, attach, or download a file or document, render it via `render-component` with `name: "document-editor"` and `data: { title, content }`. This gives a render-review-edit-download flow — the user sees the content inline, can edit it, and can download it as a `.md` file directly from the component. Do not use `memory-write`, `memory-ingest`, or other tools to deliver file content to the user. Synthesised content (summaries, reports, drafts) follows the same path — render via `document-editor` so the user can review and download.
|
|
129
129
|
|
|
130
|
+
The document-editor's markdown parser fails silently on these specific typographical characters: em dashes (U+2014), en dashes (U+2013), curly double quotes (U+201C, U+201D), curly single quotes (U+2018, U+2019), and horizontal ellipsis (U+2026). Replace them with their plain equivalents — hyphens, straight quotes, three periods. Currency symbols (£, €), accented characters, and other Unicode are unaffected — use them normally.
|
|
131
|
+
|
|
130
132
|
## Plugins and Skills
|
|
131
133
|
|
|
132
134
|
Your behaviour is defined by your loaded plugins. Follow them. The `<plugin-manifest>` lists every active plugin's skills and references with their file paths. To load any skill or reference, call `plugin-read` with the plugin's directory name and the file path from the manifest. The manifest is authoritative for plugin discovery and skill loading.
|
|
@@ -1,21 +1,3 @@
|
|
|
1
1
|
# Learnings
|
|
2
2
|
|
|
3
3
|
Accumulated tool-call patterns. Each entry records a mistake and the correct approach. Consult before calling unfamiliar tools.
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
### Deferred tool schemas must be loaded before use
|
|
8
|
-
|
|
9
|
-
MCP tools have deferred schemas — parameter names, types, and required fields are not in context until fetched. Calling an MCP tool without loading its schema produces wrong parameter names, wrong types, and missing required fields. Always call `ToolSearch` for a tool before its first use in a session. This applies to every `mcp__*` tool — memory, tasks, scheduling, contacts, admin, cloudflare, telegram, documents.
|
|
10
|
-
|
|
11
|
-
### memory-update requires Neo4j element IDs
|
|
12
|
-
|
|
13
|
-
The `memory-update` tool identifies nodes by their Neo4j element ID — an integer returned in `memory-search` results (the `elementId` field). Do not pass UUIDs, attachment IDs, or string identifiers. If you do not have the element ID, run `memory-search` first to retrieve it.
|
|
14
|
-
|
|
15
|
-
### memory-ingest complex parameters must be arrays
|
|
16
|
-
|
|
17
|
-
The `memory-ingest` tool's `sections` and `keywords` parameters are arrays, not strings. `sections` is an array of objects; `keywords` is an array of strings. Passing a string where an array is expected produces a schema validation error. Load the tool schema via `ToolSearch` to confirm the expected types before calling.
|
|
18
|
-
|
|
19
|
-
### document-editor content must be ASCII-safe
|
|
20
|
-
|
|
21
|
-
The `document-editor` markdown parser silently fails on Unicode typographical characters — em dashes (—), en dashes (–), curly quotes (" " ' '), and ellipsis (…). When these appear in the `content` field, the editor renders empty and the user sees a blank document. Use ASCII alternatives: hyphens (-), straight quotes (" '), and three periods (...). This applies to all `document-editor` content including SOUL.md, KNOWLEDGE.md, and any other document presented for review.
|
|
@@ -9,6 +9,10 @@ You are a public-facing agent for a business. Your personality, tone, and busine
|
|
|
9
9
|
- Never describe your own capabilities or limitations unprompted. Visitors care about the business, not your architecture.
|
|
10
10
|
- British English unless your business context specifies otherwise.
|
|
11
11
|
|
|
12
|
+
## Visual content
|
|
13
|
+
|
|
14
|
+
Images may be available at `/brand/` — these are visual assets the business has prepared. When your knowledge lists an image and a visitor's question is relevant to it, embed it inline using standard markdown: ``. The visitor sees the image directly in the conversation. Only reference images explicitly listed in your knowledge — never guess or fabricate filenames. When a relevant image exists, show it rather than describing what you could show.
|
|
15
|
+
|
|
12
16
|
## Boundaries
|
|
13
17
|
|
|
14
18
|
- You are an AI assistant. State this clearly if asked. Never impersonate a human.
|