@specset/cli 0.6.0 → 2.0.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.
@@ -1,94 +1,342 @@
1
1
  ---
2
2
  name: specset-closeout
3
- description: Use when managing Specset closeout and facilities data — assets, locations, products, companies, warranties, and maintenance tasks, plus their documents and links — for closeout package or O&M/facilities tasks.
3
+ description: Use when managing Specset closeout and facilities data — assets, locations, products, companies, warranties, procedures, maintenance plans, work orders, and their documents and links.
4
4
  allowed-tools: Bash Read AskUserQuestion
5
5
  ---
6
6
 
7
7
  # Closeout & Facilities Data
8
8
 
9
- Run every operation with `specset api`. All ids are plain UUIDs. Inline enums, numbers, lists, and input objects in the operation text (`-F` is strings-only; Variable Limitations and Schema Discovery live in the `specset` skill).
9
+ Run every operation with `specset api`. All ids are plain UUIDs. Inline enums, numbers, lists, and input objects in the operation text (`-F` sends strings only; Variable Limitations and Schema Discovery live in the `specset` skill).
10
10
 
11
11
  ## Entity Model
12
12
 
13
- **Locations** are the spatial scaffold (site → building → level → room), nesting via `parentId`. **Systems** are disciplines and equipment groups (HVAC, electrical, fire protection), also nested. **Products** are the spec'd make/models. **Assets** are installed instances: each asset sits at one location, is optionally an instance of one product with one responsible **company**, and can join multiple systems through role-tagged memberships. **Warranties** and **maintenance tasks** hang off assets, and maintenance tasks accumulate **completions** (service-log entries). Everything is scoped to a project.
13
+ **Locations** are the spatial scaffold (site → building → level → room), nesting through `parentId`. **Systems** are disciplines and equipment groups, also nested. **Products** are specified make/models. **Assets** are installed instances: each asset has a location, may reference a product and responsible company, and can join multiple systems. **Warranties** attach to assets.
14
14
 
15
- ## The Per-Entity Pattern
15
+ Maintenance has three layers:
16
16
 
17
- Asset, Location, Product, Company, System, Warranty, and MaintenanceTask share one surface:
17
+ - **Procedure** — reusable checklist content: ordered typed fields, descriptions, and source references.
18
+ - **MaintenancePlan** — a schedule on exactly one Product, Asset, Location, or System. Product plans fan out to installed assets. Its `tiers` are independent schedule items that pair a Procedure with a calendar cadence, meter trigger, or both.
19
+ - **WorkOrder** — a concrete occurrence or ad-hoc job. Generated work orders snapshot their checklist fields, so history remains stable when a Procedure changes.
18
20
 
19
- | Operation | Shape |
20
- |---|---|
21
- | `<entity>(id: ID!)` | Single record (`null` if missing or in another org) |
22
- | `paginated<Entities>(projectId: ID!, limit: Int, offset: Int, ...)` | Returns `{ items { ... } totalFilteredCount }` |
23
- | `search<Entities>(projectId: ID!, search: String)` | Fuzzy text lookup |
24
- | `create<Entity>(input: ...)` / `update<Entity>(input: ...)` | Update inputs take `id` plus only the fields to change |
25
- | `delete<Entity>(id: ID!)` | Hard delete — see Safety |
21
+ `MaintenanceTask` and its completion API no longer exist. Use Procedure, MaintenancePlan, and WorkOrder operations below.
26
22
 
27
- Real deviations:
23
+ ## Discover Before Mutating
28
24
 
29
- - **Asset** — create requires `projectId` + `locationId`. `paginatedAssets` also takes `search` and a `filters` input (`statuses`, `locationIds`, `productIds`, `systemIds`, `responsibleCompanyIds`, `parentIds`, `archived`) for cross-entity tracing.
30
- - **Warranty / MaintenanceTask** — asset-scoped: create inputs take `assetId` (not `projectId`); no `search<Entities>` query, and `search` on their paginated lists is ignored — scope by asset with `warrantiesByAsset(assetId)` / `maintenanceTasksByAsset(assetId)` instead.
31
- - Batch deletes exist for some entities (`deleteLocations(ids)`, `deleteCompanies(ids)`, `deleteSystems(ids)`, `deleteWarranties(ids)`, `deleteMaintenanceTasks(ids)`) — same confirmation rules as single deletes.
25
+ Input objects evolve, especially checklist fields and plan tiers. Introspect the relevant input before composing a mutation:
32
26
 
33
- Introspect the input object before composing any create or update, e.g. `{ __type(name: "CreateAssetInput") { inputFields { name type { name kind ofType { name } } } } }`.
27
+ ```bash
28
+ specset api --query '{
29
+ createPlan: __type(name: "CreateMaintenancePlanInput") {
30
+ inputFields { name type { kind name ofType { kind name } } }
31
+ }
32
+ tier: __type(name: "MaintenancePlanTierInput") {
33
+ inputFields { name type { kind name ofType { kind name } } }
34
+ }
35
+ complete: __type(name: "CompleteWorkOrderInput") {
36
+ inputFields { name type { kind name ofType { kind name } } }
37
+ }
38
+ }'
39
+ ```
40
+
41
+ Inline input objects and enums in the operation text. Use `-F` for UUIDs, dates, and other scalar strings.
42
+
43
+ ## Core Closeout Records
44
+
45
+ Asset, Location, Product, Company, System, and Warranty retain the standard surface:
46
+
47
+ | Operation | Shape |
48
+ | ------------------------------------------------------------------- | ------------------------------------------- |
49
+ | `<entity>(id: ID!)` | Single record; nullable when missing |
50
+ | `paginated<Entities>(projectId: ID!, limit: Int, offset: Int, ...)` | `{ items { ... } totalFilteredCount }` |
51
+ | `create<Entity>(input: ...)` / `update<Entity>(input: ...)` | Update inputs take `id` plus changed fields |
52
+ | `delete<Entity>(id: ID!)` | Hard delete; follow Safety below |
53
+
54
+ Notable differences:
55
+
56
+ - Asset creation requires `projectId` and `locationId`. `paginatedAssets` accepts `search` and filters such as `statuses`, `locationIds`, `productIds`, `systemIds`, `responsibleCompanyIds`, `parentIds`, and `archived`.
57
+ - Warranty is asset-scoped; use `warrantiesByAsset(assetId)` to browse one asset's warranties.
58
+ - Location, Product, Company, System, and Asset updates support reversible `archived` state. Prefer archiving to deletion.
59
+ - Batch-delete availability varies. Introspect the mutation root rather than guessing an operation name.
60
+
61
+ Asset link setters replace the complete set for the given role/relationship; omitted ids are removed and `[]` clears the set:
34
62
 
35
- Prefer `update<Entity>` for changes: pass `id` plus only what changes; pass `null` to clear an optional relationship (e.g. `productId: null`). Update inputs for Asset, Location, Product, Company, and System accept `archived: true` — a reversible soft-archive that drops the record from default lists. Prefer it over delete.
63
+ - `setAssetSystems(assetId, role, systemIds)` / `assetSystemMemberships(assetId)`
64
+ - `setAssetSubmittals(assetId, relationship, submittalIds)` / `assetSubmittalLinks(assetId)`
65
+ - `setAssetDrawingSheets(assetId, role, drawingSheetIds)` / `assetDrawingSheetLinks(assetId)`
36
66
 
37
- ## Asset Links
67
+ ## Procedures
38
68
 
39
- Three `set*` mutations each **replace** the full id set for the given `(assetId, role)` tuple — anything previously linked at that role and not in the new list is removed; pass `[]` to clear. Inline the enum and the id list:
69
+ Use `procedure(id)` and `paginatedProcedures(projectId, filters, limit, offset)`. Procedure list filters include `search` and `archived`.
40
70
 
41
- - `setAssetSystems(assetId: ID!, role: ..., systemIds: [ID!]!)` — roles: `Primary`, `Secondary`, `Control`, `MonitoredBy`, `Other`. Read back with `assetSystemMemberships(assetId)`.
42
- - `setAssetSubmittals(assetId: ID!, relationship: ..., submittalIds: [ID!]!)` — relationships: `Source`, `Revision`, `Substitution`, `Addendum`. Read back with `assetSubmittalLinks(assetId)`.
43
- - `setAssetDrawingSheets(assetId: ID!, role: ..., drawingSheetIds: [ID!]!)` — roles include `Plan`, `Schedule`, `Diagram`, `Detail`, `AsBuilt`, `Other`. Read back with `assetDrawingSheetLinks(assetId)`.
71
+ Create a reusable checklist with its fields atomically:
44
72
 
45
73
  ```bash
46
- specset api --query 'mutation($assetId: ID!, $systemId: ID!) {
47
- setAssetSystems(assetId: $assetId, role: Primary, systemIds: [$systemId]) {
48
- id role system { id name }
74
+ specset api --query 'mutation($projectId: ID!) {
75
+ createProcedure(input: {
76
+ projectId: $projectId
77
+ name: "Quarterly AHU service"
78
+ fields: [
79
+ { name: "Inspect belts", fieldType: Checkbox }
80
+ { name: "Record supply temperature", fieldType: Number, unit: "°F" }
81
+ ]
82
+ }) {
83
+ id name updatedAt
84
+ fields { id name fieldType position required }
49
85
  }
50
- }' -F assetId=<asset-id> -F systemId=<system-id>
86
+ }' -F projectId=<project-id>
51
87
  ```
52
88
 
53
- ## Entity Documents
89
+ The fields' array order sets their `position`; neither `CreateProcedureFieldInput` nor `UpdateProcedureFieldInput` accepts a `position` property. `updateProcedure(input: { id, ... })` is partial for scalar fields. Supplying `fields` performs a full checklist replacement; first read the current fields and include the `id` of every row that should retain its identity. `deleteProcedure(id, force)` is guarded when active plans use it. Do not set `force: true` without naming the affected plans and confirming the destructive change.
90
+
91
+ ## Maintenance Plans and Tiers
92
+
93
+ Read with `maintenancePlan(id)`, `paginatedMaintenancePlans(projectId, filters, limit, offset)`, or `maintenancePlansForAsset(assetId, includeProductPlans)`.
94
+
95
+ Create requires exactly one subject id (`productId`, `assetId`, `locationId`, or `systemId`) and a `frequencyKind`. Prefer `tiers`; the top-level procedure and interval fields remain a single-item compatibility surface. Supply either:
96
+
97
+ - single-tier compatibility fields: `procedureId` (or inline `procedure`) plus `intervalValue` / `intervalUnit`; or
98
+ - `tiers: [MaintenancePlanTierInput!]` for independent schedule items. Each item selects one `procedureId` or creates one inline.
99
+
100
+ Do not mix `tiers` with the single-tier procedure/cadence fields. Calendar item units are `Days`, `Weeks`, `Months`, or `Years`. Optional meter conditions live on each item as the pair `meterUnit` (`Hours` or `Cycles`) and `meterThreshold`; provide both or neither. Calendar cadences are independent and do not need to be multiples of one another. Set `skipLowerTiersOnSharedDate: true` to suppress a shorter calendar item when it naturally shares a date with a longer one; each work order still contains only its own Procedure. A suppressed item's meter condition stays armed and may create that item's work order later. `gateOnOpen` prevents repeated meter-generated work while an occurrence remains open.
101
+
102
+ ```bash
103
+ specset api --query 'mutation($assetId: ID!, $monthlyId: ID!, $quarterlyId: ID!) {
104
+ createMaintenancePlan(input: {
105
+ assetId: $assetId
106
+ name: "AHU PM program"
107
+ frequencyKind: Calendar
108
+ skipLowerTiersOnSharedDate: true
109
+ gateOnOpen: true
110
+ tiers: [
111
+ { procedureId: $monthlyId, intervalValue: 1, intervalUnit: Months }
112
+ {
113
+ procedureId: $quarterlyId
114
+ intervalValue: 3
115
+ intervalUnit: Months
116
+ meterUnit: Hours
117
+ meterThreshold: 500
118
+ }
119
+ ]
120
+ }) {
121
+ id name frequencyKind skipLowerTiersOnSharedDate gateOnOpen
122
+ tiers {
123
+ id intervalValue intervalUnit meterUnit meterThreshold
124
+ procedure { id name }
125
+ }
126
+ openWorkOrderCount
127
+ }
128
+ }' -F assetId=<asset-id> -F monthlyId=<procedure-id> -F quarterlyId=<procedure-id>
129
+ ```
130
+
131
+ For a meter-only plan, prefer the typed input: `schedule: { meter: { unit: Hours|Cycles, threshold: <n>, gateOnOpen } }` with a single tier. The legacy encoding (`frequencyKind: RuntimeHours` or `Cycles`) additionally requires plan-level `intervalValue` set to the threshold and `intervalUnit: Hours`/`Cycles` — a threshold on the item alone is rejected. For an on-demand plan, use `AsNeeded`, `EventTriggered`, or `Other` and omit calendar and meter fields.
132
+
133
+ `updateMaintenancePlan(input: { id, ... })` can retarget the subject: pass exactly one of `productId`/`assetId`/`locationId`/`systemId` (same project). Open work orders for the old subject are retired — untouched ones removed, in-progress ones canceled — and fresh chains generate for the new subject. Supplying `tiers` fully replaces the schedule-item set: include the `id` of every existing item to update/retain, omit an item only when it should be removed, and omit `id` only for new items. Read the plan and its item ids immediately before updating.
134
+
135
+ Use `createWorkOrderFromPlan(planId, targetAssetId, dueDate, coveredTierIds)` for an early/manual occurrence of a plan. Product plans require the concrete `targetAssetId`. `coveredTierIds` is optional; when supplied it states exactly which tiers the occurrence performs.
136
+
137
+ ## Work Orders
138
+
139
+ Use `workOrder(id)`, `paginatedWorkOrders(projectId, filters, limit, offset)`, and `paginatedGroupedWorkOrders(projectId, groupBy, groupKeys, filters, limit, offset)`. Grouped reads require both the grouping dimensions and the current group-key path. Useful fields include `status`, `dueDate`, `performedDate`, `priority`, `assignee`, `plan`, `planTier`, `coveredTiers`, `fields`, and `documents`.
140
+
141
+ `createWorkOrder(input)` creates ad-hoc work and requires exactly one of `assetId`, `locationId`, or `systemId`, plus `name`. Optional `fields` create a reusable inline Procedure; optional `recurrence` also creates a MaintenancePlan. Use `updateWorkOrder(input: { id, ... })` for editable metadata.
142
+
143
+ Checklist responses use `setWorkOrderFieldResponse(input)` or `setWorkOrderFieldResponses(inputs)`. Introspect `SetWorkOrderFieldResponseInput`: answer columns vary by field type (`valueNumber`, `valueText`, `valueChoice`, `status`, and `note`).
144
+
145
+ Complete work through the batch mutation even for one row:
146
+
147
+ ```bash
148
+ specset api --query 'mutation($id: ID!, $performedDate: DateTime!) {
149
+ completeWorkOrders(inputs: [{ id: $id, performedDate: $performedDate }]) {
150
+ workOrder { id status performedDate }
151
+ nextWorkOrder { id status dueDate }
152
+ }
153
+ }' -F id=<work-order-id> -F performedDate=2026-08-04
154
+ ```
155
+
156
+ Other lifecycle operations:
157
+
158
+ - `skipWorkOrder(id, notes, mode)` — closes the occurrence as skipped; omit `mode` for normal independent-item plans. Non-default modes exist only for historical merged work orders.
159
+ - `reopenWorkOrder(id)` — reopens completed/skipped work when allowed.
160
+ - `makeWorkOrderRecurring(workOrderId, recurrence)` — converts eligible ad-hoc work into a recurring plan.
161
+ - `deleteWorkOrder(id)` — hard delete.
162
+ - `bulkAssignWorkOrders(selection, assigneeId)` and `bulkDeleteWorkOrders(selection)` operate on an explicit selection object.
163
+ - `addWorkOrderDocument(workOrderId, documentId, workOrderFieldId)` and `removeWorkOrderDocument(id)` manage evidence. Removal takes the WorkOrderDocument pivot id.
164
+
165
+ ## Entity Documents and Documentation
166
+
167
+ Asset, Location, Product, Company, System, and Warranty link project Documents through entity-specific role-tagged pivots. Read `<entity>Documents(<entity>Id)`, then call `set<Entity>DocumentRoles(<entity>Id, documentId, roles)` after introspecting that entity's role enum (`AssetDocumentRole`, `ProductDocumentRole`, `SystemDocumentRole`, `LocationDocumentRole`, `CompanyDocumentRole`, `WarrantyDocumentRole`). The roles array is replaced atomically; an empty role list removes the pivot and may delete the underlying Document when it becomes orphaned. Asset also exposes `deleteAssetDocument(id)`.
168
+
169
+ Attaching a new file is a three-step flow — the same Document can then be linked to several entities (a family IOM shared by every product in the family):
170
+
171
+ ```bash
172
+ CLOUD_FILE=$(specset files upload ./aaon-rn-iom.pdf --type application/pdf --json | jq -r .id)
173
+ specset api --query "mutation { createDocumentsFromCloudFiles(cloudFileIds: [\"$CLOUD_FILE\"], projectId: \"<project-id>\") { id title } }"
174
+ specset api --query 'mutation { updateDocument(id: "<document-id>", title: "AAON RN Series — IOM") { id } }' # title drives lists and PDF bookmarks; the default is the filename
175
+ specset api --query 'mutation { setProductDocumentRoles(productId: "<product-id>", documentId: "<document-id>", roles: [OperationManual, MaintenanceManual]) { id } }'
176
+ ```
177
+
178
+ Existing project Documents work too — a submittal attachment's `document { id }` (from `paginatedSubmittals … attachments { document { id title } }`) can be linked directly as `SubmittalPackage`/`Datasheet`; the only check is that the Document belongs to the same project. To bind a slice of a large file, `extractPagesFromFileResource(id: <fileResourceId>, pages: [3, 4, 5])` returns a new CloudFile to feed into `createDocumentsFromCloudFiles`. Only PDF files merge into the O&M package export; images and other types are listed as references.
179
+
180
+ Asset, Location, Product, Company, and System also have markdown documentation blocks. `update<Entity>Documentation(id, markdown)` replaces the block; `mark<Entity>DocumentationComplete(id)` attests that it is current. Any later create/update/link on the entity (a new document pivot, a warranty document, a field edit) marks the documentation stale again, so when seeding a graph write documentation **last**. Calling `mark<Entity>DocumentationComplete` with an empty block sets the state to `NotApplicable`, which the package readiness report accepts — use it for entities that don't warrant prose rather than writing filler.
181
+
182
+ ### Writing documentation that reads well in the O&M package export
183
+
184
+ The rules below are the same ones the in-app documentation agent follows (they are one shared block; a test keeps this copy in sync).
185
+
186
+ ### Where Documentation Renders
187
+
188
+ The documentation block renders on the entity's show page and in the compiled O&M closeout package PDF — a formal handover manual the owner's facilities team keeps for decades. The manual already prints the following from structured data, so documentation never restates it:
54
189
 
55
- Each entity links to project documents through a role-tagged pivot. Find document ids with `documents(projectId: ID!) { id title }`. The pattern (uniform across Location, Product, Company, System, Warranty, MaintenanceTask; substitute the entity name):
190
+ - Identity: tag, serial number, status, install and commissioning dates, quantity, location path, manufacturer, model, spec section, classification, data-package tier.
191
+ - Warranties: kind, start and expiration dates, duration, claims contact and the terms text.
192
+ - Maintenance schedule: every plan, procedure, frequency, craft and estimated hours, built from the maintenance plan records.
193
+ - Spare parts: name, SKU, manufacturer part number, quantity and vendor, from the part records.
194
+ - Contacts: the responsible company with phone and email, plus a per-system contacts rollup.
195
+ - Linked documents: every datasheet, manual, warranty and report, listed and bound directly behind the entity's page.
56
196
 
57
- - `<entity>Documents(<entity>Id: ID!)` — list the pivots. Select `{ id roles document { id title } }`.
58
- - `set<Entity>DocumentRoles(<entity>Id: ID!, documentId: ID!, roles: [...]!)` — upsert the linkage; an empty `roles` list removes it (and the document itself if orphaned). Roles are per-entity enums — e.g. `ProductDocumentRole` is `Datasheet`, `InstallationManual`, `OperationManual`, `MaintenanceManual`, `Warranty`, `SubmittalPackage`, `Other`; introspect each entity's enum before use.
59
- - `delete<Entity>Document(id: ID!)` — takes the **pivot** id from the list query, not the document id.
197
+ ### What to Write
60
198
 
61
- Asset is the exception: it has `assetDocuments(assetId)` and `deleteAssetDocument(id)` but no set-roles mutation.
199
+ Spend the narrative on what tables cannot say: sequences of operation with real setpoints, startup and shutdown, seasonal and emergency procedures, troubleshooting, safety warnings, cross-system interfaces, and callouts where sources conflict. Describe the equipment, not the records — state how it works and how it is cared for, and fold genuine unknowns into one short "Open Items" list, one line each, without citations. A short block with two real sections beats a full template of filler: omit every section the sources don't genuinely cover, and leaving the block blank is a valid outcome for an entity with nothing to add.
62
200
 
63
- ## Documentation Blocks
201
+ Write for the building operator in plain language. Paraphrase manual procedures as proper markdown lists, and fix concatenation artifacts (double periods, misspelled manufacturer names) from sources.
64
202
 
65
- Asset, Location, Product, Company, and System each carry a markdown documentation block used by closeout exports:
203
+ Keep the block free of what the manual already prints and of anything that goes stale:
66
204
 
67
- - `update<Entity>Documentation(id: ID!, markdown: String!)` — replace the block. Pass `markdown` via `-F` (it's a plain string).
68
- - `mark<Entity>DocumentationComplete(id: ID!)` — attest the block is current (clears it from stale-documentation rollups).
205
+ - No warranty terms or dates in prose, no frequency lists or task inventories, no tag/serial/model/quantity dossiers, no contractor names or phone numbers, and no parts tables pasted from manuals — replacement-parts data belongs in part records.
206
+ - No claims about the state of the records ("no dates are populated", "this task is recorded on the checklist") — they end up contradicting the printed tables.
207
+ - No restatement of the entity's `description` field as the opening — both print in sequence.
208
+ - No internal vocabulary ("portal", "normalized", "occurrence basis", "identity guard", "MaintenanceTask", "sourceMarkdown"), entity UUIDs, or extraction and slug references.
209
+ - No all-caps sentences or blockquote dumps of manual text, and no sections whose only content is "not established in reviewed source" or "see the linked manual".
69
210
 
70
- ## Maintenance Completions
211
+ ### Citations
71
212
 
72
- Service history lives on completions under each task:
213
+ Citations are clickable in the app and plain text in the printed manual, so keep them sparse and short: at most one per section, placed at the end of the claim it supports, as a markdown link whose display text is a short label such as "Siemens O&M p. 712" or "Warranty Table p. 5". Use citation URLs exactly as returned by the tools. The entity's bound-documents list already anchors provenance, so there are no per-sentence citations, multi-citation parentheticals, full source filenames, paragraph numbers, spreadsheet cell references, or dedicated "Source:" lines (print strips those).
73
214
 
74
- - `maintenanceTaskCompletions(maintenanceTaskId: ID!)` — select `{ id performedDate dueDate performedByName result }`.
75
- - `createMaintenanceTaskCompletion(input: { maintenanceTaskId, performedDate, ... })` — `performedDate` is required; optional `dueDate`, `performedByName`, `performedByCompanyId`, `result`. Introspect `CreateMaintenanceTaskCompletionInput` for the full set.
76
- - `updateMaintenanceTaskCompletion(input: { id, ... })` / `deleteMaintenanceTaskCompletion(id: ID!)`.
77
- - Completion documents: `addMaintenanceTaskCompletionDocument(completionId: ID!, documentId: ID!)` and `deleteMaintenanceTaskCompletionDocument(id: ID!)` (pivot id).
215
+ ### Headings
216
+
217
+ The entity name renders as the page title, the block is embedded under a "Documentation" heading on the show page, and it may be re-rendered inside a parent entity's hierarchical view, so heading levels must compose:
218
+
219
+ - Top-level sections use `##`; sub-sections use `###`, then `####`, without skipping levels.
220
+ - Begin with the first content section (for example `## Description`). The entity's own name is never a heading and `#` is never used — that level is the page title, and a leading H1 matching the entity name is suppressed in print.
221
+ - Keep section names consistent with the templates below so sibling entities render with the same structure.
222
+
223
+ ### Section Templates
224
+
225
+ Use these section orderings with the operator-facing names shown; most entities warrant only one to three of them.
226
+
227
+ **Product** — the canonical home for manufacturer operating knowledge. A product with any linked submittal, datasheet, manual or warranty document gets a block; only a product with no linked source documents is left blank. Location and system blocks may roll product knowledge up but never replace it.
228
+
229
+ ```markdown
230
+ ## Description (1-3 sentences: what it is and its role — not metadata)
231
+ ## Operation (sequence of operation, setpoints, modes, interlocks)
232
+ ## Routine Maintenance (procedure steps and precautions — never frequencies; the schedule table prints those)
233
+ ## Troubleshooting
234
+ ## Safety
235
+ ## Open Items
236
+ ```
237
+
238
+ **System** — the system in its own words, spanning multiple products. A parent system covers only what is true of the whole (shared operation, shared safety): the manual prints the subsystems list and rolls up schedules, warranties and contacts, so content common to sibling systems lives once on the parent and each child gets at most one sentence of scope.
239
+
240
+ ```markdown
241
+ ## Description (what the system is and its boundary, 2-4 sentences)
242
+ ## Sequence of Operation
243
+ ## Interfaces (only real cross-system relationships: what feeds, releases, or controls what)
244
+ ## Seasonal & Emergency Procedures
245
+ ## Troubleshooting
246
+ ## Safety
247
+ ## Open Items
248
+ ```
249
+
250
+ **Location** — building or area overview.
251
+
252
+ ```markdown
253
+ ## Description
254
+ ## Access & Safety
255
+ ## Open Items
256
+ ```
257
+
258
+ **Asset** — installation-specific facts only; most assets need very little.
259
+
260
+ ```markdown
261
+ ## As-Installed Configuration (field settings, orientations, connections not captured by structured fields)
262
+ ## Commissioning Notes
263
+ ## Open Items
264
+ ```
265
+
266
+ **Company** — only what the contacts block cannot carry.
267
+
268
+ ```markdown
269
+ ## Escalation
270
+ ## Supplier Portal
271
+ ```
272
+
273
+ ### Example (Product)
274
+
275
+ ```markdown
276
+ ## Description
277
+ Passive rubber check valves preventing backflow from rivers, oceans, and storm water into outfall lines.
278
+
279
+ ## Operation
280
+ Flow opens the bill section; back-pressure closes it. No power or operator action required. Do not exceed 25 psi back-pressure [Manual p. 3](sb://doc/...).
281
+
282
+ ## Routine Maintenance
283
+ 1. Clear debris from the bill opening.
284
+ 2. Inspect the elastomer for cuts or abrasion; replace on any through-cut.
285
+
286
+ ## Troubleshooting
287
+ Standing water upstream usually means debris is holding the bill open — clear before assuming valve failure.
288
+ ```
289
+
290
+ ## Parts and Meter Readings
291
+
292
+ `createPart(input: { projectId, name, sku, manufacturerPartNumber, unit, quantity, minQuantity, maxQuantity, unitCost, stocked, bin, vendorId, assetLinks: [{ assetId, quantity }] })` creates a stocked part with its per-asset quantities; `createPartMovement(input: { partId, quantity, note })` records receipts (positive) and consumption (negative), and `countedQuantity` records a cycle count. Read with `paginatedParts(projectId, filters)`.
293
+
294
+ `logMeterReading(assetId, unit: Hours|Cycles, value, readAt)` records a runtime or cycle reading; meter-triggered plan tiers (`meterUnit`/`meterThreshold`) generate work from these. `meterReadings(assetId, unit, limit)` lists them.
295
+
296
+ ## Closeout Packages (O&M manual export)
297
+
298
+ A closeout package compiles the graph into one bookmarked PDF: front matter (cover, directory, warranty log, hyperlinked TOC), a slip sheet and overview per system (or location), a data page per asset, and every linked PDF document bound behind its owner (each unique file once). Scope is `Project` (everything), `Systems`, `Assets`, or `Locations` with `targetIds`; `orderingAxis` is `System` (default) or `Location`.
299
+
300
+ Check readiness first — it is deterministic and the same builder the compile uses, so a clean report means a clean manual:
301
+
302
+ ```bash
303
+ specset api --query '{ closeoutPackageReadiness(projectId: "<project-id>", scopeKind: Project) { warningCount warnings } }'
304
+ ```
305
+
306
+ Warning kinds and how to clear them: `DocumentationMissing`/`DocumentationOutdated` (write or mark documentation complete on the named Asset/Product/System/Location), `AssetFieldMissing` (serial number or install date), `WarrantyMissing` (every asset needs at least one Warranty), `ProductManualMissing` (each Product needs an `OperationManual` or `MaintenanceManual` document), `PlanWithoutProcedure` (a plan with no tiers), `AttachmentNotMergeable` (non-PDF), `TargetDeleted`. `EmptyScope` blocks creation.
307
+
308
+ ```bash
309
+ specset api --query 'mutation { createCloseoutPackageExport(input: { projectId: "<project-id>", scopeKind: Project, orderingAxis: System, includeAttachments: true, title: "… O&M Manual" }) { id workflow { status } } }'
310
+ specset api --query '{ closeoutPackageExport(id: "<export-id>") { completedAt warnings workflow { status progress error } fileResource { id name } } }'
311
+ specset files download <file-resource-id> --out manual.pdf # once workflow.status is Completed
312
+ ```
313
+
314
+ `paginatedCloseoutPackageExports(projectId)` lists prior exports; exports are immutable records — regenerate by creating a new one; `deleteCloseoutPackageExport(id)` removes one and its file. In the app the same flow lives under the project's Closeout → Packages page.
315
+
316
+ ## Enabling closeout
317
+
318
+ `CloseoutAuthService.isCloseoutActive` requires the org SKU **and** (unless the org is in `fmMode`) the project toggle. Both are superadmin operations; `updateOrg(skus)` replaces the whole SKU object, so read the current flags and resend them:
319
+
320
+ ```bash
321
+ specset api --query '{ org(id: "<org-id>") { skus { closeout fmMode dailyLog scheduling estimation agentSessions agentSkills customAgents whitelabel modelRoutingV2 } } }'
322
+ specset api --query 'mutation { updateOrg(id: "<org-id>", skus: { closeout: true, dailyLog: true, scheduling: true }) { id } updateProject(id: "<project-id>", toolSettings: { closeout: true }) { id } }'
323
+ ```
78
324
 
79
- ## Dedup / Re-Pointing Recipe
325
+ ## Dedup / Re-pointing
80
326
 
81
327
  For duplicate products P1 (loser) and P2 (survivor):
82
328
 
83
- 1. `paginatedAssets(projectId: ..., filters: { productIds: ["<p1-id>"] })` — collect affected assets.
84
- 2. `updateAsset(input: { id: ..., productId: "<p2-id>" })` per asset.
85
- 3. Re-run step 1 and confirm zero results.
86
- 4. `updateProduct(input: { id: "<p1-id>", archived: true })` — archive the loser; delete only if it is clearly an unreferenced mistake.
329
+ 1. Query `paginatedAssets(projectId, filters: { productIds: [P1] })` and collect impacted assets.
330
+ 2. `updateAsset(input: { id, productId: P2 })` for each asset.
331
+ 3. Re-run the filtered query and confirm zero remaining references.
332
+ 4. Archive P1 with `updateProduct(input: { id: P1, archived: true })`.
87
333
 
88
- The same shape works for duplicate companies, locations, and systems.
334
+ Use the same impact-first pattern for duplicate companies, locations, and systems.
89
335
 
90
336
  ## Safety
91
337
 
92
- - Deletes here are hard deletes with no undo — name the exact record (tag/name + id) when confirming.
93
- - Prefer `update` over delete-and-recreate, and `archived: true` over delete: deleting a product, location, or company silently nulls the references on assets that pointed at it. Check impact first with the filtered list queries.
94
- - Removing a document linkage can delete the underlying document if nothing else references it — say so when confirming.
338
+ - Deletes are hard and have no undo. Before executing, name the exact record and id and confirm the impact.
339
+ - Prefer partial updates, and prefer `archived: true` to delete where supported.
340
+ - Read Procedure fields and MaintenancePlan tiers before any full-replacement update.
341
+ - Deleting plans or force-deleting Procedures can alter future generated work. Check active plan/open-work-order counts first.
342
+ - Removing a document linkage can delete an orphaned underlying Document; disclose that before confirming.
@@ -0,0 +1,27 @@
1
+ ---
2
+ name: specset-issues
3
+ description: Use when working with a Specset project's design-review issues from the CLI — listing the queue by severity, tier, or verification state, and reading each issue's verdict.
4
+ allowed-tools: Bash Read AskUserQuestion
5
+ ---
6
+
7
+ # Specset Issues
8
+
9
+ Design reviews (Design Review, Drawing Review, Design Quality Review, Project Review) and issue-finding skills file **issues** on a project. Each issue can be **verified** by an adversarial agent that re-reads the cited sheets and sections and records a verdict — `Confirmed` or `Rejected` — with a short justification. New review issues are verified automatically; skill-run issues, issues filed before verification existed, and issues whose runs ended without a verdict are verified on demand.
10
+
11
+ ## Reading the queue
12
+
13
+ ```bash
14
+ specset issues list --project <projectId> # active, non-Cleanup issues
15
+ specset issues list --project <projectId> --severity High,Medium --notVerified --limit 500
16
+ specset issues list --project <projectId> --thread <reviewThreadId> --json # one review's findings
17
+ specset issues list --project <projectId> --thread <runThreadId> --org <slug> # a skill run, by the id agent chat printed
18
+ ```
19
+
20
+ - `--severity` takes a comma list (`High`, `Medium`, `Low`); `--notVerified` keeps only issues with no verdict; `--archived` switches to the archived set.
21
+ - `--thread` matches the thread and every review thread under it: a skill's issues belong to its delegated review, so the run id `specset agent chat` prints finds them directly. `--org` pins the organization like the other commands.
22
+ - Cleanup-tier issues (real but self-resolving, kept out of the review queue) are excluded unless you pass `--includeCleanup` — the same rule the app's default queue uses.
23
+ - The table shows `#number severity tier verdict title`; `verdict` is `Confirmed`, `Rejected`, `verifying` (run in flight) or `unverified`. `--json` returns the full rows including the originating review thread.
24
+
25
+ ## Verification
26
+
27
+ Verdicts come from the app's own verification runs: new review issues are verified automatically, and a member can start verification for an individual issue from the issue in the app. `specset issues list` shows the verdict on every row (`--notVerified` to find the ones still waiting).
@@ -8,6 +8,10 @@ allowed-tools: Bash Read AskUserQuestion
8
8
 
9
9
  This skill mutates data — confirm the project name and which files belong to it before creating anything. Inline enums, ints, lists, and input objects in the operation text (`-F` is strings-only; Variable Limitations and Schema Discovery live in the `specset` skill).
10
10
 
11
+ ## Find a project
12
+
13
+ Use `specset projects list --json` to list the active organization's projects with IDs and links. `--org <slug|id>` pins another organization, `--limit 20` bounds output, and `--archived` lists archived projects. Pass a returned ID or link to other project commands.
14
+
11
15
  ## Reading Projects
12
16
 
13
17
  ```graphql
@@ -20,17 +24,17 @@ project(id: ID!): Project!
20
24
 
21
25
  ## Creating a Project
22
26
 
23
- `createProject` does **not** add the creator as a project member, so the new project won't appear in the default `projects` list — fetch it by `project(id)`, and add membership as a separate step, mirroring the UI.
27
+ `createProject` does **not** add the creator as a project member, so the new project won't appear in the default `projects` list until someone adds them — fetch it by `project(id)`. The `createProject` mutation takes `userIds` and `teamIds` to add members in the same call; `specset projects create` doesn't, so add them as a second step.
24
28
 
25
- 1. Create it:
29
+ 1. Create it with the project command. New projects automatically support drawing graphs; no opt-in is required:
26
30
 
27
31
  ```bash
28
- specset api --query 'mutation($orgId: ID!, $name: String!) {
29
- createProject(orgId: $orgId, name: $name) { id name }
30
- }' -F orgId=<org-id> -F name='Riverside Medical Center'
32
+ specset projects create 'Riverside Medical Center' --json
31
33
  ```
32
34
 
33
- 2. Get your own user id with `{ me { id } }`, then add yourself (and anyone else the user names) as a member. `role` is an optional `OrgMemberTeamRole` enum — `Member`, `Manager`, or `Admin` — inlined when specified:
35
+ Select the active organization first with `specset org use <slug>`. Pass `--host <url>` to both commands when targeting a specific deployment. JSON output includes `id`, `name`, and `drawingGraphEnabled`. Existing projects qualify when every current sheet supports graph resolution (node version 5 or later); ledger failures do not disable graphs.
36
+
37
+ 2. Get your own user id with `{ me { id } }`, then add yourself (and anyone else the user names) as a member. `role` is an optional `OrgMemberTeamRole` enum — `Member`, `Manager`, or `Admin` — inlined when specified. To add several people or whole teams at once, use `addProjectMembers(projectId: ID!, userIds: [ID!], teamIds: [ID!])` (inline the lists); a team puts everyone on it on the project, and `teams(orgId)` lists them:
34
38
 
35
39
  ```bash
36
40
  specset api --query 'mutation($projectId: ID!, $userId: ID!) {
@@ -38,27 +42,31 @@ specset api --query 'mutation($projectId: ID!, $userId: ID!) {
38
42
  }' -F projectId=<project-id> -F userId=<user-id>
39
43
  ```
40
44
 
45
+ ## Review Settings
46
+
47
+ Drawing graphs rebuild automatically as project inputs change. Automatic
48
+ verification of newly filed review issues is a platform setting managed by
49
+ Specset; it does not verify the existing queue. Project Review has no separate
50
+ project toggle; its availability in the app comes from the organization's
51
+ configured agents.
52
+
41
53
  ## Importing Documents
42
54
 
43
- 1. Upload each PDF following Uploading Files in the `specset` skill. Each `completeUpload` returns a **cloud file id** — collect them.
44
- 2. Register the uploads on the project in a single call. The input object is inlined; ID variables are fine as its leaves:
55
+ For an existing project, inspect drawing review readiness with `specset projects drawing-graph --project <id> --json`. Add `--rebuild` to queue resolution from already extracted drawing content; this does not start sheet extraction or element backfill. Re-run the command without mutation flags to inspect readiness after the background rebuild. Eligibility is automatic for new projects and existing projects whose current sheets are all at node version 5 or later. Pin `--host` and `--org` when targeting a particular deployment or organization.
56
+
57
+ Use the document-import command for local files; it uploads them, registers them on the project, and publishes the generated drawing/specification files automatically:
45
58
 
46
59
  ```bash
47
- specset api --query 'mutation($projectId: ID!, $f1: ID!, $f2: ID!) {
48
- createProjectDocuments(input: {
49
- projectId: $projectId
50
- cloudFileIds: [$f1, $f2]
51
- autoImport: true
52
- }) { id totalCount completedCount failedCount }
53
- }' -F projectId=<project-id> -F f1=<cloud-file-id> -F f2=<cloud-file-id>
60
+ specset projects add-docs --project <project-id> ./drawings.pdf ./specifications.pdf --json
61
+ specset projects ingest-status --project <project-id> --wait --timeout 1800 --json
54
62
  ```
55
63
 
56
- 3. It returns a **BulkAction** — poll it per Waiting on Background Processing in the `specset` skill. Processing extracts each document's outline and content, indexes it for search, and splits it into candidate spec files and drawing files by page range.
64
+ `add-docs` returns upload metadata and the background bulk action immediately. `ingest-status --wait` follows processing and publication; omit `--wait` for a snapshot. Processing extracts each document's outline and content, indexes it for search, and splits it into candidate spec files and drawing files by page range. The status command checks workflow completion and file publication; use drawing-element or graph tools to inspect content quality.
57
65
 
58
66
  ### `autoImport`: publish automatically or curate
59
67
 
60
68
  - `autoImport: true` — every generated spec/drawing file is classified and published into default sets automatically. Use when the user wants everything live immediately.
61
- - Omit it (or `false`) — files land **unpromoted** for manual curation below. Use when the user wants to control set titles, membership, or ordering.
69
+ - Pass `--noAutoImport` to `add-docs` — files land **unpromoted** for manual curation below. Use when the user wants to control set titles, membership, or ordering. The raw `createProjectDocuments` API defaults to unpromoted when `autoImport` is omitted.
62
70
 
63
71
  Ask the user when their intent is ambiguous — this choice changes what gets published. The input also supports folders: `parentFolderId` / `newFolderName` for flat imports, or `folderTree` + `files` to mirror a directory structure — introspect `CreateProjectDocumentsInput` for those shapes.
64
72
 
@@ -98,6 +106,21 @@ updateDrawingSetPrecedence(projectId: ID!, drawingSetIds: [ID!]!): [DrawingSet!]
98
106
 
99
107
  4. Verify and summarize — query `project(id)` (not the membership-filtered `projects` list) with `specSets { id title }` and `drawingSets { id title }`, and report what was created.
100
108
 
109
+ ## Downloading Project Material
110
+
111
+ `specset projects hydrate` pulls a project's source material into a local directory so an agent (or a person) can read the PDFs directly instead of fetching sections one at a time:
112
+
113
+ ```bash
114
+ specset projects hydrate <projectId|link> --out ./pier-romeo # specs/, drawings/, docs/
115
+ specset projects hydrate <projectId|link> --out ./pier-romeo --include specs,drawings
116
+ specset projects hydrate <projectId|link> --json # default --out: ~/Specset/<host>/<project-name>-<projectId>/
117
+ ```
118
+
119
+ - `specs/` holds one PDF per spec section (`<number> - <title>.pdf`), `drawings/` one PDF per drawing sheet (`<sheet> - <title>.pdf`, the rendered image for legacy sheets), and `docs/` the project's other documents (reports, permits, manuals). The spec book and drawing set PDFs themselves are not repeated under `docs/` — fetch a whole book with `specset specs download <ref> --asset book` when needed.
120
+ - Re-running is idempotent: files already present at the stored size are skipped, so run it again after new documents are processed to pick up only what changed.
121
+ - `--json` returns `{ projectId, out, files: [{ path, kind, ref, bytes, sha256 }], skipped, failures }`; `ref` is the public link of the source entity. **Cite that link, never the local path**, when reporting findings from a downloaded file.
122
+ - Large projects can be hundreds of files; start with `--include specs` when only the specifications matter. A non-zero exit means some downloads failed — re-run the same command to retry just those.
123
+
101
124
  ## Archiving and Deleting
102
125
 
103
126
  - **Prefer `archiveProject(id: ID!)`** — reversible via `unarchiveProject(id: ID!)`. Archived projects drop out of default lists but keep all data.
@@ -108,3 +131,5 @@ updateDrawingSetPrecedence(projectId: ID!, drawingSetIds: [ID!]!): [DrawingSet!]
108
131
  - Authorization error on a mutation — the user lacks that permission; the same rules as the UI apply. Surface the message, don't retry.
109
132
  - A freshly created project "missing" from `projects` — that's the membership gotcha above, not a failure.
110
133
  - A document reports a failed processing status on the BulkAction — report it to the user; re-uploading is their call.
134
+
135
+ Hydration records each file's Specset link, version id and checksum in `.specset/manifest.json`. Inside a local run's workspace (`specset agent serve` with **Native tools** on) the single `… download` commands record there too, and a version already present is not fetched again.
@@ -37,7 +37,22 @@ search(
37
37
  At least one of `query` or `keywords` is required; they can be combined. Results:
38
38
 
39
39
  ```graphql
40
- { groups { referenceType totalCount items { url title previewText score projectId projectName } } nextOffset }
40
+ {
41
+ groups {
42
+ referenceType
43
+ totalCount
44
+ items {
45
+ id
46
+ url
47
+ title
48
+ previewText
49
+ score
50
+ projectId
51
+ projectName
52
+ }
53
+ }
54
+ nextOffset
55
+ }
41
56
  ```
42
57
 
43
58
  ### Project Scope (`ProjectScope`)
@@ -48,7 +63,7 @@ At least one of `query` or `keywords` is required; they can be combined. Results
48
63
 
49
64
  ### Record Types (`SourceReferenceType`)
50
65
 
51
- Filter with `referenceTypes`. Common values: `SpecSection`, `DrawingSheet`, `Document`, `Submittal`, `SubmittalAttachment`, `Rfi`, and the closeout types `Company`, `Location`, `Product`, `Asset`, `Warranty`, `MaintenanceTask`. Introspect the `SourceReferenceType` enum for the full list.
66
+ Filter semantic `search` with the record types it materializes: `SpecSection`, `DrawingSheet`, `Document`, `Submittal`, and `Rfi`. Other values exist in the shared `SourceReferenceType` enum but are not necessarily supported by this operation.
52
67
 
53
68
  ## Quick Search: `quickSearch`
54
69
 
@@ -67,6 +82,8 @@ quickSearch(
67
82
 
68
83
  Results: `{ items { id url number title referenceType score projectId projectName summary } totalCount }`
69
84
 
85
+ `quickSearch` also supports explicitly requested closeout title records: `Company`, `Location`, `System`, `Product`, `Asset`, and `MaintenancePlan`. It does not currently return `Warranty`, `Procedure`, or `WorkOrder` rows. `MaintenanceTask` is a deprecated legacy-link redirect, not a searchable record type. Omit `referenceTypes` for the standard global-search defaults, or introspect `SourceReferenceType` and use only a type supported by the selected operation.
86
+
70
87
  ## Precise Lookups
71
88
 
72
89
  When the user hands you a section or sheet number, look it up directly:
@@ -90,15 +107,16 @@ Useful item fields: `id sectionNumber title numberAndTitle` on SpecSection, `id
90
107
  ```bash
91
108
  specset api --query 'query($orgId: ID!) {
92
109
  search(orgId: $orgId, keywords: ["AHU-1"],
93
- referenceTypes: [Submittal, SubmittalAttachment],
110
+ referenceTypes: [Submittal],
94
111
  projectScope: MyProjects, limit: 20) {
95
112
  groups { referenceType totalCount
96
- items { url title previewText projectName } }
113
+ items { id url title previewText projectName } }
97
114
  }
98
115
  }' -F orgId=<org-id>
99
116
  ```
100
117
 
101
118
  ## Working With Results
102
119
 
103
- - Each result's `url` is a Specset reference URL (`sb://spec/<id>`, `sb://sub/<id>`, …); the UUID inside is the record's GraphQL `id` — extract it for follow-up queries. The scheme is documented in the `specset-tools` skill.
120
+ - Each result's `id` is the UUID for follow-up GraphQL calls. Its `url` is the clickable source link (`https://app.specbook.ai/go/spec/<uuid>`, `https://app.specbook.ai/go/sub/<uuid>`, …) and can be passed directly into Specset tools and download commands.
121
+ - In user-facing answers, include the most relevant returned `url` values as descriptive Markdown source links so the user can open the result in Specset. Never synthesize a `/go` link from a UUID that was not returned as a source link.
104
122
  - For follow-up work on what you found — updating submittals, answering RFIs, closeout records, or project/document changes — hand off to the `specset-submittals`, `specset-rfis`, `specset-closeout`, or `specset-projects` skill.