@bluestep-systems/bspecs 0.10.0

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 (83) hide show
  1. package/README.md +129 -0
  2. package/cli.js +74 -0
  3. package/package.json +30 -0
  4. package/src/prompts.js +74 -0
  5. package/src/scaffold.js +152 -0
  6. package/src/sync.js +123 -0
  7. package/src/utils.js +95 -0
  8. package/templates/claude/agents/b6p-code-review.md +81 -0
  9. package/templates/claude/agents/b6p-commenter.md +59 -0
  10. package/templates/claude/agents/b6p-task-implementer.md +77 -0
  11. package/templates/claude/hooks/block-generated-files.sh +16 -0
  12. package/templates/claude/hooks/block-tsc.sh +16 -0
  13. package/templates/claude/hooks/prettier-on-save.sh +21 -0
  14. package/templates/claude/instructions/b6p-platform.md.template +185 -0
  15. package/templates/claude/instructions/bsjs-development.md.template +430 -0
  16. package/templates/claude/instructions/conventions/always-snapshot.md.template +25 -0
  17. package/templates/claude/instructions/conventions/blueiq-no-ai-branding.md.template +11 -0
  18. package/templates/claude/instructions/conventions/date-format.md.template +27 -0
  19. package/templates/claude/instructions/conventions/endpoint-approach.md.template +9 -0
  20. package/templates/claude/instructions/conventions/formula-patterns.md.template +71 -0
  21. package/templates/claude/instructions/conventions/no-global-dollar.md.template +9 -0
  22. package/templates/claude/instructions/conventions/push-inner-draft.md.template +21 -0
  23. package/templates/claude/instructions/conventions/separate-files.md.template +17 -0
  24. package/templates/claude/instructions/conventions/single-script.md.template +28 -0
  25. package/templates/claude/instructions/conventions/snapshot-integrity.md.template +23 -0
  26. package/templates/claude/instructions/conventions/top-level-const-tdz.md.template +33 -0
  27. package/templates/claude/instructions/conventions/ts-in-template-literal.md.template +48 -0
  28. package/templates/claude/instructions/conventions/tsc-rootdir.md.template +17 -0
  29. package/templates/claude/instructions/gotchas/common-gotchas.md.template +91 -0
  30. package/templates/claude/instructions/gotchas/fetched-resource-code.md.template +9 -0
  31. package/templates/claude/instructions/index.md.template +82 -0
  32. package/templates/claude/instructions/reference/api-patterns.md.template +487 -0
  33. package/templates/claude/instructions/reference/blueiq-credit-integration-playbook.md.template +31 -0
  34. package/templates/claude/instructions/reference/chronounit-months.md.template +37 -0
  35. package/templates/claude/instructions/reference/code-patterns.md.template +265 -0
  36. package/templates/claude/instructions/reference/component-library.md.template +217 -0
  37. package/templates/claude/instructions/reference/crm-dashboard-inspo.md.template +17 -0
  38. package/templates/claude/instructions/reference/csv-parsing.md.template +18 -0
  39. package/templates/claude/instructions/reference/dashboard-design-system.md.template +38 -0
  40. package/templates/claude/instructions/reference/datetime-field-write.md.template +27 -0
  41. package/templates/claude/instructions/reference/design-system.md.template +150 -0
  42. package/templates/claude/instructions/reference/dpn-dashboard-framework.md.template +29 -0
  43. package/templates/claude/instructions/reference/endpoint-method-call.md.template +10 -0
  44. package/templates/claude/instructions/reference/endpoint-no-delete-method.md.template +9 -0
  45. package/templates/claude/instructions/reference/endpoint-output-channel.md.template +23 -0
  46. package/templates/claude/instructions/reference/endpoint-urls.md.template +15 -0
  47. package/templates/claude/instructions/reference/entry-delete.md.template +40 -0
  48. package/templates/claude/instructions/reference/file-execution.md.template +113 -0
  49. package/templates/claude/instructions/reference/http-requester.md.template +37 -0
  50. package/templates/claude/instructions/reference/id-full-vs-short.md.template +15 -0
  51. package/templates/claude/instructions/reference/internal-loopback-fetch.md.template +24 -0
  52. package/templates/claude/instructions/reference/localdate-parse.md.template +16 -0
  53. package/templates/claude/instructions/reference/merge-report-memo-json.md.template +25 -0
  54. package/templates/claude/instructions/reference/merge-report-static-index.md.template +29 -0
  55. package/templates/claude/instructions/reference/merge-report-urls.md.template +67 -0
  56. package/templates/claude/instructions/reference/multi-entry-in-multi-entry.md.template +21 -0
  57. package/templates/claude/instructions/reference/named-controls-submit.md.template +11 -0
  58. package/templates/claude/instructions/reference/new-entry-id.md.template +30 -0
  59. package/templates/claude/instructions/reference/relationship-field-set.md.template +37 -0
  60. package/templates/claude/instructions/reference/send-message-abort.md.template +37 -0
  61. package/templates/claude/instructions/reference/session-cookie-forwarding.md.template +31 -0
  62. package/templates/claude/instructions/reference/singleselect-null-copy.md.template +21 -0
  63. package/templates/claude/instructions/reference/staff-query-permission-gating.md.template +27 -0
  64. package/templates/claude/instructions/reference/timefield-vs-datetimefield.md.template +13 -0
  65. package/templates/claude/instructions/reference/user-zone-id.md.template +16 -0
  66. package/templates/claude/settings.json.template +46 -0
  67. package/templates/claude/skills/b6p-audit/SKILL.md +82 -0
  68. package/templates/claude/skills/b6p-pull/SKILL.md +123 -0
  69. package/templates/claude/skills/b6p-push/SKILL.md +70 -0
  70. package/templates/claude/skills/bug-fix/SKILL.md +28 -0
  71. package/templates/claude/skills/spec-create/SKILL.md +60 -0
  72. package/templates/claude/skills/spec-execute/SKILL.md +51 -0
  73. package/templates/claude/skills/spec-status/SKILL.md +20 -0
  74. package/templates/claude/skills/task-comment/SKILL.md +96 -0
  75. package/templates/claude/spec-templates/design.template.md +36 -0
  76. package/templates/claude/spec-templates/requirements.template.md +26 -0
  77. package/templates/claude/spec-templates/tasks.template.md +37 -0
  78. package/templates/module/README.md.template +46 -0
  79. package/templates/root/.gitignore.template +14 -0
  80. package/templates/root/.prettierrc.template +8 -0
  81. package/templates/root/CLAUDE.md.template +157 -0
  82. package/templates/root/README.md.template +58 -0
  83. package/templates/root/package.json.template +15 -0
@@ -0,0 +1,21 @@
1
+ ---
2
+ description: "Brandon's \"multi entry in a multi entry\" pattern — letting users add many notes/updates against a single BlueStep multi-entry form, stored as JSON in one memo field"
3
+ ---
4
+
5
+ **"Multi entry in a multi entry"** — Brandon's name for a pattern we reuse often. A BlueStep form is itself a multi-entry form (one record has many entries of it); this pattern lets each entry hold **multiple sub-notes/updates** without a real child form. It's a concrete application of the [merge report memo json](merge-report-memo-json.md) hack.
6
+
7
+ **What the user sees:** A custom section on the form with a **table** of entries (e.g. Notes · Author · Date · Actions) and a **"New Entry"** button. Clicking it opens a modal to add a note; rows can be edited/deleted (pencil/trash icons). Newest-first sort. Empty state ("No updates yet.").
8
+
9
+ **Data model:** One hidden memo field holds the whole list as JSON:
10
+ ```json
11
+ { "schemaVersion": 1, "entries": [
12
+ { "id": "...", "notes": "...", "author": "Full Name", "authorId": "1000201___N", "timestamp": "ISO-8601 UTC" }
13
+ ] }
14
+ ```
15
+ Each entry auto-captures **author + authorId + timestamp** at create time (author identity comes from the server's `currentUser`, durable even if the staff record changes). Extend the entry shape per use case (the inspo version added a `domain` field from a SingleSelect of active domains).
16
+
17
+ **How it's built:** exactly the server-thin/client-fat split in [merge report memo json](merge-report-memo-json.md). Client builds DOM with a tiny `el()` hyperscript helper, single `mutate(fn)` mutation cycle (mutate state → `syncToField()` → `render()`), modal shared by Add + Edit mounted on `document.body`.
18
+
19
+ **Permissions** are commonly layered on via [staff query permission gating](staff-query-permission-gating.md) (Reader/Author/Editor capability flags from the server gate the table / New Entry button / edit-delete actions).
20
+
21
+ **Reference implementations:** kaizenacademy/1466219 "Protocol Update Table" (notes-only, permission-gated, memo field `protocolUpdates`); kaizenacademy/1465899 "Treatment Plan Review Comments Display" (the original — adds Domain SingleSelect + record lock, memo field `tprComments`).
@@ -0,0 +1,11 @@
1
+ ---
2
+ description: Merge-report settings UIs render inside the BlueStep form — any HTML control with a name attribute gets submitted and breaks the save
3
+ ---
4
+
5
+ When a BlueStep merge report renders an interactive UI (the JSON-in-memo settings pattern, see tracer audit and the summitridge Visit Setting Display framework), the mount `<div>` lives **inside the BlueStep `<form>`**. On the native form Save, the browser submits every form control that has a `name` attribute. Any control YOU inject with a `name` becomes an unexpected POST field → BlueStep rejects the whole save with the generic **"There was a problem storing the data."**
6
+
7
+ **Rule: never put a `name` attribute on controls you create in a merge-report UI.** Nameless `<input>`/`<select>`/`<textarea>` are not submitted, so they're safe (the proven reference framework never names any control). Persist state yourself by writing JSON into the one hidden memo field via the `submitForm` hook.
8
+
9
+ Gotcha that bit us on Tracer Settings (havenwoodslc/1528540): assignment mode radios were given `name="asgn_<id>"` for native radio grouping. Groups (nameless controls) saved fine; adding a subtask injected the named radios and every save failed. Fix: drop the `name` — mutual exclusivity is handled by the onchange→mutate→render loop that re-sets `checked` from state, so native grouping isn't needed. Symptom signature: save works until a specific control type appears, then "problem storing the data" — check for a stray `name`.
10
+
11
+ **Sibling gotcha — never wrap merge-report UI in your own `<form>` + submit button.** The host `<form>` already wraps your markup, and HTML can't nest `<form>`s, so the browser silently DROPS your inner `<form>` (its element/id vanishes → `getElementById` returns null → your submit handler never attaches). A `<button type="submit">` then submits the HOST form → the page navigates to a generic BlueStep error page. Symptom signature: clicking submit navigates away / shows a generic error, and your JS never ran. Fix (Survey Form beh/1434661): use a plain `<div>` container, `<button type="button">`, attach a click handler, and submit via `fetch()` to your endpoint — never let the host form submit. Combine with the no-`name` rule: build radio-like pickers as nameless buttons whose selected state you manage in JS (`.is-selected`), not native radio inputs.
@@ -0,0 +1,30 @@
1
+ ---
2
+ description: Pre-commit shortId() returns "0" (id.isValid() === false). Always commit BEFORE reading the id of a freshly-created entry.
3
+ ---
4
+ **For a freshly-created MEF entry (`record.forms.X.newEntry()`), `entry.id().shortId()` returns `"0"` until `B.commit()` runs. Always commit before reading the id.**
5
+
6
+ The B.d.ts is explicit: *"Check whether an Id is valid. New Ids with a shortId of 0 are not valid."* — i.e. `entry.id().isValid() === false` until commit assigns a real database id.
7
+
8
+ ```ts
9
+ // ❌ WRONG — id is "0" here, not the real id
10
+ const entry = record.forms.foo.newEntry();
11
+ entry.fields.bar.val("baz");
12
+ const id = entry.id().shortId(); // → "0"
13
+ B.commit();
14
+ return id; // sends "0" to caller; lookup later fails
15
+
16
+ // ✅ RIGHT
17
+ const entry = record.forms.foo.newEntry();
18
+ entry.fields.bar.val("baz");
19
+ B.commit();
20
+ return entry.id().shortId(); // real id, e.g. "3610234"
21
+ ```
22
+
23
+ For existing entries (looked up via `forEach` / `byId` / etc.), the id is already populated, so the order doesn't matter — but committing the field changes still needs to happen before the function returns.
24
+
25
+ **Hit on summitridge/1470299 audit-log endpoint, 2026-05-01.** Symptom: the chatbot returned `auditLogId: "0"` on every turn; the client sent `"0"` back on the next turn; server lookup found no match (real entries had real ids); fell through to `newEntry()` again — producing one audit-log row per turn instead of one per conversation. Fixed by moving `B.commit()` above the `entry.id().shortId()` read.
26
+
27
+ **How to apply:**
28
+ - When you need the id of a newly-created MEF entry, structure as: `newEntry()` → set fields → `B.commit()` → `entry.id().shortId()`.
29
+ - If you build many new entries in a loop and need their ids, commit once at the end and only then iterate to read ids — or commit per-iteration if you need the ids interleaved with other writes.
30
+ - For sanity check during dev, you can `if (!entry.id().isValid()) throw new Error(...)` after commit to catch a missed commit early.
@@ -0,0 +1,37 @@
1
+ ---
2
+ description: "RelationshipField.set() needs the form-entry id of the field's target form — passing a Record id silently aborts the formula (no JS-catchable error). Watch for `as any` casts hiding the bug."
3
+ ---
4
+
5
+ `RelationshipField.set(id)` requires an id that points at the **form the field targets**, not the underlying Record.
6
+
7
+ If `logs.fields.clientRel` is a relationship to each client's `Hidden Client Data` form, then:
8
+
9
+ - ✅ `entry.fields.clientRel.set(client.forms.hidden.id())`
10
+ - ❌ `entry.fields.clientRel.set(client.id())` — silent abort
11
+ - ❌ `entry.fields.clientRel.set(B.util.toId(client.id().shortId()))` — silent abort
12
+
13
+ The failure mode is brutal: **the formula terminates without throwing**. The JS `try/catch` does NOT fire. `B.io.printStackTrace` does NOT run. The scheduled-formula log just stops after the last line emitted before the bad `.set()` call. The only way to localize the failure is to add a `console.log` immediately before every `.set()` and see which one becomes the last line.
14
+
15
+ ## How to know what form a relationship targets
16
+
17
+ Read the pulled declarations. Each `Record_<query>` type declares the forms accessible through `.forms.<name>`. The relationship field's target is one of those form-entry types. Field labels in the BlueStep UI sometimes hint at it ("Hidden Client Data", "Hidden Staff Data") but the declarations are authoritative.
18
+
19
+ ## Static vs dynamic
20
+
21
+ `RelationshipField.selected()` returns `EList<RelationshipFormEntry>`, where `RelationshipFormEntry extends FormEntry`. So `setting.fields.whoStatic.selected()[0]` IS a FormEntry already — pass it directly to `.set()`. No `.id()` unwrap, no `.shortId()` round-trip.
22
+
23
+ ## The cast-to-`any` smell
24
+
25
+ If `.set(...)` only type-checks with an `as any` cast, that's a red flag — the declared signature is `set(id: Id<Entry>|Entry|Id<FormEntry>|FormEntry)`. If your value doesn't fit one of those, the cast is masking the runtime bug, not fixing it. Fix the value, drop the cast.
26
+
27
+ ## Cross-log relationships (log → log)
28
+
29
+ For fields like `parentLog` on the `logs` MEF that link one log entry to another, pass the **committed parent log entry** directly: `child.fields.parentLog.set(parentLogEntry)`. The shortId-string round-trip via `B.util.toId(shortId)` is the same hazard class.
30
+
31
+ Companion pattern: separately populate the sibling `parentLogId` TextField with `parentLogEntry.id().shortId()` — that one IS a string field and is correct.
32
+
33
+ ## Related
34
+
35
+ - See [new entry id](new-entry-id.md) — must `B.commit()` before reading a new entry's `shortId()`, including before using it as a relationship target.
36
+ - See [datetime field write](datetime-field-write.md) — different field, same Graal-overload-ambiguity failure mode (silent abort, no JS catch).
37
+ - See [top level const tdz](../conventions/top-level-const-tdz.md) — another class of silent-termination bug (TDZ on top-level const).
@@ -0,0 +1,37 @@
1
+ ---
2
+ description: B.net.sendMessage(msg, abort) aborts saves — but for pre-delete formulas, the message does NOT surface in the UI
3
+ ---
4
+ In BSJS, the relatescript-style `sendMessage(msg, abort)` is on `B.net`:
5
+
6
+ ```typescript
7
+ B.net.sendMessage(message: string | Java.Throwable, abort?: boolean): void
8
+ ```
9
+
10
+ Per the docstring, `abort=true` rolls back the entire transaction; the message is supposed to display in red at the top of the page, contingent on returning the user to "the edit screen" — i.e. the message-rendering path is coupled to the standard Relate edit return.
11
+
12
+ ## Pre-delete limitation (confirmed, summitridge/1470679)
13
+
14
+ For **pre-delete formulas**, the abort works (DB transaction rolls back, deletion is prevented) but **the message never surfaces in the UI**. Tested all three patterns:
15
+
16
+ 1. `B.net.sendMessage(msg, true)` — aborts cleanly, no UI message, no log entry
17
+ 2. `throw 'msg'` — aborts, no UI message, no log entry either
18
+ 3. `B.net.response.redirect(setupRoles.viewUrl())` THEN `B.net.sendMessage(msg, true)` — aborts, no UI message
19
+
20
+ The user just sees BlueStep's generic "Problem storing the data" error, with no detail about why.
21
+
22
+ Hypothesis: pre-delete has no edit-screen return (delete isn't an edit), and BSJS's message-rendering layer doesn't have a fallback path for delete operations.
23
+
24
+ ## What works for pre-delete
25
+
26
+ - **Aborting the deletion** — any of `sendMessage(_, true)`, `throw msg`, or both — works fine.
27
+ - **NOT surfacing a custom message** — appears to be a real BSJS gap, not a code-side bug.
28
+
29
+ ## Recommended UX workaround for pre-delete
30
+
31
+ Don't rely on the abort message. Instead, show the relevant info (e.g. linked records that prevent deletion) **on the entry's view page itself** via a separate display formula or banner, so admins see the explanation before they attempt deletion.
32
+
33
+ ## What works elsewhere
34
+
35
+ - **Pre-save (form-attached post-save)** — `throw msg` aborts AND logs the message as a `PolyglotException` (we saw the multi-select error in `applyRoleToStaff` log this way).
36
+ - **Endpoints / OnDemand / Scheduled** — full UI control, including HTTP responses.
37
+ - **Linking specific forms in HTML messages** — `formEntry.editUrl()` (inherited from `BaseObject`) returns a relative URL to edit that form.
@@ -0,0 +1,31 @@
1
+ ---
2
+ description: "B.net.request.cookies() filters HttpOnly cookies; use optHeader(\"Cookie\") to grab JSESSIONID for authenticated loopback calls"
3
+ ---
4
+
5
+ To make an authenticated server-to-server HTTP call from one BlueStep endpoint to another on the same org (e.g., `/b/foo` calling `/b/bar`), forward the inbound request's **raw Cookie header**, not the parsed cookies map.
6
+
7
+ ```ts
8
+ // WRONG — parsed cookies omits JSESSIONID (HttpOnly is filtered out)
9
+ const cookies = B.net.request.cookies();
10
+ const header = Object.keys(cookies).map(k => `${k}=${cookies[k]}`).join("; ");
11
+
12
+ // RIGHT — raw header includes JSESSIONID
13
+ let header = "";
14
+ const opt = B.net.request.optHeader("Cookie");
15
+ if (opt.isPresent()) header = opt.get();
16
+
17
+ const req = B.net.httpRequester(url);
18
+ req.method("GET");
19
+ req.setHeader("Cookie", header);
20
+ req.doRequest();
21
+ ```
22
+
23
+ **Why:** `B.net.request.cookies()` defensively filters HttpOnly cookies (verified empirically on alpineacademy 2026-05-19). JSESSIONID is HttpOnly, so it's invisible to the parsed map but present in the raw `Cookie:` header.
24
+
25
+ **Symptom if you get this wrong:** `httpRequester.doRequest()` returns false and `req.error()` reports `"Cannot invoke java.io.InputStream.read(byte[], int, int) because this.in is null"` — the loopback hits BlueStep's auth gate, gets a 401/redirect, and the HTTP client's response stream collapses.
26
+
27
+ **Diagnostic shortcut:** dump `B.net.request.optHeader("Cookie")` and iterate `B.net.request.headerNames()` to see what's actually in the inbound request. Sets the baseline before chasing a phantom auth design.
28
+
29
+ **Endpoint perms:** the target endpoint should have "Request HTTP authentication (Use only for robots)" selected for the unauthenticated-fallback behavior, so unauthenticated callers get a clean 401 rather than a 302 login redirect (which breaks JSON responses).
30
+
31
+ **Caveat — session re-entrancy:** Tomcat by default serializes per-session HTTP access. The inbound chatbot request holds the user's session; the loopback enters with the same session. If it hangs ~30s and times out, this is the cause — won't show up as auth failure. Fix is to either redesign without loopback or set `<Manager pathname=""/>` / equivalent session-concurrency config (not Claude-accessible — BlueStep platform config).
@@ -0,0 +1,21 @@
1
+ ---
2
+ description: "Copying an empty SingleSelectField via val(other.val()) throws set(null.id()) → generic \"problem storing the data\"; guard with opt().isPresent()/clear()"
3
+ ---
4
+
5
+ `SingleSelectField.val()` is OptionItem/ID-based, NOT a label string. Getter `val()` returns an `OptionItem` (null when nothing selected); setter `val(opt)` is documented as `set(newValue.id())` — pure internal-ID match against the target field's own option list.
6
+
7
+ **The trap:** `targetSingle.val(sourceSingle.val())` when the source has no selection passes null → `set(null.id())` → NPE. Because it fails at **commit**, BlueStep surfaces the generic **"problem storing the data"** save error, not a formula error (no try/catch prefix). Symptom appears only for records where the single-select is empty — e.g. a newly-added option-list field that older records never filled in.
8
+
9
+ **Fix pattern** (per single-select copy):
10
+ ```js
11
+ const opt = sourceField.opt(); // Java.Optional<OptionItem>
12
+ if (opt.isPresent()) targetField.val(opt.get());
13
+ else targetField.clear(); // SelectField.clear() deselects all
14
+ ```
15
+
16
+ Notes:
17
+ - Label/value setters are separate, explicit methods: `setByName(name)`, `setByExportValue(value)`, `lookup(name,value)` — each returns a boolean success flag. Plain `val()`/`set()` never touch labels.
18
+ - Two single-selects sharing the **same List** (e.g. "Security Access Type") DO share option IDs, so `val()` copy works between them — list-ID mismatch is NOT the usual cause; null source is.
19
+ - When bulk-copying a mixed field set, separate `SingleSelectField` (null-guard) from `BooleanField` (plain `val()` copy is safe). Verify each field's type in the pulled `declarations/index.d.ts` before assuming.
20
+
21
+ Context: configbehmodular role→staff permission propagation formulas (files 1470539 applyRoleToStaff + 1470599 propagateRoleToStaff). Broke when the new Visits single-select was added; older role records had Visits empty. See [api patterns](api-patterns.md), [formula patterns](../conventions/formula-patterns.md).
@@ -0,0 +1,27 @@
1
+ ---
2
+ description: "Reusable BlueStep permission-gating concept — match current user to a staff record via a query, read a per-staff SingleSelect permission, emit capability flags to gate a merge-report UI (fail-closed, super-user override)"
3
+ ---
4
+
5
+ Reusable **permission-gating framework** for merge-report widgets ([merge report memo json](merge-report-memo-json.md) / [multi entry in multi entry](multi-entry-in-multi-entry.md)). Brandon expects to reuse this concept for similar features. A per-staff SingleSelect permission field drives what each user can do.
6
+
7
+ **Setup:** A query of staff (e.g. `allStaffFormerAndActive`, "All Staff Former and Active, Top Level Down") whose records carry a **SingleSelect permission field** on a Permissions form. In the Protocol Update Table the field was `permissions.protocolUpdates` with option labels: **`No Access`**, **`Reader Access`**, **`Author Access`**, **`Editor Access`** (labels matter exactly — match the option *displayName*, not a guessed word).
8
+
9
+ **Server resolution (fail-closed), order matters:**
10
+ 1. `!B.optUser.isPresent()` → `none` (hidden).
11
+ 2. `u.isGlobalSuper()` → full (Editor) — **checked FIRST**, because super users have no BaseRecord and would otherwise fail the staff lookup. ("Super user" = BlueStep global super, not an option value.)
12
+ 3. Match user→staff: `const id = u.primaryId();` then guard `allStaffFormerAndActive.meetsCriteria(id)` (optById THROWS if criteria fails) → `const f = allStaffFormerAndActive.optById(id); if (f.isPresent()) staff = f.get();`. Not found → `none`.
13
+ 4. Read label: `staff.forms.permissions.<field>.opt().map(o => o.displayName()).orElse('')`, then `switch`: `Reader Access`→reader, `Author Access`→author, `Editor Access`→editor; **`No Access` / null / anything unmatched → `none`** (fail closed).
14
+
15
+ Server emits capability flags, not raw data: `{ canView, canAdd, canEditDelete, currentUser }`. Mapping: reader→view only; author→view+add; editor/super→view+add+edit/delete. Even the top-level catch emits a safe no-access payload.
16
+
17
+ **Client enforcement:**
18
+ - **ALWAYS hide the memo field row first**, in every branch — even when the whole table is hidden (the hidden JSON must never be visible).
19
+ - `!canView` → render nothing, never touch the memo.
20
+ - Gate the New Entry button on `canAdd`; the Actions column + edit/delete on `canEditDelete`.
21
+ - Wire `submitForm` hook + `syncToField()` **only when canAdd||canEditDelete** — Readers/No-Access never write to the memo (not even the canonicalization pass).
22
+
23
+ **API facts used:** `B.optUser` is `Optional<User>` (use `isPresent()`/`get()`, NOT `orElse(null)` — null isn't assignable to `User`). `user.isGlobalSuper()`, `user.primaryId()` (BaseRecord id, falls back to user id), `user.fullName()`. RecordQuery `meetsCriteria(id)` + `optById(id): Optional<Record>`. SingleSelectField `.opt(): Optional<OptionItem>`, `OptionItem.displayName()` = label text. See also [singleselect null copy](singleselect-null-copy.md), [id full vs short](id-full-vs-short.md).
24
+
25
+ **Caveat (always tell Brandon):** this gates the UI only. The memo is a real field on the form, so a No-Access/Reader user could still see/alter it via the form or devtools unless that field's own BlueStep view/edit permissions are locked. True data security needs form-level field permissions, not just the merge report.
26
+
27
+ **Reference implementation:** kaizenacademy/1466219 "Protocol Update Table".
@@ -0,0 +1,13 @@
1
+ ---
2
+ description: "zonedDateTimeParts works on DateTimeField (ZonedDateTime). For TimeField (LocalTime), use a separate HH:mm extractor — the regex looks for a T-separator that LocalTime stringifies without"
3
+ ---
4
+
5
+ `zonedDateTimeParts(field)` (the helper in our chatbot endpoint) parses `String(field)` looking for `YYYY-MM-DDTHH:MM` — that's the ZonedDateTime stringification used by `DateTimeField`.
6
+
7
+ `TimeField` is backed by `LocalTime`, which stringifies as `"HH:mm:ss"` with **no T-separator and no date**. Feeding a TimeField through `zonedDateTimeParts` silently returns `{ date: null, time: null }` — no error, just dropped time-of-day.
8
+
9
+ When the declarations show `startTime: Bluestep.Relate.TimeField` or `endTime: Bluestep.Relate.TimeField` (as on `individualNotes` / `familyNotes` / `groupNotes` / `notableEvents`), reach for a TimeField-specific helper instead — e.g. `timeFieldString(field)` that returns `"HH:mm"` directly from `String(field.opt().get())`.
10
+
11
+ DateTimeField vs TimeField is easy to confuse at a glance — both look like "time" semantics. Check the declaration type before reaching for `zonedDateTimeParts`.
12
+
13
+ Related: [datetime field write](datetime-field-write.md) (DateTimeField writes use `field.val(zonedDateTime)`, not `.dateTimeVal(...)`).
@@ -0,0 +1,16 @@
1
+ ---
2
+ description: B.time.userZoneId() returns the user/system ZoneId; B.userZoneId() does NOT type-check
3
+ ---
4
+
5
+ `userZoneId(): Java.Time.ZoneId` is defined on the `Bluestep.Time` class (i.e. `B.time`), NOT on `B` directly. Use `B.time.userZoneId()`.
6
+
7
+ The `DateTimeField.dateTimeVal` jsdoc says "Same as calling val(localDateTime.atZone(B.userZoneId()))" — that's a shorthand in the comment, not a real method on `B`. TypeScript rejects `B.userZoneId()` with `Property 'userZoneId' does not exist on type 'CurrentRecordB'`.
8
+
9
+ Use in formula post-save / endpoints that need to attach a zone to a parsed `LocalDateTime` or `Instant`:
10
+
11
+ ```ts
12
+ B.time.LocalDateTime.parse('2026-05-15T14:30').atZone(B.time.userZoneId())
13
+ B.time.Instant.parse('2026-05-15T14:30:00.000Z').atZone(B.time.userZoneId())
14
+ ```
15
+
16
+ Related: [datetime field write](datetime-field-write.md) (always pass `ZonedDateTime` to `.val()` — `dateTimeVal` overloads are Graal-ambiguous).
@@ -0,0 +1,46 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Edit(**/*.md)",
5
+ "Bash(git:*)",
6
+ "Bash(ls:*)",
7
+ "Bash(cat:*)",
8
+ "Bash(node:*)",
9
+ "Bash(npm:*)",
10
+ "Bash(npx:*)",
11
+ "Edit(.claude/specs/**)",
12
+ "Write(.claude/specs/**)"
13
+ ]
14
+ },
15
+ "hooks": {
16
+ "PreToolUse": [
17
+ {
18
+ "matcher": "Edit|Write",
19
+ "hooks": [
20
+ { "type": "command", "command": ".claude/hooks/block-generated-files.sh" }
21
+ ]
22
+ },
23
+ {
24
+ "matcher": "Bash",
25
+ "hooks": [
26
+ { "type": "command", "command": ".claude/hooks/block-tsc.sh" }
27
+ ]
28
+ }
29
+ ],
30
+ "PostToolUse": [
31
+ {
32
+ "matcher": "Edit|Write",
33
+ "hooks": [
34
+ { "type": "command", "command": ".claude/hooks/prettier-on-save.sh" }
35
+ ]
36
+ }
37
+ ],
38
+ "SessionStart": [
39
+ {
40
+ "hooks": [
41
+ { "type": "command", "command": "bspecs sync --silent" }
42
+ ]
43
+ }
44
+ ]
45
+ }
46
+ }
@@ -0,0 +1,82 @@
1
+ ---
2
+ name: b6p-audit
3
+ description: Compare a local component's state against what lives on the BlueStep platform, listing files that differ. Use when the user wants to know if they (or someone else) changed something on the platform side, or before a push to be sure nothing unexpected will be overwritten.
4
+ allowed-tools: Bash(npx b6p *)
5
+ ---
6
+
7
+ # /b6p-audit — Compare local vs. platform
8
+
9
+ ## What this does
10
+
11
+ Runs `b6p audit`, which fetches the component's current state from the BlueStep platform and lists files where local content differs from platform content. **Read-only** — it does not modify anything.
12
+
13
+ Use this **on demand**, not as a pre-flight before every push. The user decides when to run it; typical triggers are:
14
+
15
+ - "I want to push but I'm not sure if someone else changed this module"
16
+ - "I haven't touched this in a week, what's different on the platform?"
17
+ - "I want to verify my local state matches platform before starting work"
18
+
19
+ For a push that immediately follows, the user can ask you to chain `/b6p-audit` then `/b6p-push`; do not auto-chain it yourself.
20
+
21
+ ## How to invoke `b6p`
22
+
23
+ `b6p` ships as a devDependency of this project (`@bluestep-systems/b6p-cli`). Always invoke it with `npx b6p`, which resolves `node_modules/.bin/b6p` cross-platform — no global install, no shell or PATH detection. If `node_modules` is missing, the user has not run `npm install` yet (see the "Install dependencies" section of the project's `README.md`).
24
+
25
+ Always pass `--yes` so b6p does not show interactive prompts that Claude cannot answer.
26
+
27
+ ## Steps
28
+
29
+ ### 1. Identify the component
30
+
31
+ If `$ARGUMENTS` contains a component path (e.g. `U######/Combined Scheduler`), use it. If empty, ask the user which component to audit.
32
+
33
+ Confirm `.b6p_metadata.json` exists at the component root — without it, audit cannot determine the destination URL.
34
+
35
+ ### 2. Run the audit
36
+
37
+ Pass `--json` so the result is parseable, and `--file` to specify a file inside the component:
38
+
39
+ ```
40
+ npx b6p --yes --json audit --file "U######/<ComponentName>/draft/scripts/app.ts"
41
+ ```
42
+
43
+ The CLI walks up from `--file` to find the component root, then compares each file against the platform.
44
+
45
+ ### 3. Parse and summarise
46
+
47
+ Read the JSON output. The shape is:
48
+
49
+ ```json
50
+ {
51
+ "changedFiles": ["draft/scripts/app.ts", "draft/info/metadata.json (new)", ...],
52
+ "baseUrl": "<DAV URL>"
53
+ }
54
+ ```
55
+
56
+ - If `changedFiles` is empty: tell the user "Local is in sync with the platform."
57
+ - If non-empty: list each path and note which side has the newer version when you can tell (a `(new)` suffix means the file exists on the platform but not locally; otherwise the file exists on both sides with different content).
58
+
59
+ ### 4. Suggest a next step (do not auto-execute)
60
+
61
+ Based on the result, suggest:
62
+
63
+ - **In sync** → "Local is in sync. You can `/b6p-push <component>` safely if you have local changes."
64
+ - **Platform has changes you don't** → "Platform has changes not present locally. Consider `/b6p-pull` to sync before continuing work, especially if you're about to push."
65
+ - **You have local changes the platform doesn't** → "These changes exist only locally. They'll be pushed when you run `/b6p-push`."
66
+ - **Both sides changed** → "Both sides have diverged. Pulling would overwrite your local changes; pushing would overwrite the platform. You probably want to decide file-by-file — open each one and merge manually before pushing."
67
+
68
+ Never auto-pull or auto-push from inside this skill. The user drives the next step.
69
+
70
+ ## What this skill must NOT do
71
+
72
+ - Do NOT pass `--pull` to `b6p audit` (that flag would auto-sync; we want read-only).
73
+ - Do NOT chain into `/b6p-pull` or `/b6p-push` without the user asking.
74
+ - Do NOT invoke `b6p` any way other than `npx b6p`, and never without `--yes`.
75
+
76
+ ## If the CLI fails
77
+
78
+ Two distinct failure modes — handle them differently:
79
+
80
+ - **`command not found` / `b6p` cannot be resolved** — the project's dependencies are not installed. Tell the user:
81
+ > `b6p` could not be resolved. Run `npm install` in the project root (see the "Install dependencies" section of this project's `README.md`).
82
+ - **Any other error** (network, auth, etc.) — the audit command is read-only so failures are usually transient. Surface the raw error to the user; suggest retrying or using `b6p auth set` if it looks like an auth issue.
@@ -0,0 +1,123 @@
1
+ ---
2
+ name: b6p-pull
3
+ description: Pull a B6P component from the BlueStep platform into the local workspace using its DAV URL, and scaffold draft/README.md if missing. Use when the user wants to bring a component down for the first time or re-sync after platform edits.
4
+ allowed-tools: Bash(npx b6p *)
5
+ ---
6
+
7
+ # /b6p-pull — Pull a component from BlueStep
8
+
9
+ ## How `b6p pull` actually works
10
+
11
+ `b6p pull` takes a **DAV URL** as its primary argument, NOT a display name:
12
+
13
+ ```
14
+ b6p pull [options] <formula-url>
15
+ ```
16
+
17
+ The user copies the DAV URL from the component's page in the BlueStep platform UI. You cannot discover or infer it. There is no fallback that takes a name.
18
+
19
+ A first pull creates the `U######/<ComponentName>/` folder (creating the U-folder if it does not exist) and populates `declarations/`, `draft/`, and `.b6p_metadata.json`.
20
+
21
+ ## How to invoke `b6p`
22
+
23
+ `b6p` ships as a devDependency of this project (`@bluestep-systems/b6p-cli`). Always invoke it with `npx`, which resolves `node_modules/.bin/b6p` cross-platform — no global install, no shell or PATH detection:
24
+
25
+ ```
26
+ npx b6p <args>
27
+ ```
28
+
29
+ If `node_modules` is missing, the user has not run `npm install` yet (see the "Install dependencies" section of the project's `README.md`).
30
+
31
+ ## Steps
32
+
33
+ ### 1. Get the DAV URL
34
+
35
+ - If `$ARGUMENTS` looks like a URL (starts with `http://` or `https://`), use it.
36
+ - If `$ARGUMENTS` is empty or looks like a display name (no scheme), STOP and ask the user:
37
+ > I need the **DAV URL** of the component, not its name. Copy it from the component's page in the BlueStep platform UI and paste it here.
38
+ - Do NOT guess the URL. Do NOT try to derive it from `.b6p_metadata.json` of other components.
39
+
40
+ ### 2. Run the pull
41
+
42
+ ```
43
+ npx b6p --yes pull "<DAV URL>"
44
+ ```
45
+
46
+ The `--yes` is **required** — without it, b6p may show an interactive confirmation prompt that you (Claude) cannot answer, and the call will hang. Always include it.
47
+
48
+ Capture the output — it prints the local path where the component landed.
49
+
50
+ ### 3. Locate the component folder
51
+
52
+ Parse the CLI output, or scan for the most recently modified `U######/<Name>/` directory under the project root. Confirm:
53
+
54
+ - `declarations/` is populated
55
+ - `.b6p_metadata.json` exists at the component root
56
+ - `draft/info/metadata.json` and `draft/info/config.json` exist
57
+ - `draft/scripts/app.ts` (or whatever `config.json:main` points at) exists
58
+
59
+ ### 4. Identify the component type
60
+
61
+ Read `draft/info/metadata.json` and `draft/info/config.json`. Signals:
62
+
63
+ - `httpOption` + `allowedMethods` + a `path` field → **Endpoint**
64
+ - `useAsHeaderInRelate` / `useForEditing` / `replaceRelateRecordSummary` + `draft/static/` directory present → **MergeReport**
65
+ - `triggerType: "POST_SAVE"` (or comparable) → **Post-Save**
66
+ - `triggerType: "SCHEDULED"` → **Scheduled**
67
+ - `triggerType: "ON_DEMAND"` → **OnDemand**
68
+ - `language: "mjs"` + no triggers + presence of formula configuration → **Formula**
69
+
70
+ If the signals are ambiguous, read `draft/scripts/app.ts` for comment headers ("// Scheduler MR" etc.) before deciding. If still unsure, ask the user.
71
+
72
+ ### 5. Handle `draft/README.md`
73
+
74
+ This is the per-module documentation that lives WITH the code (it ships to the platform on push, so anyone who pulls the module gets it).
75
+
76
+ a. **Check if it already exists and is substantive.** Read `<Component>/draft/README.md`.
77
+ - If file does not exist → proceed to scaffold (step b).
78
+ - If file exists with > 200 characters AND contains at least one `##` heading that is not literally `## Title` → it is substantive. **Leave it alone.** Tell the user: "draft/README.md already exists and looks substantive — leaving it. Read it before editing code."
79
+ - Otherwise (empty, only `# Title`, only boilerplate) → proceed to scaffold (step b), overwriting.
80
+
81
+ b. **Scaffold from the template.** Read `.claude/templates/README.md` (the module README template scaffolded by bspecs). Fill it in using inference from the code:
82
+
83
+ - **Title (`# [Component displayName]`):** use `displayName` from `draft/info/metadata.json`.
84
+ - **Type section:** use what you identified in step 4, plus the type-specific details listed in the template's commented hints (paths/methods for Endpoint, etc.).
85
+ - **Overview:** read `draft/scripts/app.ts` (and for MergeReports, also `draft/static/index.html`). Write ONE paragraph describing what the component does at runtime, based on what you actually see in the code. Do NOT speculate beyond what is visible.
86
+ - **Fields used:** scan `draft/scripts/**/*.ts` for patterns like `entry.<name>.val()`, `entry.<name>.set(...)`, `entry.<name>.selectedExportValue()`. List each unique `<name>` with access type (read = `.val()` / `.selectedExportValue()`; write = `.set(...)`). Leave FID and Form columns as `?` — those are only knowable from the platform.
87
+ - **Behavior:** translate the structure of `app.ts` into 2-5 bullets ("Receives an HTTP GET at /audits, queries B.queries.activeAudits, returns JSON list", etc.).
88
+ - **External dependencies:** scan for `B.net.fetch(...)` URLs, library imports beyond the platform globals, and any cross-component calls (e.g., `B.exports.something` or imports from `../OtherComponent/`).
89
+ - **Edge cases / known gotchas:** leave empty with the literal text `_(no inferred gotchas — fill in if you find any)_`. These can only be known by humans.
90
+
91
+ c. **If you cannot infer the Overview with reasonable confidence** (e.g., `app.ts` only imports things and has no body, or the logic is too sparse to summarize) — STOP and ask the user:
92
+ > I pulled `<ComponentName>` but cannot infer its purpose from the code alone. In one or two sentences, what does this component do? I will use your answer to scaffold `draft/README.md`.
93
+ Then fill the Overview with the user's answer and continue.
94
+
95
+ d. **Write** the rendered README to `<Component>/draft/README.md`.
96
+
97
+ ### 6. Report
98
+
99
+ Print a summary:
100
+
101
+ ```
102
+ Pulled: U######/<ComponentName>/
103
+ Type: <Endpoint|MergeReport|...>
104
+ README: <created from scaffold | preserved (already substantive)>
105
+
106
+ Next: read draft/README.md, then start editing. For a new feature use /spec-create.
107
+ ```
108
+
109
+ ## What this skill must NOT do
110
+
111
+ - Do NOT invoke `b6p` any way other than `npx b6p`.
112
+ - Do NOT accept a display name as a substitute for the DAV URL.
113
+ - Do NOT overwrite a substantive `draft/README.md`.
114
+ - Do NOT speculate beyond what the code shows when filling Overview/Behavior — if uncertain, ask.
115
+ - Do NOT scaffold a SPEC.md anywhere — the SPEC concept lives only under `.claude/specs/<feature>/` for new work, never per component.
116
+
117
+ ## If the CLI fails
118
+
119
+ Two distinct failure modes — handle them differently:
120
+
121
+ - **`command not found` / `b6p` cannot be resolved** — the project's dependencies are not installed. Do NOT retry, do NOT try alternative invocations. Tell the user:
122
+ > `b6p` could not be resolved. Run `npm install` in the project root (see the "Install dependencies" section of this project's `README.md`), then retry `/b6p-pull <DAV URL>`.
123
+ - **Any other error** (network, auth, lock, etc.) — `b6p` ran but the call failed. The VS Code b6p extension (`bsjs-push-pull`) is the equivalent fallback. Tell the user to use it via the editor UI rather than retrying the CLI in a loop.
@@ -0,0 +1,70 @@
1
+ ---
2
+ name: b6p-push
3
+ description: Push local changes for a component back to the BlueStep platform. Use when the user is ready to deploy local edits.
4
+ allowed-tools: Bash(npx b6p *) Bash(git*)
5
+ ---
6
+
7
+ # /b6p-push — Push a component to BlueStep
8
+
9
+ ## How `b6p push` actually works
10
+
11
+ `b6p push` is most reliably driven by `--file <path>`, which tells the CLI to derive the destination DAV URL from the local file's metadata (via `.b6p_metadata.json`):
12
+
13
+ ```
14
+ b6p push --file <path-inside-component>
15
+ ```
16
+
17
+ Any file inside the component works as the `--file` argument; the CLI walks up to find `.b6p_metadata.json`.
18
+
19
+ ## How to invoke `b6p`
20
+
21
+ `b6p` ships as a devDependency of this project (`@bluestep-systems/b6p-cli`). Always invoke it with `npx b6p`, which resolves `node_modules/.bin/b6p` cross-platform — no global install, no shell or PATH detection. If `node_modules` is missing, the user has not run `npm install` yet (see the "Install dependencies" section of the project's `README.md`).
22
+
23
+ ## Steps
24
+
25
+ ### 1. Identify the component
26
+
27
+ If `$ARGUMENTS` contains a component path (relative to the project root), use it. Otherwise ask the user which component to push.
28
+
29
+ ### 2. Pre-flight checks
30
+
31
+ - Run `git status` to surface what changed and flag anything unexpected.
32
+ - Briefly summarise the diff scope: "X files changed in `U######/<Component>/draft/`".
33
+ - Confirm `.b6p_metadata.json` exists inside the component — without it, `--file` cannot derive the destination URL.
34
+
35
+ ### 3. Confirm with the user
36
+
37
+ Show the summary and ask:
38
+ > Push `<ComponentName>` now?
39
+
40
+ Do not push without explicit confirmation.
41
+
42
+ ### 4. Run the push
43
+
44
+ ```
45
+ npx b6p --yes push --file "U######/<ComponentName>/draft/scripts/app.ts"
46
+ ```
47
+
48
+ Use any existing file inside the component for `--file`; `app.ts` is the most common entry point.
49
+
50
+ The `--yes` is **required** — without it, b6p may show an interactive confirmation prompt that you (Claude) cannot answer, and the call will hang. Always include it.
51
+
52
+ ### 5. Report
53
+
54
+ - The platform compiles after receiving the push. Surface any compile errors the CLI reports.
55
+ - Remind the user to verify behaviour on the platform itself — there is no local compile to fall back on.
56
+ - If `draft/README.md` was modified locally, note that the platform now has the updated docs (useful for other devs pulling the same component).
57
+
58
+ ## What this skill must NOT do
59
+
60
+ - Do NOT invoke `b6p` any way other than `npx b6p`.
61
+ - Do NOT push without showing the user the diff and getting confirmation.
62
+ - Do NOT loop on CLI failures — fall back to the VS Code b6p extension.
63
+
64
+ ## If the CLI fails
65
+
66
+ Two distinct failure modes — handle them differently:
67
+
68
+ - **`command not found` / `b6p` cannot be resolved** — the project's dependencies are not installed. Do NOT retry. Tell the user:
69
+ > `b6p` could not be resolved. Run `npm install` in the project root (see the "Install dependencies" section of this project's `README.md`), then retry `/b6p-push <component>`.
70
+ - **Any other error** (network, auth, conflict, etc.) — the VS Code b6p extension (`bsjs-push-pull`) is the equivalent fallback. Do not retry the CLI in a loop.
@@ -0,0 +1,28 @@
1
+ ---
2
+ name: bug-fix
3
+ description: Short workflow for bug fixes that don't warrant a full 3-phase spec. Use for clearly-scoped fixes.
4
+ ---
5
+
6
+ # /bug-fix — Short bug-fix workflow
7
+
8
+ ## Steps
9
+
10
+ 1. **Gather context.** Ask for (or extract from `$ARGUMENTS`):
11
+ - Bug description
12
+ - Affected component
13
+ - Expected vs actual behavior
14
+ - Steps to reproduce (if known)
15
+ 2. **Read context — scoped, not whole-file:**
16
+ - First `grep` the component's `draft/scripts/` for the symbols, error strings, or behavior named in the bug. Use the hits to find the function(s) involved.
17
+ - Read **only** the relevant functions, using `offset`/`limit` to target the lines around each hit. Do **not** load entire files in full — component source can run to thousands of lines, and a small fix rarely needs more than a few functions. As a rule of thumb, never read more than ~400 lines of a file at once; if you think you need more, narrow the grep instead.
18
+ - For broad "where does X happen across this component" questions, delegate to the Explore agent so the file bulk never enters this conversation's context.
19
+ - Read the component's `draft/README.md` for this component if it isn't already in context.
20
+ 3. **Propose:**
21
+ - Root-cause hypothesis (one or two sentences)
22
+ - Minimal fix (which files, what change)
23
+ 4. **STOP. Ask the user to approve the approach before editing.**
24
+ 5. **Implement the fix.** Touch only the files in the approved approach.
25
+ 6. **Remind the user:**
26
+ - Push via `/b6p-push <component>`
27
+ - Verify behavior on the platform (no local compile to fall back on)
28
+ - If the fix changes documented behavior, update the component's `draft/README.md` in the same change so the platform doc stays in sync