@specset/cli 0.2.0 → 0.3.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.
package/README.md CHANGED
@@ -48,16 +48,23 @@ specset agent chat --project <projectId> -m "What mechanical equipment is schedu
48
48
 
49
49
  Run `specset --help` or `specset <command> --help` for the full command reference.
50
50
 
51
- ## Use with Claude and other agents
51
+ ## Use with Claude, Codex, and ChatGPT
52
52
 
53
53
  The package bundles a family of [Agent Skills](https://agentskills.io) that teach AI agents how to drive this CLI safely — a core `specset` skill (setup, auth, GraphQL) plus domain skills for search, project setup, submittals, RFIs, closeout data, agent chat, and org administration:
54
54
 
55
55
  ```bash
56
- # Claude Code: install the whole family into ~/.claude/skills
57
- specset skill install
56
+ # Claude Code: install the whole family into ~/.claude/skills (the default)
57
+ specset skill install --target claude
58
+
59
+ # Codex or ChatGPT desktop: install into ~/.agents/skills
60
+ specset skill install --target codex
61
+ # `--target chatgpt` is an alias for the same directory
58
62
 
59
63
  # Install a subset
60
- specset skill install --only specset --only specset-search
64
+ specset skill install --target codex --only specset --only specset-search
65
+
66
+ # Override the destination for another agent or a custom setup
67
+ specset skill install --dir /path/to/skills
61
68
 
62
69
  # See what's bundled
63
70
  specset skill list
@@ -67,7 +74,7 @@ specset skill print
67
74
  specset skill print specset-submittals
68
75
  ```
69
76
 
70
- Re-run `specset skill install` after upgrading the CLI to refresh the installed copies.
77
+ Re-run `specset skill install --target <agent>` after upgrading the CLI to refresh the installed copies.
71
78
 
72
79
  ## Configuration
73
80
 
package/dist/main.js CHANGED
@@ -28757,6 +28757,52 @@ var drawingComparisonRouting = {
28757
28757
  ]
28758
28758
  };
28759
28759
 
28760
+ // apps/cli/src/probes/scenarios/routing/projectScan.ts
28761
+ var projectScanRouting = {
28762
+ name: "routing/project-scan",
28763
+ tier: "routing",
28764
+ description: "Flash conversation routes an exhaustive-inventory ask through the project-scan skill and pauses at the startProjectScan approval.",
28765
+ preconditionNotes: "target project needs a processed drawing set",
28766
+ turns: [
28767
+ {
28768
+ kind: "message",
28769
+ text: "I need a complete inventory of every fire damper and combination fire/smoke damper shown on the mechanical drawings \u2014 device type, rating, location, and the sheet and note where each is called out. Use sensible defaults for scoping \u2014 do not ask me questions before starting."
28770
+ }
28771
+ ],
28772
+ assertions: [
28773
+ expectToolCall("loadSkill", { input: { name: "project-scan" } }),
28774
+ expectToolCall("startProjectScan", {
28775
+ state: "approval-requested",
28776
+ minTimes: 1,
28777
+ maxTimes: 1
28778
+ }),
28779
+ custom("scan is scoped to drawing sheets, not the whole project", (ctx) => {
28780
+ const dispatches = ctx.toolCalls().filter((c) => c.toolName === "startProjectScan");
28781
+ const input = dispatches[0]?.input;
28782
+ const scoped = input?.coverageMode === "drawingSheets" && Boolean(
28783
+ input.sheetNumbers?.length || input.disciplines?.length || input.area || input.masterOnly
28784
+ );
28785
+ return {
28786
+ pass: scoped,
28787
+ evidence: `startProjectScan inputs: ${JSON.stringify(dispatches.map((c) => c.input))}`
28788
+ };
28789
+ }),
28790
+ custom("does not attempt the enumeration inline", (ctx) => {
28791
+ const inlineReads = ctx.toolCalls().filter(
28792
+ (c) => ["searchDrawingSheets", "getDrawingSheetContent"].includes(
28793
+ c.toolName
28794
+ )
28795
+ );
28796
+ return {
28797
+ pass: inlineReads.length <= 6,
28798
+ evidence: `${inlineReads.length} inline drawing reads`
28799
+ };
28800
+ }),
28801
+ expectAwaitingApproval("startProjectScan"),
28802
+ expectNoError()
28803
+ ]
28804
+ };
28805
+
28760
28806
  // apps/cli/src/probes/scenarios/routing/submittalReview.ts
28761
28807
  var submittalReviewRouting = {
28762
28808
  name: "routing/submittal-review",
@@ -28903,6 +28949,7 @@ var allScenarios = [
28903
28949
  submittalReviewRouting,
28904
28950
  drawingComparisonRouting,
28905
28951
  contractReviewRouting,
28952
+ projectScanRouting,
28906
28953
  userTableSkillRouting,
28907
28954
  userDocumentSkillRouting,
28908
28955
  contractReviewFullLifecycle,
@@ -30125,6 +30172,12 @@ ${authorizeUrl}
30125
30172
  try {
30126
30173
  await open(authorizeUrl.toString());
30127
30174
  } catch {
30175
+ process.stderr.write(
30176
+ `Could not open a browser automatically. Open the URL above manually.
30177
+ If your browser is on a different machine (SSH, container), press Ctrl-C
30178
+ and run \`specset login --device\` instead.
30179
+ `
30180
+ );
30128
30181
  }
30129
30182
  let callback;
30130
30183
  try {
@@ -30158,6 +30211,101 @@ ${authorizeUrl}
30158
30211
  return { userId: user.id, email: user.email, host, accessToken };
30159
30212
  }
30160
30213
 
30214
+ // apps/cli/src/oauth/deviceFlow.ts
30215
+ var startDeviceAuthorizationDocument = graphql(`
30216
+ mutation Cli_startOAuthDeviceAuthorization($clientId: String!) {
30217
+ startOAuthDeviceAuthorization(clientId: $clientId) {
30218
+ deviceCode
30219
+ userCode
30220
+ expiresIn
30221
+ interval
30222
+ }
30223
+ }
30224
+ `);
30225
+ var exchangeDeviceCodeDocument = graphql(`
30226
+ mutation Cli_exchangeOAuthDeviceCode(
30227
+ $clientId: String!
30228
+ $deviceCode: String!
30229
+ ) {
30230
+ exchangeOAuthDeviceCode(clientId: $clientId, deviceCode: $deviceCode) {
30231
+ accessToken
30232
+ user {
30233
+ id
30234
+ email
30235
+ }
30236
+ }
30237
+ }
30238
+ `);
30239
+ function sleep3(ms) {
30240
+ return new Promise((resolve) => setTimeout(resolve, ms));
30241
+ }
30242
+ function deviceErrorCode(err) {
30243
+ if (!(err instanceof ClientError)) {
30244
+ return null;
30245
+ }
30246
+ const errors = err.response.errors ?? [];
30247
+ for (const e3 of errors) {
30248
+ const code = e3.extensions?.code;
30249
+ if (typeof code === "string") {
30250
+ return code;
30251
+ }
30252
+ }
30253
+ return null;
30254
+ }
30255
+ async function runDeviceLoginFlow(host) {
30256
+ const client = createGraphQLClient({ host });
30257
+ const start = await client.request(startDeviceAuthorizationDocument, {
30258
+ clientId: OAUTH_CLIENT_ID
30259
+ });
30260
+ const { deviceCode, userCode, expiresIn, interval } = start.startOAuthDeviceAuthorization;
30261
+ const verificationUrl = `${resolveBaseUrl(host)}/device?code=${encodeURIComponent(userCode)}`;
30262
+ process.stderr.write(
30263
+ `To sign in, open this link in a browser on any device:
30264
+
30265
+ ${verificationUrl}
30266
+
30267
+ and approve the code ${userCode}.
30268
+
30269
+ Waiting for approval (code expires in ${Math.round(expiresIn / 60)} minutes)...
30270
+ `
30271
+ );
30272
+ let intervalMs = interval * 1e3;
30273
+ const deadline = Date.now() + expiresIn * 1e3;
30274
+ while (Date.now() < deadline) {
30275
+ await sleep3(intervalMs);
30276
+ try {
30277
+ const result = await client.request(exchangeDeviceCodeDocument, {
30278
+ clientId: OAUTH_CLIENT_ID,
30279
+ deviceCode
30280
+ });
30281
+ const { accessToken, user } = result.exchangeOAuthDeviceCode;
30282
+ const current = await loadConfig();
30283
+ const next = setHostConfig(current, host, { token: accessToken });
30284
+ await saveConfig({ ...next, currentHost: host });
30285
+ return { userId: user.id, email: user.email, host, accessToken };
30286
+ } catch (err) {
30287
+ const code = deviceErrorCode(err);
30288
+ if (code === "authorization_pending") {
30289
+ continue;
30290
+ }
30291
+ if (code === "slow_down") {
30292
+ intervalMs += 5e3;
30293
+ continue;
30294
+ }
30295
+ if (code === "expired_token") {
30296
+ break;
30297
+ }
30298
+ if (code === "access_denied") {
30299
+ throw new Error("Authorization was denied.");
30300
+ }
30301
+ throw err;
30302
+ }
30303
+ }
30304
+ throw new Error(
30305
+ `The code expired before it was approved. Run \`specset login --device\` again to get a new one.`
30306
+ );
30307
+ }
30308
+
30161
30309
  // apps/cli/src/commands/login.ts
30162
30310
  async function loginCommand(flags) {
30163
30311
  const host = resolveActiveHost(this.config, flags.host);
@@ -30167,7 +30315,7 @@ async function loginCommand(flags) {
30167
30315
  `Already logged in to ${host}. Run \`${logoutCmd}\` first.`
30168
30316
  );
30169
30317
  }
30170
- const result = await runLoginFlow(host);
30318
+ const result = flags.device ? await runDeviceLoginFlow(host) : await runLoginFlow(host);
30171
30319
  printResult(
30172
30320
  this.process,
30173
30321
  flags.json ?? false,
@@ -30186,6 +30334,11 @@ var login = buildCommand({
30186
30334
  brief: "Host to authenticate against (e.g. app.specset.dev, app2.specset.dev, http://localhost:3000). Added to ~/.config/specset/config.yml.",
30187
30335
  optional: true
30188
30336
  },
30337
+ device: {
30338
+ kind: "boolean",
30339
+ brief: "Sign in without a local browser: prints a code you approve from any device (SSH, containers, CI)",
30340
+ optional: true
30341
+ },
30189
30342
  json: {
30190
30343
  kind: "boolean",
30191
30344
  brief: "Emit machine-readable JSON instead of human output",
@@ -30430,9 +30583,12 @@ import path5 from "node:path";
30430
30583
  // apps/cli/src/skillAsset.ts
30431
30584
  import { existsSync as existsSync2, readdirSync, readFileSync } from "node:fs";
30432
30585
  var CORE_SKILL_NAME = "specset";
30586
+ var SKILL_VERSION_FILENAME = ".specset-version";
30433
30587
  var skillsRoot = new URL("../skills/", import.meta.url);
30434
30588
  function listPackagedSkillNames() {
30435
- return readdirSync(skillsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).filter((entry) => existsSync2(new URL(`${entry.name}/SKILL.md`, skillsRoot))).map((entry) => entry.name).sort((a3, b) => {
30589
+ return readdirSync(skillsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).filter(
30590
+ (entry) => existsSync2(new URL(`${entry.name}/SKILL.md`, skillsRoot))
30591
+ ).map((entry) => entry.name).sort((a3, b) => {
30436
30592
  if (a3 === CORE_SKILL_NAME) {
30437
30593
  return -1;
30438
30594
  }
@@ -30445,11 +30601,6 @@ function listPackagedSkillNames() {
30445
30601
  function readPackagedSkill(name2) {
30446
30602
  return readFileSync(new URL(`${name2}/SKILL.md`, skillsRoot), "utf8");
30447
30603
  }
30448
- function stampSkillVersion(content, version) {
30449
- return content.replace(/^---\n/, `---
30450
- version: ${version}
30451
- `);
30452
- }
30453
30604
  function extractSkillVersion(content) {
30454
30605
  return /^version: (.+)$/m.exec(content)?.[1]?.trim() ?? null;
30455
30606
  }
@@ -30468,27 +30619,51 @@ function cliVersion() {
30468
30619
  }
30469
30620
 
30470
30621
  // apps/cli/src/commands/skill/install.ts
30471
- async function installOne(skillsDir, name2, version) {
30472
- const destFile = path5.join(skillsDir, name2, "SKILL.md");
30473
- const desired = stampSkillVersion(readPackagedSkill(name2), version);
30474
- let existing = null;
30622
+ var SKILL_TARGET_PATHS = {
30623
+ claude: [".claude", "skills"],
30624
+ codex: [".agents", "skills"],
30625
+ chatgpt: [".agents", "skills"]
30626
+ };
30627
+ function isSkillInstallTarget(value2) {
30628
+ return Object.hasOwn(SKILL_TARGET_PATHS, value2);
30629
+ }
30630
+ function defaultSkillsDir(target, homeDir = os2.homedir()) {
30631
+ return path5.join(homeDir, ...SKILL_TARGET_PATHS[target]);
30632
+ }
30633
+ async function readOptionalText(file) {
30475
30634
  try {
30476
- existing = await fs2.readFile(destFile, "utf8");
30635
+ return await fs2.readFile(file, "utf8");
30477
30636
  } catch (err) {
30478
- if (err.code !== "ENOENT") {
30479
- throw err;
30637
+ if (err.code === "ENOENT") {
30638
+ return null;
30480
30639
  }
30640
+ throw err;
30481
30641
  }
30482
- if (existing === desired) {
30642
+ }
30643
+ async function installOne(skillsDir, name2, version) {
30644
+ const destDir = path5.join(skillsDir, name2);
30645
+ const destFile = path5.join(destDir, "SKILL.md");
30646
+ const versionFile = path5.join(destDir, SKILL_VERSION_FILENAME);
30647
+ const desired = readPackagedSkill(name2);
30648
+ const [existing, recordedVersion] = await Promise.all([
30649
+ readOptionalText(destFile),
30650
+ readOptionalText(versionFile)
30651
+ ]);
30652
+ const previousVersion = recordedVersion?.trim() || (existing ? extractSkillVersion(existing) : null);
30653
+ if (existing === desired && previousVersion === version) {
30483
30654
  return { name: name2, path: destFile, action: "current", previousVersion: null };
30484
30655
  }
30485
- await fs2.mkdir(path5.dirname(destFile), { recursive: true });
30486
- await fs2.writeFile(destFile, desired);
30656
+ await fs2.mkdir(destDir, { recursive: true });
30657
+ await Promise.all([
30658
+ fs2.writeFile(destFile, desired),
30659
+ fs2.writeFile(versionFile, `${version}
30660
+ `)
30661
+ ]);
30487
30662
  return {
30488
30663
  name: name2,
30489
30664
  path: destFile,
30490
30665
  action: existing ? "updated" : "installed",
30491
- previousVersion: existing ? extractSkillVersion(existing) : null
30666
+ previousVersion: existing ? previousVersion : null
30492
30667
  };
30493
30668
  }
30494
30669
  async function findOrphanedSkills(skillsDir, packagedNames) {
@@ -30508,6 +30683,13 @@ async function findOrphanedSkills(skillsDir, packagedNames) {
30508
30683
  continue;
30509
30684
  }
30510
30685
  try {
30686
+ const recordedVersion = await readOptionalText(
30687
+ path5.join(skillsDir, entry.name, SKILL_VERSION_FILENAME)
30688
+ );
30689
+ if (recordedVersion?.trim()) {
30690
+ orphans.push(path5.join(skillsDir, entry.name));
30691
+ continue;
30692
+ }
30511
30693
  const content = await fs2.readFile(
30512
30694
  path5.join(skillsDir, entry.name, "SKILL.md"),
30513
30695
  "utf8"
@@ -30521,7 +30703,14 @@ async function findOrphanedSkills(skillsDir, packagedNames) {
30521
30703
  return orphans;
30522
30704
  }
30523
30705
  async function skillInstallCommand(flags) {
30524
- const skillsDir = flags.dir?.trim() || path5.join(os2.homedir(), ".claude", "skills");
30706
+ const targetName = flags.target?.trim() || "claude";
30707
+ if (!isSkillInstallTarget(targetName)) {
30708
+ return new Error(
30709
+ `Unknown target "${targetName}". Available: ${Object.keys(SKILL_TARGET_PATHS).join(", ")}`
30710
+ );
30711
+ }
30712
+ const target = targetName;
30713
+ const skillsDir = flags.dir?.trim() || defaultSkillsDir(target);
30525
30714
  const version = cliVersion();
30526
30715
  const packagedNames = listPackagedSkillNames();
30527
30716
  let names = packagedNames;
@@ -30561,7 +30750,7 @@ async function skillInstallCommand(flags) {
30561
30750
  printResult(
30562
30751
  this.process,
30563
30752
  flags.json ?? false,
30564
- { dir: skillsDir, version, skills: results, orphans },
30753
+ { target, dir: skillsDir, version, skills: results, orphans },
30565
30754
  lines.join("\n")
30566
30755
  );
30567
30756
  }
@@ -30570,10 +30759,16 @@ var skillInstall = buildCommand({
30570
30759
  parameters: {
30571
30760
  positional: { kind: "tuple", parameters: [] },
30572
30761
  flags: {
30762
+ target: {
30763
+ kind: "parsed",
30764
+ parse: String,
30765
+ brief: "Agent target: claude (~/.claude/skills), codex or chatgpt (~/.agents/skills); defaults to claude",
30766
+ optional: true
30767
+ },
30573
30768
  dir: {
30574
30769
  kind: "parsed",
30575
30770
  parse: String,
30576
- brief: "Skills directory to install into (defaults to ~/.claude/skills)",
30771
+ brief: "Override the target skills directory",
30577
30772
  optional: true
30578
30773
  },
30579
30774
  only: {
@@ -30591,7 +30786,7 @@ var skillInstall = buildCommand({
30591
30786
  }
30592
30787
  },
30593
30788
  docs: {
30594
- brief: "Install the Specset agent skills into a skills directory (re-run after upgrading to refresh)"
30789
+ brief: "Install the Specset agent skills for Claude, Codex, or ChatGPT (re-run after upgrading to refresh)"
30595
30790
  }
30596
30791
  });
30597
30792
 
@@ -30631,9 +30826,7 @@ async function skillPrintCommand(_flags, name2) {
30631
30826
  `Unknown skill "${skillName}". Available: ${available.join(", ")}`
30632
30827
  );
30633
30828
  }
30634
- this.process.stdout.write(
30635
- stampSkillVersion(readPackagedSkill(skillName), cliVersion())
30636
- );
30829
+ this.process.stdout.write(readPackagedSkill(skillName));
30637
30830
  }
30638
30831
  var skillPrint = buildCommand({
30639
30832
  func: skillPrintCommand,
@@ -30652,7 +30845,7 @@ var skillPrint = buildCommand({
30652
30845
  flags: {}
30653
30846
  },
30654
30847
  docs: {
30655
- brief: "Print a bundled Specset agent skill to stdout (for non-Claude agents / custom setups)"
30848
+ brief: "Print a bundled Specset agent skill to stdout for custom setups"
30656
30849
  }
30657
30850
  });
30658
30851
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@specset/cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Command-line interface for Specset — authenticate, run GraphQL queries, and chat with Specset project agents.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://specset.com",
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: specset
3
3
  description: Core CLI for Specset — install, authenticate, switch organizations, and run GraphQL with `specset api`. Start here for any Specset task, and whenever a specset command fails with auth or org errors.
4
- allowed-tools: Bash, Read, AskUserQuestion
4
+ allowed-tools: Bash Read AskUserQuestion
5
5
  ---
6
6
 
7
7
  # Specset CLI (`specset`)
@@ -24,7 +24,7 @@ This is the core skill — setup and mechanics only. Domain workflows live in si
24
24
  | `specset-agent` | Delegating deep project questions to Specset's in-app AI agent |
25
25
  | `specset-admin` | Org members, invites, and whitelabel branding |
26
26
 
27
- `specset skill install` installs and updates the whole family (`specset skill list` shows what's bundled) — if a skill named above is missing from your skills directory, run it.
27
+ `specset skill install --target <claude|codex|chatgpt>` installs and updates the whole family (`specset skill list` shows what's bundled) — if a skill named above is missing from your skills directory, run it for the current agent.
28
28
 
29
29
  ## First-Run Setup
30
30
 
@@ -44,7 +44,15 @@ Requires Node.js 20+ with npm. If `npm` itself is missing, stop and ask the user
44
44
  specset login
45
45
  ```
46
46
 
47
- Login is interactive by design — never try to bypass it or handle credentials directly. On a headless machine (no browser), login isn't possible; direct the user to a machine with a browser. Don't launch a login mid-task without telling the user what's happening.
47
+ Login is interactive by design — never try to bypass it or handle credentials directly. Don't launch a login mid-task without telling the user what's happening.
48
+
49
+ **On a headless machine** (container, SSH, CI — anywhere the browser isn't on this machine), use the device flow instead:
50
+
51
+ ```bash
52
+ specset login --device
53
+ ```
54
+
55
+ It prints a short code and a link (`https://<host>/device?code=XXXX-XXXX`). Show both to the user and ask them to open the link on any device and approve; the command completes on its own once they do. The code expires after 15 minutes — if it does, run the command again for a fresh one.
48
56
 
49
57
  3. Pick the active organization (required for org-scoped queries):
50
58
 
@@ -167,5 +175,5 @@ Processing large PDF sets takes minutes; poll every 15–30 seconds and tell the
167
175
  - `Not logged in` — run `specset login` (opens the user's browser; tell them to complete sign-in there).
168
176
  - `No active organization selected` — run `specset org list`, then `specset org use <slug>`.
169
177
  - A lookup by id returns `null` without an error — the record usually belongs to a different org than the active one; check `specset auth status` and switch with `specset org use <slug>`.
170
- - Login requires a browser; on a headless machine, run the CLI from a machine with a browser first, or contact Specset support about headless options.
171
- - After upgrading the CLI (`npm i -g @specset/cli@latest`), refresh the skills with `specset skill install`.
178
+ - Login hangs or the browser can't reach this machine (SSH, container, CI) — use `specset login --device`: it prints a code + link the user approves from any device.
179
+ - After upgrading the CLI (`npm i -g @specset/cli@latest`), refresh the skills with `specset skill install --target <claude|codex|chatgpt>`.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: specset-admin
3
3
  description: Use when administering a Specset organization — inviting and managing members and roles, or applying whitelabel branding (colors, logos) — for org membership, permissions, or appearance tasks.
4
- allowed-tools: Bash, Read, AskUserQuestion
4
+ allowed-tools: Bash Read AskUserQuestion
5
5
  ---
6
6
 
7
7
  # Org Administration
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: specset-agent
3
3
  description: Ask Specset's in-project AI agent questions that need deep project understanding — schedules, spec interpretation, cross-document analysis — via `specset agent chat`. Use when raw GraphQL isn't enough or when unsure how to answer from data.
4
- allowed-tools: Bash, Read, AskUserQuestion
4
+ allowed-tools: Bash Read AskUserQuestion
5
5
  ---
6
6
 
7
7
  # Specset Agent Chat
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: specset-closeout
3
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.
4
- allowed-tools: Bash, Read, AskUserQuestion
4
+ allowed-tools: Bash Read AskUserQuestion
5
5
  ---
6
6
 
7
7
  # Closeout & Facilities Data
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: specset-projects
3
3
  description: Use when standing up a new Specset project or adding document sets to one — create the project, upload spec/drawing PDFs, monitor processing, and publish spec sets and drawing sets.
4
- allowed-tools: Bash, Read, AskUserQuestion
4
+ allowed-tools: Bash Read AskUserQuestion
5
5
  ---
6
6
 
7
7
  # Specset Projects
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: specset-rfis
3
3
  description: Manage Specset RFIs — list, search, create, update, and record responses including the official answer of record. Use for any RFI logging or tracking task.
4
- allowed-tools: Bash, Read, AskUserQuestion
4
+ allowed-tools: Bash Read AskUserQuestion
5
5
  ---
6
6
 
7
7
  # Specset RFIs
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: specset-search
3
3
  description: Use when the user asks to find, look up, or locate anything in their Specset projects — semantic and keyword search across specs, drawing sheets, submittals, RFIs, documents, and closeout records.
4
- allowed-tools: Bash, Read, AskUserQuestion
4
+ allowed-tools: Bash Read AskUserQuestion
5
5
  ---
6
6
 
7
7
  # Specset Search
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: specset-submittals
3
3
  description: Manage Specset submittals — list, search, create, revise, attach files, track approvers, and run AI spec-compliance reviews. Use for any submittal log or submittal review task.
4
- allowed-tools: Bash, Read, AskUserQuestion
4
+ allowed-tools: Bash Read AskUserQuestion
5
5
  ---
6
6
 
7
7
  # Specset Submittals