@rubytech/create-maxy-code 0.1.576 → 0.1.577
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/platform/lib/active-rules/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/active-rules/dist/index.js +32 -1
- package/payload/platform/lib/active-rules/dist/index.js.map +1 -1
- package/payload/platform/lib/active-rules/src/index.test.ts +52 -3
- package/payload/platform/lib/active-rules/src/index.ts +36 -2
- package/payload/platform/lib/dispatch-read/dist/allocate.d.ts +57 -0
- package/payload/platform/lib/dispatch-read/dist/allocate.d.ts.map +1 -0
- package/payload/platform/lib/dispatch-read/dist/allocate.js +33 -0
- package/payload/platform/lib/dispatch-read/dist/allocate.js.map +1 -0
- package/payload/platform/lib/dispatch-read/dist/index.d.ts +2 -0
- package/payload/platform/lib/dispatch-read/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/dispatch-read/dist/index.js +10 -1
- package/payload/platform/lib/dispatch-read/dist/index.js.map +1 -1
- package/payload/platform/lib/dispatch-read/src/__tests__/allocate.test.ts +41 -0
- package/payload/platform/lib/dispatch-read/src/allocate.ts +89 -0
- package/payload/platform/lib/dispatch-read/src/index.ts +14 -0
- package/payload/platform/plugins/admin/hooks/__tests__/prompt-optimiser-compliance.test.sh +7 -2
- package/payload/platform/plugins/admin/hooks/prompt-optimiser-compliance.sh +8 -2
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +2 -2
- package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +8 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-visit-status-write.test.ts +333 -22
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-visits-scope.test.ts +262 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/schema.sql +126 -21
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/visit-status.ts +142 -2
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/visits.ts +118 -6
- package/payload/platform/plugins/dispatch/PLUGIN.md +2 -0
- package/payload/platform/plugins/dispatch/mcp/dist/lib/allocate.d.ts +19 -50
- package/payload/platform/plugins/dispatch/mcp/dist/lib/allocate.d.ts.map +1 -1
- package/payload/platform/plugins/dispatch/mcp/dist/lib/allocate.js +1 -28
- package/payload/platform/plugins/dispatch/mcp/dist/lib/allocate.js.map +1 -1
- package/payload/platform/plugins/dispatch/skills/dispatch-page/SKILL.md +45 -6
- package/payload/platform/plugins/dispatch/skills/dispatch-page/template/planner.css +60 -0
- package/payload/platform/plugins/dispatch/skills/dispatch-page/template/planner.html +13 -0
- package/payload/platform/plugins/dispatch/skills/dispatch-page/template/planner.js +203 -5
- package/payload/platform/plugins/docs/references/internals.md +1 -1
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-path.test.d.ts +2 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-path.test.d.ts.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-path.test.js +19 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-path.test.js.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-register.test.d.ts +2 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-register.test.d.ts.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-register.test.js +110 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-register.test.js.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-verdict.test.d.ts +2 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-verdict.test.d.ts.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-verdict.test.js +22 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-verdict.test.js.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/index.js +19 -163
- package/payload/platform/plugins/telegram/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/telegram/mcp/dist/lib/account-write.d.ts +9 -0
- package/payload/platform/plugins/telegram/mcp/dist/lib/account-write.d.ts.map +1 -1
- package/payload/platform/plugins/telegram/mcp/dist/lib/account-write.js +11 -0
- package/payload/platform/plugins/telegram/mcp/dist/lib/account-write.js.map +1 -1
- package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.d.ts +30 -3
- package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.d.ts.map +1 -1
- package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.js +29 -4
- package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.js.map +1 -1
- package/payload/platform/plugins/telegram/mcp/dist/tools/webhook-register.d.ts +33 -0
- package/payload/platform/plugins/telegram/mcp/dist/tools/webhook-register.d.ts.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/tools/webhook-register.js +145 -0
- package/payload/platform/plugins/telegram/mcp/dist/tools/webhook-register.js.map +1 -0
- package/payload/platform/plugins/telegram/skills/configure/SKILL.md +4 -2
- package/payload/platform/services/telegram-channel/dist/notification.d.ts +35 -3
- package/payload/platform/services/telegram-channel/dist/notification.d.ts.map +1 -1
- package/payload/platform/services/telegram-channel/dist/notification.js +38 -5
- package/payload/platform/services/telegram-channel/dist/notification.js.map +1 -1
- package/payload/platform/services/telegram-channel/dist/server.js +1 -1
- package/payload/platform/services/telegram-channel/dist/server.js.map +1 -1
- package/payload/platform/services/webchat-channel/dist/notification.d.ts +2 -1
- package/payload/platform/services/webchat-channel/dist/notification.d.ts.map +1 -1
- package/payload/platform/services/webchat-channel/dist/notification.js.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +2 -2
- package/payload/platform/templates/agents/admin/IDENTITY.md +1 -1
- package/payload/server/server.js +508 -117
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,mBAAmB,OAAO,CAAC;AACxC,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAE1C,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,WAAW,UAAU;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,mBAAmB,OAAO,CAAC;AACxC,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAE1C,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,WAAW,UAAU;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAgCD,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,MAAM,CAUpE;AAED,UAAU,cAAc;IACtB,GAAG,EAAE,CACH,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KACvB,OAAO,CAAC;QAAE,OAAO,EAAE,KAAK,CAAC;YAAE,GAAG,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;SAAE,CAAC,CAAA;KAAE,CAAC,CAAC;CAC/D;AAMD,MAAM,MAAM,gBAAgB,GAAG,OAAO,GAAG,UAAU,GAAG,aAAa,CAAC;AACpE,MAAM,MAAM,eAAe,GAAG,gBAAgB,GAAG,aAAa,CAAC;AAE/D,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,EAAE,gBAAgB,CAAC;CAC1B;AAKD,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,EAAE,eAAe,CAAC;CACzB;AAOD,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,cAAc,EACvB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAaxB;AAgED,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,cAAc,EACvB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,aAAa,EAAE,CAAC,CAE1B;AAKD,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,cAAc,EACvB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,oBAAoB,CAAC,CAI/B;AAID,wBAAsB,aAAa,CACjC,OAAO,EAAE,cAAc,EACvB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,UAAU,EAAE,CAAC,CAEvB"}
|
|
@@ -20,11 +20,42 @@ exports.ACTIVE_CANDIDATE_FLOOR = 0.4;
|
|
|
20
20
|
// Render every rule verbatim. Injection has no cap (Task 1951): concision is
|
|
21
21
|
// enforced at write time by the conversational-memory skill so each stored
|
|
22
22
|
// value is already short, not by truncating the block here.
|
|
23
|
+
//
|
|
24
|
+
// Task 2562 — the header names the owner and the block forbids reading a
|
|
25
|
+
// speaker out of it. Selection is owner-only (resolveOwnerUserId then
|
|
26
|
+
// queryOwnerCandidates below), so every rule here belongs to one identity the
|
|
27
|
+
// header never used to name, and a rule written in the third person about the
|
|
28
|
+
// owner read as a statement about whoever was typing: on sitedesk-code nine
|
|
29
|
+
// owner rules reached a second admin, who was then greeted by the owner's name.
|
|
30
|
+
//
|
|
31
|
+
// The block deliberately does NOT claim a sender is named elsewhere. The same
|
|
32
|
+
// block feeds all three doors and only WhatsApp carries an inbound provenance
|
|
33
|
+
// preamble today (whatsapp-channel/src/notification.ts composeInboundProvenance);
|
|
34
|
+
// webchat has none and Telegram has none until Task 2561 lands. Pointing the
|
|
35
|
+
// agent at a block that is absent is worse than saying nothing about it.
|
|
36
|
+
//
|
|
37
|
+
// Copy constraints, all load-bearing. No apostrophe anywhere in the block:
|
|
38
|
+
// plugins/admin/hooks/prompt-optimiser-compliance.sh holds its whole python
|
|
39
|
+
// program inside a single-quoted `python3 -c '...'` and its rules fixtures are
|
|
40
|
+
// single-quoted bash strings. No blank line: both strip regexes are non-greedy
|
|
41
|
+
// up to the blank line before `## Context`, so the body has to be one physical
|
|
42
|
+
// line. Operator-clean otherwise (no task numbers, repo paths or em-dashes) so
|
|
43
|
+
// the outbound redactor never trips on it.
|
|
44
|
+
//
|
|
45
|
+
// The header is an exact-match anchor in three places that must move with it:
|
|
46
|
+
// COMPOSED_ADMIN_FRAME (ui/app/lib/reader/parse-transcript.ts), the strip in
|
|
47
|
+
// plugins/admin/hooks/prompt-optimiser-compliance.sh, and that hook's test
|
|
48
|
+
// fixtures. Nothing logs when either strip stops matching, so the verbatim
|
|
49
|
+
// assertions in index.test.ts are the whole reconciliation.
|
|
23
50
|
function formatStandingRulesBlock(rules) {
|
|
24
51
|
if (rules.length === 0)
|
|
25
52
|
return "";
|
|
26
53
|
const lines = rules.map((r) => `- ${r.value}`);
|
|
27
|
-
return `## Standing rules
|
|
54
|
+
return (`## Standing rules (account owner)\n` +
|
|
55
|
+
`These standing rules belong to the account owner. ` +
|
|
56
|
+
`Some of them may name the account owner in the third person. ` +
|
|
57
|
+
`None of them says who sent this turn, so do not infer from them who is speaking.\n` +
|
|
58
|
+
lines.join("\n"));
|
|
28
59
|
}
|
|
29
60
|
// The account's owner identity (Task 1461). One `AdminUser {role:'owner'}` is
|
|
30
61
|
// seeded per account (Task 1359); `null` when absent (a pre-seeding legacy
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,uEAAuE;AACvE,sEAAsE;AACtE,gFAAgF;AAChF,8EAA8E;AAC9E,cAAc;;;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -141,11 +141,60 @@ describe("resolveActiveRules — owner-scoped, uncapped, verbatim", () => {
|
|
|
141
141
|
});
|
|
142
142
|
|
|
143
143
|
describe("formatStandingRulesBlock", () => {
|
|
144
|
+
// Task 2562 — the header and the do-not-infer line are asserted verbatim
|
|
145
|
+
// because the same header string is an exact-match anchor in
|
|
146
|
+
// parse-transcript.ts (COMPOSED_ADMIN_FRAME) and in
|
|
147
|
+
// plugins/admin/hooks/prompt-optimiser-compliance.sh. Neither logs anything
|
|
148
|
+
// when it stops matching, so this assertion is the only thing standing
|
|
149
|
+
// between a copy edit and a silently broken strip.
|
|
150
|
+
const HEADER = "## Standing rules (account owner)";
|
|
151
|
+
const BODY =
|
|
152
|
+
"These standing rules belong to the account owner. Some of them may name the account owner in the third person. None of them says who sent this turn, so do not infer from them who is speaking.";
|
|
153
|
+
|
|
154
|
+
const oneRule = () => [
|
|
155
|
+
{
|
|
156
|
+
preferenceId: "p1",
|
|
157
|
+
category: "communication",
|
|
158
|
+
key: "k1",
|
|
159
|
+
value: "use British spelling",
|
|
160
|
+
confidence: 0.9,
|
|
161
|
+
observedAt: "2026-07-01T00:00:00Z",
|
|
162
|
+
},
|
|
163
|
+
];
|
|
164
|
+
|
|
144
165
|
it("returns empty string for no rules", () => {
|
|
145
166
|
expect(formatStandingRulesBlock([])).toBe("");
|
|
146
167
|
});
|
|
147
168
|
|
|
148
|
-
it("
|
|
169
|
+
it("opens with the attributed header and the do-not-infer line", () => {
|
|
170
|
+
expect(formatStandingRulesBlock(oneRule())).toBe(
|
|
171
|
+
`${HEADER}\n${BODY}\n- use British spelling`,
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("carries no apostrophe, so the hook and its shell fixtures keep quoting", () => {
|
|
176
|
+
// The rule VALUES are operator data and may contain anything; only the two
|
|
177
|
+
// lines of platform copy are constrained.
|
|
178
|
+
const platformCopy = formatStandingRulesBlock(oneRule())
|
|
179
|
+
.split("\n")
|
|
180
|
+
.slice(0, 2)
|
|
181
|
+
.join("\n");
|
|
182
|
+
expect(platformCopy).not.toContain("'");
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("contains no blank line, so both strip regexes still terminate at ## Context", () => {
|
|
186
|
+
const rules = Array.from({ length: 3 }, (_, i) => ({
|
|
187
|
+
preferenceId: `p${i}`,
|
|
188
|
+
category: "communication",
|
|
189
|
+
key: `k${i}`,
|
|
190
|
+
value: `rule ${i}`,
|
|
191
|
+
confidence: 1 - i * 0.01,
|
|
192
|
+
observedAt: "2026-07-01T00:00:00Z",
|
|
193
|
+
}));
|
|
194
|
+
expect(formatStandingRulesBlock(rules)).not.toContain("\n\n");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("renders every rule, with no cap, in resolution order", () => {
|
|
149
198
|
const rules = Array.from({ length: 20 }, (_, i) => ({
|
|
150
199
|
preferenceId: `p${i}`,
|
|
151
200
|
category: "communication",
|
|
@@ -155,7 +204,7 @@ describe("formatStandingRulesBlock", () => {
|
|
|
155
204
|
observedAt: "2026-07-01T00:00:00Z",
|
|
156
205
|
}));
|
|
157
206
|
const block = formatStandingRulesBlock(rules);
|
|
158
|
-
expect(block).
|
|
159
|
-
expect(
|
|
207
|
+
expect(block.startsWith(`${HEADER}\n${BODY}\n`)).toBe(true);
|
|
208
|
+
expect(block.split("\n").slice(2)).toEqual(rules.map((r) => `- ${r.value}`));
|
|
160
209
|
});
|
|
161
210
|
});
|
|
@@ -33,10 +33,43 @@ export interface ActiveRule {
|
|
|
33
33
|
// Render every rule verbatim. Injection has no cap (Task 1951): concision is
|
|
34
34
|
// enforced at write time by the conversational-memory skill so each stored
|
|
35
35
|
// value is already short, not by truncating the block here.
|
|
36
|
+
//
|
|
37
|
+
// Task 2562 — the header names the owner and the block forbids reading a
|
|
38
|
+
// speaker out of it. Selection is owner-only (resolveOwnerUserId then
|
|
39
|
+
// queryOwnerCandidates below), so every rule here belongs to one identity the
|
|
40
|
+
// header never used to name, and a rule written in the third person about the
|
|
41
|
+
// owner read as a statement about whoever was typing: on sitedesk-code nine
|
|
42
|
+
// owner rules reached a second admin, who was then greeted by the owner's name.
|
|
43
|
+
//
|
|
44
|
+
// The block deliberately does NOT claim a sender is named elsewhere. The same
|
|
45
|
+
// block feeds all three doors and only WhatsApp carries an inbound provenance
|
|
46
|
+
// preamble today (whatsapp-channel/src/notification.ts composeInboundProvenance);
|
|
47
|
+
// webchat has none and Telegram has none until Task 2561 lands. Pointing the
|
|
48
|
+
// agent at a block that is absent is worse than saying nothing about it.
|
|
49
|
+
//
|
|
50
|
+
// Copy constraints, all load-bearing. No apostrophe anywhere in the block:
|
|
51
|
+
// plugins/admin/hooks/prompt-optimiser-compliance.sh holds its whole python
|
|
52
|
+
// program inside a single-quoted `python3 -c '...'` and its rules fixtures are
|
|
53
|
+
// single-quoted bash strings. No blank line: both strip regexes are non-greedy
|
|
54
|
+
// up to the blank line before `## Context`, so the body has to be one physical
|
|
55
|
+
// line. Operator-clean otherwise (no task numbers, repo paths or em-dashes) so
|
|
56
|
+
// the outbound redactor never trips on it.
|
|
57
|
+
//
|
|
58
|
+
// The header is an exact-match anchor in three places that must move with it:
|
|
59
|
+
// COMPOSED_ADMIN_FRAME (ui/app/lib/reader/parse-transcript.ts), the strip in
|
|
60
|
+
// plugins/admin/hooks/prompt-optimiser-compliance.sh, and that hook's test
|
|
61
|
+
// fixtures. Nothing logs when either strip stops matching, so the verbatim
|
|
62
|
+
// assertions in index.test.ts are the whole reconciliation.
|
|
36
63
|
export function formatStandingRulesBlock(rules: ActiveRule[]): string {
|
|
37
64
|
if (rules.length === 0) return "";
|
|
38
65
|
const lines = rules.map((r) => `- ${r.value}`);
|
|
39
|
-
return
|
|
66
|
+
return (
|
|
67
|
+
`## Standing rules (account owner)\n` +
|
|
68
|
+
`These standing rules belong to the account owner. ` +
|
|
69
|
+
`Some of them may name the account owner in the third person. ` +
|
|
70
|
+
`None of them says who sent this turn, so do not infer from them who is speaking.\n` +
|
|
71
|
+
lines.join("\n")
|
|
72
|
+
);
|
|
40
73
|
}
|
|
41
74
|
|
|
42
75
|
interface MinimalSession {
|
|
@@ -60,7 +93,8 @@ export interface ActiveRuleResolution {
|
|
|
60
93
|
}
|
|
61
94
|
|
|
62
95
|
// The per-turn injection payload the UI hands each channel gateway: the
|
|
63
|
-
// formatted `## Standing rules` block plus the provenance the
|
|
96
|
+
// formatted `## Standing rules (account owner)` block plus the provenance the
|
|
97
|
+
// gateway logs.
|
|
64
98
|
export interface StandingRulesInjection {
|
|
65
99
|
block: string;
|
|
66
100
|
ownerUserId: string | null;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who can be sent to this job, and why the others cannot.
|
|
3
|
+
*
|
|
4
|
+
* The rule that matters: **a qualified worker who is already committed in the
|
|
5
|
+
* window is offered WITH the conflict shown, never dropped.** The office
|
|
6
|
+
* routinely double-books on purpose — a job runs short, a customer is on the
|
|
7
|
+
* way home, an engineer volunteers. A tool that silently removes the busy
|
|
8
|
+
* option makes that decision for them and gives no way to see it was made.
|
|
9
|
+
*
|
|
10
|
+
* Only a missing qualification excludes, and every exclusion is named with its
|
|
11
|
+
* worker and its reason, because `offered=0` with no explanation is the shape a
|
|
12
|
+
* quiet day and an unstaffable job type share.
|
|
13
|
+
*
|
|
14
|
+
* It lives in this shared lib rather than in the dispatch plugin because the
|
|
15
|
+
* portal visits push runs in `platform/ui` and cannot import a plugin's build
|
|
16
|
+
* output, and a second implementation of qualification and clash is the defect
|
|
17
|
+
* `functions/api/visits.ts:33-36` names: a gate written once per client is a
|
|
18
|
+
* gate got wrong once per client. Task 2510 moved `buildVisitKeyboard` here for
|
|
19
|
+
* the same reason. The dispatch plugin re-exports both from
|
|
20
|
+
* `mcp/src/lib/allocate.ts`, which is the import site its tools and tests
|
|
21
|
+
* already name.
|
|
22
|
+
*/
|
|
23
|
+
import type { DispatchConfig } from "./types.js";
|
|
24
|
+
import type { Commitment } from "./read.js";
|
|
25
|
+
export type { Commitment };
|
|
26
|
+
export interface Candidate {
|
|
27
|
+
readonly workerId: string;
|
|
28
|
+
readonly name: string;
|
|
29
|
+
/** Commitments overlapping the requested window. Empty means free. */
|
|
30
|
+
readonly conflicts: readonly Commitment[];
|
|
31
|
+
}
|
|
32
|
+
export interface Exclusion {
|
|
33
|
+
readonly workerId: string;
|
|
34
|
+
readonly reason: "missing-qualification";
|
|
35
|
+
}
|
|
36
|
+
export interface SelectCandidatesParams {
|
|
37
|
+
readonly jobType: string;
|
|
38
|
+
readonly cfg: DispatchConfig;
|
|
39
|
+
/** Committed visits per workerId, already narrowed to the window. */
|
|
40
|
+
readonly commitments: ReadonlyMap<string, readonly Commitment[]>;
|
|
41
|
+
}
|
|
42
|
+
export interface CandidateSelection {
|
|
43
|
+
readonly offered: readonly Candidate[];
|
|
44
|
+
readonly excluded: readonly Exclusion[];
|
|
45
|
+
/** The qualification the job type demands, echoed for the log line. */
|
|
46
|
+
readonly required: string;
|
|
47
|
+
}
|
|
48
|
+
export declare function selectCandidates(params: SelectCandidatesParams): CandidateSelection;
|
|
49
|
+
/** `[a, b)` overlap. Touching endpoints do not overlap. */
|
|
50
|
+
export declare function overlaps(a: {
|
|
51
|
+
start: string;
|
|
52
|
+
end: string;
|
|
53
|
+
}, b: {
|
|
54
|
+
start: string;
|
|
55
|
+
end: string;
|
|
56
|
+
}): boolean;
|
|
57
|
+
//# sourceMappingURL=allocate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"allocate.d.ts","sourceRoot":"","sources":["../src/allocate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAKjD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAE5C,YAAY,EAAE,UAAU,EAAE,CAAC;AAE3B,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,sEAAsE;IACtE,QAAQ,CAAC,SAAS,EAAE,SAAS,UAAU,EAAE,CAAC;CAC3C;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,uBAAuB,CAAC;CAC1C;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,GAAG,EAAE,cAAc,CAAC;IAC7B,qEAAqE;IACrE,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,UAAU,EAAE,CAAC,CAAC;CAClE;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,OAAO,EAAE,SAAS,SAAS,EAAE,CAAC;IACvC,QAAQ,CAAC,QAAQ,EAAE,SAAS,SAAS,EAAE,CAAC;IACxC,uEAAuE;IACvE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,sBAAsB,GAAG,kBAAkB,CAuBnF;AAED,2DAA2D;AAC3D,wBAAgB,QAAQ,CACtB,CAAC,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,EACjC,CAAC,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAChC,OAAO,CAET"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.selectCandidates = selectCandidates;
|
|
4
|
+
exports.overlaps = overlaps;
|
|
5
|
+
function selectCandidates(params) {
|
|
6
|
+
const { jobType, cfg, commitments } = params;
|
|
7
|
+
const type = cfg.jobTypes.find((t) => t.name === jobType);
|
|
8
|
+
if (!type) {
|
|
9
|
+
// An unknown job type is the caller's error, not a staffing outcome. Every
|
|
10
|
+
// worker is excluded for a reason that would be a lie, so none are: the
|
|
11
|
+
// caller checks the type before asking.
|
|
12
|
+
return { offered: [], excluded: [], required: "" };
|
|
13
|
+
}
|
|
14
|
+
const offered = [];
|
|
15
|
+
const excluded = [];
|
|
16
|
+
for (const worker of cfg.workers) {
|
|
17
|
+
if (!worker.qualifications.includes(type.requiredQualification)) {
|
|
18
|
+
excluded.push({ workerId: worker.workerId, reason: "missing-qualification" });
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
offered.push({
|
|
22
|
+
workerId: worker.workerId,
|
|
23
|
+
name: worker.name,
|
|
24
|
+
conflicts: commitments.get(worker.workerId) ?? [],
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return { offered, excluded, required: type.requiredQualification };
|
|
28
|
+
}
|
|
29
|
+
/** `[a, b)` overlap. Touching endpoints do not overlap. */
|
|
30
|
+
function overlaps(a, b) {
|
|
31
|
+
return a.start < b.end && b.start < a.end;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=allocate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"allocate.js","sourceRoot":"","sources":["../src/allocate.ts"],"names":[],"mappings":";;AAyDA,4CAuBC;AAGD,4BAKC;AA/BD,SAAgB,gBAAgB,CAAC,MAA8B;IAC7D,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,WAAW,EAAE,GAAG,MAAM,CAAC;IAC7C,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC;IAC1D,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,2EAA2E;QAC3E,wEAAwE;QACxE,wCAAwC;QACxC,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;IACrD,CAAC;IACD,MAAM,OAAO,GAAgB,EAAE,CAAC;IAChC,MAAM,QAAQ,GAAgB,EAAE,CAAC;IACjC,KAAK,MAAM,MAAM,IAAI,GAAG,CAAC,OAAO,EAAE,CAAC;QACjC,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,QAAQ,CAAC,IAAI,CAAC,qBAAqB,CAAC,EAAE,CAAC;YAChE,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,uBAAuB,EAAE,CAAC,CAAC;YAC9E,SAAS;QACX,CAAC;QACD,OAAO,CAAC,IAAI,CAAC;YACX,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,SAAS,EAAE,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE;SAClD,CAAC,CAAC;IACL,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,CAAC,qBAAqB,EAAE,CAAC;AACrE,CAAC;AAED,2DAA2D;AAC3D,SAAgB,QAAQ,CACtB,CAAiC,EACjC,CAAiC;IAEjC,OAAO,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC;AAC5C,CAAC"}
|
|
@@ -14,6 +14,8 @@ export { RECORDABLE_FIELDS, loadDispatchConfig, terminalJobStatuses, terminalVis
|
|
|
14
14
|
export type { RecordableField } from "./config.js";
|
|
15
15
|
export { CALLBACK_DATA_MAX_BYTES, buildVisitKeyboard, dispatchCallbackData, parseDispatchCallback, } from "./keyboard.js";
|
|
16
16
|
export type { DispatchButton, VisitKeyboard } from "./keyboard.js";
|
|
17
|
+
export { overlaps, selectCandidates } from "./allocate.js";
|
|
18
|
+
export type { Candidate, CandidateSelection, Exclusion, SelectCandidatesParams, } from "./allocate.js";
|
|
17
19
|
export { withSession } from "./session.js";
|
|
18
20
|
export { resolveAccountZone } from "./zone.js";
|
|
19
21
|
export { MAX_RANGE_DAYS, addDaysYmd, currentWeekYmd, daysBetween, minutesIntoDay, ymdInZone, zonedDayStartUtc, zonedRangeBounds, } from "./window.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,mBAAmB,EACnB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAKnD,OAAO,EACL,uBAAuB,EACvB,kBAAkB,EAClB,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,mBAAmB,EACnB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAKnD,OAAO,EACL,uBAAuB,EACvB,kBAAkB,EAClB,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAQnE,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAC3D,YAAY,EACV,SAAS,EACT,kBAAkB,EAClB,SAAS,EACT,sBAAsB,GACvB,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,EACL,cAAc,EACd,UAAU,EACV,cAAc,EACd,WAAW,EACX,cAAc,EACd,SAAS,EACT,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,aAAa,CAAC;AAGrB,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,UAAU,EACV,YAAY,EACZ,eAAe,EACf,SAAS,EACT,UAAU,EACV,cAAc,EACd,OAAO,EACP,QAAQ,EACR,UAAU,EACV,uBAAuB,EACvB,kBAAkB,EAClB,eAAe,EACf,gBAAgB,EAChB,SAAS,EACT,oBAAoB,EACpB,gBAAgB,EAChB,kBAAkB,GACnB,MAAM,WAAW,CAAC;AAInB,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AACtD,YAAY,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAGhD,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAE3F,YAAY,EAAE,UAAU,EAAE,QAAQ,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AACvF,mBAAmB,YAAY,CAAC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.readWorkerDay = exports.readJobList = exports.readDay = exports.readCounts = exports.nonZuluStarts = exports.geofenceVerdict = exports.distanceM = exports.readVisitsInWindow = exports.readVisitsForJob = exports.readVisitRecordState = exports.readVisit = exports.readSiteLocation = exports.readSiteHistory = exports.readPreviousVisits = exports.readOpenVisitsForWorker = exports.readNodeId = exports.readJobs = exports.readJob = exports.readFieldVisit = exports.readDrafts = exports.readDraft = exports.readCommitments = exports.projectVisit = exports.projectJob = exports.projectFieldVisit = exports.buildJobListQuery = exports.zonedRangeBounds = exports.zonedDayStartUtc = exports.ymdInZone = exports.minutesIntoDay = exports.daysBetween = exports.currentWeekYmd = exports.addDaysYmd = exports.MAX_RANGE_DAYS = exports.resolveAccountZone = exports.withSession = exports.parseDispatchCallback = exports.dispatchCallbackData = exports.buildVisitKeyboard = exports.CALLBACK_DATA_MAX_BYTES = exports.workerForTelegramUser = exports.terminalVisitStatuses = exports.terminalJobStatuses = exports.loadDispatchConfig = exports.RECORDABLE_FIELDS = void 0;
|
|
3
|
+
exports.readWorkerDay = exports.readJobList = exports.readDay = exports.readCounts = exports.nonZuluStarts = exports.geofenceVerdict = exports.distanceM = exports.readVisitsInWindow = exports.readVisitsForJob = exports.readVisitRecordState = exports.readVisit = exports.readSiteLocation = exports.readSiteHistory = exports.readPreviousVisits = exports.readOpenVisitsForWorker = exports.readNodeId = exports.readJobs = exports.readJob = exports.readFieldVisit = exports.readDrafts = exports.readDraft = exports.readCommitments = exports.projectVisit = exports.projectJob = exports.projectFieldVisit = exports.buildJobListQuery = exports.zonedRangeBounds = exports.zonedDayStartUtc = exports.ymdInZone = exports.minutesIntoDay = exports.daysBetween = exports.currentWeekYmd = exports.addDaysYmd = exports.MAX_RANGE_DAYS = exports.resolveAccountZone = exports.withSession = exports.selectCandidates = exports.overlaps = exports.parseDispatchCallback = exports.dispatchCallbackData = exports.buildVisitKeyboard = exports.CALLBACK_DATA_MAX_BYTES = exports.workerForTelegramUser = exports.terminalVisitStatuses = exports.terminalJobStatuses = exports.loadDispatchConfig = exports.RECORDABLE_FIELDS = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* Dispatch reads, shared by the MCP tools and the office planner.
|
|
6
6
|
*
|
|
@@ -27,6 +27,15 @@ Object.defineProperty(exports, "CALLBACK_DATA_MAX_BYTES", { enumerable: true, ge
|
|
|
27
27
|
Object.defineProperty(exports, "buildVisitKeyboard", { enumerable: true, get: function () { return keyboard_js_1.buildVisitKeyboard; } });
|
|
28
28
|
Object.defineProperty(exports, "dispatchCallbackData", { enumerable: true, get: function () { return keyboard_js_1.dispatchCallbackData; } });
|
|
29
29
|
Object.defineProperty(exports, "parseDispatchCallback", { enumerable: true, get: function () { return keyboard_js_1.parseDispatchCallback; } });
|
|
30
|
+
// Task 2511 — qualification and clash, shared by the dispatch tools and the
|
|
31
|
+
// portal visits push. One implementation, for the reason allocate.ts states:
|
|
32
|
+
// the push runs in platform/ui and cannot import a plugin's build output, and a
|
|
33
|
+
// second copy of these rules is a gate got wrong once per client.
|
|
34
|
+
// `Commitment` is NOT re-exported here: it is `readCommitments`'s own shape,
|
|
35
|
+
// declared once in read.ts and already exported below.
|
|
36
|
+
var allocate_js_1 = require("./allocate.js");
|
|
37
|
+
Object.defineProperty(exports, "overlaps", { enumerable: true, get: function () { return allocate_js_1.overlaps; } });
|
|
38
|
+
Object.defineProperty(exports, "selectCandidates", { enumerable: true, get: function () { return allocate_js_1.selectCandidates; } });
|
|
30
39
|
var session_js_1 = require("./session.js");
|
|
31
40
|
Object.defineProperty(exports, "withSession", { enumerable: true, get: function () { return session_js_1.withSession; } });
|
|
32
41
|
var zone_js_1 = require("./zone.js");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;GAWG;AACH,yCAMqB;AALnB,8GAAA,iBAAiB,OAAA;AACjB,+GAAA,kBAAkB,OAAA;AAClB,gHAAA,mBAAmB,OAAA;AACnB,kHAAA,qBAAqB,OAAA;AACrB,kHAAA,qBAAqB,OAAA;AAIvB,6EAA6E;AAC7E,6EAA6E;AAC7E,oEAAoE;AACpE,6CAKuB;AAJrB,sHAAA,uBAAuB,OAAA;AACvB,iHAAA,kBAAkB,OAAA;AAClB,mHAAA,oBAAoB,OAAA;AACpB,oHAAA,qBAAqB,OAAA;AAIvB,2CAA2C;AAAlC,yGAAA,WAAW,OAAA;AACpB,qCAA+C;AAAtC,6GAAA,kBAAkB,OAAA;AAC3B,yCASqB;AARnB,2GAAA,cAAc,OAAA;AACd,uGAAA,UAAU,OAAA;AACV,2GAAA,cAAc,OAAA;AACd,wGAAA,WAAW,OAAA;AACX,2GAAA,cAAc,OAAA;AACd,sGAAA,SAAS,OAAA;AACT,6GAAA,gBAAgB,OAAA;AAChB,6GAAA,gBAAgB,OAAA;AAGlB,qEAAqE;AACrE,qCAoBmB;AAnBjB,4GAAA,iBAAiB,OAAA;AACjB,4GAAA,iBAAiB,OAAA;AACjB,qGAAA,UAAU,OAAA;AACV,uGAAA,YAAY,OAAA;AACZ,0GAAA,eAAe,OAAA;AACf,oGAAA,SAAS,OAAA;AACT,qGAAA,UAAU,OAAA;AACV,yGAAA,cAAc,OAAA;AACd,kGAAA,OAAO,OAAA;AACP,mGAAA,QAAQ,OAAA;AACR,qGAAA,UAAU,OAAA;AACV,kHAAA,uBAAuB,OAAA;AACvB,6GAAA,kBAAkB,OAAA;AAClB,0GAAA,eAAe,OAAA;AACf,2GAAA,gBAAgB,OAAA;AAChB,oGAAA,SAAS,OAAA;AACT,+GAAA,oBAAoB,OAAA;AACpB,2GAAA,gBAAgB,OAAA;AAChB,6GAAA,kBAAkB,OAAA;AAGpB,uEAAuE;AACvE,kDAAkD;AAClD,mCAAsD;AAA7C,mGAAA,SAAS,OAAA;AAAE,yGAAA,eAAe,OAAA;AAGnC,yEAAyE;AACzE,qCAA2F;AAAlF,wGAAA,aAAa,OAAA;AAAE,qGAAA,UAAU,OAAA;AAAE,kGAAA,OAAO,OAAA;AAAE,sGAAA,WAAW,OAAA;AAAE,wGAAA,aAAa,OAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;GAWG;AACH,yCAMqB;AALnB,8GAAA,iBAAiB,OAAA;AACjB,+GAAA,kBAAkB,OAAA;AAClB,gHAAA,mBAAmB,OAAA;AACnB,kHAAA,qBAAqB,OAAA;AACrB,kHAAA,qBAAqB,OAAA;AAIvB,6EAA6E;AAC7E,6EAA6E;AAC7E,oEAAoE;AACpE,6CAKuB;AAJrB,sHAAA,uBAAuB,OAAA;AACvB,iHAAA,kBAAkB,OAAA;AAClB,mHAAA,oBAAoB,OAAA;AACpB,oHAAA,qBAAqB,OAAA;AAIvB,4EAA4E;AAC5E,6EAA6E;AAC7E,gFAAgF;AAChF,kEAAkE;AAClE,6EAA6E;AAC7E,uDAAuD;AACvD,6CAA2D;AAAlD,uGAAA,QAAQ,OAAA;AAAE,+GAAA,gBAAgB,OAAA;AAQnC,2CAA2C;AAAlC,yGAAA,WAAW,OAAA;AACpB,qCAA+C;AAAtC,6GAAA,kBAAkB,OAAA;AAC3B,yCASqB;AARnB,2GAAA,cAAc,OAAA;AACd,uGAAA,UAAU,OAAA;AACV,2GAAA,cAAc,OAAA;AACd,wGAAA,WAAW,OAAA;AACX,2GAAA,cAAc,OAAA;AACd,sGAAA,SAAS,OAAA;AACT,6GAAA,gBAAgB,OAAA;AAChB,6GAAA,gBAAgB,OAAA;AAGlB,qEAAqE;AACrE,qCAoBmB;AAnBjB,4GAAA,iBAAiB,OAAA;AACjB,4GAAA,iBAAiB,OAAA;AACjB,qGAAA,UAAU,OAAA;AACV,uGAAA,YAAY,OAAA;AACZ,0GAAA,eAAe,OAAA;AACf,oGAAA,SAAS,OAAA;AACT,qGAAA,UAAU,OAAA;AACV,yGAAA,cAAc,OAAA;AACd,kGAAA,OAAO,OAAA;AACP,mGAAA,QAAQ,OAAA;AACR,qGAAA,UAAU,OAAA;AACV,kHAAA,uBAAuB,OAAA;AACvB,6GAAA,kBAAkB,OAAA;AAClB,0GAAA,eAAe,OAAA;AACf,2GAAA,gBAAgB,OAAA;AAChB,oGAAA,SAAS,OAAA;AACT,+GAAA,oBAAoB,OAAA;AACpB,2GAAA,gBAAgB,OAAA;AAChB,6GAAA,kBAAkB,OAAA;AAGpB,uEAAuE;AACvE,kDAAkD;AAClD,mCAAsD;AAA7C,mGAAA,SAAS,OAAA;AAAE,yGAAA,eAAe,OAAA;AAGnC,yEAAyE;AACzE,qCAA2F;AAAlF,wGAAA,aAAa,OAAA;AAAE,qGAAA,UAAU,OAAA;AAAE,kGAAA,OAAO,OAAA;AAAE,sGAAA,WAAW,OAAA;AAAE,wGAAA,aAAa,OAAA"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { selectCandidates, overlaps } from "../allocate.js";
|
|
3
|
+
import type { DispatchConfig } from "../types.js";
|
|
4
|
+
|
|
5
|
+
const cfg = {
|
|
6
|
+
statuses: { job: [], visit: [], jobTransitions: [], visitTransitions: [] },
|
|
7
|
+
jobTypes: [{ name: "Boiler", requiredQualification: "gas", defaultDurationMin: 60 }],
|
|
8
|
+
workers: [
|
|
9
|
+
{ workerId: "w1", name: "Ann", qualifications: ["gas"], workingHours: {} },
|
|
10
|
+
{ workerId: "w2", name: "Ben", qualifications: [], workingHours: {} },
|
|
11
|
+
],
|
|
12
|
+
records: { checklists: {}, requiredBeforeTerminal: {} },
|
|
13
|
+
vocabulary: { job: "job", visit: "visit", worker: "worker" },
|
|
14
|
+
vehicles: { enabled: false },
|
|
15
|
+
} as unknown as DispatchConfig;
|
|
16
|
+
|
|
17
|
+
describe("selectCandidates, from the shared lib", () => {
|
|
18
|
+
it("offers a qualified worker and names why the other is excluded", () => {
|
|
19
|
+
const r = selectCandidates({ jobType: "Boiler", cfg, commitments: new Map() });
|
|
20
|
+
expect(r.offered.map((c) => c.workerId)).toEqual(["w1"]);
|
|
21
|
+
expect(r.excluded).toEqual([{ workerId: "w2", reason: "missing-qualification" }]);
|
|
22
|
+
expect(r.required).toBe("gas");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("offers a busy qualified worker WITH the conflict shown, never dropped", () => {
|
|
26
|
+
const busy = new Map([["w1", [{ visitId: "v9", start: "a", end: "b" }]]]);
|
|
27
|
+
const r = selectCandidates({ jobType: "Boiler", cfg, commitments: busy });
|
|
28
|
+
expect(r.offered).toHaveLength(1);
|
|
29
|
+
expect(r.offered[0].conflicts).toHaveLength(1);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("excludes nobody for an unknown job type, because every reason would be a lie", () => {
|
|
33
|
+
const r = selectCandidates({ jobType: "Nope", cfg, commitments: new Map() });
|
|
34
|
+
expect(r).toEqual({ offered: [], excluded: [], required: "" });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("treats touching endpoints as no overlap", () => {
|
|
38
|
+
expect(overlaps({ start: "1", end: "2" }, { start: "2", end: "3" })).toBe(false);
|
|
39
|
+
expect(overlaps({ start: "1", end: "3" }, { start: "2", end: "4" })).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who can be sent to this job, and why the others cannot.
|
|
3
|
+
*
|
|
4
|
+
* The rule that matters: **a qualified worker who is already committed in the
|
|
5
|
+
* window is offered WITH the conflict shown, never dropped.** The office
|
|
6
|
+
* routinely double-books on purpose — a job runs short, a customer is on the
|
|
7
|
+
* way home, an engineer volunteers. A tool that silently removes the busy
|
|
8
|
+
* option makes that decision for them and gives no way to see it was made.
|
|
9
|
+
*
|
|
10
|
+
* Only a missing qualification excludes, and every exclusion is named with its
|
|
11
|
+
* worker and its reason, because `offered=0` with no explanation is the shape a
|
|
12
|
+
* quiet day and an unstaffable job type share.
|
|
13
|
+
*
|
|
14
|
+
* It lives in this shared lib rather than in the dispatch plugin because the
|
|
15
|
+
* portal visits push runs in `platform/ui` and cannot import a plugin's build
|
|
16
|
+
* output, and a second implementation of qualification and clash is the defect
|
|
17
|
+
* `functions/api/visits.ts:33-36` names: a gate written once per client is a
|
|
18
|
+
* gate got wrong once per client. Task 2510 moved `buildVisitKeyboard` here for
|
|
19
|
+
* the same reason. The dispatch plugin re-exports both from
|
|
20
|
+
* `mcp/src/lib/allocate.ts`, which is the import site its tools and tests
|
|
21
|
+
* already name.
|
|
22
|
+
*/
|
|
23
|
+
import type { DispatchConfig } from "./types.js";
|
|
24
|
+
// `Commitment` is `readCommitments`'s own return shape and is declared once, in
|
|
25
|
+
// read.ts. The dispatch plugin carried a second structurally-identical
|
|
26
|
+
// declaration beside its copy of this file; consolidating to one is part of the
|
|
27
|
+
// move, because two declarations of one contract can drift on a field.
|
|
28
|
+
import type { Commitment } from "./read.js";
|
|
29
|
+
|
|
30
|
+
export type { Commitment };
|
|
31
|
+
|
|
32
|
+
export interface Candidate {
|
|
33
|
+
readonly workerId: string;
|
|
34
|
+
readonly name: string;
|
|
35
|
+
/** Commitments overlapping the requested window. Empty means free. */
|
|
36
|
+
readonly conflicts: readonly Commitment[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface Exclusion {
|
|
40
|
+
readonly workerId: string;
|
|
41
|
+
readonly reason: "missing-qualification";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface SelectCandidatesParams {
|
|
45
|
+
readonly jobType: string;
|
|
46
|
+
readonly cfg: DispatchConfig;
|
|
47
|
+
/** Committed visits per workerId, already narrowed to the window. */
|
|
48
|
+
readonly commitments: ReadonlyMap<string, readonly Commitment[]>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface CandidateSelection {
|
|
52
|
+
readonly offered: readonly Candidate[];
|
|
53
|
+
readonly excluded: readonly Exclusion[];
|
|
54
|
+
/** The qualification the job type demands, echoed for the log line. */
|
|
55
|
+
readonly required: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function selectCandidates(params: SelectCandidatesParams): CandidateSelection {
|
|
59
|
+
const { jobType, cfg, commitments } = params;
|
|
60
|
+
const type = cfg.jobTypes.find((t) => t.name === jobType);
|
|
61
|
+
if (!type) {
|
|
62
|
+
// An unknown job type is the caller's error, not a staffing outcome. Every
|
|
63
|
+
// worker is excluded for a reason that would be a lie, so none are: the
|
|
64
|
+
// caller checks the type before asking.
|
|
65
|
+
return { offered: [], excluded: [], required: "" };
|
|
66
|
+
}
|
|
67
|
+
const offered: Candidate[] = [];
|
|
68
|
+
const excluded: Exclusion[] = [];
|
|
69
|
+
for (const worker of cfg.workers) {
|
|
70
|
+
if (!worker.qualifications.includes(type.requiredQualification)) {
|
|
71
|
+
excluded.push({ workerId: worker.workerId, reason: "missing-qualification" });
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
offered.push({
|
|
75
|
+
workerId: worker.workerId,
|
|
76
|
+
name: worker.name,
|
|
77
|
+
conflicts: commitments.get(worker.workerId) ?? [],
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return { offered, excluded, required: type.requiredQualification };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** `[a, b)` overlap. Touching endpoints do not overlap. */
|
|
84
|
+
export function overlaps(
|
|
85
|
+
a: { start: string; end: string },
|
|
86
|
+
b: { start: string; end: string },
|
|
87
|
+
): boolean {
|
|
88
|
+
return a.start < b.end && b.start < a.end;
|
|
89
|
+
}
|
|
@@ -30,6 +30,20 @@ export {
|
|
|
30
30
|
} from "./keyboard.js";
|
|
31
31
|
export type { DispatchButton, VisitKeyboard } from "./keyboard.js";
|
|
32
32
|
|
|
33
|
+
// Task 2511 — qualification and clash, shared by the dispatch tools and the
|
|
34
|
+
// portal visits push. One implementation, for the reason allocate.ts states:
|
|
35
|
+
// the push runs in platform/ui and cannot import a plugin's build output, and a
|
|
36
|
+
// second copy of these rules is a gate got wrong once per client.
|
|
37
|
+
// `Commitment` is NOT re-exported here: it is `readCommitments`'s own shape,
|
|
38
|
+
// declared once in read.ts and already exported below.
|
|
39
|
+
export { overlaps, selectCandidates } from "./allocate.js";
|
|
40
|
+
export type {
|
|
41
|
+
Candidate,
|
|
42
|
+
CandidateSelection,
|
|
43
|
+
Exclusion,
|
|
44
|
+
SelectCandidatesParams,
|
|
45
|
+
} from "./allocate.js";
|
|
46
|
+
|
|
33
47
|
export { withSession } from "./session.js";
|
|
34
48
|
export { resolveAccountZone } from "./zone.js";
|
|
35
49
|
export {
|
|
@@ -66,10 +66,15 @@ CHANNEL_REFRAME_MULTI='{"type":"user","isMeta":true,"message":{"role":"user","co
|
|
|
66
66
|
# the reader anchor, so a rules-present turn is evaluated identically to a
|
|
67
67
|
# rules-absent one; without the strip the turn no longer starts with ## Context
|
|
68
68
|
# and the hook silently stops flagging on exactly the rule-carrying turns.
|
|
69
|
-
|
|
69
|
+
# Task 2562 — the header is now `## Standing rules (account owner)` and the block
|
|
70
|
+
# carries a do-not-infer line between the header and the bullets. Both fixtures
|
|
71
|
+
# hold the exact emitted copy, so a header change that misses the strip below
|
|
72
|
+
# fails here instead of silently un-flagging every rule-carrying turn again. The
|
|
73
|
+
# emitted copy carries no apostrophe, which is what lets these stay single-quoted.
|
|
74
|
+
CHANNEL_REFRAME_RULES='{"type":"user","isMeta":true,"message":{"role":"user","content":"<channel source=\"whatsapp-channel\">## Standing rules (account owner)\nThese standing rules belong to the account owner. Some of them may name the account owner in the third person. None of them says who sent this turn, so do not infer from them who is speaking.\n- Always greet the client by name\n- Use British spelling\n\n## Context\nplease draft the works order PDF for order 926\n\n## Instruction\nChoose the installed specialist that owns this request and dispatch it with the Agent tool.</channel>"}}'
|
|
70
75
|
# Same rules-prefixed shape, one-word Context payload — after the prefix strip the
|
|
71
76
|
# trivial filter must still read just the payload (`yes`), not the rules block.
|
|
72
|
-
CHANNEL_REFRAME_RULES_TRIVIAL='{"type":"user","isMeta":true,"message":{"role":"user","content":"<channel source=\"whatsapp-channel\">## Standing rules\n- Always greet the client by name\n- Use British spelling\n\n## Context\nyes\n\n## Instruction\nChoose the installed specialist that owns this request and dispatch it with the Agent tool.</channel>"}}'
|
|
77
|
+
CHANNEL_REFRAME_RULES_TRIVIAL='{"type":"user","isMeta":true,"message":{"role":"user","content":"<channel source=\"whatsapp-channel\">## Standing rules (account owner)\nThese standing rules belong to the account owner. Some of them may name the account owner in the third person. None of them says who sent this turn, so do not infer from them who is speaking.\n- Always greet the client by name\n- Use British spelling\n\n## Context\nyes\n\n## Instruction\nChoose the installed specialist that owns this request and dispatch it with the Agent tool.</channel>"}}'
|
|
73
78
|
|
|
74
79
|
assert_flagged() {
|
|
75
80
|
local name="$1" t="$2" out
|
|
@@ -149,13 +149,19 @@ while inner.startswith("<channel"):
|
|
|
149
149
|
# (`## Standing rules\n- …\n\n`) above `## Context`, so a rules-present channel turn
|
|
150
150
|
# no longer begins with `## Context` and both the reframe detection and the trivial
|
|
151
151
|
# strip below silently stopped firing on exactly those turns. Strip an optional
|
|
152
|
-
# leading standing-rules block — anchored to the
|
|
152
|
+
# leading standing-rules block — anchored to the standing-rules header (not any
|
|
153
153
|
# prefix), non-greedy up to the `\n\n` immediately before `## Context` — so both see
|
|
154
154
|
# the `## Context` payload as before rules injection. Channel-only and match-gated on
|
|
155
155
|
# a following `## Context`, so a rules-absent channel turn is byte-identical to before
|
|
156
156
|
# and a typed (non-channel) prompt is untouched.
|
|
157
|
+
# Task 2562 — the header now carries the owner attribution and the block carries a
|
|
158
|
+
# do-not-infer line, both consumed by the same non-greedy body. The suffix is
|
|
159
|
+
# optional: a session spawned before the upgrade keeps composing the bare header
|
|
160
|
+
# until it is recycled, and this hook reads that turn at Stop time. This whole
|
|
161
|
+
# program is inside a single-quoted python3 -c, which is why the emitted copy
|
|
162
|
+
# carries no apostrophe.
|
|
157
163
|
if is_channel:
|
|
158
|
-
inner = re.sub(r"^## Standing rules\n[\s\S]*?\n\n(?=## Context\n)", "", inner, count=1)
|
|
164
|
+
inner = re.sub(r"^## Standing rules(?: \(account owner\))?\n[\s\S]*?\n\n(?=## Context\n)", "", inner, count=1)
|
|
159
165
|
|
|
160
166
|
# Task 805 — a channel-wrapped reframe turn (the ## Context / ## Instruction
|
|
161
167
|
# sections of composeAdminContent) is directive-equivalent: the directive hook suppresses
|
|
@@ -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:986f31a5e7a43c78ee8048c088c79c112c01acf1956544e40bd5adf0dad6f76b
|
|
5
5
|
brand: maxy-code
|
|
6
6
|
product-name: Maxy
|
|
7
7
|
---
|
|
@@ -6160,7 +6160,7 @@ Standing rules the operator gives over a channel are `Preference` nodes. Two mec
|
|
|
6160
6160
|
|
|
6161
6161
|
**Reconcile by meaning on write (`profile-update`).** The existence check is two-tier. First an exact `(category, key)` match — the classic fast path, unchanged. When that misses, a semantic-neighbour lookup runs over the `preference_embedding` vector index, scoped to the same `accountId` and `scope:'admin'`, non-trashed. If the nearest neighbour is at or above `SAME_SUBJECT_COSINE` (0.85), the write lands on that node through the existing reinforce/update/contradict curve instead of minting a duplicate; the matched neighbours are returned in the tool result so the agent can classify future writes against real prior state. The confidence constants are unchanged — only the join key widens, so a rephrasing under a new `key` no longer forks the rule and a correction demotes the prior node rather than adding a contradictory one.
|
|
6162
6162
|
|
|
6163
|
-
**Inject the active rule-set every turn (channel gateways).** The one definition of "this account's active rules" is `@maxy/active-rules`: the account owner's admin-scope preferences (resolve the seeded `AdminUser {role:'owner'}`, then select through that owner's `UserProfile` via `HAS_PREFERENCE`), a floor-filtered (`confidence >= 0.4`), confidence-ordered query that returns every such preference verbatim — no cap, no de-confliction, no candidate limit. Concision is enforced at write time by the `conversational-memory` skill (each stored value is a single minimal imperative) and by the write-time semantic reconcile (a same-subject write at or above `SAME_SUBJECT_COSINE` folds into the existing node rather than minting a duplicate), so the injected block stays small because each stored preference is short and non-redundant, not because injection truncates. A preference owned by a non-owner profile (an operator's, stranded on the operator's own profile while managing the account) is never injected; an account with no seeded owner injects nothing. `profile-read` exposes this same owner-scoped set as `activeRules`, and the three inbound gateways (`wa-gateway`, `telegram-gateway`, `webchat-gateway`) fetch it for the effective session account and attach it as `standingRules` on the inbound frame; the channel builders prepend it as a `## Standing rules` block above `## Context
|
|
6163
|
+
**Inject the active rule-set every turn (channel gateways).** The one definition of "this account's active rules" is `@maxy/active-rules`: the account owner's admin-scope preferences (resolve the seeded `AdminUser {role:'owner'}`, then select through that owner's `UserProfile` via `HAS_PREFERENCE`), a floor-filtered (`confidence >= 0.4`), confidence-ordered query that returns every such preference verbatim — no cap, no de-confliction, no candidate limit. Concision is enforced at write time by the `conversational-memory` skill (each stored value is a single minimal imperative) and by the write-time semantic reconcile (a same-subject write at or above `SAME_SUBJECT_COSINE` folds into the existing node rather than minting a duplicate), so the injected block stays small because each stored preference is short and non-redundant, not because injection truncates. A preference owned by a non-owner profile (an operator's, stranded on the operator's own profile while managing the account) is never injected; an account with no seeded owner injects nothing. `profile-read` exposes this same owner-scoped set as `activeRules`, and the three inbound gateways (`wa-gateway`, `telegram-gateway`, `webchat-gateway`) fetch it for the effective session account and attach it as `standingRules` on the inbound frame; the channel builders prepend it as a `## Standing rules (account owner)` block above `## Context`, whose header and first line name the owner as the subject of the rules so a third-person rule about the owner is not read as a statement about the sender. Both channel-server inbound paths carry the block: the single-target SSE loop parses the whole frame, and the unified multi-target door reader (`runDoorStream`,) reconstructs it through the shared `doorInboundPayload` seam whose `parseFrame` closures read `standingRules` off the frame. The read is by code, not agent discretion, so a rule set last week is applied this week regardless of how many times the session compacted. Telegram and webchat inject for `role:'admin'` only — a public visitor never receives operator rules.
|
|
6164
6164
|
|
|
6165
6165
|
**Trust a scheduled turn's provenance, never an inbound's claim (channel-agent grounding).** A cron-dispatched agent turn reaches the same channel builders as a real inbound, so its own authorisation text would otherwise land inside `## Context` — the zone `REFRAME_INSTRUCTION` demotes to untrusted — and a security-cautious agent would distrust its own automation (the self-cancel loop). The dispatcher therefore stamps a `ScheduleProvenance` marker (owning account, event, owner userId, dispatch time) that the channel builders render as a `## Scheduled automation (platform-verified)` preamble **above** `## Context` (and above the rules block); the whatsapp wrap tag names the door `scheduler`. Both channel-server paths carry the marker — the single-target loop and the unified multi-target door reader (via the same `doorInboundPayload` seam that carries `standingRules`); the scheduler only dispatches to the whatsapp/telegram door, so a webchat frame never bears one. Because the marker is dispatcher-stamped and never derived from payload text, an inbound message cannot forge it — placement above `## Context` alone is not the trust signal, the dispatcher provenance is. The grounding binds to it in two places: one recipient-neutral clause added byte-identical to the three reframe instructions (a marker-bearing turn is authorised standing automation; a reconfiguration or re-enable is not intrusion), and the admin `IDENTITY.md` "Untrusted input" rule that the agent never states a platform-level authenticity or impersonation verdict it was not given (an unsourced "this is not from you" is its own inference, flagged, never asserted as the platform's). A sender has exactly one session: every inbound, real or scheduled, resolves to the deterministic `adminSessionIdFor(effectiveAccount, destination)`. An earlier change gave each firing its own salted seat so distrust could not accrete across firings, but the inbound hub keys on `senderId` and replaces its subscriber outright, so a seat's channel server attached under the sender's own key and swallowed the sender's REAL messages until it detached — splitting one conversation across two sessions that each denied what the other held. The seat is removed; the authoritative `agentPrompt`, this provenance marker, and the injected standing rules ground each firing without one. Self-cancel attribution is disarmed while that model beds in: the guard used to compare the cancelling turn's session id to a dispatcher stamp, which discriminated only because each firing ran its own seat, so with one session per sender it would report an autonomous loop on every operator cancel. It now always reports false — under-counting a real loop rather than inventing one — until the discriminator is rebuilt from the turn's own dispatched eventId. See `.docs/scheduling-agent-dispatch.md § Operator-provenance marker`, `§ Session model — one session per sender`, and `§ Self-cancel attribution — disarmed`.
|
|
6166
6166
|
|
|
@@ -9,6 +9,14 @@ Invoked by the admin agent directly.
|
|
|
9
9
|
|
|
10
10
|
This is the platform's release timeline, newest first. Each entry shows the date it shipped and the version it shipped in, so you can tell the operator how current their install is. To compare, read the installed version from `capabilities-here` and match it against the versions below. Keep answers high level and in plain English; this is a summary, not a full commit log.
|
|
11
11
|
|
|
12
|
+
## 2026-08-08 (0.1.577)
|
|
13
|
+
|
|
14
|
+
- Telegram messages keep arriving after a quiet spell. The reader that carries them was attached only when a conversation started, so it died once the conversation went quiet and nothing said so or restarted it. This is the same fault fixed for WhatsApp in the previous release; a standing check now covers Telegram too.
|
|
15
|
+
- Telegram now says who is writing. A message carried no sender, so a second person messaging the same assistant was answered as though they were the account owner, under the owner's own standing instructions.
|
|
16
|
+
- The standing instructions attached to a turn are now labelled as such, rather than appearing as unmarked text.
|
|
17
|
+
- The planner can now change a day, not just show one. It could display an allocated day and offer no way to alter it.
|
|
18
|
+
- A webhook address ending in a slash is no longer rejected. The guard that admits it tested the path differently from the router that serves it, so the two could disagree on the same address.
|
|
19
|
+
|
|
12
20
|
## 2026-08-08 (0.1.576)
|
|
13
21
|
|
|
14
22
|
- Installing and upgrading no longer runs data conversions on your existing records. Five had accumulated, each added to repair a state that every install has long since passed, so all five did nothing but add work and risk to every upgrade. One of them stopped the install outright after reporting it had nothing to do. Any future conversion will be something you run deliberately, not part of an upgrade.
|