@rubytech/create-realagent 1.0.825 → 1.0.828

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.
Files changed (102) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/task-secrets/dist/index.d.ts +40 -0
  3. package/payload/platform/lib/task-secrets/dist/index.d.ts.map +1 -0
  4. package/payload/platform/lib/task-secrets/dist/index.js +44 -0
  5. package/payload/platform/lib/task-secrets/dist/index.js.map +1 -0
  6. package/payload/platform/lib/task-secrets/src/__tests__/redact-secrets.test.ts +127 -0
  7. package/payload/platform/lib/task-secrets/src/index.ts +77 -0
  8. package/payload/platform/lib/task-secrets/tsconfig.json +9 -0
  9. package/payload/platform/lib/task-secrets/vitest.config.ts +9 -0
  10. package/payload/platform/neo4j/schema.cypher +34 -2
  11. package/payload/platform/package.json +2 -2
  12. package/payload/platform/plugins/admin/hooks/archive-ingest-surface-gate.sh +19 -13
  13. package/payload/platform/plugins/admin/skills/business-profile/SKILL.md +2 -2
  14. package/payload/platform/plugins/admin/skills/onboarding/SKILL.md +13 -12
  15. package/payload/platform/plugins/admin/skills/plugin-management/SKILL.md +4 -4
  16. package/payload/platform/plugins/admin/skills/public-agent-manager/SKILL.md +2 -2
  17. package/payload/platform/plugins/admin/skills/stream-log-review/SKILL.md +6 -6
  18. package/payload/platform/plugins/admin/skills/unzip-attachment/references/safety.md +1 -1
  19. package/payload/platform/plugins/cloudflare/references/manual-setup.md +3 -3
  20. package/payload/platform/plugins/cloudflare/skills/setup-tunnel/SKILL.md +4 -4
  21. package/payload/platform/plugins/docs/references/cloudflare.md +2 -2
  22. package/payload/platform/plugins/docs/references/internals.md +2 -2
  23. package/payload/platform/plugins/docs/references/plugins-guide.md +1 -1
  24. package/payload/platform/plugins/docs/references/troubleshooting.md +2 -1
  25. package/payload/platform/plugins/linkedin-import/skills/linkedin-import/SKILL.md +2 -2
  26. package/payload/platform/plugins/linkedin-import/skills/linkedin-import/references/connections.md +1 -1
  27. package/payload/platform/plugins/memory/PLUGIN.md +1 -1
  28. package/payload/platform/plugins/memory/mcp/dist/index.js +6 -41
  29. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  30. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/llm-classifier.test.js +51 -0
  31. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/llm-classifier.test.js.map +1 -1
  32. package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.d.ts +19 -4
  33. package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.d.ts.map +1 -1
  34. package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.js +139 -56
  35. package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.js.map +1 -1
  36. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest.test.d.ts +2 -0
  37. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest.test.d.ts.map +1 -0
  38. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest.test.js +61 -0
  39. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest.test.js.map +1 -0
  40. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts +34 -0
  41. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts.map +1 -1
  42. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js +241 -0
  43. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js.map +1 -1
  44. package/payload/platform/plugins/memory/references/graph-primitives.md +5 -5
  45. package/payload/platform/plugins/memory/references/schema-base.md +6 -3
  46. package/payload/platform/plugins/memory/skills/document-ingest/SKILL.md +6 -6
  47. package/payload/platform/plugins/tasks/PLUGIN.md +1 -1
  48. package/payload/platform/plugins/tasks/mcp/dist/index.js +11 -2
  49. package/payload/platform/plugins/tasks/mcp/dist/index.js.map +1 -1
  50. package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.d.ts +19 -2
  51. package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.d.ts.map +1 -1
  52. package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.js +17 -1
  53. package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.js.map +1 -1
  54. package/payload/platform/plugins/whatsapp-import/PLUGIN.md +17 -15
  55. package/payload/platform/plugins/whatsapp-import/bin/ingest.mjs +313 -366
  56. package/payload/platform/plugins/whatsapp-import/bin/whatsapp-ingest.sh +27 -60
  57. package/payload/platform/plugins/whatsapp-import/lib/dist/delta-cursor.d.ts +18 -0
  58. package/payload/platform/plugins/whatsapp-import/lib/dist/delta-cursor.d.ts.map +1 -0
  59. package/payload/platform/plugins/whatsapp-import/lib/dist/delta-cursor.js +31 -0
  60. package/payload/platform/plugins/whatsapp-import/lib/dist/delta-cursor.js.map +1 -0
  61. package/payload/platform/plugins/whatsapp-import/lib/dist/derive-keys.d.ts +27 -12
  62. package/payload/platform/plugins/whatsapp-import/lib/dist/derive-keys.d.ts.map +1 -1
  63. package/payload/platform/plugins/whatsapp-import/lib/dist/derive-keys.js +40 -20
  64. package/payload/platform/plugins/whatsapp-import/lib/dist/derive-keys.js.map +1 -1
  65. package/payload/platform/plugins/whatsapp-import/lib/dist/index.d.ts +7 -4
  66. package/payload/platform/plugins/whatsapp-import/lib/dist/index.d.ts.map +1 -1
  67. package/payload/platform/plugins/whatsapp-import/lib/dist/index.js +9 -6
  68. package/payload/platform/plugins/whatsapp-import/lib/dist/index.js.map +1 -1
  69. package/payload/platform/plugins/whatsapp-import/lib/dist/sessionize.d.ts +25 -0
  70. package/payload/platform/plugins/whatsapp-import/lib/dist/sessionize.d.ts.map +1 -0
  71. package/payload/platform/plugins/whatsapp-import/lib/dist/sessionize.js +48 -0
  72. package/payload/platform/plugins/whatsapp-import/lib/dist/sessionize.js.map +1 -0
  73. package/payload/platform/plugins/whatsapp-import/lib/dist/to-classifier-input.d.ts +3 -0
  74. package/payload/platform/plugins/whatsapp-import/lib/dist/to-classifier-input.d.ts.map +1 -0
  75. package/payload/platform/plugins/whatsapp-import/lib/dist/to-classifier-input.js +47 -0
  76. package/payload/platform/plugins/whatsapp-import/lib/dist/to-classifier-input.js.map +1 -0
  77. package/payload/platform/plugins/whatsapp-import/lib/src/__tests__/delta-append.test.ts +163 -0
  78. package/payload/platform/plugins/whatsapp-import/lib/src/__tests__/sessionize.test.ts +91 -0
  79. package/payload/platform/plugins/whatsapp-import/lib/src/__tests__/to-classifier-input.test.ts +59 -0
  80. package/payload/platform/plugins/whatsapp-import/lib/src/delta-cursor.ts +54 -0
  81. package/payload/platform/plugins/whatsapp-import/lib/src/derive-keys.ts +55 -32
  82. package/payload/platform/plugins/whatsapp-import/lib/src/index.ts +9 -6
  83. package/payload/platform/plugins/whatsapp-import/lib/src/sessionize.ts +81 -0
  84. package/payload/platform/plugins/whatsapp-import/lib/src/to-classifier-input.ts +48 -0
  85. package/payload/platform/plugins/whatsapp-import/skills/whatsapp-import/SKILL.md +66 -73
  86. package/payload/platform/plugins/whatsapp-import/skills/whatsapp-import/references/conversation-archive-shape.md +143 -0
  87. package/payload/platform/plugins/whatsapp-import/skills/whatsapp-import/references/export-parse.md +2 -2
  88. package/payload/platform/templates/specialists/agents/database-operator.md +17 -18
  89. package/payload/server/chunk-T2OPNP3L.js +654 -0
  90. package/payload/server/chunk-ZTBTX3IO.js +642 -0
  91. package/payload/server/cloudflare-task-tracker-BAMJY4MH.js +17 -0
  92. package/payload/server/cloudflare-task-tracker-CR6TL4VL.js +19 -0
  93. package/payload/server/public/assets/{admin-DOkUspG1.js → admin-BNwPsMhJ.js} +2 -2
  94. package/payload/server/public/assets/{graph-LLMJa4Ch.js → graph-N_Bw-8oT.js} +1 -1
  95. package/payload/server/public/assets/{page-DoaF3DB0.js → page-BKLGP-th.js} +1 -1
  96. package/payload/server/public/graph.html +2 -2
  97. package/payload/server/public/index.html +2 -2
  98. package/payload/server/server.js +291 -172
  99. package/payload/platform/plugins/whatsapp-import/lib/src/__tests__/filter-gate.test.ts +0 -172
  100. package/payload/platform/plugins/whatsapp-import/lib/src/__tests__/ingest-idempotence.test.ts +0 -141
  101. package/payload/platform/plugins/whatsapp-import/lib/src/filter.ts +0 -136
  102. package/payload/platform/plugins/whatsapp-import/skills/whatsapp-import-enrich/SKILL.md +0 -333
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-realagent",
3
- "version": "1.0.825",
3
+ "version": "1.0.828",
4
4
  "description": "Install Real Agent — Built for agents. By agents.",
5
5
  "bin": {
6
6
  "create-realagent": "./dist/index.js"
@@ -0,0 +1,40 @@
1
+ export type JsonValue = string | number | boolean | null | JsonValue[] | {
2
+ [key: string]: JsonValue;
3
+ };
4
+ export interface FormSchema {
5
+ /**
6
+ * Field names whose values are secret and must never land on a Task node.
7
+ * Declared at the form definition (e.g. `cloudflare-setup-form`'s schema in
8
+ * `cloudflare-setup-types.ts`), not at the writer. Writers pass the form
9
+ * schema through unmodified — the contract is "secrets are declared once
10
+ * where the form is defined, redaction is enforced once at write time."
11
+ */
12
+ secretFields?: string[];
13
+ }
14
+ export interface RedactResult {
15
+ /**
16
+ * Payload with every `secretFields` entry removed. Non-secret keys keep
17
+ * their values intact (including null, false, 0, empty string). Keys not
18
+ * mentioned in `secretFields` always pass through — secrets are an
19
+ * allow-list of names to drop, not the entire universe.
20
+ */
21
+ redacted: Record<string, JsonValue>;
22
+ /** Number of fields that were stripped. Callers log this when > 0. */
23
+ droppedFields: number;
24
+ /** Names of fields that were stripped, sorted for stable log lines. */
25
+ droppedFieldNames: string[];
26
+ }
27
+ /**
28
+ * Strip every secret-tagged field from `payload` per `schema`. Pure: no IO,
29
+ * no logging, no thrown errors. Caller is responsible for the
30
+ * `[task] redacted ...` log line when `droppedFields > 0`.
31
+ *
32
+ * Edge-case behaviour:
33
+ * - `payload` undefined / null → empty result, droppedFields=0.
34
+ * - `schema` undefined / no `secretFields` → every key passes through.
35
+ * - A secret field whose key is absent from `payload` → no-op (not counted).
36
+ * - Non-string values on a secret field → key still dropped, value never
37
+ * inspected. Redaction is by KEY, not by value-shape heuristic.
38
+ */
39
+ export declare function redactSecrets(payload: Record<string, JsonValue> | null | undefined, schema: FormSchema | null | undefined): RedactResult;
40
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAYA,MAAM,MAAM,SAAS,GACjB,MAAM,GACN,MAAM,GACN,OAAO,GACP,IAAI,GACJ,SAAS,EAAE,GACX;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC;AAEjC,MAAM,WAAW,UAAU;IACzB;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,YAAY;IAC3B;;;;;OAKG;IACH,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACpC,sEAAsE;IACtE,aAAa,EAAE,MAAM,CAAC;IACtB,uEAAuE;IACvE,iBAAiB,EAAE,MAAM,EAAE,CAAC;CAC7B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,IAAI,GAAG,SAAS,EACrD,MAAM,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,GACpC,YAAY,CAgBd"}
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ // Task 890 — outcome contract for audit Task input persistence.
3
+ //
4
+ // Doctrine: every audit Task records enough operator-meaningful information to
5
+ // explain what happened, AND no audit Task contains secret material. Mechanism
6
+ // is centralised here, not at the per-kind writer: form/action input
7
+ // definitions tag secret fields at the source of truth, and writers strip
8
+ // those fields via `redactSecrets` before persisting `inputs.<field>` props
9
+ // onto the Task node.
10
+ //
11
+ // Single file by design — no per-kind branching, no factory. A future
12
+ // schema shape change extends `FormSchema` here; the contract stays one place.
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.redactSecrets = redactSecrets;
15
+ /**
16
+ * Strip every secret-tagged field from `payload` per `schema`. Pure: no IO,
17
+ * no logging, no thrown errors. Caller is responsible for the
18
+ * `[task] redacted ...` log line when `droppedFields > 0`.
19
+ *
20
+ * Edge-case behaviour:
21
+ * - `payload` undefined / null → empty result, droppedFields=0.
22
+ * - `schema` undefined / no `secretFields` → every key passes through.
23
+ * - A secret field whose key is absent from `payload` → no-op (not counted).
24
+ * - Non-string values on a secret field → key still dropped, value never
25
+ * inspected. Redaction is by KEY, not by value-shape heuristic.
26
+ */
27
+ function redactSecrets(payload, schema) {
28
+ if (!payload) {
29
+ return { redacted: {}, droppedFields: 0, droppedFieldNames: [] };
30
+ }
31
+ const secrets = new Set(schema?.secretFields ?? []);
32
+ const redacted = {};
33
+ const dropped = [];
34
+ for (const [key, value] of Object.entries(payload)) {
35
+ if (secrets.has(key)) {
36
+ dropped.push(key);
37
+ continue;
38
+ }
39
+ redacted[key] = value;
40
+ }
41
+ dropped.sort();
42
+ return { redacted, droppedFields: dropped.length, droppedFieldNames: dropped };
43
+ }
44
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,gEAAgE;AAChE,EAAE;AACF,+EAA+E;AAC/E,+EAA+E;AAC/E,qEAAqE;AACrE,0EAA0E;AAC1E,4EAA4E;AAC5E,sBAAsB;AACtB,EAAE;AACF,sEAAsE;AACtE,+EAA+E;;AA+C/E,sCAmBC;AA/BD;;;;;;;;;;;GAWG;AACH,SAAgB,aAAa,CAC3B,OAAqD,EACrD,MAAqC;IAErC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,aAAa,EAAE,CAAC,EAAE,iBAAiB,EAAE,EAAE,EAAE,CAAC;IACnE,CAAC;IACD,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,MAAM,EAAE,YAAY,IAAI,EAAE,CAAC,CAAC;IACpD,MAAM,QAAQ,GAA8B,EAAE,CAAC;IAC/C,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACnD,IAAI,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YACrB,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAClB,SAAS;QACX,CAAC;QACD,QAAQ,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;IACxB,CAAC;IACD,OAAO,CAAC,IAAI,EAAE,CAAC;IACf,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,OAAO,CAAC,MAAM,EAAE,iBAAiB,EAAE,OAAO,EAAE,CAAC;AACjF,CAAC"}
@@ -0,0 +1,127 @@
1
+ // Task 890 — no-secret-leak contract test.
2
+ //
3
+ // Verifies that `redactSecrets` strips every key declared as a secret in the
4
+ // form schema, and is meaningful (not a no-op) — removing the `secret` tag
5
+ // from a field MUST cause a deliberately-secret value to leak into the result,
6
+ // proving the test would catch a regression.
7
+
8
+ import { describe, expect, it } from 'vitest';
9
+ import { redactSecrets, type FormSchema } from '../index.js';
10
+
11
+ const SECRET_VALUE = 'hunter2';
12
+ const SECRET_REGEX = /password|token|api[_-]?key|secret|credential|bearer/i;
13
+
14
+ describe('redactSecrets', () => {
15
+ it('drops every key listed in secretFields', () => {
16
+ const schema: FormSchema = { secretFields: ['password', 'apiKey'] };
17
+ const payload = {
18
+ adminLabel: 'maxy-test',
19
+ adminDomain: 'maxy.bot',
20
+ password: SECRET_VALUE,
21
+ apiKey: 'sk-abc',
22
+ };
23
+ const { redacted, droppedFields, droppedFieldNames } = redactSecrets(payload, schema);
24
+
25
+ expect(redacted).toEqual({ adminLabel: 'maxy-test', adminDomain: 'maxy.bot' });
26
+ expect(droppedFields).toBe(2);
27
+ expect(droppedFieldNames).toEqual(['apiKey', 'password']);
28
+ });
29
+
30
+ it('passes every key through when schema has no secretFields', () => {
31
+ const payload = { mode: 'personal', emailProvided: true };
32
+ const { redacted, droppedFields } = redactSecrets(payload, {});
33
+ expect(redacted).toEqual(payload);
34
+ expect(droppedFields).toBe(0);
35
+ });
36
+
37
+ it('passes every key through when schema is undefined', () => {
38
+ const payload = { mode: 'personal' };
39
+ const { redacted, droppedFields } = redactSecrets(payload, undefined);
40
+ expect(redacted).toEqual(payload);
41
+ expect(droppedFields).toBe(0);
42
+ });
43
+
44
+ it('returns empty result for null/undefined payload', () => {
45
+ expect(redactSecrets(null, { secretFields: ['x'] })).toEqual({
46
+ redacted: {},
47
+ droppedFields: 0,
48
+ droppedFieldNames: [],
49
+ });
50
+ expect(redactSecrets(undefined, undefined)).toEqual({
51
+ redacted: {},
52
+ droppedFields: 0,
53
+ droppedFieldNames: [],
54
+ });
55
+ });
56
+
57
+ it('preserves falsy non-secret values (false, 0, empty string, null)', () => {
58
+ const payload = { a: false, b: 0, c: '', d: null, secret: 's' };
59
+ const { redacted } = redactSecrets(payload, { secretFields: ['secret'] });
60
+ expect(redacted).toEqual({ a: false, b: 0, c: '', d: null });
61
+ });
62
+
63
+ it('does not count secret keys absent from payload', () => {
64
+ const schema: FormSchema = { secretFields: ['password', 'missingField'] };
65
+ const { droppedFields, droppedFieldNames } = redactSecrets({ password: SECRET_VALUE }, schema);
66
+ expect(droppedFields).toBe(1);
67
+ expect(droppedFieldNames).toEqual(['password']);
68
+ });
69
+
70
+ it('drops a secret key regardless of its value shape', () => {
71
+ const schema: FormSchema = { secretFields: ['password'] };
72
+ const cases: Array<{ password: unknown }> = [
73
+ { password: 'string-value' },
74
+ { password: 42 },
75
+ { password: null },
76
+ { password: { nested: 'object' } },
77
+ { password: ['array', 'of', 'strings'] },
78
+ ];
79
+ for (const c of cases) {
80
+ const { redacted } = redactSecrets(c as Record<string, never>, schema);
81
+ expect(redacted).not.toHaveProperty('password');
82
+ }
83
+ });
84
+
85
+ it('CONTRACT: no recorded property value contains a secret regex match (cloudflare-setup-form)', () => {
86
+ // Synthetic cloudflare submission shape — mirrors CloudflareSetupRequest
87
+ // including both the operator-supplied secret (password) and the
88
+ // wire-protocol envelope (session_key, messageId) the schema also drops.
89
+ const cloudflareSchema: FormSchema = { secretFields: ['password', 'session_key', 'messageId'] };
90
+ const payload = {
91
+ adminLabel: 'maxy-test',
92
+ adminDomain: 'maxy.bot',
93
+ publicLabel: 'public',
94
+ publicDomain: 'maxy.bot',
95
+ apex: 'maxy.bot',
96
+ password: SECRET_VALUE,
97
+ tunnelName: 'maxy-test',
98
+ session_key: 'sk-conversation-correlation-12345678',
99
+ messageId: '11111111-2222-3333-4444-555555555555',
100
+ };
101
+ const { redacted } = redactSecrets(payload, cloudflareSchema);
102
+
103
+ // Scan every recorded value for any known-secret regex match.
104
+ for (const [key, value] of Object.entries(redacted)) {
105
+ const stringified = JSON.stringify(value);
106
+ expect(stringified, `field ${key} carried a secret-shaped value`).not.toMatch(SECRET_REGEX);
107
+ expect(stringified, `field ${key} carried the secret literal`).not.toContain(SECRET_VALUE);
108
+ }
109
+ // Wire-envelope fields must also be absent from recorded props.
110
+ expect(redacted).not.toHaveProperty('session_key');
111
+ expect(redacted).not.toHaveProperty('messageId');
112
+ });
113
+
114
+ it('CONTRACT (falsifiability): removing the secret tag causes the password to leak — proves the test is meaningful', () => {
115
+ // This test is the canary that proves the no-secret-leak guarantee comes
116
+ // from the schema, not from coincidence. With `secretFields` empty (the
117
+ // schema-author forgot to tag `password`), the value MUST leak into the
118
+ // recorded payload — otherwise the contract test above would still pass
119
+ // even with a broken schema, and would catch nothing.
120
+ const brokenSchema: FormSchema = { secretFields: [] };
121
+ const payload = { adminLabel: 'maxy-test', password: SECRET_VALUE };
122
+ const { redacted } = redactSecrets(payload, brokenSchema);
123
+
124
+ expect(redacted.password).toBe(SECRET_VALUE);
125
+ expect(JSON.stringify(redacted)).toContain(SECRET_VALUE);
126
+ });
127
+ });
@@ -0,0 +1,77 @@
1
+ // Task 890 — outcome contract for audit Task input persistence.
2
+ //
3
+ // Doctrine: every audit Task records enough operator-meaningful information to
4
+ // explain what happened, AND no audit Task contains secret material. Mechanism
5
+ // is centralised here, not at the per-kind writer: form/action input
6
+ // definitions tag secret fields at the source of truth, and writers strip
7
+ // those fields via `redactSecrets` before persisting `inputs.<field>` props
8
+ // onto the Task node.
9
+ //
10
+ // Single file by design — no per-kind branching, no factory. A future
11
+ // schema shape change extends `FormSchema` here; the contract stays one place.
12
+
13
+ export type JsonValue =
14
+ | string
15
+ | number
16
+ | boolean
17
+ | null
18
+ | JsonValue[]
19
+ | { [key: string]: JsonValue };
20
+
21
+ export interface FormSchema {
22
+ /**
23
+ * Field names whose values are secret and must never land on a Task node.
24
+ * Declared at the form definition (e.g. `cloudflare-setup-form`'s schema in
25
+ * `cloudflare-setup-types.ts`), not at the writer. Writers pass the form
26
+ * schema through unmodified — the contract is "secrets are declared once
27
+ * where the form is defined, redaction is enforced once at write time."
28
+ */
29
+ secretFields?: string[];
30
+ }
31
+
32
+ export interface RedactResult {
33
+ /**
34
+ * Payload with every `secretFields` entry removed. Non-secret keys keep
35
+ * their values intact (including null, false, 0, empty string). Keys not
36
+ * mentioned in `secretFields` always pass through — secrets are an
37
+ * allow-list of names to drop, not the entire universe.
38
+ */
39
+ redacted: Record<string, JsonValue>;
40
+ /** Number of fields that were stripped. Callers log this when > 0. */
41
+ droppedFields: number;
42
+ /** Names of fields that were stripped, sorted for stable log lines. */
43
+ droppedFieldNames: string[];
44
+ }
45
+
46
+ /**
47
+ * Strip every secret-tagged field from `payload` per `schema`. Pure: no IO,
48
+ * no logging, no thrown errors. Caller is responsible for the
49
+ * `[task] redacted ...` log line when `droppedFields > 0`.
50
+ *
51
+ * Edge-case behaviour:
52
+ * - `payload` undefined / null → empty result, droppedFields=0.
53
+ * - `schema` undefined / no `secretFields` → every key passes through.
54
+ * - A secret field whose key is absent from `payload` → no-op (not counted).
55
+ * - Non-string values on a secret field → key still dropped, value never
56
+ * inspected. Redaction is by KEY, not by value-shape heuristic.
57
+ */
58
+ export function redactSecrets(
59
+ payload: Record<string, JsonValue> | null | undefined,
60
+ schema: FormSchema | null | undefined,
61
+ ): RedactResult {
62
+ if (!payload) {
63
+ return { redacted: {}, droppedFields: 0, droppedFieldNames: [] };
64
+ }
65
+ const secrets = new Set(schema?.secretFields ?? []);
66
+ const redacted: Record<string, JsonValue> = {};
67
+ const dropped: string[] = [];
68
+ for (const [key, value] of Object.entries(payload)) {
69
+ if (secrets.has(key)) {
70
+ dropped.push(key);
71
+ continue;
72
+ }
73
+ redacted[key] = value;
74
+ }
75
+ dropped.sort();
76
+ return { redacted, droppedFields: dropped.length, droppedFieldNames: dropped };
77
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src"],
8
+ "exclude": ["src/__tests__"]
9
+ }
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: 'node',
6
+ globals: false,
7
+ include: ['src/__tests__/**/*.test.ts'],
8
+ },
9
+ });
@@ -258,6 +258,38 @@ OPTIONS {
258
258
  }
259
259
  };
260
260
 
261
+ // ----------------------------------------------------------
262
+ // ConversationArchive — chunked WhatsApp/messaging archive (Task 891).
263
+ //
264
+ // :ConversationArchive parent + :Section:Conversation chunks (HAS_SECTION + NEXT chain).
265
+ // MERGE-keyed on conversationIdentity = sha256(accountId + ":" + sortedParticipantElementIds).
266
+ // :Section base label is shared with KnowledgeDocument's children — chunks
267
+ // reuse the section_embedding vector index above and the universal fulltext
268
+ // index below.
269
+ //
270
+ // :PARTICIPANT_IN edges from :Person/:AdminUser to :ConversationArchive are
271
+ // MERGEd on every ingest (idempotent); the operator confirms each participant
272
+ // up front via the whatsapp-import skill flow.
273
+ // ----------------------------------------------------------
274
+
275
+ CREATE CONSTRAINT conversation_archive_identity_unique IF NOT EXISTS
276
+ FOR (a:ConversationArchive) REQUIRE a.conversationIdentity IS UNIQUE;
277
+
278
+ CREATE INDEX conversation_archive_account IF NOT EXISTS
279
+ FOR (a:ConversationArchive) ON (a.accountId);
280
+
281
+ CREATE INDEX conversation_archive_session IF NOT EXISTS
282
+ FOR (a:ConversationArchive) ON (a.createdBySession);
283
+
284
+ CREATE VECTOR INDEX conversation_archive_embedding IF NOT EXISTS
285
+ FOR (a:ConversationArchive) ON (a.embedding)
286
+ OPTIONS {
287
+ indexConfig: {
288
+ `vector.dimensions`: 768,
289
+ `vector.similarity_function`: 'cosine'
290
+ }
291
+ };
292
+
261
293
  // Universal full-text BM25 index for hybrid keyword search (Task 748).
262
294
  //
263
295
  // Every operator-meaningful label written by the platform is in the index union;
@@ -276,7 +308,7 @@ OPTIONS {
276
308
  // Label union — every operator-meaningful label:
277
309
  // - Business identity: LocalBusiness, Service, PriceSpecification, OpeningHoursSpecification, Organization
278
310
  // - People: Person, UserProfile, Preference, AdminUser, AccessGrant
279
- // - Knowledge: KnowledgeDocument, Section, Chunk (legacy), DigitalDocument, CreativeWork,
311
+ // - Knowledge: KnowledgeDocument, ConversationArchive (Task 891), Section, Chunk (legacy), DigitalDocument, CreativeWork,
280
312
  // Question, FAQPage, DefinedTerm, Review, ImageObject
281
313
  // - Conversational: Conversation, AdminConversation, PublicConversation, Message,
282
314
  // UserMessage, AssistantMessage, ToolCall
@@ -309,7 +341,7 @@ OPTIONS {
309
341
  CREATE FULLTEXT INDEX entity_search IF NOT EXISTS
310
342
  FOR (n:LocalBusiness|Service|PriceSpecification|OpeningHoursSpecification|Organization
311
343
  |Person|UserProfile|Preference|AdminUser|AccessGrant
312
- |KnowledgeDocument|Section|Chunk|DigitalDocument|CreativeWork|Question|FAQPage|DefinedTerm|Review|ImageObject
344
+ |KnowledgeDocument|ConversationArchive|Section|Chunk|DigitalDocument|CreativeWork|Question|FAQPage|DefinedTerm|Review|ImageObject
313
345
  |Conversation|AdminConversation|PublicConversation|Message|UserMessage|AssistantMessage|ToolCall
314
346
  |Task|Project|Event
315
347
  |Workflow|WorkflowStep|WorkflowRun|StepResult
@@ -6,8 +6,8 @@
6
6
  "plugins/*/mcp"
7
7
  ],
8
8
  "scripts": {
9
- "build": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/oauth-llm/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && tsc -p lib/brand-templating/tsconfig.json && tsc -p lib/entitlement/tsconfig.json && tsc -p plugins/whatsapp-import/lib/tsconfig.json && NODE_OPTIONS='--max-old-space-size=8192' tsc -b plugins/*/mcp/tsconfig.json",
10
- "build:lib": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/oauth-llm/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && tsc -p lib/brand-templating/tsconfig.json && tsc -p lib/entitlement/tsconfig.json && tsc -p plugins/whatsapp-import/lib/tsconfig.json",
9
+ "build": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/oauth-llm/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && tsc -p lib/brand-templating/tsconfig.json && tsc -p lib/entitlement/tsconfig.json && tsc -p lib/task-secrets/tsconfig.json && tsc -p plugins/whatsapp-import/lib/tsconfig.json && NODE_OPTIONS='--max-old-space-size=8192' tsc -b plugins/*/mcp/tsconfig.json",
10
+ "build:lib": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/oauth-llm/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && tsc -p lib/brand-templating/tsconfig.json && tsc -p lib/entitlement/tsconfig.json && tsc -p lib/task-secrets/tsconfig.json && tsc -p plugins/whatsapp-import/lib/tsconfig.json",
11
11
  "build:memory": "tsc -p plugins/memory/mcp/tsconfig.json",
12
12
  "build:contacts": "tsc -p plugins/contacts/mcp/tsconfig.json",
13
13
  "build:telegram": "tsc -p plugins/telegram/mcp/tsconfig.json",
@@ -1,22 +1,28 @@
1
1
  #!/usr/bin/env bash
2
- # Archive-ingest surface gate (Task 855; supersedes Task 846).
2
+ # Archive-ingest surface gate (Task 855, updated by Task 891).
3
3
  #
4
4
  # Five enforcements, one script — phase decided by `hook_event_name` on stdin.
5
5
  # Task 855 narrows the database-operator subagent's effective surface during
6
6
  # WhatsApp archive ingestion to exactly one Bash entry
7
7
  # (`whatsapp-import/bin/whatsapp-ingest.sh`) plus read-only neighbours, by
8
- # blocking the legacy MCP deviation tools mechanically.
8
+ # blocking the legacy MCP deviation tools mechanically. Task 891 retired the
9
+ # `whatsapp-export-insight-pass` tool entirely (Phase 2 enrichment moved to a
10
+ # separate follow-up task that will operate on :Section:Conversation chunks);
11
+ # the tool name is added to the BLOCK list so any agent that still references
12
+ # it from a stale skill or runbook gets a loud denial instead of MCP-not-found.
9
13
  #
10
- # 1. PreToolUse on the three legacy WhatsApp MCP tools — BLOCK unconditionally.
11
- # The single deterministic Bash entry (Task 855) is the only supported path
12
- # for `archiveType=whatsapp-export`. The legacy MCP tools' source remains
13
- # until cleanup; the gate stops them being invoked.
14
+ # 1. PreToolUse on the four legacy WhatsApp MCP tools — BLOCK unconditionally.
15
+ # The single deterministic Bash entry is the only supported path for
16
+ # `archiveType=whatsapp-export`. Tool source for #1-#3 remains until cleanup;
17
+ # `whatsapp-export-insight-pass` source was deleted by Task 891 and the
18
+ # block here catches stale references.
14
19
  # mcp__memory__whatsapp-export-parse
15
20
  # mcp__memory__whatsapp-export-insight-write
16
- # mcp__memory__memory-archive-write (only when `archiveType` is
17
- # `whatsapp-export`; LinkedIn
18
- # and other archiveTypes pass
19
- # through unchanged.)
21
+ # mcp__memory__whatsapp-export-insight-pass (Task 891 deleted, retired)
22
+ # mcp__memory__memory-archive-write (only when `archiveType` is
23
+ # `whatsapp-export`; LinkedIn
24
+ # and other archiveTypes pass
25
+ # through unchanged.)
20
26
  #
21
27
  # 2. PreToolUse Edit/Write/NotebookEdit: deny writes under
22
28
  # `*platform/plugins/*/lib/*` (parser/CSV-shape source for any *-import or
@@ -136,9 +142,9 @@ fi
136
142
 
137
143
  # --- Block 2: legacy WhatsApp MCP tools — denied unconditionally ----------
138
144
  case "$TOOL_NAME" in
139
- mcp__memory__whatsapp-export-parse|mcp__memory__whatsapp-export-insight-write)
145
+ mcp__memory__whatsapp-export-parse|mcp__memory__whatsapp-export-insight-write|mcp__memory__whatsapp-export-insight-pass)
140
146
  emit_decision "block" "denied-mcp-legacy" \
141
- "Blocked: ${TOOL_NAME} is the Task 804 legacy path. Task 855 ships the deterministic Bash entry — invoke 'bash platform/plugins/whatsapp-import/bin/whatsapp-ingest.sh <archive> --owner-element-id <id> --subject-person-id <id> --scope <admin|public> --filter <all|senders=<csv>|date-range=<from>..<to>>' once. Parse, archive-write, and insight all run in-process; do not call legacy MCP tools."
147
+ "Blocked: ${TOOL_NAME} is a retired path. Task 891 ships the chunked-archive contract — invoke 'bash platform/plugins/whatsapp-import/bin/whatsapp-ingest.sh <archive> --owner-element-id <id> --participant-person-ids <csv> --scope <admin|public>' once. Parse, sessionize, classify (mode=chat), and memory-ingest (parentLabel=ConversationArchive) all run in-process; do not call legacy MCP tools. Phase 2 insight derivation is deferred to its own follow-up task."
142
148
  ;;
143
149
  esac
144
150
 
@@ -168,7 +174,7 @@ if [ "$TOOL_NAME" = "mcp__memory__memory-archive-write" ]; then
168
174
  ARCHIVE_TYPE=$(extract_tool_input_field archiveType)
169
175
  if [ "$ARCHIVE_TYPE" = "whatsapp-export" ]; then
170
176
  emit_decision "block" "denied-mcp-legacy archiveType=whatsapp-export" \
171
- "Blocked: memory-archive-write with archiveType='whatsapp-export' is the Task 804 legacy path. Task 855 ships the deterministic Bash entry — invoke 'bash platform/plugins/whatsapp-import/bin/whatsapp-ingest.sh <archive> --owner-element-id <id> --subject-person-id <id> --scope <admin|public> --filter <all|senders=<csv>|date-range=<from>..<to>>' once. Other archiveTypes (linkedin-connections, …) flow through memory-archive-write unchanged."
177
+ "Blocked: memory-archive-write with archiveType='whatsapp-export' is a retired path. Task 891 ships the chunked-archive contract — invoke 'bash platform/plugins/whatsapp-import/bin/whatsapp-ingest.sh <archive> --owner-element-id <id> --participant-person-ids <csv> --scope <admin|public>' once. Other archiveTypes (linkedin-connections, …) flow through memory-archive-write unchanged."
172
178
  fi
173
179
  fi
174
180
 
@@ -10,14 +10,14 @@ Applies when the business owner wants to set up, complete, or update their busin
10
10
  ## When to Activate
11
11
 
12
12
  - Admin asks to set up, edit, or complete their business profile
13
- - Onboarding step 9 delegates here on first run **only when the operator picked `business-owner` mode** (Task 704). Personal mode bootstraps a `Person` with `role: "admin-personal"` directly inside step 9 and does NOT invoke this skill.
13
+ - Onboarding step 9 delegates here on first run **only when the operator picked `business-owner` mode**. Personal mode bootstraps a `Person` with `role: "admin-personal"` directly inside step 9 and does NOT invoke this skill.
14
14
  - Session-start graph check shows a `LocalBusiness` node with missing data (no hours, no services, empty description)
15
15
  - The graph-write gate rejects a `memory-write` or `memory-update` with `no-admin-user` or `no-local-business` — the error message directs the agent here. (For personal-mode accounts, the gate is satisfied by the personal-profile `Person` node, so this rejection should not fire — if it does, the personal-profile node is missing or its `role` is wrong.)
16
16
  - Admin asks questions like "update my address", "add our opening hours", "change the phone number"
17
17
 
18
18
  ## First-run path (no LocalBusiness yet)
19
19
 
20
- The `AdminUser` and the operator's personal-profile `Person` were written deterministically at PIN setup time (Task 830 — `writeAdminUserAndPerson`), so this skill no longer creates the AdminUser. When onboarding step 9 invokes this skill in `business-owner` mode, or when the graph-write gate reports `no-local-business`, create the LocalBusiness:
20
+ The `AdminUser` and the operator's personal-profile `Person` are written deterministically at PIN setup time, so this skill no longer creates the AdminUser. When onboarding step 9 invokes this skill in `business-owner` mode, or when the graph-write gate reports `no-local-business`, create the LocalBusiness:
21
21
 
22
22
  1. **LocalBusiness.** Ask the user for the business name first — this is the only mandatory property. Call `memory-write` with `labels: ["LocalBusiness"]`, minimal `properties: { name }`, `scope: "shared"`, `relationships: [{ type: "OWNS", direction: "incoming", targetNodeId: "<AdminUser-elementId>" }]`. The `LocalBusiness` label is on the exempt set, so the gate passes for this write. Find the AdminUser elementId via `memory-search` for the operator's name; the AdminUser already exists from PIN setup.
23
23
  2. **Capture the remaining domains** (address, hours, services, FAQs, brand assets) via `memory-update` on the `LocalBusiness` node and `memory-write` for related nodes (`OpeningHoursSpecification`, `Service`, `FAQPage`, `ImageObject`). With LocalBusiness present, the gate passes for all subsequent writes.
@@ -203,24 +203,25 @@ Pin the operator's persona and bootstrap the graph nodes that satisfy the graph-
203
203
 
204
204
  **Call `onboarding-step9-mode` with the chosen mode before any graph write or skill invocation.** The tool emits the diagnostic log line and returns the deterministic next-action prose.
205
205
 
206
- **Open the action record with `task-create` (Task 885 process-provenance doctrine).** Before any graph write or skill invocation, call:
206
+ **Open the action record with `task-create` (process-provenance + audit input contract).** Before any graph write or skill invocation, call `task-create` with:
207
207
 
208
- ```
209
- task-create
210
- name: "Establish operator owner — onboarding step 9"
211
- description: "<one-line summary of the chosen mode and what entities will be produced>"
212
- status: "running"
213
- kind: "onboarding-establish-owner"
214
- inputsProvided: ["mode"]
215
- ```
208
+ - `name`: "Establish operator owner — onboarding step 9"
209
+ - `description`: a one-line summary describing the chosen mode and what entities will be produced
210
+ - `status`: `"running"`
211
+ - `kind`: `"onboarding-establish-owner"`
212
+ - `inputsProvided`: the keys you actually pass on `inputs` (e.g. `["mode"]`); records the call shape
213
+ - `inputs`: the form payload known at creation time — `{ mode: "personal" | "business-owner" }`
214
+ - `inputSchema`: `{ secretFields: [] }` — the step-9 mode select carries no secrets; declare the empty list explicitly so the contract is visible at the call site
216
215
 
217
216
  The returned `taskId` is the action-provenance handle for this step — every subsequent `memory-write` for an action-provenance-gated label (`Person`, `UserProfile`, `AdminUser`, `Organization`, `LocalBusiness`) MUST pass it as `producedByTaskId` so the inbound `:PRODUCED` edge from the Task is composed into the write. The Task is auto-linked to the current `AdminConversation` via `RAISED_DURING` (this is what makes `MATCH (c:AdminConversation)<-[:RAISED_DURING]-(t:Task)-[:PRODUCED]->(entity)` traversable from the conversation that initiated onboarding).
218
217
 
218
+ **Recording what you collect.** The `onboarding-establish-owner` Task is the audit record for step 9. Record every operator-meaningful fact you collect using the existing surfaces — `description` for the running summary, `note` (via `task-update`) for facts as they land (email collected, phone declined, resolved Person/UserProfile elementIds), `appendStep` for phase markers. Agent's judgement decides which slot. Contract: by `task-complete`, a reader of the Task properties can reconstruct what was collected, what was declined, and which entities were touched without consulting any other source. No secrets in any property regardless of slot — `task-create`'s `inputs` is centrally redacted via `inputSchema.secretFields`; the post-creation slots carry no schema, so the agent's responsibility is to never write secret material into them.
219
+
219
220
  Then branch on the mode.
220
221
 
221
222
  ### `business-owner`
222
223
 
223
- Invoke the `business-profile` skill, passing the `taskId` so the skill can thread it as `producedByTaskId` into every `memory-write` it issues. The skill follows its first-run path: create the `AdminUser` node, create the `LocalBusiness` node, create the `Organization` node, collect identity + address + whichever additional domains (hours, services, FAQs, brand assets) the user provides. When `business-profile` reports that the required nodes exist in the graph, call `task-update(appendStep:"business-profile-complete")` then write the `HAS_PROFILE` edge from the personal-profile `Person` to the operator's `UserProfile` via `memory-update` (one `memory-search` to resolve both elementIds; the `UserProfile` already exists from step 6 onwards via the lazy-create in `loadUserProfile`). Then call `task-complete(taskId)` and `onboarding-complete-step` with step 9. Do not mark step 9 complete before the required nodes + the HAS_PROFILE edge exist — the gate's precondition must be real, not just recorded.
224
+ Invoke the `business-profile` skill, passing the `taskId` so the skill can thread it as `producedByTaskId` into every `memory-write` it issues. The skill follows its first-run path: create the `AdminUser` node, create the `LocalBusiness` node, create the `Organization` node, collect identity + address + whichever additional domains (hours, services, FAQs, brand assets) the user provides. When `business-profile` reports that the required nodes exist in the graph, call `task-update` with both `appendStep:"business-profile-complete"` AND `note:"Business profile complete — AdminUser=<elementId>, LocalBusiness=<elementId>, Organization=<elementId>"` (the resolved elementIds from the skill's writes; one call carries both the phase marker and the audit content). Then write the `HAS_PROFILE` edge from the personal-profile `Person` to the operator's `UserProfile` via `memory-update` (one `memory-search` to resolve both elementIds; the `UserProfile` already exists from step 6 onwards via the lazy-create in `loadUserProfile`). Then call `task-complete(taskId)` and `onboarding-complete-step` with step 9. Do not mark step 9 complete before the required nodes + the HAS_PROFILE edge exist — the gate's precondition must be real, not just recorded.
224
225
 
225
226
  ### `personal`
226
227
 
@@ -228,9 +229,9 @@ Personal mode does not register a `LocalBusiness`. The `AdminUser` and personal-
228
229
 
229
230
  1. **Ask the user for their email AND phone number** in one short conversational message — {{productName}} stores both on the personal-profile Person for downstream features (notifications, contact-method matching, identity-coverage signal that the agent uses to detect missing identity in future turns). The user may decline either; record what they provide. (Operator-identity fix: the system-prompt's "Identity coverage" block surfaces missing identity fields on every turn, so a partial answer becomes a follow-up prompt rather than a silent gap.)
230
231
  2. **Attach the identity to Person.** Call `profile-update` with `personFields: { email: "<value>", telephone: "<value>" }` (omit either key the user declined). The tool resolves the personal-profile Person via `(au:AdminUser {userId:$you})-[:OWNS]->(p:Person)` server-side and writes the canonical `email`/`telephone` fields. Use canonical `telephone` — `phone` is the schema synonym, not the canonical name; `profile-update` rejects `phone` rather than silently rewriting it. The tool throws if the OWNS edge is missing rather than silently no-oping (the PIN-setup path is the only place that edge is created — a missing edge means PIN setup never ran or was rolled back).
231
- 3. **Append the step.** Call `task-update(appendStep:"identity-attached")`.
232
+ 3. **Append the step + record the facts.** Call `task-update` with both `appendStep:"identity-attached"` AND `note:"Identity attached — email=<value-or-declined>, telephone=<value-or-declined>"` (use the actual values the operator supplied; for declines write the literal `declined`). One call, both fields. The note carries the operator-meaningful audit content; the step is the phase marker.
232
233
  4. **Link the personal-profile `Person` to the `UserProfile`.** Call `memory-search` to resolve the `UserProfile` elementId for the operator (the lazy `loadUserProfile` write created it on the first admin session). Then call `memory-update` on the Person to add the `HAS_PROFILE` edge to the UserProfile. (`HAS_PROFILE` from `:Person` is a sibling pattern to the existing `AdminUser→HAS_PROFILE→UserProfile`; both are valid sources for the same edge type. See [schema-base.md Relationship Patterns](../../../memory/references/schema-base.md).)
233
- 5. **Close the action record.** Call `task-update(appendStep:"profile-linked")` then `task-complete(taskId)`.
234
+ 5. **Close the action record.** Call `task-update` with both `appendStep:"profile-linked"` AND `note:"Profile linked — Person=<elementId>, UserProfile=<elementId>"` (the resolved elementIds from steps 2 and 4). Then call `task-complete(taskId)`.
234
235
  6. **Mark step 9 complete.** Call `onboarding-complete-step` with step 9.
235
236
 
236
237
  After step 9 completes in personal mode, tell the user that {{productName}} is configured for personal use — their employer (if any) is not registered here. If they later become the operator for a business of their own, they can ask {{productName}} to set up a business profile, which invokes the `business-profile` skill directly.
@@ -22,7 +22,7 @@ Built-in plugins under `$PLATFORM_ROOT/plugins/`:
22
22
 
23
23
  - Enable: call `plugin-toggle-enabled` with `pluginName` and `action: "enable"`. Activates the plugin's behaviour embed and MCP server from the next session. Plugins with scheduled behaviour (declared via `lifecycle` in PLUGIN.md frontmatter) are activated automatically by the platform heartbeat within one minute — no separate setup command needed.
24
24
  - Disable: call `plugin-toggle-enabled` with `pluginName` and `action: "disable"`. Deactivates from the next session. Any scheduled Events owned by the plugin (`sourcePlugin` field) are cancelled automatically by the platform heartbeat.
25
- - The tool refuses core plugins (admin, memory, docs, cloudflare, anthropic) and refuses plugins not installed under `$PLATFORM_ROOT/plugins/`. Direct `Edit` of `account.json` is denied by the pre-tool-use hook (Task 831) — `plugin-toggle-enabled` is the only legitimate path.
25
+ - The tool refuses core plugins (admin, memory, docs, cloudflare, anthropic) and refuses plugins not installed under `$PLATFORM_ROOT/plugins/`. Direct `Edit` of `account.json` is denied by the pre-tool-use hook — `plugin-toggle-enabled` is the only legitimate path.
26
26
 
27
27
  ## Premium Plugins
28
28
 
@@ -38,11 +38,11 @@ When the user asks about a specific premium plugin (e.g., "tell me about the tea
38
38
 
39
39
  ### Recording a purchase
40
40
 
41
- `purchasedPlugins` is an entitlement-bearing field (Task 831): the effective list derives from the Rubytech-signed entitlement payload, not from raw `account.json`. The pre-tool-use hook denies any agent write to `account.json`. Direct purchase recording is therefore unavailable to the agent.
41
+ `purchasedPlugins` is an entitlement-bearing field: the effective list derives from the Rubytech-signed entitlement payload, not from raw `account.json`. The pre-tool-use hook denies any agent write to `account.json`. Direct purchase recording is therefore unavailable to the agent.
42
42
 
43
- - **Production path:** Task 832 — Rubytech-side issuance endpoint signs an updated entitlement payload after a Stripe purchase event and delivers it to `~/<configDir>/entitlement.json`. The verifier picks up the new payload at the next session start.
43
+ - **Production path:** the Rubytech-side issuance endpoint signs an updated entitlement payload after a Stripe purchase event and delivers it to `~/<configDir>/entitlement.json`. The verifier picks up the new payload at the next session start.
44
44
  - **Pre-launch / dev path:** the founder runs `node platform/scripts/generate-entitlement-fixture.mjs sign --account-id <id> --email <e> --tier <t> --plugins a,b,c --out ~/<configDir>/entitlement.json` to produce a signed test payload. The agent does not invoke this script.
45
- - **Personal-mode installs (`brand.json.commercialMode: false`, the default today):** purchasedPlugins is read from raw `account.json` via implicit-trust mode. Pre-launch the agent has no way to add purchases without operator intervention; this is intentional during the gap before Task 832 ships.
45
+ - **Personal-mode installs (`brand.json.commercialMode: false`, the default today):** purchasedPlugins is read from raw `account.json` via implicit-trust mode. Pre-launch the agent has no way to add purchases without operator intervention; this is intentional during the gap before signed-issuance ships.
46
46
 
47
47
  If the user asks the agent to record a purchase, explain that their purchase is processed by Rubytech billing and the entitlement payload arrives on their device automatically — no agent action is required.
48
48
 
@@ -195,7 +195,7 @@ After creation, no template metadata persists in the agent's files. The resultin
195
195
  9. **KNOWLEDGE.md generation** — populate from the now-tagged set plus keyword matches using the `update-knowledge` skill workflow
196
196
  10. Write `config.json` with selected model, plugins, status "active", `liveMemory`, and `knowledgeKeywords`. This is the last gated write — placed after IDENTITY.md, SOUL.md, and KNOWLEDGE.md to prevent cascade failure if one gate stalls.
197
197
  11. Check context budget — auto-summarise if over threshold
198
- 12. **Project the agent into the graph** — delegate to the `database-operator` specialist with the instruction: "Project public agent `{slug}` into the graph by POSTing to `/api/admin/agents/{slug}/project` (Task 837 surface)." The route reads the on-disk files and idempotently MERGEs the `:Agent` node, the four owned `:KnowledgeDocument` projections (IDENTITY/SOUL/KNOWLEDGE/KNOWLEDGE-SUMMARY when present, namespaced `attachmentId="agent:<slug>:<role>"`), the `HAS_*` edges, and the `USES_KNOWLEDGE` edges to every operator-tagged doc. Loud-fail: if the route returns non-2xx, surface the error to the user verbatim — the agent's files exist on disk but its graph projection is incomplete, which means the operator's /graph view will not show this agent. Re-running the projection is safe; it is the same idempotent MERGE.
198
+ 12. **Project the agent into the graph** — delegate to the `database-operator` specialist with the instruction: "Project public agent `{slug}` into the graph by POSTing to `/api/admin/agents/{slug}/project`." The route reads the on-disk files and idempotently MERGEs the `:Agent` node, the four owned `:KnowledgeDocument` projections (IDENTITY/SOUL/KNOWLEDGE/KNOWLEDGE-SUMMARY when present, namespaced `attachmentId="agent:<slug>:<role>"`), the `HAS_*` edges, and the `USES_KNOWLEDGE` edges to every operator-tagged doc. Loud-fail: if the route returns non-2xx, surface the error to the user verbatim — the agent's files exist on disk but its graph projection is incomplete, which means the operator's /graph view will not show this agent. Re-running the projection is safe; it is the same idempotent MERGE.
199
199
  13. Confirm creation: "Agent created. Visitors can reach it at `/{slug}`"
200
200
 
201
201
  ### List
@@ -245,7 +245,7 @@ Deletion removes the entire agent directory (`agents/{slug}/`) — not individua
245
245
  - If no other agents remain (or the user declines to set a new default), clear the `defaultAgent` field by calling `account-update` with `field: "defaultAgent"`, `value: ""`. Tell the user that visitors to the root URL will see a "no agents" message until a new agent is created.
246
246
 
247
247
  **Cleanup sequence:**
248
- - Issue `DELETE /api/admin/agents/{slug}` — the route runs `deleteAgentProjection` (Task 837) FIRST to remove the `:Agent` node and its four owned `:KnowledgeDocument` projections, then `rmSync` the directory. Loud-fail: if graph cleanup throws, files are preserved and a 500 is returned with the error text. Re-issuing the DELETE is safe (idempotent on both layers). Operator-tagged docs survive the projection delete — only the `USES_KNOWLEDGE` edges from this Agent are removed.
248
+ - Issue `DELETE /api/admin/agents/{slug}` — the route removes the `:Agent` node and its four owned `:KnowledgeDocument` projections FIRST, then deletes the directory. Loud-fail: if graph cleanup throws, files are preserved and a 500 is returned with the error text. Re-issuing the DELETE is safe (idempotent on both layers). Operator-tagged docs survive the projection delete — only the `USES_KNOWLEDGE` edges from this Agent are removed.
249
249
  - Verify the directory no longer exists
250
250
  - Report what was removed (list the files that were in the directory)
251
251