@rubytech/create-maxy-code 0.1.498 → 0.1.500
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 +3 -7
- package/payload/platform/lib/active-rules/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/active-rules/dist/index.js +20 -52
- package/payload/platform/lib/active-rules/dist/index.js.map +1 -1
- package/payload/platform/lib/active-rules/src/index.test.ts +35 -69
- package/payload/platform/lib/active-rules/src/index.ts +17 -58
- package/payload/platform/plugins/admin/PLUGIN.md +1 -1
- package/payload/platform/plugins/admin/hooks/__tests__/admin-authoring-observer.test.sh +135 -0
- package/payload/platform/plugins/admin/hooks/__tests__/prompt-optimiser-directive.test.sh +11 -2
- package/payload/platform/plugins/admin/hooks/prompt-optimiser-directive.sh +6 -3
- package/payload/platform/plugins/admin/mcp/dist/index.js +52 -107
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/admin/mcp/dist/skill-resolution.d.ts +8 -4
- package/payload/platform/plugins/admin/mcp/dist/skill-resolution.d.ts.map +1 -1
- package/payload/platform/plugins/admin/mcp/dist/skill-resolution.js +67 -50
- package/payload/platform/plugins/admin/mcp/dist/skill-resolution.js.map +1 -1
- package/payload/platform/plugins/admin/mcp/dist/store-skill-core.d.ts +33 -0
- package/payload/platform/plugins/admin/mcp/dist/store-skill-core.d.ts.map +1 -0
- package/payload/platform/plugins/admin/mcp/dist/store-skill-core.js +122 -0
- package/payload/platform/plugins/admin/mcp/dist/store-skill-core.js.map +1 -0
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +11 -5
- package/payload/platform/plugins/contacts/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/contacts/PLUGIN.md +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/index.js +3 -12
- package/payload/platform/plugins/contacts/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/__tests__/contact-lookup.test.js +105 -2
- package/payload/platform/plugins/contacts/mcp/dist/tools/__tests__/contact-lookup.test.js.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-lookup.d.ts +11 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-lookup.d.ts.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-lookup.js +56 -4
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-lookup.js.map +1 -1
- package/payload/platform/plugins/docs/references/contacts-guide.md +2 -0
- package/payload/platform/plugins/docs/references/internals.md +2 -2
- package/payload/platform/plugins/docs/references/outlook-guide.md +4 -0
- package/payload/platform/plugins/docs/references/plugins-guide.md +2 -2
- package/payload/platform/plugins/email/mcp/dist/__tests__/compose-signature.test.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/compose-signature.test.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/compose-signature.test.js +53 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/compose-signature.test.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/signature.test.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/signature.test.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/signature.test.js +50 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/signature.test.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/compose.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/compose.js +15 -2
- package/payload/platform/plugins/email/mcp/dist/lib/compose.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/signature.d.ts +29 -0
- package/payload/platform/plugins/email/mcp/dist/lib/signature.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/signature.js +55 -0
- package/payload/platform/plugins/email/mcp/dist/lib/signature.js.map +1 -0
- package/payload/platform/plugins/email/references/email-reference.md +10 -0
- package/payload/platform/plugins/email/skills/email-composition/SKILL.md +9 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-read-active-rules.test.js +10 -9
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-read-active-rules.test.js.map +1 -1
- package/payload/platform/plugins/memory/skills/conversational-memory/SKILL.md +2 -0
- package/payload/platform/plugins/memory/skills/preference-audit/SKILL.md +16 -0
- package/payload/platform/plugins/outlook/skills/outlook/SKILL.md +6 -0
- package/payload/platform/plugins/prompt-optimiser/skills/prompt-optimiser/SKILL.md +1 -1
- package/payload/platform/plugins/prompt-optimiser/skills/prompt-optimiser/__tests__/restatement-ladder.test.sh +2 -1
- package/payload/platform/services/claude-session-manager/dist/index.js +26 -1
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/preference-reconciliation-audit.d.ts +19 -19
- package/payload/platform/services/claude-session-manager/dist/preference-reconciliation-audit.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/preference-reconciliation-audit.js +28 -34
- package/payload/platform/services/claude-session-manager/dist/preference-reconciliation-audit.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/skill-registration-drift.d.ts +15 -0
- package/payload/platform/services/claude-session-manager/dist/skill-registration-drift.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/skill-registration-drift.js +61 -0
- package/payload/platform/services/claude-session-manager/dist/skill-registration-drift.js.map +1 -1
- package/payload/platform/templates/agents/admin/IDENTITY.md +1 -1
- package/payload/server/public/assets/{AdminLoginScreens-Ce5aBBWF.js → AdminLoginScreens-CKg_Dlpv.js} +1 -1
- package/payload/server/public/assets/{admin-BWb8me-E.js → admin-wtSUU5N0.js} +1 -1
- package/payload/server/public/assets/chat-DRuIxGtE.js +1 -0
- package/payload/server/public/assets/data-BdZCvyvN.js +1 -0
- package/payload/server/public/assets/{operator-icJwMkZF.js → operator-DglDYmdV.js} +1 -1
- package/payload/server/public/assets/{page-iG57nKOg.js → page-C-5sTAE-.js} +2 -2
- package/payload/server/public/assets/{page-DUgrGrJp.js → page-C8G5p4jr.js} +1 -1
- package/payload/server/public/assets/{public-CkOumzpf.js → public-DqJjXR4p.js} +1 -1
- package/payload/server/public/assets/{useVoiceRecorder-Cu6zvuFx.js → useVoiceRecorder-Cj-VZDKO.js} +1 -1
- package/payload/server/public/chat.html +3 -3
- package/payload/server/public/data.html +3 -3
- package/payload/server/public/index.html +4 -4
- package/payload/server/public/operator.html +4 -4
- package/payload/server/public/public.html +3 -3
- package/payload/server/server.js +89 -63
- package/payload/server/public/assets/chat-qtOiJX-Y.js +0 -1
- package/payload/server/public/assets/data-BjqC4hyM.js +0 -1
package/package.json
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
export declare const SAME_SUBJECT_COSINE = 0.85;
|
|
2
|
-
export declare const INJECTION_RULE_CAP = 12;
|
|
3
2
|
export declare const ACTIVE_CANDIDATE_FLOOR = 0.4;
|
|
4
|
-
export declare const ACTIVE_CANDIDATE_LIMIT = 30;
|
|
5
3
|
export interface RuleCandidate {
|
|
6
4
|
preferenceId: string;
|
|
7
5
|
category: string;
|
|
@@ -19,9 +17,7 @@ export interface ActiveRule {
|
|
|
19
17
|
confidence: number;
|
|
20
18
|
observedAt: string;
|
|
21
19
|
}
|
|
22
|
-
export declare function
|
|
23
|
-
export declare function deconflictBySubject(records: RuleCandidate[], cosineThreshold?: number): ActiveRule[];
|
|
24
|
-
export declare function formatStandingRulesBlock(rules: ActiveRule[], cap?: number): string;
|
|
20
|
+
export declare function formatStandingRulesBlock(rules: ActiveRule[]): string;
|
|
25
21
|
interface MinimalSession {
|
|
26
22
|
run: (q: string, p: Record<string, unknown>) => Promise<{
|
|
27
23
|
records: Array<{
|
|
@@ -43,7 +39,7 @@ export interface StandingRulesInjection {
|
|
|
43
39
|
}
|
|
44
40
|
export declare function resolveOwnerUserId(session: MinimalSession, accountId: string): Promise<string | null>;
|
|
45
41
|
export declare function queryActiveRuleCandidates(session: MinimalSession, accountId: string): Promise<RuleCandidate[]>;
|
|
46
|
-
export declare function resolveActiveRules(session: MinimalSession, accountId: string
|
|
47
|
-
export declare function activeRuleSet(session: MinimalSession, accountId: string
|
|
42
|
+
export declare function resolveActiveRules(session: MinimalSession, accountId: string): Promise<ActiveRuleResolution>;
|
|
43
|
+
export declare function activeRuleSet(session: MinimalSession, accountId: string): Promise<ActiveRule[]>;
|
|
48
44
|
export {};
|
|
49
45
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
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;AAKD,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,MAAM,CAIpE;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;AAID,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"}
|
|
@@ -5,56 +5,25 @@
|
|
|
5
5
|
// free of neo4j-driver types so both an MCP process and the UI can import the
|
|
6
6
|
// built dist.
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
-
exports.
|
|
9
|
-
exports.cosine = cosine;
|
|
10
|
-
exports.deconflictBySubject = deconflictBySubject;
|
|
8
|
+
exports.ACTIVE_CANDIDATE_FLOOR = exports.SAME_SUBJECT_COSINE = void 0;
|
|
11
9
|
exports.formatStandingRulesBlock = formatStandingRulesBlock;
|
|
12
10
|
exports.resolveOwnerUserId = resolveOwnerUserId;
|
|
13
11
|
exports.queryActiveRuleCandidates = queryActiveRuleCandidates;
|
|
14
12
|
exports.resolveActiveRules = resolveActiveRules;
|
|
15
13
|
exports.activeRuleSet = activeRuleSet;
|
|
14
|
+
// Write-time only: profile-update routes a same-subject write (a neighbour at
|
|
15
|
+
// or above this raw-cosine threshold) into the existing node instead of minting
|
|
16
|
+
// a duplicate. Injection no longer dedups — it surfaces every stored active
|
|
17
|
+
// preference verbatim (Task 1951) — so this constant has no read-side use.
|
|
16
18
|
exports.SAME_SUBJECT_COSINE = 0.85;
|
|
17
|
-
exports.INJECTION_RULE_CAP = 12;
|
|
18
19
|
exports.ACTIVE_CANDIDATE_FLOOR = 0.4;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
let dot = 0;
|
|
24
|
-
let na = 0;
|
|
25
|
-
let nb = 0;
|
|
26
|
-
for (let i = 0; i < a.length; i++) {
|
|
27
|
-
dot += a[i] * b[i];
|
|
28
|
-
na += a[i] * a[i];
|
|
29
|
-
nb += b[i] * b[i];
|
|
30
|
-
}
|
|
31
|
-
if (na === 0 || nb === 0)
|
|
32
|
-
return 0;
|
|
33
|
-
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
34
|
-
}
|
|
35
|
-
// records MUST be confidence-desc on entry. Greedy: the first record of a
|
|
36
|
-
// cluster is the winner; any later record within cosineThreshold of an
|
|
37
|
-
// already-kept winner is a rephrasing and is dropped. A record with no usable
|
|
38
|
-
// embedding is never folded — it is kept as its own subject (we cannot prove it
|
|
39
|
-
// is a duplicate).
|
|
40
|
-
function deconflictBySubject(records, cosineThreshold = exports.SAME_SUBJECT_COSINE) {
|
|
41
|
-
const winners = [];
|
|
42
|
-
for (const r of records) {
|
|
43
|
-
const emb = r.embedding;
|
|
44
|
-
const isDup = Array.isArray(emb) &&
|
|
45
|
-
emb.length > 0 &&
|
|
46
|
-
winners.some((w) => Array.isArray(w.embedding) &&
|
|
47
|
-
w.embedding.length > 0 &&
|
|
48
|
-
cosine(emb, w.embedding) >= cosineThreshold);
|
|
49
|
-
if (!isDup)
|
|
50
|
-
winners.push(r);
|
|
51
|
-
}
|
|
52
|
-
return winners.map(({ embedding: _embedding, ...rest }) => rest);
|
|
53
|
-
}
|
|
54
|
-
function formatStandingRulesBlock(rules, cap = exports.INJECTION_RULE_CAP) {
|
|
20
|
+
// Render every rule verbatim. Injection has no cap (Task 1951): concision is
|
|
21
|
+
// enforced at write time by the conversational-memory skill so each stored
|
|
22
|
+
// value is already short, not by truncating the block here.
|
|
23
|
+
function formatStandingRulesBlock(rules) {
|
|
55
24
|
if (rules.length === 0)
|
|
56
25
|
return "";
|
|
57
|
-
const lines = rules.
|
|
26
|
+
const lines = rules.map((r) => `- ${r.value}`);
|
|
58
27
|
return `## Standing rules\n${lines.join("\n")}`;
|
|
59
28
|
}
|
|
60
29
|
// The account's owner identity (Task 1461). One `AdminUser {role:'owner'}` is
|
|
@@ -80,11 +49,8 @@ async function resolveOwnerUserId(session, accountId) {
|
|
|
80
49
|
// most-recent wins de-confliction. The trashed predicate mirrors graph-trash's
|
|
81
50
|
// `notTrashed()` exactly (label + deletedAt) so this lib stays dependency-free.
|
|
82
51
|
async function queryOwnerCandidates(session, accountId, ownerUserId) {
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
// rejects it ("not a valid value. Must be a non-negative integer"). This lib
|
|
86
|
-
// is dependency-free (no neo4j-driver), so it cannot call `neo4j.int()`; the
|
|
87
|
-
// constant is a hardcoded integer, so inlining it is safe and gate-clean.
|
|
52
|
+
// No LIMIT — every owner admin preference above the floor is a candidate
|
|
53
|
+
// (Task 1951). The result is bounded by how many the owner has, not by a cap.
|
|
88
54
|
const result = await session.run(`
|
|
89
55
|
MATCH (up:UserProfile {accountId: $accountId, userId: $ownerUserId})-[:HAS_PREFERENCE]->(pref:Preference {scope: 'admin'})
|
|
90
56
|
WHERE NOT pref:Trashed AND pref.deletedAt IS NULL
|
|
@@ -94,7 +60,6 @@ async function queryOwnerCandidates(session, accountId, ownerUserId) {
|
|
|
94
60
|
pref.key AS key, pref.value AS value, pref.confidence AS confidence,
|
|
95
61
|
pref.observedAt AS observedAt, pref.embedding AS embedding
|
|
96
62
|
ORDER BY pref.confidence DESC, pref.observedAt DESC
|
|
97
|
-
LIMIT ${exports.ACTIVE_CANDIDATE_LIMIT}
|
|
98
63
|
`, { accountId, ownerUserId, floor: exports.ACTIVE_CANDIDATE_FLOOR });
|
|
99
64
|
return result.records.map((r) => ({
|
|
100
65
|
preferenceId: r.get("preferenceId"),
|
|
@@ -128,14 +93,17 @@ async function resolveCandidates(session, accountId) {
|
|
|
128
93
|
async function queryActiveRuleCandidates(session, accountId) {
|
|
129
94
|
return (await resolveCandidates(session, accountId)).candidates;
|
|
130
95
|
}
|
|
131
|
-
// The account's active
|
|
132
|
-
|
|
96
|
+
// The account's active rule-set plus its provenance. Every owner candidate is
|
|
97
|
+
// returned verbatim, in `confidence DESC, observedAt DESC` order — no cap, no
|
|
98
|
+
// de-confliction (Task 1951). Only the read-facing `embedding` is dropped.
|
|
99
|
+
async function resolveActiveRules(session, accountId) {
|
|
133
100
|
const { candidates, ownerUserId, source } = await resolveCandidates(session, accountId);
|
|
134
|
-
|
|
101
|
+
const rules = candidates.map(({ embedding: _embedding, ...rest }) => rest);
|
|
102
|
+
return { rules, ownerUserId, source };
|
|
135
103
|
}
|
|
136
104
|
// The rule array alone — the unchanged call boundary for profile-read and the
|
|
137
105
|
// ui/server injection wrapper.
|
|
138
|
-
async function activeRuleSet(session, accountId
|
|
139
|
-
return (await resolveActiveRules(session, accountId
|
|
106
|
+
async function activeRuleSet(session, accountId) {
|
|
107
|
+
return (await resolveActiveRules(session, accountId)).rules;
|
|
140
108
|
}
|
|
141
109
|
//# sourceMappingURL=index.js.map
|
|
@@ -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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"}
|
|
@@ -1,76 +1,12 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import {
|
|
3
|
-
cosine,
|
|
4
|
-
deconflictBySubject,
|
|
5
3
|
formatStandingRulesBlock,
|
|
6
4
|
resolveActiveRules,
|
|
7
5
|
activeRuleSet,
|
|
8
6
|
queryActiveRuleCandidates,
|
|
9
|
-
INJECTION_RULE_CAP,
|
|
10
7
|
type RuleCandidate,
|
|
11
8
|
} from "./index.js";
|
|
12
9
|
|
|
13
|
-
const vec = (n: number, fill: number): number[] => Array(n).fill(fill);
|
|
14
|
-
|
|
15
|
-
function cand(over: Partial<RuleCandidate>): RuleCandidate {
|
|
16
|
-
return {
|
|
17
|
-
preferenceId: "p",
|
|
18
|
-
category: "communication",
|
|
19
|
-
key: "k",
|
|
20
|
-
value: "v",
|
|
21
|
-
confidence: 0.5,
|
|
22
|
-
observedAt: "2026-07-01T00:00:00Z",
|
|
23
|
-
embedding: vec(4, 1),
|
|
24
|
-
...over,
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
describe("cosine", () => {
|
|
29
|
-
it("is 1 for identical vectors and 0 for an empty one", () => {
|
|
30
|
-
expect(cosine([1, 0, 0], [1, 0, 0])).toBeCloseTo(1, 6);
|
|
31
|
-
expect(cosine([], [1, 2, 3])).toBe(0);
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
describe("deconflictBySubject", () => {
|
|
36
|
-
it("folds a near-duplicate into the higher-confidence winner", () => {
|
|
37
|
-
const a = cand({
|
|
38
|
-
preferenceId: "a",
|
|
39
|
-
key: "header_rule",
|
|
40
|
-
value: "use header A",
|
|
41
|
-
confidence: 0.8,
|
|
42
|
-
embedding: [1, 0, 0, 0],
|
|
43
|
-
});
|
|
44
|
-
const b = cand({
|
|
45
|
-
preferenceId: "b",
|
|
46
|
-
key: "doc_header",
|
|
47
|
-
value: "use header A restated",
|
|
48
|
-
confidence: 0.5,
|
|
49
|
-
embedding: [0.99, 0.01, 0, 0],
|
|
50
|
-
});
|
|
51
|
-
const out = deconflictBySubject([a, b], 0.85);
|
|
52
|
-
expect(out.map((r) => r.preferenceId)).toEqual(["a"]);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("keeps genuinely different subjects", () => {
|
|
56
|
-
const a = cand({ preferenceId: "a", embedding: [1, 0, 0, 0] });
|
|
57
|
-
const b = cand({ preferenceId: "b", embedding: [0, 1, 0, 0] });
|
|
58
|
-
expect(deconflictBySubject([a, b], 0.85).map((r) => r.preferenceId)).toEqual([
|
|
59
|
-
"a",
|
|
60
|
-
"b",
|
|
61
|
-
]);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("never folds a candidate with no embedding", () => {
|
|
65
|
-
const a = cand({ preferenceId: "a", embedding: [1, 0, 0, 0] });
|
|
66
|
-
const b = cand({ preferenceId: "b", embedding: null });
|
|
67
|
-
expect(deconflictBySubject([a, b], 0.85).map((r) => r.preferenceId)).toEqual([
|
|
68
|
-
"a",
|
|
69
|
-
"b",
|
|
70
|
-
]);
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
|
|
74
10
|
// A fake Neo4j session that dispatches on query text: the owner-resolution
|
|
75
11
|
// query (matches `role: 'owner'`) returns a configurable owner userId or none;
|
|
76
12
|
// the candidate query (matches HAS_PREFERENCE) returns configurable pref rows
|
|
@@ -115,7 +51,7 @@ const prefRow = (over: Partial<RuleCandidate>): Record<string, unknown> => ({
|
|
|
115
51
|
...over,
|
|
116
52
|
});
|
|
117
53
|
|
|
118
|
-
describe("resolveActiveRules — owner-scoped
|
|
54
|
+
describe("resolveActiveRules — owner-scoped, uncapped, verbatim", () => {
|
|
119
55
|
it("returns the owner's prefs with source=owner when an owner exists", async () => {
|
|
120
56
|
const s = fakeSession({
|
|
121
57
|
ownerId: "owner-35fed05e",
|
|
@@ -132,6 +68,36 @@ describe("resolveActiveRules — owner-scoped selection", () => {
|
|
|
132
68
|
expect(s.candidateRuns).toBe(1);
|
|
133
69
|
});
|
|
134
70
|
|
|
71
|
+
it("returns MORE than 12 candidates with no cap and no truncation", async () => {
|
|
72
|
+
const rows = Array.from({ length: 20 }, (_, i) =>
|
|
73
|
+
prefRow({
|
|
74
|
+
preferenceId: `p${i}`,
|
|
75
|
+
value: `rule ${i}`,
|
|
76
|
+
confidence: 1 - i * 0.01,
|
|
77
|
+
}),
|
|
78
|
+
);
|
|
79
|
+
const s = fakeSession({ ownerId: "owner-x", prefRows: rows });
|
|
80
|
+
const res = await resolveActiveRules(s, "acct-1");
|
|
81
|
+
expect(res.rules.length).toBe(20);
|
|
82
|
+
expect(res.rules.map((r) => r.preferenceId)).toEqual(
|
|
83
|
+
rows.map((r) => r.preferenceId),
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("preserves the candidate order verbatim (no reordering, no dedup)", async () => {
|
|
88
|
+
// Two rows that a cosine dedup would once have folded (identical embeddings)
|
|
89
|
+
// are BOTH returned now — injection never dedups.
|
|
90
|
+
const s = fakeSession({
|
|
91
|
+
ownerId: "owner-1",
|
|
92
|
+
prefRows: [
|
|
93
|
+
prefRow({ preferenceId: "a", value: "use header A", embedding: [1, 0, 0, 0] }),
|
|
94
|
+
prefRow({ preferenceId: "b", value: "use header A restated", embedding: [1, 0, 0, 0] }),
|
|
95
|
+
],
|
|
96
|
+
});
|
|
97
|
+
const res = await resolveActiveRules(s, "acct-1");
|
|
98
|
+
expect(res.rules.map((r) => r.preferenceId)).toEqual(["a", "b"]);
|
|
99
|
+
});
|
|
100
|
+
|
|
135
101
|
it("returns empty with source=no-owner and never runs the candidate query", async () => {
|
|
136
102
|
const s = fakeSession({ ownerId: null });
|
|
137
103
|
const res = await resolveActiveRules(s, "acct-1");
|
|
@@ -176,10 +142,10 @@ describe("resolveActiveRules — owner-scoped selection", () => {
|
|
|
176
142
|
|
|
177
143
|
describe("formatStandingRulesBlock", () => {
|
|
178
144
|
it("returns empty string for no rules", () => {
|
|
179
|
-
expect(formatStandingRulesBlock([]
|
|
145
|
+
expect(formatStandingRulesBlock([])).toBe("");
|
|
180
146
|
});
|
|
181
147
|
|
|
182
|
-
it("
|
|
148
|
+
it("renders every rule, with no cap", () => {
|
|
183
149
|
const rules = Array.from({ length: 20 }, (_, i) => ({
|
|
184
150
|
preferenceId: `p${i}`,
|
|
185
151
|
category: "communication",
|
|
@@ -188,8 +154,8 @@ describe("formatStandingRulesBlock", () => {
|
|
|
188
154
|
confidence: 1 - i * 0.01,
|
|
189
155
|
observedAt: "2026-07-01T00:00:00Z",
|
|
190
156
|
}));
|
|
191
|
-
const block = formatStandingRulesBlock(rules
|
|
157
|
+
const block = formatStandingRulesBlock(rules);
|
|
192
158
|
expect(block).toContain("## Standing rules");
|
|
193
|
-
expect((block.match(/^- /gm) ?? []).length).toBe(
|
|
159
|
+
expect((block.match(/^- /gm) ?? []).length).toBe(20);
|
|
194
160
|
});
|
|
195
161
|
});
|
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
// free of neo4j-driver types so both an MCP process and the UI can import the
|
|
5
5
|
// built dist.
|
|
6
6
|
|
|
7
|
+
// Write-time only: profile-update routes a same-subject write (a neighbour at
|
|
8
|
+
// or above this raw-cosine threshold) into the existing node instead of minting
|
|
9
|
+
// a duplicate. Injection no longer dedups — it surfaces every stored active
|
|
10
|
+
// preference verbatim (Task 1951) — so this constant has no read-side use.
|
|
7
11
|
export const SAME_SUBJECT_COSINE = 0.85;
|
|
8
|
-
export const INJECTION_RULE_CAP = 12;
|
|
9
12
|
export const ACTIVE_CANDIDATE_FLOOR = 0.4;
|
|
10
|
-
export const ACTIVE_CANDIDATE_LIMIT = 30;
|
|
11
13
|
|
|
12
14
|
export interface RuleCandidate {
|
|
13
15
|
preferenceId: string;
|
|
@@ -28,52 +30,12 @@ export interface ActiveRule {
|
|
|
28
30
|
observedAt: string;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
let nb = 0;
|
|
36
|
-
for (let i = 0; i < a.length; i++) {
|
|
37
|
-
dot += a[i] * b[i];
|
|
38
|
-
na += a[i] * a[i];
|
|
39
|
-
nb += b[i] * b[i];
|
|
40
|
-
}
|
|
41
|
-
if (na === 0 || nb === 0) return 0;
|
|
42
|
-
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// records MUST be confidence-desc on entry. Greedy: the first record of a
|
|
46
|
-
// cluster is the winner; any later record within cosineThreshold of an
|
|
47
|
-
// already-kept winner is a rephrasing and is dropped. A record with no usable
|
|
48
|
-
// embedding is never folded — it is kept as its own subject (we cannot prove it
|
|
49
|
-
// is a duplicate).
|
|
50
|
-
export function deconflictBySubject(
|
|
51
|
-
records: RuleCandidate[],
|
|
52
|
-
cosineThreshold: number = SAME_SUBJECT_COSINE,
|
|
53
|
-
): ActiveRule[] {
|
|
54
|
-
const winners: RuleCandidate[] = [];
|
|
55
|
-
for (const r of records) {
|
|
56
|
-
const emb = r.embedding;
|
|
57
|
-
const isDup =
|
|
58
|
-
Array.isArray(emb) &&
|
|
59
|
-
emb.length > 0 &&
|
|
60
|
-
winners.some(
|
|
61
|
-
(w) =>
|
|
62
|
-
Array.isArray(w.embedding) &&
|
|
63
|
-
w.embedding.length > 0 &&
|
|
64
|
-
cosine(emb, w.embedding) >= cosineThreshold,
|
|
65
|
-
);
|
|
66
|
-
if (!isDup) winners.push(r);
|
|
67
|
-
}
|
|
68
|
-
return winners.map(({ embedding: _embedding, ...rest }) => rest);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function formatStandingRulesBlock(
|
|
72
|
-
rules: ActiveRule[],
|
|
73
|
-
cap: number = INJECTION_RULE_CAP,
|
|
74
|
-
): string {
|
|
33
|
+
// Render every rule verbatim. Injection has no cap (Task 1951): concision is
|
|
34
|
+
// enforced at write time by the conversational-memory skill so each stored
|
|
35
|
+
// value is already short, not by truncating the block here.
|
|
36
|
+
export function formatStandingRulesBlock(rules: ActiveRule[]): string {
|
|
75
37
|
if (rules.length === 0) return "";
|
|
76
|
-
const lines = rules.
|
|
38
|
+
const lines = rules.map((r) => `- ${r.value}`);
|
|
77
39
|
return `## Standing rules\n${lines.join("\n")}`;
|
|
78
40
|
}
|
|
79
41
|
|
|
@@ -139,11 +101,8 @@ async function queryOwnerCandidates(
|
|
|
139
101
|
accountId: string,
|
|
140
102
|
ownerUserId: string,
|
|
141
103
|
): Promise<RuleCandidate[]> {
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
// rejects it ("not a valid value. Must be a non-negative integer"). This lib
|
|
145
|
-
// is dependency-free (no neo4j-driver), so it cannot call `neo4j.int()`; the
|
|
146
|
-
// constant is a hardcoded integer, so inlining it is safe and gate-clean.
|
|
104
|
+
// No LIMIT — every owner admin preference above the floor is a candidate
|
|
105
|
+
// (Task 1951). The result is bounded by how many the owner has, not by a cap.
|
|
147
106
|
const result = await session.run(
|
|
148
107
|
`
|
|
149
108
|
MATCH (up:UserProfile {accountId: $accountId, userId: $ownerUserId})-[:HAS_PREFERENCE]->(pref:Preference {scope: 'admin'})
|
|
@@ -154,7 +113,6 @@ async function queryOwnerCandidates(
|
|
|
154
113
|
pref.key AS key, pref.value AS value, pref.confidence AS confidence,
|
|
155
114
|
pref.observedAt AS observedAt, pref.embedding AS embedding
|
|
156
115
|
ORDER BY pref.confidence DESC, pref.observedAt DESC
|
|
157
|
-
LIMIT ${ACTIVE_CANDIDATE_LIMIT}
|
|
158
116
|
`,
|
|
159
117
|
{ accountId, ownerUserId, floor: ACTIVE_CANDIDATE_FLOOR },
|
|
160
118
|
);
|
|
@@ -201,14 +159,16 @@ export async function queryActiveRuleCandidates(
|
|
|
201
159
|
return (await resolveCandidates(session, accountId)).candidates;
|
|
202
160
|
}
|
|
203
161
|
|
|
204
|
-
// The account's active
|
|
162
|
+
// The account's active rule-set plus its provenance. Every owner candidate is
|
|
163
|
+
// returned verbatim, in `confidence DESC, observedAt DESC` order — no cap, no
|
|
164
|
+
// de-confliction (Task 1951). Only the read-facing `embedding` is dropped.
|
|
205
165
|
export async function resolveActiveRules(
|
|
206
166
|
session: MinimalSession,
|
|
207
167
|
accountId: string,
|
|
208
|
-
cap: number = INJECTION_RULE_CAP,
|
|
209
168
|
): Promise<ActiveRuleResolution> {
|
|
210
169
|
const { candidates, ownerUserId, source } = await resolveCandidates(session, accountId);
|
|
211
|
-
|
|
170
|
+
const rules = candidates.map(({ embedding: _embedding, ...rest }) => rest);
|
|
171
|
+
return { rules, ownerUserId, source };
|
|
212
172
|
}
|
|
213
173
|
|
|
214
174
|
// The rule array alone — the unchanged call boundary for profile-read and the
|
|
@@ -216,7 +176,6 @@ export async function resolveActiveRules(
|
|
|
216
176
|
export async function activeRuleSet(
|
|
217
177
|
session: MinimalSession,
|
|
218
178
|
accountId: string,
|
|
219
|
-
cap: number = INJECTION_RULE_CAP,
|
|
220
179
|
): Promise<ActiveRule[]> {
|
|
221
|
-
return (await resolveActiveRules(session, accountId
|
|
180
|
+
return (await resolveActiveRules(session, accountId)).rules;
|
|
222
181
|
}
|
|
@@ -196,7 +196,7 @@ Tools are available via the `admin` MCP server.
|
|
|
196
196
|
- `hooks/mcp-tool-missing.sh` — **PostToolUse hook on `mcp__.*` (directive 3).** Defence-in-depth for the `No such tool available: mcp__…` failure class that the name-binding is built to eliminate. Fires on any MCP tool call; no-op unless the `tool_response` carries `No such tool available` AND the qualified name resolves to a maxy plugin (read from the generated `hooks/lib/maxy-mcp-plugins.txt`). On a maxy match it logs one deterministic `[mcp-tool-missing] server=<server> tool=<tool>` line and exits 2 with a fixed envelope on stderr, so the agent relays a named server-unavailable failure instead of narrating "warming up" or blind-retrying. A missing non-maxy bridge tool (Playwright etc., upstream-owned) passes through (exit 0). The maxy-plugin list is regenerated and gate-diffed by `platform/scripts/check-canonical-tool-names.mjs`.
|
|
197
197
|
- `hooks/post-tool-use-agent.sh` — **PostToolUse hook on `Agent`.** Drains any subagent hook-decision buffers under `~/.maxy-code/logs/hook-decisions/` modified since this parent's previous PostToolUse-Agent fire (cursor file keyed by parent session id), prints one `[hook-propagate]` line per record to stdout — Claude Code attaches the stdout as a `hook_success` attachment on the parent JSONL, making the records grep-queryable from the parent session alone. Rotates consumed buffers to `consumed/`. Emits one `[hook-propagate-census] parentSession=<…> subagentHooksObserved=<N> attachmentsEmitted=<M>` line per fire to stdout and server.log; `N != M` is the propagation regression signal. The companion emitter library `hooks/lib/hook-emit.sh` is sourced by `post-tool-use-agent.sh` and any other hook that records a block decision (4 KB stderr truncation, `truncated=true` set on the record).
|
|
198
198
|
- `hooks/admin-authoring-observer.sh` — **PostToolUse hook on Write and Edit.** Observation only — never blocks; exits 0 on every path. Fires when the admin agent (not a specialist subagent — gated by `MAXY_SPECIALIST` env) writes or edits a file under `<accountDir>/output/`. Walks the session transcript from the latest real-user turn forward to detect any prior `Task` `tool_use` whose `subagent_type` starts with `specialists:`. Emits one stderr line `[admin-authoring] inline-write path=<rel> priorSpecialistSpawnInTurn=<true|false|unknown>`. A `false` value on a long-form prose file is the regression signal this hook was designed to make visible — the BioSymm proposal session (admin authored a customer-facing proposal inline despite content-producer being installed) is the failure mode this surfaces mechanically. Mechanical enforcement (refuse the write, force a re-spawn) is deferred per the task spec.
|
|
199
|
-
- `hooks/prompt-optimiser-directive.sh` — **UserPromptSubmit hook.** Injects the standing prompt-optimiser restatement directive plus the per-turn
|
|
199
|
+
- `hooks/prompt-optimiser-directive.sh` — **UserPromptSubmit hook.** Injects the standing prompt-optimiser restatement directive plus the per-turn four-rung routing ladder as `additionalContext`: **(1)** delegate to the specialist that owns the deliverable via the Agent tool, where the brief states the outcome plus binding constraints, never lines/anchors/literal text; **(2)** only if none fits, load an admin-usable skill with `skill-load`; **(3)** only if the deliverable recurs and no specialist or skill owns it, author its owning skill via `skill-builder` (or `agent-builder` for a worker agent) and produce through it; **(4)** only if it is a genuine one-off, author inline as the named last resort. Re-emits the full agent roster (`agents/admin/AGENTS.md`) and the full admin-usable skills list (`agents/admin/ADMIN-SKILLS.md`) every turn by reading the two generated files from the account dir (the hook fires with the account dir as cwd); it never walks the plugins tree per turn. Fail-open is **visible**: a missing list logs `[prompt-optimiser] missing=<AGENTS.md\|ADMIN-SKILLS.md> emitting-partial` to stderr and the ladder still injects. The trivial-turn skip (one-word confirmation, slash-command, direct continuation) is unchanged. **Staleness:** `ADMIN-SKILLS.md` is regenerated only by `setup-account.sh`; a plugin add/remove since the last setup leaves the list stale — compare `ADMIN-SKILLS.md` mtime against the newest `SKILL.md` mtime and re-run setup to refresh. The list generator is `platform/scripts/lib/admin-skills-bootstrap.sh`; it logs `[admin-skills] scanned=<N> admin-usable=<M> no-declaration=<K>` (failure signature: `admin-usable=0` while `scanned>0`, or any `missing-declaration` line). The directive also carries a standing CAPABILITY-QUESTIONS-ARE-OWNED-WORK clause (how-to / "do you have instructions for X" / config questions about platform features are answered from the owning specialist or plugin tool/reference, never from training memory), and the hook appends a durable `<ts> [prompt-optimiser-directive] injected len=<n> session=<id>` breadcrumb to `$LOG_DIR/prompt-optimiser-directive.log` so per-turn injection is greppable, not stderr-only. The directive is also **suppressed on native channel turns** — when the parsed `.prompt` starts with the `<channel source=` event marker, the hook logs `[prompt-optimiser-directive] skipped reason=channel-turn session=<id>` to stderr and exits without injecting, because the channel service already reframes the inbound into a select-and-dispatch turn (`composeAdminContent`, see `.docs/whatsapp-inbound-lifeline.md`). Marker-matched at start-of-prompt only, so an admin/Terminal prompt that merely mentions "channel" still gets the directive; fail-open injects if the prompt cannot be parsed.
|
|
200
200
|
- `hooks/preference-consult-directive.sh` — **UserPromptSubmit hook.** Injects a standing `<preference-adherence>` block naming the two-layer preference architecture (layer 1 the fixed block already in front of the agent every turn; layer 2 the account's own saved `Preference` records, read with `profile-read`, holding signature policy, header, naming and styling) and two directives: consult the relevant layer-2 preferences with `profile-read` before any customer-facing document deliverable, and store finished deliverables under `documents/` or the project folder because `output/` is scratch of last resort. Prompt-independent and always exit 0 (fail-open). Breadcrumb (stderr): `[pref-wrapper] op=inject`. This is the advisory companion to the `preference-consult-gate` hard gate.
|
|
201
201
|
- `hooks/datetime-inject.sh` — **UserPromptSubmit hook.** Injects a fresh `<datetime>` block as `additionalContext` on every turn (`refresh=per-turn`), so the agent always has the current instant — date, time, timezone, weekday, and the raw UTC ISO — without hand-computing it and without depending on Claude Code's spawn-time "Today's date" line going stale. The instant is computed fresh each turn; the timezone is **not** computed here — it is the operator's own zone, resolved from their `UserProfile.timezone` on the graph once per admin spawn and stamped into `MAXY_ACCOUNT_TZ` (see `platform/services/claude-session-manager/src/account-timezone.ts`). When `MAXY_ACCOUNT_TZ` is empty (a non-admin turn, an unset profile timezone, or a neo4j hiccup at spawn) the block falls back to the server's own zone and labels the source, so a reader can always tell which clock the agent saw. This is the current-time surface the `datetime` skill points at. **Fail-open** on every path (missing `python3` or `node`, any formatting error → exit 0, empty stdout — the turn is never blocked). The hook appends `<ts> [datetime-inject] injected tz=<zone> source=<graph|server-fallback> refresh=per-turn session=<id>` to `$LOG_DIR/datetime-inject.log` so "did the agent have the right time this turn, and from which clock?" is answerable from logs without reproduction.
|
|
202
202
|
- `hooks/prompt-optimiser-compliance.sh` — **Stop hook.** After each admin turn, reads the just-finished turn from `transcript_path` and appends `<ts> [prompt-optimiser-compliance] directive-fired no-route-taken session=<id8> prompt="<clip>"` to `$LOG_DIR/prompt-optimiser-directive.log` (and stderr) when the routing directive fired, the prompt was non-trivial (not a slash-command, not a one-word confirmation), and the turn took **no route** — no `Agent` dispatch, no `Skill` load, no `ToolSearch`, no `mcp__*` tool call. This is the standing compliance signal that surfaces the session-`da0b12d4` failure class (agent answers a capability question from memory) as a visible event instead of a silent stale answer. Directive-fired is detected by the marker `PROMPT-OPTIMISER DIRECTIVE` in the turn slice, so it is robust to the CC-version difference in how `UserPromptSubmit` `additionalContext` is recorded (`attachment`/`hook_success` vs `hook_additional_context`). **Known limitation:** "direct continuation of the prior turn" is not detectable from the transcript, so a continuation turn that legitimately needs no route can be flagged; treat the log as a review signal, not a gate. **Fail-open:** no python3, no `transcript_path`, or an unreadable transcript → exit 0, no output. Lives in the same log as the directive breadcrumb, so a single `grep` interleaves "fired" and "no-route" into one per-session timeline; cross-check via `platform/scripts/logs-read.sh <sessionKey>`. This is a lightweight transcript read, not a per-turn spawn (contrast the turn recorder below).
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Task 1961 — the classless inline-write line is the recurring-deliverable
|
|
3
|
+
# arming signal.
|
|
4
|
+
#
|
|
5
|
+
# admin-authoring-observer.sh emits, on stderr, one line when the admin
|
|
6
|
+
# process (MAXY_SPECIALIST empty) writes under <accountDir>/output/:
|
|
7
|
+
# [admin-authoring] inline-write path=<rel> priorSpecialistSpawnInTurn=<true|false|unknown>
|
|
8
|
+
#
|
|
9
|
+
# priorSpecialistSpawnInTurn=false is the arming signal: an inline output
|
|
10
|
+
# write with no specialist dispatched earlier in the turn. This suite pins
|
|
11
|
+
# fixture transcripts via ADMIN_AUTHORING_TRANSCRIPT_PATH and asserts:
|
|
12
|
+
# - false when the transcript has no specialists: Task after the last user turn
|
|
13
|
+
# - true when it does
|
|
14
|
+
# - unknown when no transcript is available
|
|
15
|
+
# - no line at all for writes outside output/, or from a specialist process
|
|
16
|
+
set -u
|
|
17
|
+
HOOK="$(cd "$(dirname "$0")/.." && pwd)/admin-authoring-observer.sh"
|
|
18
|
+
[[ -x "$HOOK" ]] || { echo "FAIL: $HOOK not executable"; exit 1; }
|
|
19
|
+
|
|
20
|
+
PASS=0; FAIL=0
|
|
21
|
+
pass() { PASS=$((PASS+1)); echo "PASS: $1"; }
|
|
22
|
+
fail() { FAIL=$((FAIL+1)); echo "FAIL: $1" >&2; }
|
|
23
|
+
|
|
24
|
+
TMP=$(mktemp -d)
|
|
25
|
+
trap 'rm -rf "$TMP"' EXIT
|
|
26
|
+
ACCT="$TMP/account"
|
|
27
|
+
mkdir -p "$ACCT/output"
|
|
28
|
+
|
|
29
|
+
# Fixture transcripts.
|
|
30
|
+
NO_SPECIALIST="$TMP/no-specialist.jsonl"
|
|
31
|
+
cat >"$NO_SPECIALIST" <<'JSON'
|
|
32
|
+
{"type":"user","message":{"role":"user","content":"draft a quote for the Smith job"}}
|
|
33
|
+
{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"On it."}]}}
|
|
34
|
+
JSON
|
|
35
|
+
|
|
36
|
+
WITH_SPECIALIST="$TMP/with-specialist.jsonl"
|
|
37
|
+
cat >"$WITH_SPECIALIST" <<'JSON'
|
|
38
|
+
{"type":"user","message":{"role":"user","content":"draft a quote for the Smith job"}}
|
|
39
|
+
{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","name":"Task","input":{"subagent_type":"specialists:content-producer"}}]}}
|
|
40
|
+
JSON
|
|
41
|
+
|
|
42
|
+
# A Task whose subagent_type is not a specialist: must not count as a spawn.
|
|
43
|
+
NON_SPECIALIST_TASK="$TMP/non-specialist-task.jsonl"
|
|
44
|
+
cat >"$NON_SPECIALIST_TASK" <<'JSON'
|
|
45
|
+
{"type":"user","message":{"role":"user","content":"draft a quote for the Smith job"}}
|
|
46
|
+
{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","name":"Task","input":{"subagent_type":"general-purpose"}}]}}
|
|
47
|
+
JSON
|
|
48
|
+
|
|
49
|
+
# A specialist dispatched in a PRIOR turn must not arm the current turn: the
|
|
50
|
+
# hook walks forward only from the last real-user turn.
|
|
51
|
+
PRIOR_TURN_SPECIALIST="$TMP/prior-turn-specialist.jsonl"
|
|
52
|
+
cat >"$PRIOR_TURN_SPECIALIST" <<'JSON'
|
|
53
|
+
{"type":"user","message":{"role":"user","content":"earlier request"}}
|
|
54
|
+
{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","name":"Task","input":{"subagent_type":"specialists:content-producer"}}]}}
|
|
55
|
+
{"type":"user","message":{"role":"user","content":"draft a quote for the Smith job"}}
|
|
56
|
+
{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"On it."}]}}
|
|
57
|
+
JSON
|
|
58
|
+
|
|
59
|
+
# input <tool_name> <file_path> — builds the PostToolUse payload the hook parses.
|
|
60
|
+
mk_input() {
|
|
61
|
+
python3 -c '
|
|
62
|
+
import json, sys
|
|
63
|
+
print(json.dumps({
|
|
64
|
+
"hook_event_name": "PostToolUse",
|
|
65
|
+
"tool_name": sys.argv[1],
|
|
66
|
+
"tool_input": {"file_path": sys.argv[2]},
|
|
67
|
+
"transcript_path": ""
|
|
68
|
+
}, separators=(",", ":")))
|
|
69
|
+
' "$1" "$2"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# run <transcript-or-empty> <MAXY_SPECIALIST> <tool_name> <file_path> — fires the
|
|
73
|
+
# hook from inside the account dir (its output/ scope keys off $PWD) and returns
|
|
74
|
+
# stderr.
|
|
75
|
+
run() {
|
|
76
|
+
local transcript="$1" specialist="$2" tool="$3" fp="$4"
|
|
77
|
+
( cd "$ACCT" \
|
|
78
|
+
&& mk_input "$tool" "$fp" \
|
|
79
|
+
| ADMIN_AUTHORING_TRANSCRIPT_PATH="$transcript" MAXY_SPECIALIST="$specialist" \
|
|
80
|
+
bash "$HOOK" 2>&1 1>/dev/null )
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
ARM_RE='\[admin-authoring\] inline-write path=output/quote\.html priorSpecialistSpawnInTurn='
|
|
84
|
+
|
|
85
|
+
# 1. Arming signal: inline output write, no specialist in the turn.
|
|
86
|
+
OUT=$(run "$NO_SPECIALIST" "" Write "output/quote.html")
|
|
87
|
+
echo "$OUT" | grep -qE "${ARM_RE}false" \
|
|
88
|
+
&& pass "arming: priorSpecialistSpawnInTurn=false on inline output write" \
|
|
89
|
+
|| fail "arming false expected, got: $OUT"
|
|
90
|
+
|
|
91
|
+
# 2. Owned: a specialist was dispatched earlier in the same turn.
|
|
92
|
+
OUT=$(run "$WITH_SPECIALIST" "" Write "output/quote.html")
|
|
93
|
+
echo "$OUT" | grep -qE "${ARM_RE}true" \
|
|
94
|
+
&& pass "owned: priorSpecialistSpawnInTurn=true when specialist dispatched" \
|
|
95
|
+
|| fail "true expected, got: $OUT"
|
|
96
|
+
|
|
97
|
+
# 3. Unknown: no transcript available — line still fires, value is not the arming one.
|
|
98
|
+
OUT=$(run "$TMP/does-not-exist.jsonl" "" Write "output/quote.html")
|
|
99
|
+
echo "$OUT" | grep -qE "${ARM_RE}unknown" \
|
|
100
|
+
&& pass "unknown: priorSpecialistSpawnInTurn=unknown with no transcript" \
|
|
101
|
+
|| fail "unknown expected, got: $OUT"
|
|
102
|
+
|
|
103
|
+
# 4. Edit is observed like Write.
|
|
104
|
+
OUT=$(run "$NO_SPECIALIST" "" Edit "output/quote.html")
|
|
105
|
+
echo "$OUT" | grep -qE "${ARM_RE}false" \
|
|
106
|
+
&& pass "Edit under output/ is observed" \
|
|
107
|
+
|| fail "Edit expected arming line, got: $OUT"
|
|
108
|
+
|
|
109
|
+
# 4a. A non-specialist Task in the turn does not count as a spawn — still armed.
|
|
110
|
+
OUT=$(run "$NON_SPECIALIST_TASK" "" Write "output/quote.html")
|
|
111
|
+
echo "$OUT" | grep -qE "${ARM_RE}false" \
|
|
112
|
+
&& pass "arming: non-specialist Task does not disarm" \
|
|
113
|
+
|| fail "false expected for non-specialist Task, got: $OUT"
|
|
114
|
+
|
|
115
|
+
# 4b. A specialist in a PRIOR turn does not disarm the current turn.
|
|
116
|
+
OUT=$(run "$PRIOR_TURN_SPECIALIST" "" Write "output/quote.html")
|
|
117
|
+
echo "$OUT" | grep -qE "${ARM_RE}false" \
|
|
118
|
+
&& pass "arming: prior-turn specialist does not disarm current turn" \
|
|
119
|
+
|| fail "false expected for prior-turn specialist, got: $OUT"
|
|
120
|
+
|
|
121
|
+
# 5. Scope guard — write outside output/ emits no line.
|
|
122
|
+
OUT=$(run "$NO_SPECIALIST" "" Write "notes/scratch.txt")
|
|
123
|
+
echo "$OUT" | grep -q '\[admin-authoring\]' \
|
|
124
|
+
&& fail "write outside output/ must emit nothing, got: $OUT" \
|
|
125
|
+
|| pass "scope guard: write outside output/ emits no line"
|
|
126
|
+
|
|
127
|
+
# 6. Scope guard — a specialist process (MAXY_SPECIALIST set) emits no line.
|
|
128
|
+
OUT=$(run "$NO_SPECIALIST" "content-producer" Write "output/quote.html")
|
|
129
|
+
echo "$OUT" | grep -q '\[admin-authoring\]' \
|
|
130
|
+
&& fail "specialist process must emit nothing, got: $OUT" \
|
|
131
|
+
|| pass "scope guard: specialist process emits no line"
|
|
132
|
+
|
|
133
|
+
echo "----"
|
|
134
|
+
echo "PASS=$PASS FAIL=$FAIL"
|
|
135
|
+
[[ "$FAIL" -eq 0 ]]
|