@rubytech/create-maxy-code 0.1.501 → 0.1.503

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 (77) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/docs/superpowers/plans/2026-07-25-task-1974-uploads-intake-inbox.md +235 -0
  3. package/payload/platform/docs/superpowers/plans/2026-07-25-task-1976-intra-folder-hygiene.md +215 -0
  4. package/payload/platform/plugins/admin/PLUGIN.md +1 -0
  5. package/payload/platform/plugins/admin/hooks/__tests__/quote-engine-gate.test.sh +98 -0
  6. package/payload/platform/plugins/admin/hooks/quote-engine-gate.sh +68 -0
  7. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +2 -2
  8. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +13 -0
  9. package/payload/platform/plugins/docs/references/internals.md +1 -1
  10. package/payload/platform/scripts/lib/provision-account-dir.sh +2 -1
  11. package/payload/platform/services/telegram-channel/dist/instructions.js +2 -2
  12. package/payload/platform/services/telegram-channel/dist/instructions.js.map +1 -1
  13. package/payload/platform/services/webchat-channel/dist/instructions.js +2 -2
  14. package/payload/platform/services/webchat-channel/dist/instructions.js.map +1 -1
  15. package/payload/platform/services/whatsapp-channel/dist/notification.js +2 -2
  16. package/payload/platform/services/whatsapp-channel/dist/notification.js.map +1 -1
  17. package/payload/platform/templates/account-schema/SCHEMA.md +36 -1
  18. package/payload/platform/templates/specialists/agents/data-manager.md +4 -2
  19. package/payload/server/public/activity.html +5 -5
  20. package/payload/server/public/assets/AdminLoginScreens-Bv_bKKeL.js +1 -0
  21. package/payload/server/public/assets/AdminShell-Gq8DU_ig.js +2 -0
  22. package/payload/server/public/assets/{activity-CVdVSw5V.js → activity-DZFYDHNA.js} +1 -1
  23. package/payload/server/public/assets/{admin-If1QLlS3.js → admin-DLUOixl6.js} +1 -1
  24. package/payload/server/public/assets/{bot-8b26IlQY.js → bot-CrEhKW1l.js} +1 -1
  25. package/payload/server/public/assets/{browser-F40DXfKA.js → browser-Bt4-LKLU.js} +1 -1
  26. package/payload/server/public/assets/calendar-B8ujkn_K.js +1 -0
  27. package/payload/server/public/assets/chat-lQXd6gsM.js +1 -0
  28. package/payload/server/public/assets/chevron-left-CQ8rcBoO.js +1 -0
  29. package/payload/server/public/assets/chevron-right-CSRSqblN.js +1 -0
  30. package/payload/server/public/assets/clock-B7Ba8AZL.js +1 -0
  31. package/payload/server/public/assets/data-BdlGqjKM.js +1 -0
  32. package/payload/server/public/assets/{file-text-B1HjPFbM.js → file-text-BpLcZKld.js} +1 -1
  33. package/payload/server/public/assets/{graph-CIC-mgKR.js → graph-HB0199x-.js} +3 -3
  34. package/payload/server/public/assets/{graph-labels-BnpHCHaQ.js → graph-labels-Bx19ZYvJ.js} +1 -1
  35. package/payload/server/public/assets/{maximize-2-BL7QVTcQ.js → maximize-2-BhR-Ec21.js} +1 -1
  36. package/payload/server/public/assets/operator-Bf3hP3qo.js +1 -0
  37. package/payload/server/public/assets/page-Bs24tO-C.js +32 -0
  38. package/payload/server/public/assets/page-FnCmAeHc.js +1 -0
  39. package/payload/server/public/assets/{public-BI21tOkm.js → public-DZHPsyKX.js} +1 -1
  40. package/payload/server/public/assets/{rotate-ccw-C_20Bc3E.js → rotate-ccw-D9qOBLig.js} +1 -1
  41. package/payload/server/public/assets/{routines-Ci3N8wqV.js → routines-lGiYaMiX.js} +1 -1
  42. package/payload/server/public/assets/skills-y-h0P9rC.js +1 -0
  43. package/payload/server/public/assets/{tasks-DC2Yf2Ev.js → tasks-Cyc1EfLA.js} +1 -1
  44. package/payload/server/public/assets/{time-entry-format-CNcjs2HV.js → time-entry-format-RkCW7hKu.js} +1 -1
  45. package/payload/server/public/assets/{triangle-alert-CngnWtjD.js → triangle-alert-k8ulav-N.js} +1 -1
  46. package/payload/server/public/assets/{useCopyFeedback-DZ9Bw5CI.js → useCopyFeedback-DN1WA5vU.js} +1 -1
  47. package/payload/server/public/assets/useSubAccountSwitcher-fvBunpOC.css +1 -0
  48. package/payload/server/public/assets/useSubAccountSwitcher-iQGqNJhI.js +9 -0
  49. package/payload/server/public/assets/useVoiceRecorder-v--TIxaM.js +2 -0
  50. package/payload/server/public/assets/{wrench-dky2-13x.js → wrench-CKhz8KnQ.js} +1 -1
  51. package/payload/server/public/browser.html +4 -4
  52. package/payload/server/public/calendar.html +7 -7
  53. package/payload/server/public/chat.html +14 -14
  54. package/payload/server/public/data.html +11 -11
  55. package/payload/server/public/graph.html +9 -9
  56. package/payload/server/public/index.html +15 -15
  57. package/payload/server/public/operator.html +15 -15
  58. package/payload/server/public/public.html +14 -14
  59. package/payload/server/public/routines.html +6 -6
  60. package/payload/server/public/skills.html +5 -5
  61. package/payload/server/public/tasks.html +6 -6
  62. package/payload/server/server.js +840 -748
  63. package/payload/server/public/assets/AdminLoginScreens-C3LZOPIY.js +0 -1
  64. package/payload/server/public/assets/AdminShell-BlbUNAPc.js +0 -2
  65. package/payload/server/public/assets/calendar-DZiHiBwV.js +0 -1
  66. package/payload/server/public/assets/chat-uD28FVCS.js +0 -1
  67. package/payload/server/public/assets/chevron-left-lBxD57tD.js +0 -1
  68. package/payload/server/public/assets/chevron-right-C-lDig9Y.js +0 -1
  69. package/payload/server/public/assets/clock-DqRNsY40.js +0 -1
  70. package/payload/server/public/assets/data-BsFyBN85.js +0 -1
  71. package/payload/server/public/assets/operator-B62h_TaC.js +0 -1
  72. package/payload/server/public/assets/page-BUUBseJZ.js +0 -32
  73. package/payload/server/public/assets/page-Bw-tYjw7.js +0 -1
  74. package/payload/server/public/assets/skills-CiflzOko.js +0 -1
  75. package/payload/server/public/assets/useSubAccountSwitcher-CHwQhkaI.js +0 -9
  76. package/payload/server/public/assets/useSubAccountSwitcher-yFQqnpQ8.css +0 -1
  77. package/payload/server/public/assets/useVoiceRecorder-C3G6aqZr.js +0 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.501",
3
+ "version": "0.1.503",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -0,0 +1,235 @@
1
+ # Task 1974 — uploads/ intake inbox 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:** Reclassify the account `uploads/` directory from tool-owned scratch to a temporary intake inbox, and add a `stranded-intake` reconcile count so a node file-reference left pointing into `uploads/` is visible.
6
+
7
+ **Architecture:** Doctrine plus audit change, no code path change. Three prose files are edited (`SCHEMA.md`, `data-manager.md`, `internals.md`), then the `platform-architecture` SKILL.md twin is regenerated from the docs corpus so the no-drift gate passes. Detection-only, mirroring the outbound `output/` rule landed by task 1930.
8
+
9
+ **Tech Stack:** Markdown doctrine files; Node regen scripts (`docs/scripts/copy-docs.mjs`, `maxy-code/scripts/assemble-architecture-skill.mjs`); the drift gate `maxy-code/platform/scripts/check-architecture-skill-no-drift.mjs`; the bash test `fs-schema-guard.test.sh`.
10
+
11
+ ## Global Constraints
12
+
13
+ - Zero em-dashes in all shipped prose.
14
+ - The 1930 outbound rule must survive intact: the `output/` promote-out sentence and the `scratch-refs` / `stranded` counts stay present. `stranded-intake` is appended to the output contract, never a rename of `stranded`.
15
+ - `uploads` stays in the `allowed-top-level` block of SCHEMA.md (files still land there).
16
+ - The write-path guard, the admin-UI upload handler, and `memory-ingest` `sourcePath` behaviour are unchanged.
17
+ - Reconcile output contract after this task: `unreachable=N broken-refs=M scratch-refs=P stranded=Q stranded-intake=R`.
18
+ - Paths are relative to the `maxy-code/` subtree root inside the worktree.
19
+
20
+ ---
21
+
22
+ ### Task 1: SCHEMA.md — reclassify uploads/ as intake inbox
23
+
24
+ **Files:**
25
+ - Modify: `platform/templates/account-schema/SCHEMA.md:31-39`
26
+
27
+ **Interfaces:**
28
+ - Produces: the doctrine text data-manager.md and internals.md refer to. No code symbols.
29
+
30
+ - [ ] **Step 1: Remove `uploads/` from the "recreated on the next write" list.**
31
+
32
+ At `SCHEMA.md:36-39`, change the list so `uploads/` is no longer among tool-owned rebuilt dirs:
33
+
34
+ ```
35
+ The internal structure of these is owned by the writing tool and is recreated on
36
+ the next write: `url-get/`, `output/`, `generated/`, `extracted/`,
37
+ any published/served tree (`sites/`, `public/`), `agents/`, `specialists/`, and
38
+ platform-internal areas (`cache/`, `secrets/`, `state/`, `logs/`, `tmp/`).
39
+ ```
40
+
41
+ - [ ] **Step 2: Add the intake-inbox rule paragraph.**
42
+
43
+ Immediately after the `output/` promote-out paragraph (ends at `SCHEMA.md:34`), insert a new paragraph. It states uploads is a temporary intake inbox, a file is promoted to its canonical bucket when its node is created, no persisted graph reference points into `uploads/`, and the per-`<attachmentId>` reason it is safe to move out of (unlike `output/`):
44
+
45
+ ```
46
+ `uploads/` is a temporary intake inbox, not rebuilt scratch. A file lands at
47
+ `uploads/<attachmentId>/` on upload and is promoted to its canonical entity
48
+ bucket when its graph node is created: a Person or Organization file to
49
+ `contacts/<name>/`, a Project file to `projects/<name>/`, anything else to
50
+ `documents/`. A persisted graph reference must never point into `uploads/`.
51
+ Unlike `output/`, each upload writes its own `uploads/<attachmentId>/` dir and no
52
+ tool walks or rebuilds the subtree, so promoting a file out of it through the
53
+ `data-manager` specialist is safe.
54
+ ```
55
+
56
+ - [ ] **Step 3: Verify the edit.**
57
+
58
+ Run:
59
+ ```bash
60
+ cd platform/templates/account-schema
61
+ # uploads no longer in the recreated-on-next-write list
62
+ awk '/recreated on/,/logs.*tmp/' SCHEMA.md | grep -c 'uploads/' | grep -qx 0 && echo "OK: uploads removed from scratch list"
63
+ # intake-inbox rule present
64
+ grep -q 'temporary intake inbox' SCHEMA.md && echo "OK: intake rule present"
65
+ # uploads still allowed-top-level
66
+ grep -A40 'allowed-top-level' SCHEMA.md | grep -qx 'uploads' && echo "OK: uploads still allowed-top-level"
67
+ # 1930 output/ rule survives
68
+ grep -q 'promoted out of `output/`' SCHEMA.md && echo "OK: 1930 output rule intact"
69
+ # zero em-dashes in this file
70
+ ! grep -q '—' SCHEMA.md && echo "OK: no em-dash"
71
+ ```
72
+ Expected: all five OK lines.
73
+
74
+ - [ ] **Step 4: Commit.**
75
+
76
+ ```bash
77
+ git add platform/templates/account-schema/SCHEMA.md
78
+ git commit -m "task: reclassify uploads/ as intake inbox in account SCHEMA.md (1974)"
79
+ ```
80
+
81
+ ---
82
+
83
+ ### Task 2: data-manager.md — reconcile case (e), walk uploads/, promotion permission, output contract
84
+
85
+ **Files:**
86
+ - Modify: `platform/templates/specialists/agents/data-manager.md:24` (reconcile brief), `:30` (output contract), and the "What success looks like" area for the promotion permission.
87
+
88
+ **Interfaces:**
89
+ - Consumes: the SCHEMA.md intake-inbox rule from Task 1.
90
+ - Produces: the `stranded-intake=R` field that internals.md documents in Task 3.
91
+
92
+ - [ ] **Step 1: Add reconcile case (e) and include uploads/ in the walk.**
93
+
94
+ At `data-manager.md:24`, extend the counted list. After case (d), add case (e): a graph file-reference that resolves into `uploads/`, the inbound intake mirror of the `output/` scratch case. State that the walk now includes `uploads/` (a file under `uploads/` with no referencing node folds into `unreachable`). Exact appended clause inside the sentence that enumerates (a) to (d):
95
+
96
+ ```
97
+ , and (e) graph file-references that resolve into the `uploads/` intake inbox,
98
+ which is a temporary landing area a file should have been promoted out of when
99
+ its node was created. The walk now includes `uploads/`; a file under `uploads/`
100
+ that no node references counts under (a) unreachable.
101
+ ```
102
+
103
+ - [ ] **Step 2: Add the stewardship promotion permission for uploads/.**
104
+
105
+ In "## What success looks like" (after `data-manager.md:16`, the paired-move paragraph), add a sentence permitting promotion out of `uploads/`:
106
+
107
+ ```
108
+ - A file in the `uploads/` intake inbox whose graph node exists is promoted to
109
+ its canonical bucket (Person or Organization to `contacts/<name>/`, Project to
110
+ `projects/<name>/`, else `documents/`), paired with a `database-operator`
111
+ `sourcePath` update in the same pass, exactly as any other move.
112
+ ```
113
+
114
+ - [ ] **Step 3: Change the output contract.**
115
+
116
+ At `data-manager.md:30`, change the reconcile summary line:
117
+
118
+ ```
119
+ For the reconcile audit: `unreachable=N broken-refs=M scratch-refs=P stranded=Q stranded-intake=R`.
120
+ ```
121
+
122
+ - [ ] **Step 4: Verify the edit.**
123
+
124
+ Run:
125
+ ```bash
126
+ cd platform/templates/specialists/agents
127
+ grep -q 'stranded-intake=R' data-manager.md && echo "OK: contract has stranded-intake"
128
+ grep -q 'scratch-refs=P stranded=Q' data-manager.md && echo "OK: 1930 fields survive"
129
+ grep -q '(e) graph file-references that resolve into the `uploads/`' data-manager.md && echo "OK: case e present"
130
+ grep -q 'promoted to' data-manager.md && grep -q 'sourcePath` update' data-manager.md && echo "OK: promotion permission present"
131
+ ! grep -q '—' data-manager.md && echo "OK: no em-dash"
132
+ ```
133
+ Expected: five OK lines.
134
+
135
+ - [ ] **Step 5: Commit.**
136
+
137
+ ```bash
138
+ git add platform/templates/specialists/agents/data-manager.md
139
+ git commit -m "task: data-manager reconcile counts uploads/ intake drift as stranded-intake (1974)"
140
+ ```
141
+
142
+ ---
143
+
144
+ ### Task 3: internals.md — document the reclassification and the stranded-intake count
145
+
146
+ **Files:**
147
+ - Modify: `platform/plugins/docs/references/internals.md:411`
148
+
149
+ **Interfaces:**
150
+ - Consumes: the reconcile contract from Task 2.
151
+ - Produces: the corpus text the architecture SKILL.md twin regenerates from in Task 4.
152
+
153
+ - [ ] **Step 1: Append the intake-inbox rule to the placement paragraph.**
154
+
155
+ At `internals.md:411`, the sentence currently ends: "...is counted by the `data-manager` reconcile audit (`scratch-refs`, `stranded`); making that audit run on a standing periodic cadence is a filed follow-up." Extend it with the inbound mirror. Append after the `scratch-refs`/`stranded` clause:
156
+
157
+ ```
158
+ The inbound mirror is the `uploads/` intake inbox: an uploaded file lands at `uploads/<attachmentId>/` and is promoted to its canonical entity bucket when its graph node is created, so a persisted node reference resolving into `uploads/` is intake drift, counted by the same reconcile audit as `stranded-intake`. Unlike `output/`, `uploads/` is never rebuilt by a tool, so the `data-manager` may promote a file out of it. The reconcile output contract is `unreachable=N broken-refs=M scratch-refs=P stranded=Q stranded-intake=R`.
159
+ ```
160
+
161
+ - [ ] **Step 2: Verify the edit.**
162
+
163
+ Run:
164
+ ```bash
165
+ grep -q 'stranded-intake' platform/plugins/docs/references/internals.md && echo "OK: internals mentions count"
166
+ grep -q 'intake inbox' platform/plugins/docs/references/internals.md && echo "OK: internals mentions reclassification"
167
+ ! grep -q '—' platform/plugins/docs/references/internals.md && echo "OK: no em-dash added"
168
+ ```
169
+ Expected: three OK lines. (The last check should already pass before the edit; keep the added prose em-dash-free.)
170
+
171
+ - [ ] **Step 3: Commit.**
172
+
173
+ ```bash
174
+ git add platform/plugins/docs/references/internals.md
175
+ git commit -m "task: internals.md documents uploads/ intake inbox and stranded-intake count (1974)"
176
+ ```
177
+
178
+ ---
179
+
180
+ ### Task 4: Regenerate the architecture skill twin and pass the gates
181
+
182
+ **Files:**
183
+ - Modify (generated): `platform/plugins/admin/skills/platform-architecture/SKILL.md`
184
+ - Run: `docs/scripts/copy-docs.mjs`, `maxy-code/scripts/assemble-architecture-skill.mjs`, `platform/scripts/check-architecture-skill-no-drift.mjs`, `platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh`
185
+
186
+ **Interfaces:**
187
+ - Consumes: the edited internals.md from Task 3.
188
+ - Produces: a SKILL.md byte-equal to what the gate regenerates.
189
+
190
+ - [ ] **Step 1: Regenerate the corpus and the skill twin.**
191
+
192
+ Run from the worktree repo root:
193
+ ```bash
194
+ node docs/scripts/copy-docs.mjs
195
+ node maxy-code/scripts/assemble-architecture-skill.mjs --brand maxy-code
196
+ ```
197
+ Expected: `[copy-docs]` and `[platform-arch-assemble] op=write` lines, no error.
198
+
199
+ - [ ] **Step 2: Run the drift gate.**
200
+
201
+ Run:
202
+ ```bash
203
+ node maxy-code/platform/scripts/check-architecture-skill-no-drift.mjs --brand maxy-code
204
+ ```
205
+ Expected: exit 0, no drift reported. If it reports drift, the regen in Step 1 did not run or the SKILL.md was not re-written; re-run Step 1.
206
+
207
+ - [ ] **Step 3: Run the write-guard test (regression boundary).**
208
+
209
+ Run:
210
+ ```bash
211
+ bash maxy-code/platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh
212
+ ```
213
+ Expected: all assertions pass. The guard is unchanged; `uploads/` is still an allowed write, so this stays green.
214
+
215
+ - [ ] **Step 4: Commit the regenerated twin (and any corpus artefact the gate expects committed).**
216
+
217
+ ```bash
218
+ git add platform/plugins/admin/skills/platform-architecture/SKILL.md
219
+ git status --porcelain docs/public
220
+ # stage docs/public/llms-full.*.txt only if it is a tracked artefact that changed
221
+ git commit -m "task: regenerate platform-architecture skill twin for 1974"
222
+ ```
223
+
224
+ ---
225
+
226
+ ## Land (Phase 6 handles this — not an implementation task)
227
+
228
+ LANES.md and task-file archival are done in Phase 6c of the sprint, not here. The task's "LANES Ready row" line describes the pre-implementation state; because this sprint implements and archives in one motion, the end state is: task file under `.tasks/archive/`, no Ready row referencing 1974, and the archive sentence naming 1974. This is a deliberate deviation from the task's literal LANES line, recorded in Plan Conformity.
229
+
230
+ ## Self-Review
231
+
232
+ - **Spec coverage:** SCHEMA.md reclassification (Task 1), data-manager.md case (e) + walk + promotion + contract (Task 2), internals.md + twin regen (Tasks 3-4), LANES (Phase 6c). Every in-scope item maps to a task.
233
+ - **Out of scope confirmed untouched:** no auto-promotion hook, no cadence (1931), no CC-native uploads reach, no write-guard edit, no ingest-path edit.
234
+ - **Type consistency:** the single shared token is the output-contract string; it reads `unreachable=N broken-refs=M scratch-refs=P stranded=Q stranded-intake=R` identically in Tasks 2 and 3.
235
+ - **Placeholder scan:** none; every edit carries its literal text.
@@ -0,0 +1,215 @@
1
+ # Task 1976 Intra-folder Filing Hygiene 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:** Teach the `data-manager` reconcile audit to count three intra-folder drift kinds (superseded versions, format twins, stray subfolders) and state the three governing rules in `SCHEMA.md`, `data-manager.md`, and `internals.md`, so version and format sprawl becomes visible before a cleanup is asked for.
6
+
7
+ **Architecture:** Detection-and-doctrine only, matching the 1930/1974 precedent. Three markdown source files gain the rules and counts; the `platform-architecture/SKILL.md` twin is regenerated from `internals.md` by the assembler, never hand-edited. No new write-time guard, no automatic deletion.
8
+
9
+ **Tech Stack:** Markdown templates, Node ESM regen scripts (`docs/scripts/copy-docs.mjs`, `maxy-code/scripts/assemble-architecture-skill.mjs`), Bash test harness (`fs-schema-guard.test.sh`), drift gate (`check-architecture-skill-no-drift.mjs`).
10
+
11
+ ## Global Constraints
12
+
13
+ - Zero em-dashes in shipped prose (use commas, colons, or full stops).
14
+ - Detection-only: the reconcile audit counts, it never removes. Removal is a stewardship dispatch under operator direction (deletion is destructive).
15
+ - The three new fields are **appended** to the output contract, never substituted: `scratch-refs`, `stranded`, and `stranded-intake` all survive.
16
+ - The no-subfolder rule is scoped to `projects/<name>/` and `contacts/<name>/` only. `documents/` is allowed one document-folder deep (`documents/<doc>/file`) and is out of scope for the subfolder rule.
17
+ - Version-group definition: `<base>.ext` and `<base>-vN.ext` sharing basename stem and extension are versions of one deliverable.
18
+ - Format-twin definition: same basename where a delivered `.pdf` sits beside its source render `.html`/`.md`; count the source renders whose delivered twin exists.
19
+ - The reconcile has no automated harness (it runs as a Sonnet agent). Verification of the counts is manual on a seeded folder; the automated gates that must stay green are `fs-schema-guard.test.sh` and `check-architecture-skill-no-drift.mjs`.
20
+
21
+ ---
22
+
23
+ ### Task 1: SCHEMA.md — Intra-folder hygiene section
24
+
25
+ **Files:**
26
+ - Modify: `platform/templates/account-schema/SCHEMA.md` (insert a new `## Intra-folder hygiene` section between `## Naming convention` and `## Tool-owned`)
27
+
28
+ **Interfaces:**
29
+ - Produces: the three named rules (current-version-only, one-canonical-format, no-subfolder) that Task 2 and Task 3 reference by name.
30
+
31
+ - [ ] **Step 1: Insert the section** after the Naming convention section (currently ending at line 27), before `## Tool-owned`:
32
+
33
+ ```markdown
34
+ ## Intra-folder hygiene
35
+
36
+ The operator-data buckets hold one current copy of each deliverable, in one
37
+ canonical format, with no subtree. Three rules keep them that way; the standing
38
+ reconcile counts any that predate them, and `data-manager` clears them on a
39
+ stewardship dispatch.
40
+
41
+ - **Current version only.** `<base>.ext` and `<base>-vN.ext` (for example
42
+ `charlotte-deck.pdf` and `charlotte-deck-v2.pdf` through
43
+ `charlotte-deck-v6.pdf`) are versions of one deliverable, not separate files.
44
+ Keep the newest; the superseded copies are removed through `data-manager`.
45
+ - **One canonical format.** A delivered `.pdf` and its source render sharing the
46
+ same basename (`charlotte-deck.html` beside `charlotte-deck.pdf`,
47
+ `invoice-rbt-2026-005.html` beside `invoice-rbt-2026-005.pdf`) are one
48
+ deliverable in two formats. The pdf is the deliverable; the `.html` or `.md`
49
+ render that produced it is not a second one and is removed through
50
+ `data-manager`.
51
+ - **No subfolder.** No folder nests under `projects/<name>/` or
52
+ `contacts/<name>/`; files live directly inside the entity folder. This is the
53
+ flat rule from the buckets above, restated as a standing invariant: the write
54
+ guard blocks the depth as you create it, and the reconcile counts any subtree
55
+ that predates the guard or was created off-path.
56
+ ```
57
+
58
+ - [ ] **Step 2: Verify the section reads and has no em-dashes**
59
+
60
+ Run: `grep -n 'Intra-folder hygiene' platform/templates/account-schema/SCHEMA.md && ! grep -n '—' platform/templates/account-schema/SCHEMA.md && echo "no em-dash"`
61
+ Expected: the heading line prints, then `no em-dash`.
62
+
63
+ - [ ] **Step 3: Verify the write guard is untouched (green baseline holds)**
64
+
65
+ Run: `bash platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh 2>&1 | tail -1`
66
+ Expected: `----- 22 passed, 0 failed -----`
67
+
68
+ - [ ] **Step 4: Commit**
69
+
70
+ ```bash
71
+ git add platform/templates/account-schema/SCHEMA.md
72
+ git commit -m "task: SCHEMA.md states intra-folder hygiene rules (1976)"
73
+ ```
74
+
75
+ ---
76
+
77
+ ### Task 2: data-manager.md — three counts, output contract, stewardship
78
+
79
+ **Files:**
80
+ - Modify: `platform/templates/specialists/agents/data-manager.md` (reconcile brief at line 25, output contract at line 31, "What success looks like" at line 17)
81
+
82
+ **Interfaces:**
83
+ - Consumes: the three rule names from Task 1.
84
+ - Produces: the output-contract line `unreachable=N broken-refs=M scratch-refs=P stranded=Q stranded-intake=R versioned=S format-twins=T stray-subdir=U` that Task 3 mirrors verbatim.
85
+
86
+ - [ ] **Step 1: Extend the reconcile brief** (line 25). Insert immediately before `Move nothing, dispatch nothing.`:
87
+
88
+ ```
89
+ Then, per operator-data entity folder (`projects/<name>/`, `contacts/<name>/`), count three intra-folder drifts from disk structure alone, no graph resolution needed: (f) files in a version group, where `<base>.ext` and `<base>-vN.ext` share a basename stem and extension and all but the newest are superseded, counting every superseded copy as `versioned`; (g) format twins, a source-render file (`.html`, `.md`) whose delivered `.pdf` twin of the same basename sits in the same folder, counting each such render as `format-twins`; (h) stray subfolders nested under an entity folder, which the flat rule forbids, counting each subfolder as `stray-subdir`.
90
+ ```
91
+
92
+ - [ ] **Step 2: Extend the output contract** (line 31). Change the reconcile-audit string from:
93
+
94
+ ```
95
+ `unreachable=N broken-refs=M scratch-refs=P stranded=Q stranded-intake=R`
96
+ ```
97
+
98
+ to:
99
+
100
+ ```
101
+ `unreachable=N broken-refs=M scratch-refs=P stranded=Q stranded-intake=R versioned=S format-twins=T stray-subdir=U`
102
+ ```
103
+
104
+ - [ ] **Step 3: Add the stewardship bullet** to "What success looks like", immediately after the `uploads/` promotion bullet (line 17):
105
+
106
+ ```
107
+ - Superseded versions and redundant format twins are removed, and a subfolder nested under `projects/<name>/` or `contacts/<name>/` is flattened or re-filed, when a stewardship brief asks for it. Each removal or move pairs with a `database-operator` reference update in the same pass, exactly as any other move: the surviving current render keeps the node reference, and a removed file's node is repointed or its reference dropped. Removal is destructive, so it happens only on an explicit stewardship dispatch, never in the read-only reconcile audit.
108
+ ```
109
+
110
+ - [ ] **Step 4: Verify the contract line and no em-dashes**
111
+
112
+ Run: `grep -c 'versioned=S format-twins=T stray-subdir=U' platform/templates/specialists/agents/data-manager.md && grep -o 'scratch-refs=P stranded=Q stranded-intake=R' platform/templates/specialists/agents/data-manager.md && ! grep '—' platform/templates/specialists/agents/data-manager.md && echo "no em-dash"`
113
+ Expected: `1`, then the preserved `scratch-refs=P stranded=Q stranded-intake=R` fragment, then `no em-dash`.
114
+
115
+ - [ ] **Step 5: Commit**
116
+
117
+ ```bash
118
+ git add platform/templates/specialists/agents/data-manager.md
119
+ git commit -m "task: data-manager reconcile counts versioned/format-twins/stray-subdir (1976)"
120
+ ```
121
+
122
+ ---
123
+
124
+ ### Task 3: internals.md — reconcile rules and contract string
125
+
126
+ **Files:**
127
+ - Modify: `platform/plugins/docs/references/internals.md` (the reconcile paragraph at line 411)
128
+
129
+ **Interfaces:**
130
+ - Consumes: the output-contract line from Task 2 (must match verbatim).
131
+ - Produces: the corpus source the assembler reads to regenerate the SKILL.md twin in Task 4.
132
+
133
+ - [ ] **Step 1: Insert the intra-folder description** into line 411, immediately before the final sentence `The reconcile output contract is ...`:
134
+
135
+ ```
136
+ Beyond placement and intake, the same audit counts intra-folder sprawl inside each operator-data entity folder from disk structure alone: superseded version copies (`<base>.ext` and `<base>-vN.ext` are one deliverable, all but the newest counted `versioned`), format twins (a source `.html`/`.md` render kept beside its delivered `.pdf` of the same basename, counted `format-twins`), and subfolders nested under `projects/<name>/` or `contacts/<name>/` that the flat rule forbids (counted `stray-subdir`). These are read-only counts; removal or flattening happens only on a `data-manager` stewardship dispatch, each paired with a `database-operator` reference update.
137
+ ```
138
+
139
+ - [ ] **Step 2: Update the contract string** in the same paragraph, from:
140
+
141
+ ```
142
+ The reconcile output contract is `unreachable=N broken-refs=M scratch-refs=P stranded=Q stranded-intake=R`.
143
+ ```
144
+
145
+ to:
146
+
147
+ ```
148
+ The reconcile output contract is `unreachable=N broken-refs=M scratch-refs=P stranded=Q stranded-intake=R versioned=S format-twins=T stray-subdir=U`.
149
+ ```
150
+
151
+ - [ ] **Step 3: Verify the string matches Task 2 and no em-dashes in the changed region**
152
+
153
+ Run: `grep -c 'versioned=S format-twins=T stray-subdir=U' platform/plugins/docs/references/internals.md && grep -o 'counted `stray-subdir`' platform/plugins/docs/references/internals.md`
154
+ Expected: `1`, then `counted `stray-subdir``.
155
+
156
+ - [ ] **Step 4: Commit**
157
+
158
+ ```bash
159
+ git add platform/plugins/docs/references/internals.md
160
+ git commit -m "task: internals.md documents intra-folder hygiene counts (1976)"
161
+ ```
162
+
163
+ ---
164
+
165
+ ### Task 4: Regenerate the platform-architecture SKILL.md twin and gate it
166
+
167
+ **Files:**
168
+ - Regenerate (do not hand-edit): `platform/plugins/admin/skills/platform-architecture/SKILL.md`
169
+
170
+ **Interfaces:**
171
+ - Consumes: the edited `internals.md` from Task 3 (via the docs corpus).
172
+
173
+ - [ ] **Step 1: Rebuild the docs corpus** (mirrors internals.md into `docs/public/llms-full.<brand>.txt`). From the repo root:
174
+
175
+ Run: `cd "$(git rev-parse --show-toplevel)" && node docs/scripts/copy-docs.mjs 2>&1 | tail -2`
176
+ Expected: no error.
177
+
178
+ - [ ] **Step 2: Assemble both brand skills** (the maxy-code run writes SKILL.md):
179
+
180
+ Run: `cd "$(git rev-parse --show-toplevel)" && node maxy-code/scripts/assemble-architecture-skill.mjs --brand maxy-code && node maxy-code/scripts/assemble-architecture-skill.mjs --brand realagent-code`
181
+ Expected: `[platform-arch-assemble] op=write brand=maxy-code path=.../SKILL.md ...`.
182
+
183
+ - [ ] **Step 3: Run the drift gate**
184
+
185
+ Run: `cd maxy-code && node platform/scripts/check-architecture-skill-no-drift.mjs 2>&1 | tail -3`
186
+ Expected: `[check-architecture-skill-no-drift] ok brand=maxy-code (...)` and `... ok brand=realagent-code (...)`.
187
+
188
+ - [ ] **Step 4: Confirm the new fields reached the regenerated twin**
189
+
190
+ Run: `grep -c 'versioned=S format-twins=T stray-subdir=U' maxy-code/platform/plugins/admin/skills/platform-architecture/SKILL.md`
191
+ Expected: `1` (or more).
192
+
193
+ - [ ] **Step 5: Commit**
194
+
195
+ ```bash
196
+ git add maxy-code/platform/plugins/admin/skills/platform-architecture/SKILL.md maxy-code/docs docs/public 2>/dev/null; git add -A
197
+ git commit -m "task: regenerate platform-architecture skill twin for 1976"
198
+ ```
199
+
200
+ ---
201
+
202
+ ## Self-Review
203
+
204
+ **Spec coverage:**
205
+ - SCHEMA.md three rules → Task 1. ✓
206
+ - data-manager three counts + contract + stewardship → Task 2. ✓
207
+ - internals.md rules + counts → Task 3. ✓
208
+ - SKILL.md regenerated twin → Task 4. ✓
209
+ - LANES.md Ready row + archive → Phase 6 of the sprint (not a plan task; the sprint owns archive/LANES). ✓
210
+
211
+ **Placeholder scan:** every edit shows verbatim insert text. No TBD/TODO. ✓
212
+
213
+ **Type consistency:** the output-contract string is byte-identical in Task 2, Task 3, and the Task 4 grep: `unreachable=N broken-refs=M scratch-refs=P stranded=Q stranded-intake=R versioned=S format-twins=T stray-subdir=U`. ✓
214
+
215
+ **Out of scope (deferred, no task needed here):** automatic deletion, a write-time guard for versions/twins, per-vertical canonical-format policy, the `uploads/` intake (1974, landed), the `/data` 524 (1975, archived), the `output/` promote-out counts (1930, landed). These are stated out-of-scope in the task brief, not dropped work.
@@ -189,6 +189,7 @@ Tools are available via the `admin` MCP server.
189
189
  ## Hooks
190
190
 
191
191
  - `hooks/quote-render-gate.sh` — **PreToolUse matcher=`mcp__plugin_browser_browser__browser-pdf-save`.** Makes the deterministic quote-render path the only route a client-facing quote PDF can reach a customer. Blocks (exit 2) a `browser-pdf-save` whose output path is a client-quote artefact (`memory/users/<phone>/documents/quote-<jobId>.pdf` or `…-breakdown.pdf`) when no render receipt exists at `quoting/jobs/<jobId>/render-receipt.json` — the receipt `quote-render` writes only after its reconcile / margin-leak / link-liveness gates all pass. A hand-authored quote (the SiteDesk 2026-07-14 bypass: `Write` HTML → `browser-pdf-save`, `quote-render` never called, an internal back-test caveat naming another client folded into the free-text region) has no receipt, so the print is refused rather than delivered. **Block message:** `Blocked: this client quote was not produced by the quote-render tool, so its fixed layout and the reconcile, margin-leak and link-liveness gates never ran. Run quote-render for this job (it files the document and its receipt), then print the client HTML path it returns to PDF.` **Log line** (stderr, on a block): `[quote-render] op=bypass jobId=<jobId> detail=client quote PDF has no render receipt` (or `reason=bad-jobid` when the filename does not resolve to a job). Every non-quote PDF — brochures, invoices, the admin-scope internal view — and the deterministic path itself (receipt present) pass at exit 0. **Fail-open** on an uninspectable call (tty / empty stdin / no `python3`): `browser-pdf-save` is a general-purpose tool and the agent never controls this hook's stdin, so failing open opens no bypass while never bricking unrelated PDFs.
192
+ - `hooks/quote-engine-gate.sh` — **PreToolUse matcher=`Bash`** (a third command on the same `Bash` matcher as `archive-ingest-surface-gate` and `fs-schema-guard-bash-pre`, any exit 2 blocks). Makes the deterministic `quote-engine-run` / `quote-render` tools the only route the account's persisted quoting scripts can run. Blocks (exit 2) a `Bash` `command` that executes `quoting/engine.mjs` or `quoting/render.mjs` directly — the pricing and render scripts must run through their MCP tools, which apply the reconcile / margin-leak / link-liveness gates and write the figures and render receipts. A hand-run `node quoting/engine.mjs` (the SiteDesk 2026-07-25 bypass: a repricing dispatched to content-producer that ran the engine by hand and re-rendered off the deterministic path) skips all of that, so it is refused. It fires only on an execution (a JS runtime invoking the script, or the script run at command position); a read or inspect of the file (`cat`/`grep`/`git diff quoting/engine.mjs`) and every unrelated `Bash` command pass at exit 0. A `mode=verify` reproduction is gated too — it must also run through `quote-engine-run`, so a hand-run `node quoting/engine.mjs mode=verify` is refused. Known fail-open gap (accepted, consistent with the soft posture): a `cd quoting && node engine.mjs` drops the `quoting/` prefix and is not matched. **Block message:** `Blocked: the quoting engine and render scripts must run through their tools, not directly. Use the quote-engine-run tool to price a job (it files the figures) and the quote-render tool to produce the documents (it files them behind the reconcile, margin-leak and link-liveness gates). Running quoting/engine.mjs or quoting/render.mjs by hand skips those gates and files no receipt.` **Log line** (stderr, on a block): `[quote-engine] op=bypass reason=direct-script-exec`. **Fail-open** on an uninspectable call (tty / empty stdin / no `python3`): `Bash` is a general-purpose tool and the agent never controls this hook's stdin, so failing open opens no bypass while never bricking unrelated commands.
192
193
  - `hooks/preference-consult-gate.sh` — **PreToolUse gate on customer-facing document deliverables.** Registered against `mcp__plugin_browser_browser__browser-pdf-save` (a second command on the same matcher as `quote-render-gate`, either exit 2 blocks) and against the email/Outlook send tools plus `SendUserFile`. It fires only in the document-deliverable shape: a `browser-pdf-save` or `SendUserFile` whose path is under `memory/users/<phone>/documents/` (or a `SendUserFile` of a `.pdf`/`.html`/`.docx`), and `email-send`/`email-reply`/`email-draft-send`/`outlook-mail-send`/`outlook-mail-reply`/`outlook-draft-send` carrying a non-empty `attachments`. For a gated call it reads the session transcript (`transcript_path`) and blocks (exit 2) when no `profile-read` tool call ran after the last user message, because the account's layer-2 `Preference` records (signature policy, header, naming, styling) were not consulted this turn, the exact miss that dropped a customer signature on a delivered document. **Block message:** `Blocked: this customer document is about to go out without the account's saved preferences being checked this turn. Those records hold the signature, header, naming and styling rules. Run profile-read for this account, apply anything relevant, then send again.` **Log line** (stderr): `[preference-gate] op=bypass tool=<name> detail=no profile-read this turn` on a block, `op=allow tool=<name> consulted=true` on an allowed document deliverable. Every non-document tool/shape, and every casual channel reply or plain-text email, passes at exit 0. **Fail-open** on an uninspectable call (tty / empty stdin / no `python3` / transcript missing or unreadable). Scope decision: the gate fires on document deliverables only, not on every send, so it never blocks routine chat. The standing reconcile counterpart for stranded `output/` deliverables is filed as a follow-up task.
193
194
  - `hooks/quote-render-pdf-conformance.sh` — **PostToolUse matcher=`mcp__plugin_browser_browser__browser-pdf-save`.** The PDF-time companion to the PreToolUse `quote-render-gate`: the HTML gates run inside `quote-render` before the receipt, but the two PDF-only conformance checks cannot, because the PDF does not exist until `browser-pdf-save` runs. For a gated client-quote PDF (`memory/users/<phone>/documents/quote-<jobId>.pdf` or `…-breakdown.pdf`) whose render receipt names the source HTML, it blocks (exit 2) when the printed PDF's `/URI` annotation count is below the source HTML's absolute-link count (a rasterised or flattened print that dropped links) or when an embedded `https://` link does not answer 200 (a Sign-Online link a pending redeploy would 404). The offline `/URI`-count check runs first and short-circuits, so a link-loss regression never depends on the network. **Block message:** `Blocked: the printed quote PDF is missing links the document declared, or an embedded link is not live (…). Do not send it. If a Sign-Online link is not live yet, deploy the signing page first; then re-render and re-print so the PDF carries every link the document declares.` **Log line** (stderr, on a block): `[quote-render] op=bypass jobId=<jobId> reason=pdf-link-conformance detail=<uri-count …|link <url> -> <code>>`. Every non-quote PDF, and any call with no receipt (the PreToolUse gate owns that block), passes at exit 0. **Fail-open** on an uninspectable call (tty / empty stdin / no `python3` / receipt or PDF absent). **Reach on upgrade:** `provision-account-dir.sh` writes this PostToolUse matcher only at account-provision time, so accounts provisioned before it landed keep a stale matcher and need a settings backfill (tracked as a follow-up task), the same reach limit the AskUserQuestion carrier gate hit.
194
195
  - `hooks/webfetch-preflight.mjs` — short-circuits WebFetch on JS-SPA shells with a structured `WEBFETCH_CANNOT_READ_JS_SPA` error so the agent surfaces a loud failure to the owner instead of paying the 60s extraction timeout. Fail-open on any internal error.
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env bash
2
+ # Regression test for quote-engine-gate.sh.
3
+ #
4
+ # The gate is a PreToolUse hook on Bash. It refuses a Bash command that runs the
5
+ # persisted quoting engine or render script directly (quoting/engine.mjs or
6
+ # quoting/render.mjs), because those scripts must run only through the
7
+ # deterministic quote-engine-run / quote-render MCP tools (which apply the gates
8
+ # and write receipts). A mode=verify engine run and every unrelated command pass.
9
+ #
10
+ # Covers:
11
+ # A. Bash running quoting/engine.mjs → BLOCKED (exit 2)
12
+ # B. Bash running quoting/render.mjs → BLOCKED
13
+ # C. engine.mjs run with mode=verify by hand → BLOCKED (verify runs through the tool too)
14
+ # D. engine.mjs run with a `# mode=verify` tail → BLOCKED (no token-anywhere escape)
15
+ # E. unrelated Bash (ls quoting) → ALLOWED
16
+ # F. a quote-engine-run MCP tool call → ALLOWED (not Bash)
17
+ # G. empty stdin → ALLOWED (fail-open)
18
+ # H. a non-Bash tool → ALLOWED (matcher defence-in-depth)
19
+ # I. a block emits the op=bypass fault line
20
+ # J. reading the script (cat) is not execution → ALLOWED
21
+ # K. grepping the script is not execution → ALLOWED
22
+ # L. the script run at command position → BLOCKED
23
+
24
+ set -u
25
+
26
+ HOOK="$(cd "$(dirname "$0")/.." && pwd)/quote-engine-gate.sh"
27
+ if [[ ! -x "$HOOK" ]]; then
28
+ echo "FAIL: $HOOK not executable" >&2
29
+ exit 1
30
+ fi
31
+
32
+ PASS=0
33
+ FAIL=0
34
+
35
+ run_case() {
36
+ local name="$1" stdin="$2" expected_exit="$3"
37
+ local actual_exit
38
+ ( printf '%s' "$stdin" | bash "$HOOK" >/dev/null 2>/dev/null )
39
+ actual_exit=$?
40
+ if [[ "$actual_exit" -eq "$expected_exit" ]]; then
41
+ echo "PASS: $name (exit=$actual_exit)"
42
+ PASS=$((PASS + 1))
43
+ else
44
+ echo "FAIL: $name (expected exit=$expected_exit, got=$actual_exit)" >&2
45
+ FAIL=$((FAIL + 1))
46
+ fi
47
+ }
48
+
49
+ bash_env() { # command string (JSON-escaped by caller)
50
+ printf '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"%s"}}' "$1"
51
+ }
52
+
53
+ run_case "A: Bash runs quoting/engine.mjs → BLOCKED" \
54
+ "$(bash_env "node quoting/engine.mjs < quoting/job-1854.json")" 2
55
+
56
+ run_case "B: Bash runs quoting/render.mjs → BLOCKED" \
57
+ "$(bash_env "node quoting/render.mjs")" 2
58
+
59
+ run_case "C: engine.mjs mode=verify by hand → BLOCKED" \
60
+ "$(bash_env "node quoting/engine.mjs mode=verify")" 2
61
+
62
+ run_case "D: engine.mjs with # mode=verify tail → BLOCKED" \
63
+ "$(bash_env "node quoting/engine.mjs < job.json # mode=verify")" 2
64
+
65
+ run_case "E: unrelated Bash → ALLOWED" \
66
+ "$(bash_env "ls quoting")" 0
67
+
68
+ run_case "J: reading the script (cat) → ALLOWED" \
69
+ "$(bash_env "cat quoting/engine.mjs")" 0
70
+
71
+ run_case "K: grepping the script → ALLOWED" \
72
+ "$(bash_env "grep foo quoting/render.mjs")" 0
73
+
74
+ run_case "L: script at command position → BLOCKED" \
75
+ "$(bash_env "./quoting/engine.mjs < job.json")" 2
76
+
77
+ run_case "F: quote-engine-run MCP call → ALLOWED" \
78
+ '{"hook_event_name":"PreToolUse","tool_name":"mcp__plugin_sitedesk-job_sitedesk-job__quote-engine-run","tool_input":{"job":"1854","mode":"quote"}}' 0
79
+
80
+ run_case "G: empty stdin → ALLOWED (fail-open)" \
81
+ "" 0
82
+
83
+ run_case "H: non-Bash tool → ALLOWED" \
84
+ '{"hook_event_name":"PreToolUse","tool_name":"Write","tool_input":{"file_path":"/x/quoting/engine.mjs"}}' 0
85
+
86
+ # I: a block emits the op=bypass fault line on stderr.
87
+ STDERR=$( printf '%s' "$(bash_env "node quoting/engine.mjs")" | bash "$HOOK" 2>&1 >/dev/null )
88
+ if printf '%s' "$STDERR" | grep -q '\[quote-engine\] op=bypass reason=direct-script-exec'; then
89
+ echo "PASS: I: block emits op=bypass reason=direct-script-exec"
90
+ PASS=$((PASS + 1))
91
+ else
92
+ echo "FAIL: I: op=bypass line absent (got: $STDERR)" >&2
93
+ FAIL=$((FAIL + 1))
94
+ fi
95
+
96
+ echo "──────── quote-engine-gate test summary ────────"
97
+ echo "PASS=$PASS FAIL=$FAIL"
98
+ [[ "$FAIL" -eq 0 ]]
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env bash
2
+ # quote-engine-gate — PreToolUse gate on Bash.
3
+ #
4
+ # The account's quoting method is applied by two persisted scripts,
5
+ # quoting/engine.mjs (pricing) and quoting/render.mjs (documents). They must run
6
+ # ONLY through their MCP tools — quote-engine-run and quote-render — which apply
7
+ # the reconcile / margin-leak / link-liveness gates and write the figures and
8
+ # render receipts. Running either script directly from a Bash command bypasses
9
+ # all of that (the SiteDesk 2026-07-25 bypass: a repricing dispatched to
10
+ # content-producer that ran `node quoting/engine.mjs` and re-rendered by hand).
11
+ # This gate refuses that Bash call and points the agent at the tools. It fires
12
+ # only on an execution of the persisted script (a JS runtime invoking it, or the
13
+ # script run at command position), never on a read like `cat quoting/engine.mjs`.
14
+ # A mode=verify reproduction must also run through the quote-engine-run tool, so
15
+ # a hand-run `node quoting/engine.mjs mode=verify` is gated too.
16
+ #
17
+ # Exit codes: 0 = allow, 2 = block (stderr shown to the agent). Fail OPEN on an
18
+ # uninspectable call (Bash is a general-purpose tool and the agent never controls
19
+ # this hook's stdin, so failing open opens no bypass while never bricking
20
+ # unrelated commands). No task numbers or internal refs in operator-visible text.
21
+
22
+ set -uo pipefail
23
+
24
+ # No envelope (tty or empty stdin) → allow: cannot inspect, must not brick Bash.
25
+ if [ -t 0 ]; then exit 0; fi
26
+ INPUT=$(cat)
27
+ [ -z "$INPUT" ] && exit 0
28
+
29
+ # Print "block" when the Bash command runs a persisted quoting script directly
30
+ # (and is not a mode=verify run), or nothing for any other call.
31
+ DECISION=$(INPUT="$INPUT" python3 - <<'PY' 2>/dev/null || true
32
+ import os, json, re, sys
33
+ try:
34
+ d = json.loads(os.environ["INPUT"])
35
+ except Exception:
36
+ sys.exit(0)
37
+ if (d.get("tool_name", "") or "") != "Bash":
38
+ sys.exit(0)
39
+ cmd = (d.get("tool_input", {}) or {}).get("command", "") or ""
40
+ if not cmd:
41
+ sys.exit(0)
42
+ script = r'quoting/(engine|render)\.mjs\b'
43
+ # Block only an EXECUTION of the persisted script, not a read/inspect of it:
44
+ # - a JS runtime invoking it: `node|nodejs|bun|deno|ts-node ... quoting/engine.mjs`
45
+ # - the script run directly at command position: `./quoting/engine.mjs`, or the
46
+ # path as a bare command after a separator.
47
+ # A read (`cat|grep|git diff|head|wc quoting/engine.mjs`) leaves the path as an
48
+ # argument, not at command position, so it is not matched and passes.
49
+ # Verify runs are NOT carved out: `mode=verify` must also go through the
50
+ # quote-engine-run tool, never a hand-run of the persisted script.
51
+ executed = (
52
+ re.search(r'\b(?:node|nodejs|bun|deno|ts-node)\b[^;&|\n]*' + script, cmd)
53
+ or re.search(r'(?:^|[;&|]\s*)\.?/?' + script, cmd)
54
+ )
55
+ # Known fail-open gap (accepted, consistent with this gate's soft posture): a
56
+ # cd-relative run like `cd quoting && node engine.mjs` drops the `quoting/`
57
+ # prefix and is not matched.
58
+ if not executed:
59
+ sys.exit(0)
60
+ print("block")
61
+ PY
62
+ )
63
+
64
+ [ "$DECISION" = "block" ] || exit 0
65
+
66
+ echo "[quote-engine] op=bypass reason=direct-script-exec" >&2
67
+ echo "Blocked: the quoting engine and render scripts must run through their tools, not directly. Use the quote-engine-run tool to price a job (it files the figures) and the quote-render tool to produce the documents (it files them behind the reconcile, margin-leak and link-liveness gates). Running quoting/engine.mjs or quoting/render.mjs by hand skips those gates and files no receipt." >&2
68
+ exit 2