@rubytech/create-sitedesk-code 0.1.524 → 0.1.525

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 (117) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/plugins/admin/skills/superpowers-sprint/SKILL.md +26 -0
  3. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +6 -0
  4. package/payload/platform/plugins/memory/PLUGIN.md +10 -0
  5. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js +5 -1
  6. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js.map +1 -1
  7. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/temporal-type-preserve.test.d.ts +2 -0
  8. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/temporal-type-preserve.test.d.ts.map +1 -0
  9. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/temporal-type-preserve.test.js +141 -0
  10. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/temporal-type-preserve.test.js.map +1 -0
  11. package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.d.ts +1 -1
  12. package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.d.ts.map +1 -1
  13. package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.js +6 -0
  14. package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.js.map +1 -1
  15. package/payload/platform/plugins/memory/mcp/dist/lib/temporal-type-preserve.d.ts +45 -0
  16. package/payload/platform/plugins/memory/mcp/dist/lib/temporal-type-preserve.d.ts.map +1 -0
  17. package/payload/platform/plugins/memory/mcp/dist/lib/temporal-type-preserve.js +122 -0
  18. package/payload/platform/plugins/memory/mcp/dist/lib/temporal-type-preserve.js.map +1 -0
  19. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-by-name.test.js +26 -0
  20. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-by-name.test.js.map +1 -1
  21. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-type-preserve.test.d.ts +2 -0
  22. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-type-preserve.test.d.ts.map +1 -0
  23. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-type-preserve.test.js +201 -0
  24. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-type-preserve.test.js.map +1 -0
  25. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.d.ts.map +1 -1
  26. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js +89 -33
  27. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js.map +1 -1
  28. package/payload/platform/plugins/memory/mcp/vitest.config.ts +2 -0
  29. package/payload/platform/plugins/scheduling/PLUGIN.md +6 -0
  30. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/meeting-retype.test.d.ts +2 -0
  31. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/meeting-retype.test.d.ts.map +1 -0
  32. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/meeting-retype.test.js +148 -0
  33. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/meeting-retype.test.js.map +1 -0
  34. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.d.ts +83 -6
  35. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.d.ts.map +1 -1
  36. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.js +90 -5
  37. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.js.map +1 -1
  38. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/routine-roster-audit.test.js +111 -2
  39. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/routine-roster-audit.test.js.map +1 -1
  40. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.d.ts +38 -0
  41. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.d.ts.map +1 -1
  42. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.js +116 -0
  43. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.js.map +1 -1
  44. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +13 -1
  45. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
  46. package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js +81 -8
  47. package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js.map +1 -1
  48. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-event-agent.test.js +39 -0
  49. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-event-agent.test.js.map +1 -1
  50. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts.map +1 -1
  51. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js +9 -1
  52. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js.map +1 -1
  53. package/payload/platform/scripts/__tests__/check-canonical-tool-names.test.sh +162 -0
  54. package/payload/platform/scripts/__tests__/check-no-task-id-leaks.test.sh +53 -0
  55. package/payload/platform/scripts/__tests__/task-id-citation.test.sh +72 -0
  56. package/payload/platform/scripts/check-canonical-tool-names.mjs +36 -4
  57. package/payload/platform/scripts/check-no-task-id-leaks.mjs +80 -62
  58. package/payload/platform/scripts/lib/canonical-tool-names.mjs +130 -26
  59. package/payload/platform/scripts/lib/task-id-citation.mjs +67 -0
  60. package/payload/platform/.docs/search-surface-contract.md +0 -58
  61. package/payload/platform/docs/superpowers/plans/2026-06-02-task-610-follower-202-retry.md +0 -372
  62. package/payload/platform/docs/superpowers/plans/2026-06-04-public-agent-knowledge-delivery.md +0 -230
  63. package/payload/platform/docs/superpowers/plans/2026-06-23-account-filesystem-schema.md +0 -544
  64. package/payload/platform/docs/superpowers/plans/2026-07-11-task-1557-operator-on-behalf-preference-attribution.md +0 -542
  65. package/payload/platform/docs/superpowers/plans/2026-07-13-account-schema-ontology-projection.md +0 -547
  66. package/payload/platform/docs/superpowers/plans/2026-07-14-graph-top-level-labels-ontology-single-source.md +0 -458
  67. package/payload/platform/docs/superpowers/plans/2026-07-17-task-1736-retire-mcp-stderr-tee.md +0 -397
  68. package/payload/platform/docs/superpowers/plans/2026-07-18-graph-native-ledger.md +0 -807
  69. package/payload/platform/docs/superpowers/plans/2026-07-18-ledger-write-path-fix.md +0 -109
  70. package/payload/platform/docs/superpowers/plans/2026-07-20-storage-pages-create.md +0 -82
  71. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1704-data-portal-standing-audit.md +0 -673
  72. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1789-reseat-channel-row-fork.md +0 -1244
  73. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1818-loop-gate-app-routes.md +0 -462
  74. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1819-top-level-label-allowlist.md +0 -321
  75. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1829-platform-ui-typecheck.md +0 -830
  76. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1831-data-portal-class-derived-allowlist.md +0 -2177
  77. package/payload/platform/docs/superpowers/plans/2026-07-21-graph-caption-resolver.md +0 -124
  78. package/payload/platform/docs/superpowers/plans/2026-07-21-task-1877-bash-schema-enforcement.md +0 -107
  79. package/payload/platform/docs/superpowers/plans/2026-07-21-task-1887-reconcile-allowed-top-level.md +0 -205
  80. package/payload/platform/docs/superpowers/plans/2026-07-22-task-1899-reconcile-report-only.md +0 -748
  81. package/payload/platform/docs/superpowers/plans/2026-07-22-task-1910-two-way-portal-exchange.md +0 -1716
  82. package/payload/platform/docs/superpowers/plans/2026-07-23-task-1930-adherence-enforcement.md +0 -422
  83. package/payload/platform/docs/superpowers/plans/2026-07-24-task-1942-subaccount-switcher-brand-head.md +0 -327
  84. package/payload/platform/docs/superpowers/plans/2026-07-25-task-1974-uploads-intake-inbox.md +0 -235
  85. package/payload/platform/docs/superpowers/plans/2026-07-25-task-1976-intra-folder-hygiene.md +0 -215
  86. package/payload/platform/docs/superpowers/plans/2026-07-26-task-1902-account-owned-entry-declarations.md +0 -555
  87. package/payload/platform/docs/superpowers/plans/2026-07-26-task-1926-portal-push-concurrency-guard.md +0 -482
  88. package/payload/platform/docs/superpowers/plans/2026-07-26-task-2023-sidebar-sessions-async-reads.md +0 -394
  89. package/payload/platform/docs/superpowers/plans/2026-07-27-task-2016-disabled-agent-routing.md +0 -624
  90. package/payload/platform/docs/superpowers/plans/2026-07-27-task-2028-declared-file-write-deny.md +0 -303
  91. package/payload/platform/docs/superpowers/plans/2026-07-27-task-2052-account-machinery-write-fence.md +0 -250
  92. package/payload/platform/docs/superpowers/plans/2026-07-28-task-2097-email-signature-set.md +0 -65
  93. package/payload/platform/docs/superpowers/specs/2026-06-02-task-610-follower-202-retry-design.md +0 -116
  94. package/payload/platform/docs/superpowers/specs/2026-06-23-account-filesystem-schema-design.md +0 -170
  95. package/payload/platform/docs/superpowers/specs/2026-07-11-task-1557-operator-on-behalf-preference-attribution-design.md +0 -90
  96. package/payload/platform/docs/superpowers/specs/2026-07-13-account-schema-ontology-projection-design.md +0 -178
  97. package/payload/platform/docs/superpowers/specs/2026-07-14-graph-top-level-labels-ontology-single-source-design.md +0 -119
  98. package/payload/platform/docs/superpowers/specs/2026-07-17-task-1736-retire-mcp-stderr-tee-design.md +0 -128
  99. package/payload/platform/docs/superpowers/specs/2026-07-18-graph-native-ledger-design.md +0 -241
  100. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1704-data-portal-standing-audit-design.md +0 -177
  101. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1789-reseat-channel-row-fork-design.md +0 -201
  102. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1818-loop-gate-app-routes-design.md +0 -217
  103. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1819-top-level-label-allowlist-design.md +0 -80
  104. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1820-cpu-triage-admin-tools-design.md +0 -97
  105. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1829-platform-ui-typecheck-design.md +0 -163
  106. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1831-data-portal-class-derived-allowlist-design.md +0 -163
  107. package/payload/platform/docs/superpowers/specs/2026-07-22-task-1899-reconcile-report-only-design.md +0 -121
  108. package/payload/platform/docs/superpowers/specs/2026-07-22-task-1910-two-way-portal-exchange-design.md +0 -103
  109. package/payload/platform/docs/superpowers/specs/2026-07-23-task-1930-adherence-enforcement-design.md +0 -81
  110. package/payload/platform/docs/superpowers/specs/2026-07-24-task-1942-subaccount-switcher-brand-head-design.md +0 -35
  111. package/payload/platform/docs/superpowers/specs/2026-07-26-task-1902-account-owned-entry-declarations-design.md +0 -232
  112. package/payload/platform/docs/superpowers/specs/2026-07-26-task-1926-portal-push-concurrency-guard-design.md +0 -200
  113. package/payload/platform/docs/superpowers/specs/2026-07-26-task-2014-ui-suite-nondeterminism-design.md +0 -169
  114. package/payload/platform/docs/superpowers/specs/2026-07-27-task-2016-disabled-agent-routing-design.md +0 -139
  115. package/payload/platform/docs/superpowers/specs/2026-07-27-task-2028-declared-file-write-deny-design.md +0 -127
  116. package/payload/platform/docs/superpowers/specs/2026-07-27-task-2052-account-machinery-write-fence-design.md +0 -150
  117. package/payload/platform/docs/superpowers/specs/2026-07-28-task-2097-email-signature-set-design.md +0 -155
@@ -1,303 +0,0 @@
1
- # Task 2028 — declared-file write deny: implementation plan
2
-
3
- > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
-
5
- **Goal:** The write guard blocks an agent `Write`/`Edit`/`NotebookEdit` to a plugin-declared account-root file, naming the owning plugin, while an operator bucket in the same fence still passes.
6
-
7
- **Architecture:** The merge emits a third generated region carrying `name<TAB>owning-plugin` inside a ```` ```declared-files ```` fence. The guard parses that fence with the same one-line awk it already uses for `allowed-top-level`, and denies after the top-level check passes. `allowed-top-level` itself is untouched, so the standing reconcile is unaffected.
8
-
9
- **Tech Stack:** bash (PreToolUse/PostToolUse hooks), python3 (schema merge), bash test suites.
10
-
11
- ## Global Constraints
12
-
13
- - No task numbers or internal refs in any operator-visible string (existing hook rule, stated in both hook headers).
14
- - The guard never hard-codes a file list; the account's own `SCHEMA.md` is the only source.
15
- - Fail-open posture is preserved: no `declared-files` fence means nothing to deny.
16
- - The block log keeps the existing shape: `[fs-guard] blocked path=<rel> reason=<...>`.
17
- - Spec: `platform/docs/superpowers/specs/2026-07-27-task-2028-declared-file-write-deny-design.md`.
18
-
19
- ---
20
-
21
- ### Task 1: The merge emits the declared-files region
22
-
23
- **Files:**
24
- - Modify: `platform/scripts/lib/account-schema-owned-dirs.py` (module constants; `merge()`)
25
- - Test: `platform/scripts/__tests__/account-schema-owned-dirs.test.sh`
26
-
27
- **Interfaces:**
28
- - Produces: a region delimited by `<!-- declared-files:start -->` / `<!-- declared-files:end -->` containing a ```` ```declared-files ```` fence, one `name<TAB>plugin` line per declared file in `resolve()` order. Task 2's guard consumes exactly that fence.
29
-
30
- - [ ] **Step 1: Write the failing test**
31
-
32
- Append to `platform/scripts/__tests__/account-schema-owned-dirs.test.sh`, after the existing platform-plugin declarations block:
33
-
34
- ```bash
35
- # --- declared-files fence: one name<TAB>plugin line per declared file -------
36
- DECL="$(awk '/^```declared-files$/{f=1;next} /^```$/{f=0} f' "$A_MX/SCHEMA.md")"
37
- assert_grep "$(printf 'wa-channel-bindings.json\tadmin')" "$DECL" "mx-decl-wa"
38
- assert_grep "$(printf 'agents-disabled.json\tadmin')" "$DECL" "mx-decl-agents-disabled"
39
- assert_grep "$(printf 'data-portal.json\tcloudflare')" "$DECL" "mx-decl-portal"
40
- assert_grep "$(printf 'calendar-availability.json\tscheduling')" "$DECL" "mx-decl-availability"
41
- assert_nogrep "e-sign" "$DECL" "mx-decl-excludes-dirs"
42
- assert_grep "declared-files:start" "$(cat "$A_MX/SCHEMA.md")" "mx-decl-marker"
43
- ```
44
-
45
- - [ ] **Step 2: Run the test to verify it fails**
46
-
47
- Run: `bash platform/scripts/__tests__/account-schema-owned-dirs.test.sh`
48
- Expected: FAIL on `mx-decl-wa` and the other new assertions (no such fence yet).
49
-
50
- - [ ] **Step 3: Write the implementation**
51
-
52
- In `platform/scripts/lib/account-schema-owned-dirs.py`, beside the existing marker constants:
53
-
54
- ```python
55
- DECL_START = "<!-- declared-files:start -->"
56
- DECL_END = "<!-- declared-files:end -->"
57
- ```
58
-
59
- In `merge()`, add the strip beside the two existing strips:
60
-
61
- ```python
62
- text = _strip_region(text, DECL_START, DECL_END)
63
- ```
64
-
65
- and append the region after the ontology region is appended:
66
-
67
- ```python
68
- declared = [e for e in owned if e["kind"] == "file"]
69
- if declared:
70
- region = [DECL_START, "## Declared files (written by their owning plugin)", "",
71
- "Each file below is written whole by the plugin named beside it. It is",
72
- "control-plane state, not operator data, and is not hand-edited: change",
73
- "it through the owning plugin's own tool. The write guard reads this",
74
- "block and blocks a write whose first path segment is one of these",
75
- "names, which the allowed-top-level set above cannot express — that set",
76
- "lists what may exist at the root, not what may be authored by hand.", "",
77
- "```declared-files"]
78
- for e in declared:
79
- region.append(f"{e['name']}\t{e['plugin']}")
80
- region.append("```")
81
- region.append(DECL_END)
82
- text = text.rstrip("\n") + "\n\n" + "\n".join(region) + "\n"
83
- ```
84
-
85
- - [ ] **Step 4: Run the tests to verify they pass**
86
-
87
- Run: `bash platform/scripts/__tests__/account-schema-owned-dirs.test.sh`
88
- Expected: PASS, including the pre-existing `idempotent-second-merge` assertion.
89
-
90
- - [ ] **Step 5: Commit**
91
-
92
- ```bash
93
- git add platform/scripts/lib/account-schema-owned-dirs.py platform/scripts/__tests__/account-schema-owned-dirs.test.sh
94
- git commit -m "feat(2028): merge emits a declared-files fence carrying each file's owning plugin"
95
- ```
96
-
97
- ---
98
-
99
- ### Task 2: The guard denies a write to a declared file
100
-
101
- **Files:**
102
- - Modify: `platform/plugins/admin/hooks/fs-schema-guard.sh` (header comment; new check after the top-level check)
103
- - Test: `platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh`
104
-
105
- **Interfaces:**
106
- - Consumes: the ```` ```declared-files ```` fence from Task 1, lines `name<TAB>plugin`.
107
- - Produces: `[fs-guard] blocked path=<rel> reason=declared-file` on stderr, exit 2.
108
-
109
- - [ ] **Step 1: Write the failing test**
110
-
111
- Append to `platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh`, after the `ACCT_D` setup:
112
-
113
- ```bash
114
- # A third account modelling a merged schema: the eight declared files are in the
115
- # allowed set (as the merge unions them) AND in the declared-files fence.
116
- ACCT_F=$(mktemp -d)
117
- trap 'rm -rf "$ACCT" "$ACCT_D" "$ACCT_F" "$ACCT_L"' EXIT
118
- DECLARED_FILES="webchat-channel-bindings.json wa-channel-bindings.json telegram-channel-bindings.json canonical-webchat-session.json session-titles.json agents-disabled.json data-portal.json calendar-availability.json"
119
- seed_declared() { # $1=acct $2=with-fence(yes|no)
120
- cp "$TEMPLATE" "$1/SCHEMA.md"
121
- for f in $DECLARED_FILES; do
122
- awk -v n="$f" '1; /^```allowed-top-level$/{print n}' "$1/SCHEMA.md" > "$1/SCHEMA.md.tmp" && mv "$1/SCHEMA.md.tmp" "$1/SCHEMA.md"
123
- done
124
- [ "$2" = "yes" ] || return 0
125
- { echo; echo '<!-- declared-files:start -->'; echo '```declared-files'
126
- for f in $DECLARED_FILES; do
127
- case "$f" in
128
- data-portal.json) printf '%s\tcloudflare\n' "$f" ;;
129
- calendar-availability.json) printf '%s\tscheduling\n' "$f" ;;
130
- *) printf '%s\tadmin\n' "$f" ;;
131
- esac
132
- done
133
- echo '```'; echo '<!-- declared-files:end -->'; } >> "$1/SCHEMA.md"
134
- }
135
- seed_declared "$ACCT_F" yes
136
- mkdir -p "$ACCT_F/projects/acme"
137
-
138
- # A fourth account in the pre-2028 state: the names are in the allowed set, but
139
- # no declared-files fence exists. Pins that the deny comes from the fence and
140
- # not from a list baked into the hook.
141
- ACCT_L=$(mktemp -d)
142
- seed_declared "$ACCT_L" no
143
-
144
- for f in $DECLARED_FILES; do
145
- case "$f" in
146
- data-portal.json) owner=cloudflare ;;
147
- calendar-availability.json) owner=scheduling ;;
148
- *) owner=admin ;;
149
- esac
150
- run_case "declared block $f" "$(mkenv Write file_path "$f")" 2 "fs-guard. blocked path=$f reason=declared-file" "$ACCT_F"
151
- run_case "declared owner $f" "$(mkenv Write file_path "$f")" 2 "$owner plugin" "$ACCT_F"
152
- done
153
- run_case "operator bucket allow" "$(mkenv Write file_path 'projects/acme/a.txt')" 0 "" "$ACCT_F"
154
- run_case "declared edit block" "$(mkenv Edit file_path 'wa-channel-bindings.json')" 2 "reason=declared-file" "$ACCT_F"
155
- run_case "no fence no deny" "$(mkenv Write file_path 'wa-channel-bindings.json')" 0 "" "$ACCT_L"
156
- ```
157
-
158
- - [ ] **Step 2: Run the test to verify it fails**
159
-
160
- Run: `bash platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh`
161
- Expected: FAIL on every `declared block`/`declared owner` case with `exit=0, want 2`; `no fence no deny` already passes.
162
-
163
- - [ ] **Step 3: Write the implementation**
164
-
165
- In `platform/plugins/admin/hooks/fs-schema-guard.sh`, immediately after the top-level check block:
166
-
167
- ```bash
168
- # Declared-file check. A file a plugin declares in its PLUGIN.md is written
169
- # whole by that plugin, so an agent write to it is blocked even though its name
170
- # is in the allowed set: that set lists what may exist at the account root, not
171
- # what may be authored by hand. The set and its owner come from the account's
172
- # own SCHEMA.md — the hook hard-codes no list. No fence (a brand declaring no
173
- # files, or a schema predating the block) means nothing to deny, the same
174
- # fail-open posture as the missing-schema case above.
175
- DECLARED=""
176
- if [ -f "$ACCOUNT_DIR/SCHEMA.md" ]; then
177
- DECLARED=$(awk '/^```declared-files$/{f=1;next} /^```$/{f=0} f' "$ACCOUNT_DIR/SCHEMA.md")
178
- fi
179
- if [ -n "$DECLARED" ] && printf '%s\n' "$DECLARED" | awk -F'\t' -v n="$SEG0" '$1==n{found=1} END{exit !found}'; then
180
- OWNER=$(printf '%s\n' "$DECLARED" | awk -F'\t' -v n="$SEG0" '$1==n{print $2; exit}')
181
- [ -z "$OWNER" ] && OWNER="owning"
182
- echo "[fs-guard] blocked path=$REL reason=declared-file" >&2
183
- echo "Blocked: '$SEG0' is written whole by the $OWNER plugin. It is control-plane state, not operator data, and a hand-written copy is overwritten by its owner's next write. Change it through the $OWNER plugin's own tool." >&2
184
- exit 2
185
- fi
186
- ```
187
-
188
- Update the header comment's reason list to `reason=<top-level|declared-file|over-deep|bad-name>` and add one line to the enforcement summary: a target whose first segment is a declared file is blocked.
189
-
190
- - [ ] **Step 4: Run the tests to verify they pass**
191
-
192
- Run: `bash platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh`
193
- Expected: PASS on all cases, including the pre-existing ten.
194
-
195
- - [ ] **Step 5: Mutation check**
196
-
197
- Comment out the `exit 2` inside the new block, re-run the suite, confirm the 16 declared cases fail, then restore.
198
-
199
- - [ ] **Step 6: Commit**
200
-
201
- ```bash
202
- git add platform/plugins/admin/hooks/fs-schema-guard.sh platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh
203
- git commit -m "feat(2028): the write guard denies a write to a plugin-declared file"
204
- ```
205
-
206
- ---
207
-
208
- ### Task 3: The Bash post-hook decision is stated and pinned
209
-
210
- **Files:**
211
- - Modify: `platform/plugins/admin/hooks/fs-schema-guard-bash-post.sh` (header comment only)
212
- - Test: `platform/plugins/admin/hooks/__tests__/fs-schema-guard-bash.test.sh`
213
-
214
- **Interfaces:**
215
- - Consumes: nothing new. The behaviour under test is the existing allowed-set diff.
216
-
217
- - [ ] **Step 1: Write the failing test**
218
-
219
- Append to `platform/plugins/admin/hooks/__tests__/fs-schema-guard-bash.test.sh`:
220
-
221
- ```bash
222
- # --- 10. Bash creating a declared file -> silent, by decision ---------------
223
- # A declared file is in the allowed set, so the diff never flags it. Kept
224
- # deliberately: this hook sees only names that appeared during the command and
225
- # cannot tell a hand-written copy from the owner's own script creating it.
226
- D=$(new_acct)
227
- awk '1; /^```allowed-top-level$/{print "wa-channel-bindings.json"}' "$D/SCHEMA.md" > "$D/SCHEMA.md.tmp" && mv "$D/SCHEMA.md.tmp" "$D/SCHEMA.md"
228
- run_pre "$D" "$(env_json PreToolUse Bash "$SID")" "$ef"
229
- printf '{}' > "$D/wa-channel-bindings.json"
230
- run_post "$D" "$(env_json PostToolUse Bash "$SID")" "$ef"
231
- check "post silent on declared file" 0 $? "" "$(cat "$ef")"
232
- ```
233
-
234
- - [ ] **Step 2: Run the test**
235
-
236
- Run: `bash platform/plugins/admin/hooks/__tests__/fs-schema-guard-bash.test.sh`
237
- Expected: PASS. This test pins existing behaviour rather than driving a change; it fails only if a future edit makes the post hook flag declared files.
238
-
239
- - [ ] **Step 3: State the decision in the hook**
240
-
241
- Add to the header comment of `platform/plugins/admin/hooks/fs-schema-guard-bash-post.sh`:
242
-
243
- ```
244
- # A declared file (a name a plugin declares as its own in PLUGIN.md) is in the
245
- # allowed set, so this hook never flags one. That silence is a decision, not an
246
- # accident: the diff sees only names that appeared during this command, so it
247
- # cannot tell the agent hand-writing the file from the agent running the owner's
248
- # own script (the portal index push, the availability publish), and feedback
249
- # would fire on the legitimate path. The Write/Edit guard blocks the hand-write
250
- # it can actually identify; this hook stays quiet. A test pins the silence.
251
- ```
252
-
253
- - [ ] **Step 4: Re-run both hook suites**
254
-
255
- Run: `bash platform/plugins/admin/hooks/__tests__/fs-schema-guard-bash.test.sh && bash platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh`
256
- Expected: PASS on both.
257
-
258
- - [ ] **Step 5: Commit**
259
-
260
- ```bash
261
- git add platform/plugins/admin/hooks/fs-schema-guard-bash-post.sh platform/plugins/admin/hooks/__tests__/fs-schema-guard-bash.test.sh
262
- git commit -m "docs(2028): state and pin the Bash post-hook's silence on declared files"
263
- ```
264
-
265
- ---
266
-
267
- ### Task 4: End-to-end against the real brand tree
268
-
269
- **Files:** none modified. Verification only.
270
-
271
- - [ ] **Step 1: Merge a real schema and read the fence**
272
-
273
- ```bash
274
- ACCT=$(mktemp -d) && cp platform/templates/account-schema/SCHEMA.md "$ACCT/SCHEMA.md"
275
- PROJECT_DIR="$PWD/platform" python3 platform/scripts/lib/account-schema-owned-dirs.py merge "$ACCT"
276
- awk '/^```declared-files$/{f=1;next} /^```$/{f=0} f' "$ACCT/SCHEMA.md"
277
- ```
278
-
279
- Expected: eight `name<TAB>plugin` lines.
280
-
281
- - [ ] **Step 2: Drive the real hook against that merged account**
282
-
283
- ```bash
284
- ( cd "$ACCT" && printf '{"tool_name":"Write","tool_input":{"file_path":"wa-channel-bindings.json"}}' \
285
- | bash "$OLDPWD/platform/plugins/admin/hooks/fs-schema-guard.sh"; echo "exit=$?" )
286
- ```
287
-
288
- Expected: `reason=declared-file` on stderr and `exit=2`, against the same envelope that returned exit 0 before this change.
289
-
290
- - [ ] **Step 3: Confirm the reconcile still names no declared file as a stray**
291
-
292
- ```bash
293
- PROJECT_DIR="$PWD/platform" python3 platform/scripts/lib/account-schema-owned-dirs.py reconcile "$ACCT" | grep ownedFile
294
- ```
295
-
296
- Expected: `present=true` on every line.
297
-
298
- ---
299
-
300
- ## Self-review
301
-
302
- - Spec coverage: emission (Task 1), deny (Task 2), Bash decision (Task 3), the spec's testing section (Tasks 1-3 plus the mutation check in Task 2 and the end-to-end in Task 4).
303
- - One spec sentence is tightened here: the "no fence" case uses an account whose allowed set already carries the names, because under the pristine template those names block as `top-level` and would not prove anything about the new branch.
@@ -1,250 +0,0 @@
1
- # Account-machinery write fence Implementation Plan
2
-
3
- > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
-
5
- **Goal:** Stop the account agent writing the five entries in the shipped account schema that are the account's own machinery rather than operator data.
6
-
7
- **Architecture:** A new fenced `agent-denied-top-level` block in the shipped `SCHEMA.md` template lists the five names with a per-name reason. `fs-schema-guard.sh` parses it with the same awk shape it already uses for the other two fences and blocks a write whose first path segment matches, reporting `reason=account-machinery`. The `allowed-top-level` fence is not touched, so the standing reconcile keeps reading exactly what it reads today.
8
-
9
- **Tech Stack:** bash, awk, python3. The hook and its tests are plain bash; no build and no `npm install`.
10
-
11
- ## Global Constraints
12
-
13
- - The deny set and each name's reason come from the account's own `SCHEMA.md`. The hook hard-codes no list.
14
- - `allowed-top-level` is not modified. All five names stay in it, so the standing reconcile never reports them as strays.
15
- - No fence means nothing is denied. An account whose `SCHEMA.md` predates this change keeps today's behaviour, matching the guard's existing fail-open posture on a missing schema and on a missing `declared-files` fence.
16
- - No task numbers or internal refs in any operator-visible string.
17
- - Block log line shape is fixed: `[fs-guard] blocked path=<rel> reason=account-machinery`.
18
- - The separator inside the fence is a literal TAB, matching the `declared-files` fence.
19
- - `fs-schema-guard-bash-post.sh` is not modified.
20
- - Baseline before any change: `fs-schema-guard.test.sh` is `39 passed, 0 failed`; `fs-schema-guard-bash.test.sh` is `15 passed, 0 failed`.
21
-
22
- ---
23
-
24
- ### Task 1: Ship the deny fence in the account schema template
25
-
26
- **Files:**
27
- - Modify: `platform/templates/account-schema/SCHEMA.md` (append a section after the "## Allowed top-level entries" section)
28
- - Test: `platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh`
29
-
30
- **Interfaces:**
31
- - Consumes: nothing.
32
- - Produces: a fenced block whose header line is exactly three backticks followed by `agent-denied-top-level`, one line per name as `name<TAB>reason`, in the order `account.json`, `SCHEMA.md`, `secrets`, `.claude`, `.git`. Task 2 parses it.
33
-
34
- This task is data only. After it the guard's behaviour is unchanged, and every pre-existing case must still pass — that is the evidence the appended fence disturbs no existing reader.
35
-
36
- - [ ] **Step 1: Write the failing assertions**
37
-
38
- Append these two assertions to `platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh`, immediately after the existing `allowed-set parse` assertion and before the `echo "----- $PASS passed, $FAIL failed -----"` line:
39
-
40
- ````bash
41
- # Deny-fence parse == documented set. The guard reads this fence, so pinning its
42
- # contents here means a name cannot be added or dropped without a test edit.
43
- EXPECT_D="account.json SCHEMA.md secrets .claude .git"
44
- GOT_D=$(awk '/^```agent-denied-top-level$/{f=1;next} /^```$/{f=0} f' "$ACCT/SCHEMA.md" | cut -f1 | tr '\n' ' ' | sed 's/ *$//')
45
- if [ "$GOT_D" = "$EXPECT_D" ]; then echo "PASS: deny-set parse"; PASS=$((PASS+1));
46
- else echo "FAIL: deny-set parse: got [$GOT_D]" >&2; FAIL=$((FAIL+1)); fi
47
-
48
- # Every deny entry carries a reason column: the block message has no owning
49
- # plugin to name, so the schema must supply the explanation.
50
- NOREASON=$(awk '/^```agent-denied-top-level$/{f=1;next} /^```$/{f=0} f' "$ACCT/SCHEMA.md" | awk -F'\t' 'NF<2 || $2==""{print $1}')
51
- if [ -z "$NOREASON" ]; then echo "PASS: deny-set reasons"; PASS=$((PASS+1));
52
- else echo "FAIL: deny-set reasons missing for [$NOREASON]" >&2; FAIL=$((FAIL+1)); fi
53
- ````
54
-
55
- - [ ] **Step 2: Run the suite to verify the new assertion fails**
56
-
57
- Run: `bash platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh`
58
-
59
- Expected: `FAIL: deny-set parse: got []` on stderr, and the run ends `----- 40 passed, 1 failed -----` with a non-zero exit. `deny-set reasons` passes at this point only because an absent fence yields no rows; it becomes meaningful after Step 3.
60
-
61
- - [ ] **Step 3: Add the fence to the template**
62
-
63
- Append this section to the end of `platform/templates/account-schema/SCHEMA.md`, after the closing fence of the `allowed-top-level` block. **The separator between each name and its reason is a literal TAB character, not spaces.**
64
-
65
- `````markdown
66
- ## Entries you may not write
67
-
68
- Five of the entries above are this account's own machinery rather than operator
69
- data. Each is listed below with the reason it is off-limits. The write guard
70
- blocks a write whose first path segment matches one of them and quotes that
71
- reason back. Change any of them through the code that owns it: platform code
72
- writes all five, and none is written through an agent file edit.
73
-
74
- ```agent-denied-top-level
75
- account.json the account's identity and settings, changed through the account and admin tools
76
- SCHEMA.md the layout rules the write guard itself reads, so editing it widens its own fence
77
- secrets provisioned credentials, written by the code that mints each one
78
- .claude the agent's own settings and hooks, seeded when the account is provisioned
79
- .git the account directory's git internals, managed by git
80
- ```
81
- `````
82
-
83
- - [ ] **Step 4: Verify the tab separator landed**
84
-
85
- Run:
86
-
87
- ````bash
88
- awk '/^```agent-denied-top-level$/{f=1;next} /^```$/{f=0} f' platform/templates/account-schema/SCHEMA.md | cat -A | head -5
89
- ````
90
-
91
- Expected: each of the five lines shows `^I` between the name and the reason, and ends `$`. If any line shows spaces instead, fix it before continuing — the guard splits on TAB and a space-separated line parses as a name with no reason.
92
-
93
- - [ ] **Step 5: Run the full guard suite**
94
-
95
- Run: `bash platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh`
96
-
97
- Expected: `----- 41 passed, 0 failed -----`. Every pre-existing case must still pass, which is the proof the appended fence does not disturb the `allowed-top-level` parse, the `declared-files` parse, or any existing account fixture.
98
-
99
- - [ ] **Step 6: Run the neighbouring suites that read the same schema**
100
-
101
- Run: `bash platform/plugins/admin/hooks/__tests__/fs-schema-guard-bash.test.sh`
102
-
103
- Expected: `----- 15 passed, 0 failed -----`
104
-
105
- Run: `bash platform/scripts/__tests__/account-schema-owned-dirs.test.sh`
106
-
107
- Expected: the suite's own pass line with zero failures. This is the regression check that the owned-dirs merge still finds and rewrites the `allowed-top-level` fence and its three marker-delimited regions with an extra fence present in the file.
108
-
109
- - [ ] **Step 7: Commit**
110
-
111
- ````bash
112
- git add maxy-code/platform/templates/account-schema/SCHEMA.md maxy-code/platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh
113
- git commit -m "feat(2052): ship the agent-denied-top-level fence in the account schema template"
114
- ````
115
-
116
- ---
117
-
118
- ### Task 2: The guard denies a write to a fenced name
119
-
120
- **Files:**
121
- - Modify: `platform/plugins/admin/hooks/fs-schema-guard.sh` (header comment block at lines 2-22; new branch inserted after the top-level check's closing `fi` at line 89)
122
- - Test: `platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh`
123
-
124
- **Interfaces:**
125
- - Consumes: the `agent-denied-top-level` fence from Task 1, `name<TAB>reason` per line.
126
- - Produces: exit 2 with `[fs-guard] blocked path=<rel> reason=account-machinery` on stderr, plus an operator-visible line quoting that name's reason. Nothing later depends on it.
127
-
128
- - [ ] **Step 1: Write the failing tests**
129
-
130
- Add this block to `platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh`, immediately after the `run_case "no fence no deny" ...` line and before the `# Allowed-set parse == documented set.` comment:
131
-
132
- ````bash
133
- # Account machinery: five entries in the allowed set are the account's own
134
- # control plane, denied by name in the agent-denied-top-level fence. $ACCT is
135
- # the pristine template copy, so it carries the fence. $ACCT_M models a schema
136
- # written before the fence existed (names in the allowed set, fence stripped),
137
- # which pins the deny to the fence rather than to a list baked into the hook.
138
- ACCT_M=$(mktemp -d)
139
- trap 'rm -rf "$ACCT" "$ACCT_D" "$ACCT_F" "$ACCT_L" "$ACCT_M"' EXIT
140
- awk '/^```agent-denied-top-level$/{skip=1;next} skip && /^```$/{skip=0;next} !skip' "$TEMPLATE" > "$ACCT_M/SCHEMA.md"
141
-
142
- DENIED_NAMES="account.json SCHEMA.md secrets .claude .git"
143
- for n in $DENIED_NAMES; do
144
- run_case "machinery block $n" "$(mkenv Write file_path "$n")" 2 "fs-guard. blocked path=$n reason=account-machinery"
145
- done
146
- # Segment-0 match: a nested path under a denied entry blocks for the same reason.
147
- run_case "machinery nested settings" "$(mkenv Write file_path '.claude/settings.json')" 2 "fs-guard. blocked path=.claude/settings.json reason=account-machinery"
148
- run_case "machinery nested secret" "$(mkenv Write file_path 'secrets/cloudflare.env')" 2 "reason=account-machinery"
149
- # Every write tool, not just Write.
150
- run_case "machinery edit blocks" "$(mkenv Edit file_path 'account.json')" 2 "reason=account-machinery"
151
- run_case "machinery notebook blocks" "$(mkenv NotebookEdit notebook_path '.claude/n.ipynb')" 2 "reason=account-machinery"
152
- # The schema's own reason reaches the operator-visible message.
153
- run_case "machinery reason text" "$(mkenv Write file_path 'SCHEMA.md')" 2 "widens its own fence"
154
- # An operator bucket under the same schema is unaffected.
155
- run_case "machinery bucket allow" "$(mkenv Write file_path 'projects/acme/a.txt')" 0 ""
156
- # No fence, no deny.
157
- for n in $DENIED_NAMES; do
158
- run_case "no deny fence allows $n" "$(mkenv Write file_path "$n")" 0 "" "$ACCT_M"
159
- done
160
- ````
161
-
162
- - [ ] **Step 2: Run the suite to verify the new cases fail**
163
-
164
- Run: `bash platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh`
165
-
166
- Expected: the ten `machinery *` cases FAIL with `(exit=0, want 2; ...)`. The five `no deny fence allows *` cases and `machinery bucket allow` already pass. The run ends `----- 47 passed, 10 failed -----` with a non-zero exit.
167
-
168
- - [ ] **Step 3: Add the deny branch to the guard**
169
-
170
- In `platform/plugins/admin/hooks/fs-schema-guard.sh`, insert this block immediately after the top-level check's closing `fi` (line 89) and before the `# Declared-file check.` comment:
171
-
172
- ````bash
173
- # Account-machinery check. Some entries in the allowed set are the account's own
174
- # control plane rather than operator data: the identity file, the schema this
175
- # hook parses, the credential store, the agent's own settings and hooks, and the
176
- # git dir. The allowed set says which names may EXIST at the account root, not
177
- # which an agent may author, so each of these is named in a second fence together
178
- # with the reason it is off-limits. Every one of them is written by platform code
179
- # from bash or Node, never through Write/Edit/NotebookEdit, so denying them costs
180
- # no legitimate writer. The set and its reasons come from the account's own
181
- # SCHEMA.md, so the hook hard-codes no list. There is no owning plugin to name
182
- # here, which is why this fence carries a reason column the declared-files fence
183
- # does not need. No fence (a schema written before the fence existed) means
184
- # nothing to deny — the same fail-open posture as the missing-schema case above.
185
- DENIED=$(awk '/^```agent-denied-top-level$/{f=1;next} /^```$/{f=0} f' "$ACCOUNT_DIR/SCHEMA.md")
186
- if [ -n "$DENIED" ] && printf '%s\n' "$DENIED" | awk -F'\t' -v n="$SEG0" '$1==n{found=1} END{exit !found}'; then
187
- WHY=$(printf '%s\n' "$DENIED" | awk -F'\t' -v n="$SEG0" '$1==n{print $2; exit}')
188
- echo "[fs-guard] blocked path=$REL reason=account-machinery" >&2
189
- echo "Blocked: '$SEG0' is this account's own machinery, not operator data: $WHY. Change it through the code that owns it, never by hand." >&2
190
- exit 2
191
- fi
192
- ````
193
-
194
- - [ ] **Step 4: Update the hook's header comment**
195
-
196
- In the same file, in the header comment block, insert this bullet after the `allowed top-level set` bullet and before the `plugin-declared file` bullet, so the header lists all three rules the hook enforces:
197
-
198
- ````bash
199
- # - a target whose first path segment is named in the fenced
200
- # ```agent-denied-top-level block is blocked even though that name is in the
201
- # allowed set: those entries are the account's own machinery (identity,
202
- # schema, credentials, agent settings, git dir), written by platform code
203
- # and never by an agent file edit. The block message quotes the reason the
204
- # fence carries for that name;
205
- ````
206
-
207
- Then extend the exit-codes paragraph's log-line list so it reads:
208
-
209
- ````bash
210
- # [fs-guard] blocked path=<rel> reason=<top-level|account-machinery|declared-file|over-deep|bad-name>
211
- ````
212
-
213
- - [ ] **Step 5: Run the suite to verify it passes**
214
-
215
- Run: `bash platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh`
216
-
217
- Expected: `----- 57 passed, 0 failed -----`
218
-
219
- - [ ] **Step 6: Mutation check**
220
-
221
- Comment out the `exit 2` inside the new branch and re-run the suite.
222
-
223
- Run: `bash platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh`
224
-
225
- Expected: the ten `machinery *` cases FAIL. Then restore the line and re-run.
226
-
227
- Expected after restore: `----- 57 passed, 0 failed -----`
228
-
229
- - [ ] **Step 7: Confirm the untouched surfaces still pass**
230
-
231
- Run: `bash platform/plugins/admin/hooks/__tests__/fs-schema-guard-bash.test.sh`
232
-
233
- Expected: `----- 15 passed, 0 failed -----`. The Bash post-hook still reads only `allowed-top-level` and `declared-files`, and still stays silent on all five names.
234
-
235
- Run: `bash platform/scripts/__tests__/account-schema-owned-dirs.test.sh`
236
-
237
- Expected: zero failures.
238
-
239
- - [ ] **Step 8: Commit**
240
-
241
- ````bash
242
- git add maxy-code/platform/plugins/admin/hooks/fs-schema-guard.sh maxy-code/platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh
243
- git commit -m "feat(2052): deny agent writes to the account's own machinery"
244
- ````
245
-
246
- ---
247
-
248
- ## Reach note for the debrief
249
-
250
- `provision-account-dir.sh:40-41` copies the template to `<accountDir>/SCHEMA.md`, and `setup-account.sh:59` calls that for the house account only. The standing all-accounts reconcile merges just the marker-delimited regions, so it never carries the template body. The house account therefore gains the fence on the next install, while a client sub-account, provisioned once at creation, keeps a fence-less schema indefinitely and the guard stays fail-open there. That gap is filed as `.tasks/pending/2056-the-account-machinery-deny-fence-never-reaches-an-existing-sub-account.md`, following the durable-backfill pattern of Task 1683 and Task 1929. Separately, `platform/` ships inside the bundled `create-*-code` payload, so reaching a device at all needs an operator-gated installer publish plus an `npx` upgrade. Both belong in the debrief's known-limitations section.
@@ -1,65 +0,0 @@
1
- # Task 2097 — `email-signature-set` implementation plan
2
-
3
- Design: [`2026-07-28-task-2097-email-signature-set-design.md`](../specs/2026-07-28-task-2097-email-signature-set-design.md).
4
-
5
- ## Registration surfaces — corrected against the tree
6
-
7
- The task file names `ADMIN_CORE_TOOLS`. It does not exist: `grep -rn 'ADMIN_CORE_TOOLS' platform/` returns
8
- nothing outside comments. The live surfaces, each confirmed by reading the file, are four:
9
-
10
- | # | Surface | Evidence |
11
- |---|---|---|
12
- | 1 | `plugins/email/mcp/src/index.ts` | `eagerTool(server, "email-setup", …)` at line 124 |
13
- | 2 | `plugins/email/PLUGIN.md` | frontmatter `description:`, `tools:` list, Capabilities, Skills table |
14
- | 3 | `platform/templates/specialists/agents/personal-assistant.md` | the only template matching `grep -rl email-setup platform/templates/` |
15
- | 4 | `services/claude-session-manager/src/canonical-tool-names.generated.ts` | header: generated from `PLUGIN.md` `tools:` blocks, `check-canonical-tool-names.mjs` fails the build on drift |
16
-
17
- Surface 4 is not in the task file and would have broken the build. It is regenerated, never hand-edited:
18
- `cd platform && npm run gen:canonical-tools`.
19
-
20
- Three further files match `email-draft-send` and are deliberately **not** touched:
21
- `plugins/admin/hooks/preference-consult-gate.sh`, `scripts/lib/provision-account-dir.sh`, and the
22
- `plugins/admin/PLUGIN.md` prose describing them. All three are the outbound-send preference gate,
23
- matching sends that carry attachments. `email-signature-set` sends nothing, so it is not a gate member.
24
-
25
- ## Steps
26
-
27
- 1. **`lib/signature.ts`** — add `SIGNATURE_BASENAME`, `signatureDirFor(accountId)`, and
28
- `saveSignature(accountDir, parts)`. `loadSignature` / `applySignature` unchanged.
29
- `signatureDirFor` is `accountDirFor(accountId) || process.env.ACCOUNT_DIR || undefined`, which is the
30
- expression `compose.ts:112` already computes, with an empty-string env normalised to `undefined`
31
- (both already load as no-signature, so behaviour is unchanged). No import cycle: `attachment-resolve.ts`
32
- imports only `node:fs` and `node:path`.
33
- 2. **`lib/compose.ts`** — replace the inline `sigDir` expression with `signatureDirFor(accountId)` and the
34
- literal `"email-signature"` in `assetNote` with `SIGNATURE_BASENAME`. `accountDirFor` stays imported for
35
- the attachment path at line 75.
36
- 3. **`tools/email-signature-set.ts`** — refuse an all-absent call and an unresolvable directory before any
37
- write, call `saveSignature`, log, return the resolved paths.
38
- 4. **`index.ts`** — import and `eagerTool` register, placed after the `email-setup` block. Guard with
39
- `refuseNoAccount`, wrap in try/catch like `email-setup`. The handler's local result variable is named
40
- `message`, not `text`, so it cannot shadow the `text` parameter.
41
- 5. **`PLUGIN.md`** — `tools:` entry (`riskClass: write_local`, both allowlists false, matching `email-setup`),
42
- a Config clause in the frontmatter `description:`, a Capabilities bullet, a Skills-table row.
43
- 6. **`personal-assistant.md`** — add `mcp__plugin_email_email__email-signature-set` after `email-setup`.
44
- 7. **Regenerate** `canonical-tool-names.generated.ts`.
45
- 8. **`skills/email-signature/SKILL.md`** — new skill.
46
- 9. **`skills/email-composition/SKILL.md`** — one added rule, see below.
47
- 10. **`__tests__/email-signature-set.test.ts`** — committed, not ephemeral: the plugin already carries 34
48
- test files and the task file mandates a regression test.
49
-
50
- ## Addition beyond the task file
51
-
52
- The task specifies that the new `email-signature` skill carries the rule "never hand-build a signature
53
- inside a draft body". That skill activates when the operator asks to set or change a signature. The
54
- incident happened while **drafting**, when that skill is not loaded. `grep -in signature` over
55
- `skills/email-composition/SKILL.md` returns zero hits, so the drafting surface currently says nothing
56
- about signatures.
57
-
58
- So the rule is also stated in one sentence in `email-composition`, which is the skill that is loaded while
59
- drafting. Without it the doctrine lives only in a skill absent at the moment it is needed. Declared in Plan
60
- Conformity as an addition.
61
-
62
- ## Verification
63
-
64
- `npm run build` and `npx vitest run` in `plugins/email/mcp`, then `npm run gen:canonical-tools` and the
65
- `check-canonical-tool-names.mjs` gate in `platform/`. No browser surface, no Pi hook surface.