@rubytech/create-maxy-code 0.1.472 → 0.1.473
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/__tests__/google-client-id-env.test.js +12 -10
- package/dist/index.js +6 -29
- package/dist/port-resolution.js +5 -5
- package/package.json +1 -1
- package/payload/platform/config/brand.json +1 -0
- package/payload/platform/docs/superpowers/plans/2026-07-18-ledger-write-path-fix.md +109 -0
- package/payload/platform/lib/ledger-core/dist/reconcile.d.ts +7 -0
- package/payload/platform/lib/ledger-core/dist/reconcile.d.ts.map +1 -1
- package/payload/platform/lib/ledger-core/dist/reconcile.js +5 -2
- package/payload/platform/lib/ledger-core/dist/reconcile.js.map +1 -1
- package/payload/platform/lib/ledger-core/src/__tests__/reconcile.test.ts +20 -0
- package/payload/platform/lib/ledger-core/src/reconcile.ts +12 -2
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +24 -5
- package/payload/platform/plugins/docs/references/google-guide.md +10 -3
- package/payload/platform/plugins/docs/references/ledger.md +9 -1
- package/payload/platform/plugins/docs/references/troubleshooting.md +4 -0
- package/payload/platform/plugins/google/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/google/PLUGIN.md +4 -1
- package/payload/platform/plugins/google/mcp/dist/__tests__/account-register.test.js +79 -28
- package/payload/platform/plugins/google/mcp/dist/__tests__/account-register.test.js.map +1 -1
- package/payload/platform/plugins/google/mcp/dist/__tests__/boot.test.d.ts +2 -0
- package/payload/platform/plugins/google/mcp/dist/__tests__/boot.test.d.ts.map +1 -0
- package/payload/platform/plugins/google/mcp/dist/__tests__/boot.test.js +54 -0
- package/payload/platform/plugins/google/mcp/dist/__tests__/boot.test.js.map +1 -0
- package/payload/platform/plugins/google/mcp/dist/__tests__/google-client.test.js +26 -1
- package/payload/platform/plugins/google/mcp/dist/__tests__/google-client.test.js.map +1 -1
- package/payload/platform/plugins/google/mcp/dist/__tests__/index-registration.test.js +3 -2
- package/payload/platform/plugins/google/mcp/dist/__tests__/index-registration.test.js.map +1 -1
- package/payload/platform/plugins/google/mcp/dist/index.d.ts +9 -5
- package/payload/platform/plugins/google/mcp/dist/index.d.ts.map +1 -1
- package/payload/platform/plugins/google/mcp/dist/index.js +38 -37
- package/payload/platform/plugins/google/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/google/mcp/dist/lib/google-client.d.ts +10 -7
- package/payload/platform/plugins/google/mcp/dist/lib/google-client.d.ts.map +1 -1
- package/payload/platform/plugins/google/mcp/dist/lib/google-client.js +25 -13
- package/payload/platform/plugins/google/mcp/dist/lib/google-client.js.map +1 -1
- package/payload/platform/plugins/google/mcp/dist/tools/account-register.d.ts +58 -17
- package/payload/platform/plugins/google/mcp/dist/tools/account-register.d.ts.map +1 -1
- package/payload/platform/plugins/google/mcp/dist/tools/account-register.js +143 -37
- package/payload/platform/plugins/google/mcp/dist/tools/account-register.js.map +1 -1
- package/payload/platform/plugins/google/references/auth.md +91 -74
- package/payload/platform/plugins/ledger/PLUGIN.md +13 -1
- package/payload/platform/plugins/ledger/mcp/dist/index.js +23 -15
- package/payload/platform/plugins/ledger/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/ledger/mcp/dist/lib/read.d.ts +11 -1
- package/payload/platform/plugins/ledger/mcp/dist/lib/read.d.ts.map +1 -1
- package/payload/platform/plugins/ledger/mcp/dist/lib/read.js +20 -1
- package/payload/platform/plugins/ledger/mcp/dist/lib/read.js.map +1 -1
- package/payload/platform/plugins/ledger/mcp/dist/lib/types.d.ts +14 -11
- package/payload/platform/plugins/ledger/mcp/dist/lib/types.d.ts.map +1 -1
- package/payload/platform/plugins/ledger/mcp/dist/lib/types.js.map +1 -1
- package/payload/platform/plugins/ledger/mcp/dist/lib/write-rejects.d.ts +12 -0
- package/payload/platform/plugins/ledger/mcp/dist/lib/write-rejects.d.ts.map +1 -0
- package/payload/platform/plugins/ledger/mcp/dist/lib/write-rejects.js +50 -0
- package/payload/platform/plugins/ledger/mcp/dist/lib/write-rejects.js.map +1 -0
- package/payload/platform/plugins/ledger/mcp/dist/tools/cash-record.d.ts +5 -5
- package/payload/platform/plugins/ledger/mcp/dist/tools/cash-record.d.ts.map +1 -1
- package/payload/platform/plugins/ledger/mcp/dist/tools/cash-record.js +11 -11
- package/payload/platform/plugins/ledger/mcp/dist/tools/cash-record.js.map +1 -1
- package/payload/platform/plugins/ledger/mcp/dist/tools/invoice-record.d.ts +7 -2
- package/payload/platform/plugins/ledger/mcp/dist/tools/invoice-record.d.ts.map +1 -1
- package/payload/platform/plugins/ledger/mcp/dist/tools/invoice-record.js +9 -3
- package/payload/platform/plugins/ledger/mcp/dist/tools/invoice-record.js.map +1 -1
- package/payload/platform/plugins/ledger/mcp/dist/tools/payment-record.d.ts +7 -0
- package/payload/platform/plugins/ledger/mcp/dist/tools/payment-record.d.ts.map +1 -1
- package/payload/platform/plugins/ledger/mcp/dist/tools/payment-record.js +3 -4
- package/payload/platform/plugins/ledger/mcp/dist/tools/payment-record.js.map +1 -1
- package/payload/platform/plugins/ledger/mcp/dist/tools/reconcile.d.ts +1 -1
- package/payload/platform/plugins/ledger/mcp/dist/tools/reconcile.d.ts.map +1 -1
- package/payload/platform/plugins/ledger/mcp/dist/tools/reconcile.js +4 -1
- package/payload/platform/plugins/ledger/mcp/dist/tools/reconcile.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/activity-rows.d.ts +27 -2
- package/payload/platform/services/claude-session-manager/dist/activity-rows.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/activity-rows.js +33 -2
- package/payload/platform/services/claude-session-manager/dist/activity-rows.js.map +1 -1
- 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 +1 -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/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +3 -0
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/templates/specialists/agents/personal-assistant.md +1 -1
- package/payload/server/{chunk-BSTIQX52.js → chunk-Y6V45R6M.js} +1 -12
- package/payload/server/maxy-edge.js +1 -1
- package/payload/server/public/activity.html +6 -6
- package/payload/server/public/assets/{AdminLoginScreens-BfamGqS7.js → AdminLoginScreens-B7TQZV8u.js} +1 -1
- package/payload/server/public/assets/{AdminShell-5nHOAzXO.js → AdminShell-DAuri4Yh.js} +1 -1
- package/payload/server/public/assets/{Checkbox-CD6aHwac.js → Checkbox-B1XhnhPk.js} +1 -1
- package/payload/server/public/assets/activity-kRwzxEKU.js +1 -0
- package/payload/server/public/assets/{admin-Cpcw_YE-.js → admin-BdcccSb5.js} +1 -1
- package/payload/server/public/assets/{browser-0znOwk1W.js → browser-B6woQ2qX.js} +1 -1
- package/payload/server/public/assets/{calendar-CykigHdg.js → calendar-CalCeg5T.js} +1 -1
- package/payload/server/public/assets/chat-9s2-L06v.js +1 -0
- package/payload/server/public/assets/chevron-left-C3ho8SnB.js +1 -0
- package/payload/server/public/assets/data-Cfr-7ERI.js +1 -0
- package/payload/server/public/assets/{graph-JV5_xF3V.js → graph-HeZaatSQ.js} +1 -1
- package/payload/server/public/assets/{graph-labels-DPYORjBH.js → graph-labels-4W7JuOgG.js} +1 -1
- package/payload/server/public/assets/{maximize-2-8vKr3h21.js → maximize-2-DKx-aTPb.js} +1 -1
- package/payload/server/public/assets/{operator-Liul1QEq.js → operator-DD9L2VJM.js} +1 -1
- package/payload/server/public/assets/{page-Cg7B1RQx.js → page-Ceado9Kr.js} +1 -1
- package/payload/server/public/assets/{page-CYRA87CB.js → page-tJxw0zNO.js} +1 -1
- package/payload/server/public/assets/{public-B4HMtpjt.js → public-BYFWxyeC.js} +1 -1
- package/payload/server/public/assets/{rotate-ccw-sz5IQSnr.js → rotate-ccw-DrzdV9h7.js} +1 -1
- package/payload/server/public/assets/{tasks-D9oYgEzs.js → tasks-DSwoxGzh.js} +1 -1
- package/payload/server/public/assets/{time-entry-format-CSuCexpM.js → time-entry-format-Bn79w6tq.js} +1 -1
- package/payload/server/public/assets/{triangle-alert-ClS81-CU.js → triangle-alert-CjDVh6r-.js} +1 -1
- package/payload/server/public/assets/{useCopyFeedback-Cn6YgeNx.js → useCopyFeedback-DXHkkJw-.js} +1 -1
- package/payload/server/public/assets/{useSelectionMode-BaD-ZZRk.js → useSelectionMode-D2H8jAhE.js} +1 -1
- package/payload/server/public/assets/{useSubAccountSwitcher-CNZL3PKn.css → useSubAccountSwitcher-Br7Z6Cpu.css} +1 -1
- package/payload/server/public/assets/{useVoiceRecorder-BOzHrDAU.js → useVoiceRecorder-zUvPe55I.js} +1 -1
- package/payload/server/public/browser.html +5 -5
- package/payload/server/public/calendar.html +6 -6
- package/payload/server/public/chat.html +12 -12
- package/payload/server/public/data.html +11 -11
- package/payload/server/public/graph.html +11 -11
- package/payload/server/public/index.html +14 -14
- package/payload/server/public/operator.html +14 -14
- package/payload/server/public/public.html +12 -12
- package/payload/server/public/tasks.html +5 -5
- package/payload/server/server.js +1414 -1324
- package/payload/server/public/assets/activity-Dj2nJzPw.js +0 -1
- package/payload/server/public/assets/chat-DIOJZ411.js +0 -1
- package/payload/server/public/assets/chevron-left-Cq7UvOI0.js +0 -1
- package/payload/server/public/assets/data-lHgqlhmu.js +0 -1
- /package/payload/server/public/assets/{useSubAccountSwitcher-CvjWJSGv.js → useSubAccountSwitcher-CUJpgkKI.js} +0 -0
|
@@ -20,7 +20,7 @@ import assert from "node:assert/strict";
|
|
|
20
20
|
import { buildClaudeSessionManagerUnitFile } from "../port-resolution.js";
|
|
21
21
|
const ACCOUNT_ID = "12345678-9abc-def0-1234-56789abcdef0";
|
|
22
22
|
const GOOGLE_ID = "1032860640027-p61l59fnog4mad5o7grginpaps9jnu8c.apps.googleusercontent.com";
|
|
23
|
-
const
|
|
23
|
+
const SECRET = "GOCSPX-test-value-not-real";
|
|
24
24
|
const OUTLOOK_ID = "0fa68fe2-8bec-4007-a0c7-776636fbcf97";
|
|
25
25
|
function build(opts) {
|
|
26
26
|
return buildClaudeSessionManagerUnitFile({
|
|
@@ -36,20 +36,22 @@ function build(opts) {
|
|
|
36
36
|
...opts,
|
|
37
37
|
});
|
|
38
38
|
}
|
|
39
|
-
test("
|
|
40
|
-
const unit = build({ googleClientId: GOOGLE_ID,
|
|
39
|
+
test("id and secret present stamps both Environment lines", () => {
|
|
40
|
+
const unit = build({ googleClientId: GOOGLE_ID, googleClientSecret: SECRET });
|
|
41
41
|
assert.ok(unit.includes(`Environment=GOOGLE_CLIENT_ID=${GOOGLE_ID}`));
|
|
42
|
-
assert.ok(unit.includes(`Environment=
|
|
42
|
+
assert.ok(unit.includes(`Environment=GOOGLE_CLIENT_SECRET=${SECRET}`));
|
|
43
|
+
// The hosted callback is withdrawn (Task 1791); nothing consumes a public base.
|
|
44
|
+
assert.ok(!unit.includes("GOOGLE_PUBLIC_BASE"));
|
|
43
45
|
});
|
|
44
|
-
test("a client id with no
|
|
46
|
+
test("a client id with no secret stamps NEITHER line", () => {
|
|
45
47
|
const unit = build({ googleClientId: GOOGLE_ID });
|
|
46
48
|
assert.ok(!unit.includes("GOOGLE_CLIENT_ID"), "a half-stamp must not ship");
|
|
47
|
-
assert.ok(!unit.includes("
|
|
49
|
+
assert.ok(!unit.includes("GOOGLE_CLIENT_SECRET"));
|
|
48
50
|
});
|
|
49
|
-
test("a
|
|
50
|
-
const unit = build({
|
|
51
|
+
test("a secret with no client id stamps NEITHER line", () => {
|
|
52
|
+
const unit = build({ googleClientSecret: SECRET });
|
|
51
53
|
assert.ok(!unit.includes("GOOGLE_CLIENT_ID"));
|
|
52
|
-
assert.ok(!unit.includes("
|
|
54
|
+
assert.ok(!unit.includes("GOOGLE_CLIENT_SECRET"));
|
|
53
55
|
});
|
|
54
56
|
test("a brand shipping no google app stamps neither line", () => {
|
|
55
57
|
const unit = build({});
|
|
@@ -59,7 +61,7 @@ test("a brand shipping no google app stamps neither line", () => {
|
|
|
59
61
|
test("google stamping leaves the outlook pair intact", () => {
|
|
60
62
|
const unit = build({
|
|
61
63
|
googleClientId: GOOGLE_ID,
|
|
62
|
-
|
|
64
|
+
googleClientSecret: SECRET,
|
|
63
65
|
outlookClientId: OUTLOOK_ID,
|
|
64
66
|
});
|
|
65
67
|
assert.ok(unit.includes(`Environment=OUTLOOK_CLIENT_ID=${OUTLOOK_ID}`));
|
package/dist/index.js
CHANGED
|
@@ -51,26 +51,6 @@ catch (err) {
|
|
|
51
51
|
}
|
|
52
52
|
const INSTALL_DIR = resolve(process.env.HOME ?? "/root", BRAND.installDir);
|
|
53
53
|
const PERSIST_DIR = resolve(process.env.HOME ?? "/root", BRAND.configDir);
|
|
54
|
-
// Task 1761 — GOOGLE_PUBLIC_BASE is the https origin Google redirects back to
|
|
55
|
-
// after consent. It is a property of THIS INSTALL's tunnel, not of the brand,
|
|
56
|
-
// so it is read from the same cloudflared/tunnel.state the uninstaller reads.
|
|
57
|
-
// Defined once here because BOTH env paths need it: the darwin .env mirror and
|
|
58
|
-
// the Linux systemd unit. Absent tunnel → unset, and google-account-register
|
|
59
|
-
// refuses with an actionable message rather than minting a consent URL whose
|
|
60
|
-
// redirect_uri Google will reject on its byte-for-byte comparison.
|
|
61
|
-
const googlePublicBase = (() => {
|
|
62
|
-
try {
|
|
63
|
-
const tunnelState = join(PERSIST_DIR, "cloudflared/tunnel.state");
|
|
64
|
-
if (!existsSync(tunnelState))
|
|
65
|
-
return "";
|
|
66
|
-
const parsed = JSON.parse(readFileSync(tunnelState, "utf-8"));
|
|
67
|
-
return parsed.domain ? `https://public.${parsed.domain}` : "";
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
// Unreadable tunnel state is not fatal to the install; the value stays unset.
|
|
71
|
-
return "";
|
|
72
|
-
}
|
|
73
|
-
})();
|
|
74
54
|
const LOG_DIR = join(PERSIST_DIR, "logs");
|
|
75
55
|
const LOG_FILE = join(LOG_DIR, `install-${new Date().toISOString().replace(/[:.]/g, "-")}.log`);
|
|
76
56
|
// every `claude plugin` shellout must read/write the per-brand
|
|
@@ -3741,13 +3721,10 @@ function installServiceDarwin() {
|
|
|
3741
3721
|
}
|
|
3742
3722
|
logFile(` [installer] op=outlook-client-id brand=${BRAND.hostname} stamped=${BRAND.outlookClientId ? "true" : "false"}`);
|
|
3743
3723
|
// Task 1761 — same upsert/strip discipline for the Google vendor app.
|
|
3744
|
-
|
|
3745
|
-
// it must match the authorized redirect URI on the vendor app byte-for-byte,
|
|
3746
|
-
// so it is derived from the brand's public host, not guessed at call time.
|
|
3747
|
-
if (BRAND.googleClientId) {
|
|
3724
|
+
if (BRAND.googleClientId && BRAND.googleClientSecret) {
|
|
3748
3725
|
for (const [key, value] of [
|
|
3749
3726
|
["GOOGLE_CLIENT_ID", BRAND.googleClientId],
|
|
3750
|
-
["
|
|
3727
|
+
["GOOGLE_CLIENT_SECRET", BRAND.googleClientSecret],
|
|
3751
3728
|
]) {
|
|
3752
3729
|
const re = new RegExp(`^${key}=.*$`, "m");
|
|
3753
3730
|
if (re.test(envContent))
|
|
@@ -3757,11 +3734,11 @@ function installServiceDarwin() {
|
|
|
3757
3734
|
}
|
|
3758
3735
|
}
|
|
3759
3736
|
else {
|
|
3760
|
-
for (const key of ["GOOGLE_CLIENT_ID", "
|
|
3737
|
+
for (const key of ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"]) {
|
|
3761
3738
|
envContent = envContent.replace(new RegExp(`^${key}=.*\\n?`, "m"), "");
|
|
3762
3739
|
}
|
|
3763
3740
|
}
|
|
3764
|
-
logFile(` [installer] op=google-client-id brand=${BRAND.hostname} stamped=${BRAND.googleClientId ? "true" : "false"}
|
|
3741
|
+
logFile(` [installer] op=google-client-id brand=${BRAND.hostname} stamped=${BRAND.googleClientId && BRAND.googleClientSecret ? "true" : "false"} clientId=${BRAND.googleClientId ? "set" : "unset"} secret=${BRAND.googleClientSecret ? "set" : "unset"}`);
|
|
3765
3742
|
writeFileSync(envPath, envContent);
|
|
3766
3743
|
logFile(` .env: DISPLAY_MODE=${DISPLAY_MODE}, EMBED_MODEL=${EMBED_MODEL}, EMBED_DIMENSIONS=${EMBED_DIMS}, NEO4J_URI=bolt://localhost:${NEO4J_PORT}, PORT=${PORT}, MAXY_UI_INTERNAL_PORT=${PORT} (darwin-collapsed), CLAUDE_SESSION_MANAGER_PORT=${BRAND.claudeSessionManagerPort}, HOSTNAME=0.0.0.0, ACCOUNT_ID=${installAccountId.slice(0, 8)}…, CLAUDE_CONFIG_DIR=${persistDir}/.claude, CLAUDE_BIN=${canonicalClaude ?? "unresolved(PATH)"}, NODE_BIN=${nodeBin}, PATH=${darwinServiceEnvPath}`);
|
|
3767
3744
|
}
|
|
@@ -4386,7 +4363,7 @@ function installService() {
|
|
|
4386
4363
|
cdpPort: CDP_PORT_BRAND,
|
|
4387
4364
|
outlookClientId: BRAND.outlookClientId,
|
|
4388
4365
|
googleClientId: BRAND.googleClientId,
|
|
4389
|
-
|
|
4366
|
+
googleClientSecret: BRAND.googleClientSecret,
|
|
4390
4367
|
});
|
|
4391
4368
|
writeFileSync(join(serviceDir, claudeSessionManagerUnitName), claudeSessionManagerUnit);
|
|
4392
4369
|
logFile(` ${claudeSessionManagerUnitName}: CLAUDE_SESSION_MANAGER_PORT=${CLAUDE_SESSION_MANAGER_PORT_BRAND} MAXY_UI_INTERNAL_PORT=${MAXY_UI_INTERNAL_PORT}`);
|
|
@@ -4397,7 +4374,7 @@ function installService() {
|
|
|
4397
4374
|
// Task 1761 — the same standing signal for the Google vendor app. Both parts
|
|
4398
4375
|
// are reported because a client id without a public base stamps NEITHER: a
|
|
4399
4376
|
// consent URL built without the redirect origin is one Google rejects.
|
|
4400
|
-
logFile(` [installer] op=google-client-id brand=${BRAND.hostname} stamped=${BRAND.googleClientId &&
|
|
4377
|
+
logFile(` [installer] op=google-client-id brand=${BRAND.hostname} stamped=${BRAND.googleClientId && BRAND.googleClientSecret ? "true" : "false"} clientId=${BRAND.googleClientId ? "set" : "unset"} secret=${BRAND.googleClientSecret ? "set" : "unset"}`);
|
|
4401
4378
|
// Task 250 — write the brand-agnostic `claude-ptys.slice` unit. Every
|
|
4402
4379
|
// brand's session manager drops its per-spawn scope units under this
|
|
4403
4380
|
// slice. Writing it once per install is idempotent (the content is
|
package/dist/port-resolution.js
CHANGED
|
@@ -135,11 +135,11 @@ export function buildClaudeSessionManagerUnitFile(o) {
|
|
|
135
135
|
const outlookEnv = o.outlookClientId
|
|
136
136
|
? `\nEnvironment=OUTLOOK_CLIENT_ID=${o.outlookClientId}\nEnvironment=OUTLOOK_TENANT_ID=common`
|
|
137
137
|
: "";
|
|
138
|
-
// Task
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
const googleEnv = o.googleClientId && o.
|
|
142
|
-
? `\nEnvironment=GOOGLE_CLIENT_ID=${o.googleClientId}\nEnvironment=
|
|
138
|
+
// Task 1791 — id and secret land together. A client id without its secret
|
|
139
|
+
// produces a consent URL the user can complete but an exchange that cannot,
|
|
140
|
+
// which fails later and less legibly than not offering the flow at all.
|
|
141
|
+
const googleEnv = o.googleClientId && o.googleClientSecret
|
|
142
|
+
? `\nEnvironment=GOOGLE_CLIENT_ID=${o.googleClientId}\nEnvironment=GOOGLE_CLIENT_SECRET=${o.googleClientSecret}`
|
|
143
143
|
: "";
|
|
144
144
|
return `[Unit]
|
|
145
145
|
Description=${o.productName} Claude Session Manager
|
package/package.json
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"claudeSessionManagerPort": 19400,
|
|
17
17
|
"outlookClientId": "",
|
|
18
18
|
"googleClientId": "",
|
|
19
|
+
"googleClientSecret": "",
|
|
19
20
|
"commercialMode": false,
|
|
20
21
|
"vertical": "schema-knowledge-work",
|
|
21
22
|
"shipsPremiumBundles": ["venture-studio", "writer-craft", "teaching", "management-consulting"],
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Ledger write path fix — implementation plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax.
|
|
4
|
+
|
|
5
|
+
**Goal:** Every ledger write tool successfully creates its node, because relationships carry the element id `writeNodeWithEdges` actually requires.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Delete the ledger's invented `GraphRelationship` and the cast that forced it past the type checker; import the real type so the shape becomes a compile-time constraint. Resolve each relationship target to an element id before writing. Add a census counter so a dead write path can no longer read as a healthy empty ledger.
|
|
8
|
+
|
|
9
|
+
**Spec:** [`.tasks/1788-...md`](../../../../.tasks/1788-ledger-write-tools-pass-an-invented-relationship-shape-so-every-write-is-rejected.md). No separate design doc; the task carries the decisions.
|
|
10
|
+
|
|
11
|
+
## Global constraints
|
|
12
|
+
|
|
13
|
+
- Node 22. Never `npm install` in this worktree; deps are symlinked from the parent.
|
|
14
|
+
- Build and test from `maxy-code/platform`.
|
|
15
|
+
- The four write tools must not be tested with a stubbed `write`. That stub is why the bug shipped.
|
|
16
|
+
- Do not touch `lib/ledger-core`'s arithmetic (money, balance, ageing) — only `reconcile.ts` gains a counter.
|
|
17
|
+
- Baseline before changes: 56 ledger tests, 508 memory-plugin tests.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
### Task 1: Make the wrong shape a compile error
|
|
22
|
+
|
|
23
|
+
**Files:** `plugins/ledger/mcp/src/lib/types.ts`, `plugins/ledger/mcp/src/index.ts`
|
|
24
|
+
|
|
25
|
+
**Produces:** the real `GraphRelationship { type, direction, targetNodeId }` in scope for every tool.
|
|
26
|
+
|
|
27
|
+
- [ ] **Step 1: Prove the current code is wrong at the type level.** In `types.ts`, delete the local `GraphRelationship` and re-export the real one:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
export type { GraphRelationship } from "../../../../../lib/graph-write/dist/index.js";
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- [ ] **Step 2: Delete the cast in `index.ts`.** Replace the `writeNodeWithEdges as unknown as (...)` wrapper with a direct reference. Run `tsc -p plugins/ledger/mcp/tsconfig.json` and record every error. Expect failures in `invoice-record.ts`, `payment-record.ts`, `cash-record.ts` on `targetLabel`/`targetProps`. **Those errors are the bug** — they are what the cast was hiding.
|
|
34
|
+
|
|
35
|
+
- [ ] **Step 3: Fix the attribution while here.** `createdBy` becomes `{ agent: agentSlug, session: sessionId, tool, source: "ledger" }` with no `?? ""`, so `graph-write:254`'s `agent ?? source ?? "unknown"` chain can work.
|
|
36
|
+
|
|
37
|
+
- [ ] **Step 4: Commit** once the remaining errors are only the three tool files.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
### Task 2: Resolve targets to element ids
|
|
42
|
+
|
|
43
|
+
**Files:** `plugins/ledger/mcp/src/lib/read.ts`, `tools/{invoice-record,payment-record,cash-record}.ts`, `index.ts`, `PLUGIN.md`
|
|
44
|
+
|
|
45
|
+
**Consumes:** the real type from Task 1.
|
|
46
|
+
|
|
47
|
+
- [ ] **Step 1: `readInvoice` returns the node's id.** Add `nodeId: string` to `InvoiceRead`; add `elementId(i) AS nodeId` to the Cypher and map it.
|
|
48
|
+
|
|
49
|
+
- [ ] **Step 2: Write the failing tests first.** For each of the three tools, assert the relationship carries a `targetNodeId` equal to the id supplied or resolved, and that no relationship carries `targetLabel`:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
it('targets the counterparty by element id', async () => {
|
|
53
|
+
const write = okWrite()
|
|
54
|
+
await invoiceRecord({ ...base, counterpartyNodeId: '4:abc:12', session: {} as never, write })
|
|
55
|
+
const rel = write.mock.calls[0]![0].relationships[0]
|
|
56
|
+
expect(rel.targetNodeId).toBe('4:abc:12')
|
|
57
|
+
expect(rel).not.toHaveProperty('targetLabel')
|
|
58
|
+
})
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- [ ] **Step 3: Implement.** `invoice-record` and `cash-record` take a required `counterpartyNodeId`. `payment-record` and `credit-record` use `invoice.nodeId` from the read. Each `zod` schema gains the field with a description telling the agent to look the organisation up first.
|
|
62
|
+
|
|
63
|
+
- [ ] **Step 4: Reject a blank id** before writing, with `op=reject reason=missing-counterparty`, so the failure names the cause rather than surfacing as `unresolved-target`.
|
|
64
|
+
|
|
65
|
+
- [ ] **Step 5: `tsc` clean, ledger tests green, commit.**
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
### Task 3: The test that would have caught it
|
|
70
|
+
|
|
71
|
+
**Files:** `plugins/ledger/mcp/src/tools/__tests__/write-contract.test.ts` (new)
|
|
72
|
+
|
|
73
|
+
One integration test per write tool that calls the **real** `writeNodeWithEdges` against a fake `Session` (`run` + `close`), asserting the issued Cypher carries the resolved target id and that no `unresolved-target` rejection occurs.
|
|
74
|
+
|
|
75
|
+
- [ ] **Step 1: Write it against the current code and watch it fail** if Tasks 1 and 2 were somehow incomplete.
|
|
76
|
+
- [ ] **Step 2: Confirm it passes.**
|
|
77
|
+
- [ ] **Step 3: Prove it is load-bearing.** Temporarily reintroduce `targetLabel` in one tool; the build must fail. Revert.
|
|
78
|
+
- [ ] **Step 4: Commit.**
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
### Task 4: Make a dead write path visible
|
|
83
|
+
|
|
84
|
+
**Files:** `lib/ledger-core/src/reconcile.ts`, `plugins/ledger/mcp/src/lib/read.ts`, `tools/reconcile.ts`, `ui/app/lib/ledger-census.ts`, tests
|
|
85
|
+
|
|
86
|
+
`[ledger-census]` currently prints `invoices=0 payments=0` identically whether the ledger is healthy-and-empty or wholly broken. That is why this bug ran all day unseen.
|
|
87
|
+
|
|
88
|
+
- [ ] **Step 1: Failing test.** `reconcileLedger` alarms when `writeRejects24h > 0`.
|
|
89
|
+
- [ ] **Step 2: Implement.** Add `writeRejects24h` to `LedgerCensusRows`, `LedgerCensusFinding`, the alarm condition, and `formatCensusLine`.
|
|
90
|
+
- [ ] **Step 3: Source the count.** Both the census and the reconcile tool count `[graph-write] reject` lines in `<LOG_DIR>/mcp-ledger-*.log` with a timestamp inside the trailing 24 hours.
|
|
91
|
+
- [ ] **Step 4: Tests green, commit.**
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
### Task 5: Docs
|
|
96
|
+
|
|
97
|
+
**Files:** `plugins/ledger/PLUGIN.md`, `plugins/docs/references/ledger.md`
|
|
98
|
+
|
|
99
|
+
State that the counterparty must be looked up first and passed by id, that the ledger never creates organisations, and what `writeRejects24h` means. Keep the shipped reference free of em-dashes.
|
|
100
|
+
|
|
101
|
+
- [ ] Commit.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Self-review
|
|
106
|
+
|
|
107
|
+
**Spec coverage.** Type import and cast removal: Task 1. Attribution: Task 1 step 3. Element-id resolution for all four write tools: Task 2. The banned-stub integration test: Task 3. Census counter: Task 4. Docs: Task 5. Out-of-scope items (duplicate-org matching, `:InvoiceLine`, read tools, ledger-core arithmetic) are untouched.
|
|
108
|
+
|
|
109
|
+
**Risk.** Task 2 changes the tool signatures, so any agent prompt or skill that calls `ledger-invoice-record` with a bare counterparty name breaks. Nothing calls these tools yet except live agent turns, and they have never succeeded, so there is no working caller to regress.
|
|
@@ -31,6 +31,12 @@ export interface LedgerCensusRows {
|
|
|
31
31
|
storedBalanceProps: number;
|
|
32
32
|
paymentKeys: readonly PaymentKey[];
|
|
33
33
|
declaredNotLive: readonly string[];
|
|
34
|
+
/** Count of `[graph-write] reject` lines from the ledger's MCP logs in the
|
|
35
|
+
* trailing 24 hours. Without it the census cannot tell a healthy empty
|
|
36
|
+
* ledger from one whose write path is wholly dead: both print
|
|
37
|
+
* `invoices=0 payments=0`. Task 1788, after exactly that went unseen for a
|
|
38
|
+
* day. */
|
|
39
|
+
writeRejects24h: number;
|
|
34
40
|
}
|
|
35
41
|
export interface LedgerCensusFinding {
|
|
36
42
|
invoices: number;
|
|
@@ -39,6 +45,7 @@ export interface LedgerCensusFinding {
|
|
|
39
45
|
storedBalanceProps: number;
|
|
40
46
|
suspectDuplicates: number;
|
|
41
47
|
declaredNotLive: readonly string[];
|
|
48
|
+
writeRejects24h: number;
|
|
42
49
|
alarm: boolean;
|
|
43
50
|
}
|
|
44
51
|
export declare function reconcileLedger(rows: LedgerCensusRows, _now: number): LedgerCensusFinding;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reconcile.d.ts","sourceRoot":"","sources":["../src/reconcile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH;;;;mEAImE;AACnE,eAAO,MAAM,kBAAkB,EAAE,SAAS,MAAM,EAO/C,CAAA;AAED,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,cAAc,EAAE,MAAM,CAAA;IACtB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,WAAW,EAAE,SAAS,UAAU,EAAE,CAAA;IAClC,eAAe,EAAE,SAAS,MAAM,EAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"reconcile.d.ts","sourceRoot":"","sources":["../src/reconcile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH;;;;mEAImE;AACnE,eAAO,MAAM,kBAAkB,EAAE,SAAS,MAAM,EAO/C,CAAA;AAED,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,cAAc,EAAE,MAAM,CAAA;IACtB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,WAAW,EAAE,SAAS,UAAU,EAAE,CAAA;IAClC,eAAe,EAAE,SAAS,MAAM,EAAE,CAAA;IAClC;;;;eAIW;IACX,eAAe,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,cAAc,EAAE,MAAM,CAAA;IACtB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,iBAAiB,EAAE,MAAM,CAAA;IACzB,eAAe,EAAE,SAAS,MAAM,EAAE,CAAA;IAClC,eAAe,EAAE,MAAM,CAAA;IACvB,KAAK,EAAE,OAAO,CAAA;CACf;AAwBD,wBAAgB,eAAe,CAAC,IAAI,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,GAAG,mBAAmB,CAmBzF;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,mBAAmB,GAAG,MAAM,CAQ/D"}
|
|
@@ -52,7 +52,8 @@ export function reconcileLedger(rows, _now) {
|
|
|
52
52
|
const alarm = rows.orphanPayments > 0 ||
|
|
53
53
|
rows.storedBalanceProps > 0 ||
|
|
54
54
|
suspectDuplicates > 0 ||
|
|
55
|
-
rows.declaredNotLive.length > 0
|
|
55
|
+
rows.declaredNotLive.length > 0 ||
|
|
56
|
+
rows.writeRejects24h > 0;
|
|
56
57
|
return {
|
|
57
58
|
invoices: rows.invoices,
|
|
58
59
|
payments: rows.payments,
|
|
@@ -60,6 +61,7 @@ export function reconcileLedger(rows, _now) {
|
|
|
60
61
|
storedBalanceProps: rows.storedBalanceProps,
|
|
61
62
|
suspectDuplicates,
|
|
62
63
|
declaredNotLive: rows.declaredNotLive,
|
|
64
|
+
writeRejects24h: rows.writeRejects24h,
|
|
63
65
|
alarm,
|
|
64
66
|
};
|
|
65
67
|
}
|
|
@@ -72,6 +74,7 @@ export function formatCensusLine(f) {
|
|
|
72
74
|
return (`[ledger-census] invoices=${f.invoices} payments=${f.payments} ` +
|
|
73
75
|
`orphanPayments=${f.orphanPayments} storedBalanceProps=${f.storedBalanceProps} ` +
|
|
74
76
|
`suspectDuplicates=${f.suspectDuplicates} ` +
|
|
75
|
-
`declaredNotLive=${f.declaredNotLive.length === 0 ? "none" : f.declaredNotLive.join(",")}`
|
|
77
|
+
`declaredNotLive=${f.declaredNotLive.length === 0 ? "none" : f.declaredNotLive.join(",")} ` +
|
|
78
|
+
`writeRejects24h=${f.writeRejects24h}`);
|
|
76
79
|
}
|
|
77
80
|
//# sourceMappingURL=reconcile.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reconcile.js","sourceRoot":"","sources":["../src/reconcile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH;;;;mEAImE;AACnE,MAAM,CAAC,MAAM,kBAAkB,GAAsB;IACnD,qCAAqC;IACrC,mCAAmC;IACnC,oBAAoB;IACpB,sBAAsB;IACtB,yBAAyB;IACzB,gBAAgB;CACjB,CAAA;
|
|
1
|
+
{"version":3,"file":"reconcile.js","sourceRoot":"","sources":["../src/reconcile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH;;;;mEAImE;AACnE,MAAM,CAAC,MAAM,kBAAkB,GAAsB;IACnD,qCAAqC;IACrC,mCAAmC;IACnC,oBAAoB;IACpB,sBAAsB;IACtB,yBAAyB;IACzB,gBAAgB;CACjB,CAAA;AAmCD;;;;;;;;GAQG;AACH,SAAS,sBAAsB,CAAC,IAA2B;IACzD,MAAM,MAAM,GAAG,IAAI,GAAG,EAAuB,CAAA;IAC7C,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;QACrB,MAAM,SAAS,GAAG,GAAG,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,IAAI,EAAE,CAAA;QACxD,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,IAAI,GAAG,EAAU,CAAA;QACtD,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;QACpB,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,GAAG,CAAC,CAAA;IAC5B,CAAC;IACD,IAAI,KAAK,GAAG,CAAC,CAAA;IACb,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,MAAM,EAAE;QAAE,KAAK,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,GAAG,CAAC,CAAC,CAAA;IACrE,OAAO,KAAK,CAAA;AACd,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,IAAsB,EAAE,IAAY;IAClE,MAAM,iBAAiB,GAAG,sBAAsB,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;IAClE,MAAM,KAAK,GACT,IAAI,CAAC,cAAc,GAAG,CAAC;QACvB,IAAI,CAAC,kBAAkB,GAAG,CAAC;QAC3B,iBAAiB,GAAG,CAAC;QACrB,IAAI,CAAC,eAAe,CAAC,MAAM,GAAG,CAAC;QAC/B,IAAI,CAAC,eAAe,GAAG,CAAC,CAAA;IAE1B,OAAO;QACL,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,cAAc,EAAE,IAAI,CAAC,cAAc;QACnC,kBAAkB,EAAE,IAAI,CAAC,kBAAkB;QAC3C,iBAAiB;QACjB,eAAe,EAAE,IAAI,CAAC,eAAe;QACrC,eAAe,EAAE,IAAI,CAAC,eAAe;QACrC,KAAK;KACN,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,CAAsB;IACrD,OAAO,CACL,4BAA4B,CAAC,CAAC,QAAQ,aAAa,CAAC,CAAC,QAAQ,GAAG;QAChE,kBAAkB,CAAC,CAAC,cAAc,uBAAuB,CAAC,CAAC,kBAAkB,GAAG;QAChF,qBAAqB,CAAC,CAAC,iBAAiB,GAAG;QAC3C,mBAAmB,CAAC,CAAC,eAAe,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG;QAC3F,mBAAmB,CAAC,CAAC,eAAe,EAAE,CACvC,CAAA;AACH,CAAC"}
|
|
@@ -8,6 +8,7 @@ const clean = {
|
|
|
8
8
|
storedBalanceProps: 0,
|
|
9
9
|
paymentKeys: [],
|
|
10
10
|
declaredNotLive: [],
|
|
11
|
+
writeRejects24h: 0,
|
|
11
12
|
}
|
|
12
13
|
const NOW = Date.parse('2026-07-18T00:00:00Z')
|
|
13
14
|
|
|
@@ -84,4 +85,23 @@ describe('reconcileLedger', () => {
|
|
|
84
85
|
expect(LEDGER_INDEX_NAMES).toHaveLength(6)
|
|
85
86
|
expect(LEDGER_INDEX_NAMES).toContain('cash_entry_account_entryid_unique')
|
|
86
87
|
})
|
|
88
|
+
|
|
89
|
+
it('alarms when the write path is rejecting', () => {
|
|
90
|
+
// The case this counter exists for: on 2026-07-18 every ledger write was
|
|
91
|
+
// rejected all day while the census printed invoices=0 payments=0, which
|
|
92
|
+
// is byte-identical to a healthy ledger nobody has used yet.
|
|
93
|
+
const f = reconcileLedger({ ...clean, writeRejects24h: 4 }, NOW)
|
|
94
|
+
expect(f.alarm).toBe(true)
|
|
95
|
+
expect(formatCensusLine(f)).toContain('writeRejects24h=4')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('a healthy empty ledger is distinguishable from a broken one', () => {
|
|
99
|
+
const empty = { ...clean, invoices: 0, payments: 0 }
|
|
100
|
+
const broken = { ...empty, writeRejects24h: 4 }
|
|
101
|
+
expect(formatCensusLine(reconcileLedger(empty, NOW))).not.toEqual(
|
|
102
|
+
formatCensusLine(reconcileLedger(broken, NOW)),
|
|
103
|
+
)
|
|
104
|
+
expect(reconcileLedger(empty, NOW).alarm).toBe(false)
|
|
105
|
+
expect(reconcileLedger(broken, NOW).alarm).toBe(true)
|
|
106
|
+
})
|
|
87
107
|
})
|
|
@@ -41,6 +41,12 @@ export interface LedgerCensusRows {
|
|
|
41
41
|
storedBalanceProps: number
|
|
42
42
|
paymentKeys: readonly PaymentKey[]
|
|
43
43
|
declaredNotLive: readonly string[]
|
|
44
|
+
/** Count of `[graph-write] reject` lines from the ledger's MCP logs in the
|
|
45
|
+
* trailing 24 hours. Without it the census cannot tell a healthy empty
|
|
46
|
+
* ledger from one whose write path is wholly dead: both print
|
|
47
|
+
* `invoices=0 payments=0`. Task 1788, after exactly that went unseen for a
|
|
48
|
+
* day. */
|
|
49
|
+
writeRejects24h: number
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
export interface LedgerCensusFinding {
|
|
@@ -50,6 +56,7 @@ export interface LedgerCensusFinding {
|
|
|
50
56
|
storedBalanceProps: number
|
|
51
57
|
suspectDuplicates: number
|
|
52
58
|
declaredNotLive: readonly string[]
|
|
59
|
+
writeRejects24h: number
|
|
53
60
|
alarm: boolean
|
|
54
61
|
}
|
|
55
62
|
|
|
@@ -81,7 +88,8 @@ export function reconcileLedger(rows: LedgerCensusRows, _now: number): LedgerCen
|
|
|
81
88
|
rows.orphanPayments > 0 ||
|
|
82
89
|
rows.storedBalanceProps > 0 ||
|
|
83
90
|
suspectDuplicates > 0 ||
|
|
84
|
-
rows.declaredNotLive.length > 0
|
|
91
|
+
rows.declaredNotLive.length > 0 ||
|
|
92
|
+
rows.writeRejects24h > 0
|
|
85
93
|
|
|
86
94
|
return {
|
|
87
95
|
invoices: rows.invoices,
|
|
@@ -90,6 +98,7 @@ export function reconcileLedger(rows: LedgerCensusRows, _now: number): LedgerCen
|
|
|
90
98
|
storedBalanceProps: rows.storedBalanceProps,
|
|
91
99
|
suspectDuplicates,
|
|
92
100
|
declaredNotLive: rows.declaredNotLive,
|
|
101
|
+
writeRejects24h: rows.writeRejects24h,
|
|
93
102
|
alarm,
|
|
94
103
|
}
|
|
95
104
|
}
|
|
@@ -104,6 +113,7 @@ export function formatCensusLine(f: LedgerCensusFinding): string {
|
|
|
104
113
|
`[ledger-census] invoices=${f.invoices} payments=${f.payments} ` +
|
|
105
114
|
`orphanPayments=${f.orphanPayments} storedBalanceProps=${f.storedBalanceProps} ` +
|
|
106
115
|
`suspectDuplicates=${f.suspectDuplicates} ` +
|
|
107
|
-
`declaredNotLive=${f.declaredNotLive.length === 0 ? "none" : f.declaredNotLive.join(",")}`
|
|
116
|
+
`declaredNotLive=${f.declaredNotLive.length === 0 ? "none" : f.declaredNotLive.join(",")} ` +
|
|
117
|
+
`writeRejects24h=${f.writeRejects24h}`
|
|
108
118
|
)
|
|
109
119
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: platform-architecture
|
|
3
3
|
description: Use when grounding any documented-surface claim about what Maxy ships — plugins, skills, specialists, install/deploy flows, internals. This is the install catalogue, not evidence of what is enabled on the current account. For install state on this account, call `capabilities-here`; for documented surface, cite the `Source:` URL inline.
|
|
4
|
-
content-hash: sha256:
|
|
4
|
+
content-hash: sha256:c15214d349505b6337668b0362b849c9c5301fd25304800cf7384888a6a21435
|
|
5
5
|
brand: maxy-code
|
|
6
6
|
product-name: Maxy
|
|
7
7
|
---
|
|
@@ -636,6 +636,12 @@ If you need statutory accounts, this is not that. It is the answer to "who owes
|
|
|
636
636
|
|
|
637
637
|
**Ask for a statement.** Everything owed to you and everything you owe, sorted into 0-30, 31-60, 61-90 and 90+ days overdue, plus net cash.
|
|
638
638
|
|
|
639
|
+
## You look the customer up first
|
|
640
|
+
|
|
641
|
+
When you record an invoice or a cash movement, the agent finds the company or person in your records first, then attaches the money to that exact record. It will not create a new company for you, and the name that appears on the invoice is taken from the record it attached to, so the two can never disagree.
|
|
642
|
+
|
|
643
|
+
That is on purpose. Deciding whether "Beagle Direct Ltd" is the same firm as an existing "Beagle Direct" is a judgement about your records, not a bookkeeping step, and getting it wrong quietly splits one customer's payment history across two entries.
|
|
644
|
+
|
|
639
645
|
## Three behaviours worth knowing about
|
|
640
646
|
|
|
641
647
|
**What is outstanding is never stored.** It is worked out from the payments each time you ask. This means the figure cannot fall out of step with the payments behind it. If it were stored, a payment recorded by any route that forgot to update it would leave the invoice quietly reporting a wrong number, and nothing would fail.
|
|
@@ -648,7 +654,9 @@ If you need statutory accounts, this is not that. It is the answer to "who owes
|
|
|
648
654
|
|
|
649
655
|
Some ways this can go wrong announce themselves. Others do not: a payment that ends up attached to no invoice, a stale balance field reappearing, or the same payment recorded twice under two different ids. None of those produce an error at the time, because nothing goes wrong at the moment they happen.
|
|
650
656
|
|
|
651
|
-
So there is a check that runs by itself every fifteen minutes and writes one line to the log beginning `[ledger-census]`. When it is healthy the counters are all zero. When something is wrong the line ends with `alarm=true` and says which of
|
|
657
|
+
So there is a check that runs by itself every fifteen minutes and writes one line to the log beginning `[ledger-census]`. When it is healthy the counters are all zero. When something is wrong the line ends with `alarm=true` and says which of them it found.
|
|
658
|
+
|
|
659
|
+
One of those counters is the number of writes that were refused in the last day. It is there because of a real failure: for a whole day every attempt to record money was refused, and the check still read as healthy, because a ledger nobody has used and a ledger that cannot be written to both show nothing.
|
|
652
660
|
|
|
653
661
|
You can also ask for that check on demand at any time, and it reports the same thing in plain words.
|
|
654
662
|
|
|
@@ -2218,9 +2226,16 @@ force Google's annual third-party security assessment. Calendar is only a
|
|
|
2218
2226
|
## Connecting an account
|
|
2219
2227
|
|
|
2220
2228
|
`google-account-register` returns a consent URL. Relay it to the user: they open
|
|
2221
|
-
it on any device, sign in, and approve.
|
|
2222
|
-
|
|
2223
|
-
|
|
2229
|
+
it on any device, sign in, and approve.
|
|
2230
|
+
|
|
2231
|
+
Their browser then lands on a page that **fails to load**. That is expected, not
|
|
2232
|
+
a fault — the vendor app is a Desktop client, so Google redirects to a loopback
|
|
2233
|
+
address on the user's own device where nothing is listening. The address bar
|
|
2234
|
+
carries the code. Ask them to copy it and pass it to
|
|
2235
|
+
`google-account-register-submit`, which completes the connection.
|
|
2236
|
+
|
|
2237
|
+
If they paste the whole `code=...` fragment, or a value containing `%2F`, the
|
|
2238
|
+
tool handles it. `google-account-register-poll` reports the outcome at any point.
|
|
2224
2239
|
|
|
2225
2240
|
The register tool does not block and does not wait. A second register while one
|
|
2226
2241
|
is already live is refused rather than overwriting it; that refusal names how
|
|
@@ -4891,6 +4906,10 @@ To deliberately disconnect the wrong account, use the header menu's **Disconnect
|
|
|
4891
4906
|
3. `grep '\[auth-health\]' ~/.${brand}/logs/server.log | tail -n 5` — the heartbeat fires every five minutes. `status=dead expiresIn=...` means the refresh token is gone; only a re-login fixes it. `status=ok` heartbeats with no spawns in between mean the credentials file is healthy and the failure lives elsewhere.
|
|
4892
4907
|
4. The spawn-failure surface now carries `reason=auth-refresh-failed` (with `authStatus` in the JSON body) instead of generic `pid-file-timeout` whenever the credentials file is in `dead` or `expired` state at the moment of failure — visible in `grep '\[spawn-failed\]'` on server.log.
|
|
4893
4908
|
|
|
4909
|
+
**A WhatsApp message you can see on your phone never appears in the app.** First check whether it was ever stored, rather than assuming it routed elsewhere: ask Maxy for the most recent ingest reconcile, or `grep '\[wa-ingest-audit\]' ~/.${brand}/logs/server.log | tail -1`. A healthy line reads `op=reconcile … unpersisted=0 inFlight=0 guard=installed`. If `unpersisted` is above zero, the `ids=` field names the exact message ids that arrived at the wire and were never written to the store — those messages are genuinely lost to the platform, not hidden in another workspace or sub-account, and they cannot be re-fetched because WhatsApp only delivers an acked message once. Forward the message from your phone to get it back in; it re-enters as a new message through the normal path.
|
|
4910
|
+
|
|
4911
|
+
The audit runs every 15 minutes and holds a stanza for 10 minutes before judging it, so a message can take up to 25 minutes to be reported. The wait is deliberate: a large batch of photos arriving at once can take several minutes to finish storing, and a false "a message was lost" alarm would make the real one easy to ignore. Three classes are resolved rather than reported, each counted separately: `skippedStatus` (status broadcasts, never stored), `skippedReaction` (reactions, stored under the message they react to rather than under their own id — a consequence being that a lost *reaction* is not detectable), and `declined` (messages carrying neither text nor a file, such as a shared contact card, a poll, or a delete-for-everyone, which are dropped by design). `oldestUnpersistedMin` tells you whether a reported loss is new or one you have already seen, since the same id is re-reported every 15 minutes for a day. `guard=absent` or `inFlight` stuck above zero on an idle install are both faults worth surfacing to support: the first means the media-download crash guard failed to install at boot, the second means its counter leaked. `contained` counts crashes the guard prevented — a number that climbs while media traffic stays flat is also worth reporting.
|
|
4912
|
+
|
|
4894
4913
|
---
|
|
4895
4914
|
|
|
4896
4915
|
## Memory Not Working
|
|
@@ -12,9 +12,16 @@ force Google's annual third-party security assessment. Calendar is only a
|
|
|
12
12
|
## Connecting an account
|
|
13
13
|
|
|
14
14
|
`google-account-register` returns a consent URL. Relay it to the user: they open
|
|
15
|
-
it on any device, sign in, and approve.
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
it on any device, sign in, and approve.
|
|
16
|
+
|
|
17
|
+
Their browser then lands on a page that **fails to load**. That is expected, not
|
|
18
|
+
a fault — the vendor app is a Desktop client, so Google redirects to a loopback
|
|
19
|
+
address on the user's own device where nothing is listening. The address bar
|
|
20
|
+
carries the code. Ask them to copy it and pass it to
|
|
21
|
+
`google-account-register-submit`, which completes the connection.
|
|
22
|
+
|
|
23
|
+
If they paste the whole `code=...` fragment, or a value containing `%2F`, the
|
|
24
|
+
tool handles it. `google-account-register-poll` reports the outcome at any point.
|
|
18
25
|
|
|
19
26
|
The register tool does not block and does not wait. A second register while one
|
|
20
27
|
is already live is refused rather than overwriting it; that refusal names how
|
|
@@ -24,6 +24,12 @@ If you need statutory accounts, this is not that. It is the answer to "who owes
|
|
|
24
24
|
|
|
25
25
|
**Ask for a statement.** Everything owed to you and everything you owe, sorted into 0-30, 31-60, 61-90 and 90+ days overdue, plus net cash.
|
|
26
26
|
|
|
27
|
+
## You look the customer up first
|
|
28
|
+
|
|
29
|
+
When you record an invoice or a cash movement, the agent finds the company or person in your records first, then attaches the money to that exact record. It will not create a new company for you, and the name that appears on the invoice is taken from the record it attached to, so the two can never disagree.
|
|
30
|
+
|
|
31
|
+
That is on purpose. Deciding whether "Beagle Direct Ltd" is the same firm as an existing "Beagle Direct" is a judgement about your records, not a bookkeeping step, and getting it wrong quietly splits one customer's payment history across two entries.
|
|
32
|
+
|
|
27
33
|
## Three behaviours worth knowing about
|
|
28
34
|
|
|
29
35
|
**What is outstanding is never stored.** It is worked out from the payments each time you ask. This means the figure cannot fall out of step with the payments behind it. If it were stored, a payment recorded by any route that forgot to update it would leave the invoice quietly reporting a wrong number, and nothing would fail.
|
|
@@ -36,7 +42,9 @@ If you need statutory accounts, this is not that. It is the answer to "who owes
|
|
|
36
42
|
|
|
37
43
|
Some ways this can go wrong announce themselves. Others do not: a payment that ends up attached to no invoice, a stale balance field reappearing, or the same payment recorded twice under two different ids. None of those produce an error at the time, because nothing goes wrong at the moment they happen.
|
|
38
44
|
|
|
39
|
-
So there is a check that runs by itself every fifteen minutes and writes one line to the log beginning `[ledger-census]`. When it is healthy the counters are all zero. When something is wrong the line ends with `alarm=true` and says which of
|
|
45
|
+
So there is a check that runs by itself every fifteen minutes and writes one line to the log beginning `[ledger-census]`. When it is healthy the counters are all zero. When something is wrong the line ends with `alarm=true` and says which of them it found.
|
|
46
|
+
|
|
47
|
+
One of those counters is the number of writes that were refused in the last day. It is there because of a real failure: for a whole day every attempt to record money was refused, and the check still read as healthy, because a ledger nobody has used and a ledger that cannot be written to both show nothing.
|
|
40
48
|
|
|
41
49
|
You can also ask for that check on demand at any time, and it reports the same thing in plain words.
|
|
42
50
|
|
|
@@ -147,6 +147,10 @@ To deliberately disconnect the wrong account, use the header menu's **Disconnect
|
|
|
147
147
|
3. `grep '\[auth-health\]' ~/.${brand}/logs/server.log | tail -n 5` — the heartbeat fires every five minutes. `status=dead expiresIn=...` means the refresh token is gone; only a re-login fixes it. `status=ok` heartbeats with no spawns in between mean the credentials file is healthy and the failure lives elsewhere.
|
|
148
148
|
4. The spawn-failure surface now carries `reason=auth-refresh-failed` (with `authStatus` in the JSON body) instead of generic `pid-file-timeout` whenever the credentials file is in `dead` or `expired` state at the moment of failure — visible in `grep '\[spawn-failed\]'` on server.log.
|
|
149
149
|
|
|
150
|
+
**A WhatsApp message you can see on your phone never appears in the app.** First check whether it was ever stored, rather than assuming it routed elsewhere: ask {{productName}} for the most recent ingest reconcile, or `grep '\[wa-ingest-audit\]' ~/.${brand}/logs/server.log | tail -1`. A healthy line reads `op=reconcile … unpersisted=0 inFlight=0 guard=installed`. If `unpersisted` is above zero, the `ids=` field names the exact message ids that arrived at the wire and were never written to the store — those messages are genuinely lost to the platform, not hidden in another workspace or sub-account, and they cannot be re-fetched because WhatsApp only delivers an acked message once. Forward the message from your phone to get it back in; it re-enters as a new message through the normal path.
|
|
151
|
+
|
|
152
|
+
The audit runs every 15 minutes and holds a stanza for 10 minutes before judging it, so a message can take up to 25 minutes to be reported. The wait is deliberate: a large batch of photos arriving at once can take several minutes to finish storing, and a false "a message was lost" alarm would make the real one easy to ignore. Three classes are resolved rather than reported, each counted separately: `skippedStatus` (status broadcasts, never stored), `skippedReaction` (reactions, stored under the message they react to rather than under their own id — a consequence being that a lost *reaction* is not detectable), and `declined` (messages carrying neither text nor a file, such as a shared contact card, a poll, or a delete-for-everyone, which are dropped by design). `oldestUnpersistedMin` tells you whether a reported loss is new or one you have already seen, since the same id is re-reported every 15 minutes for a day. `guard=absent` or `inFlight` stuck above zero on an idle install are both faults worth surfacing to support: the first means the media-download crash guard failed to install at boot, the second means its counter leaked. `contained` counts crashes the guard prevented — a number that climbs while media traffic stays flat is also worth reporting.
|
|
153
|
+
|
|
150
154
|
---
|
|
151
155
|
|
|
152
156
|
## Memory Not Working
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "google",
|
|
3
|
-
"description": "Live Google Calendar for one account via the Google Calendar API v3 — read plus control. Per-account OAuth authorization-code + PKCE; the consent URL is minted by the plugin and the code is exchanged by platform/ui, which holds the house-only client secret. One account can connect several Google accounts; every operational tool takes an optional account selector (the email address) and refuses ambiguously when two or more are connected and none is named. There is deliberately no Gmail surface — mail runs on the `email` plugin's IMAP/SMTP path, and Gmail's restricted scopes would force Google's annual third-party security assessment, whereas Calendar is only a sensitive scope. Tools — google-account-register / google-account-register-poll: connect a Google Calendar (returns a consent URL,
|
|
3
|
+
"description": "Live Google Calendar for one account via the Google Calendar API v3 — read plus control. Per-account OAuth authorization-code + PKCE; the consent URL is minted by the plugin and the code is exchanged by platform/ui, which holds the house-only client secret. One account can connect several Google accounts; every operational tool takes an optional account selector (the email address) and refuses ambiguously when two or more are connected and none is named. There is deliberately no Gmail surface — mail runs on the `email` plugin's IMAP/SMTP path, and Gmail's restricted scopes would force Google's annual third-party security assessment, whereas Calendar is only a sensitive scope. Tools — google-account-register / google-account-register-submit / google-account-register-poll: connect a Google Calendar (returns a consent URL; the user approves, lands on a page that fails to load by design, and pastes the code back to submit); google-calendar-list: events in the next rangeDays days, recurring series expanded; google-calendar-event: one event by id; google-calendar-create / google-calendar-update / google-calendar-cancel: control events, attendees notified, update changes only the fields named; google-calendar-respond: accept, decline or tentatively accept an invitation, leaving other attendees' responses untouched; google-calendar-freebusy: busy blocks for this or named calendars; google-account-info: auth state for one connected Google account; google-account-list: every connected Google account with its own status.",
|
|
4
4
|
"version": "0.1.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Rubytech LLC"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: google
|
|
3
|
-
description: Live Google Calendar for one account via the Google Calendar API v3 — read plus control. Per-account OAuth authorization-code + PKCE; the consent URL is minted by the plugin and the code is exchanged by platform/ui, which holds the house-only client secret. One account can connect several Google accounts; every operational tool takes an optional account selector (the email address) and refuses ambiguously when two or more are connected and none is named. There is deliberately no Gmail surface — mail runs on the `email` plugin's IMAP/SMTP path, and Gmail's restricted scopes would force Google's annual third-party security assessment, whereas Calendar is only a sensitive scope. Tools — google-account-register / google-account-register-poll: connect a Google Calendar (returns a consent URL,
|
|
3
|
+
description: Live Google Calendar for one account via the Google Calendar API v3 — read plus control. Per-account OAuth authorization-code + PKCE; the consent URL is minted by the plugin and the code is exchanged by platform/ui, which holds the house-only client secret. One account can connect several Google accounts; every operational tool takes an optional account selector (the email address) and refuses ambiguously when two or more are connected and none is named. There is deliberately no Gmail surface — mail runs on the `email` plugin's IMAP/SMTP path, and Gmail's restricted scopes would force Google's annual third-party security assessment, whereas Calendar is only a sensitive scope. Tools — google-account-register / google-account-register-submit / google-account-register-poll: connect a Google Calendar (returns a consent URL; the user approves, lands on a page that fails to load by design, and pastes the code back to submit); google-calendar-list: events in the next rangeDays days, recurring series expanded; google-calendar-event: one event by id; google-calendar-create / google-calendar-update / google-calendar-cancel: control events, attendees notified, update changes only the fields named; google-calendar-respond: accept, decline or tentatively accept an invitation, leaving other attendees' responses untouched; google-calendar-freebusy: busy blocks for this or named calendars; google-account-info: auth state for one connected Google account; google-account-list: every connected Google account with its own status.
|
|
4
4
|
tools:
|
|
5
5
|
- name: google-account-register
|
|
6
6
|
publicAllowlist: false
|
|
@@ -8,6 +8,9 @@ tools:
|
|
|
8
8
|
- name: google-account-register-poll
|
|
9
9
|
publicAllowlist: false
|
|
10
10
|
adminAllowlist: false
|
|
11
|
+
- name: google-account-register-submit
|
|
12
|
+
publicAllowlist: false
|
|
13
|
+
adminAllowlist: false
|
|
11
14
|
- name: google-calendar-list
|
|
12
15
|
publicAllowlist: false
|
|
13
16
|
adminAllowlist: false
|