@etiquekit/etq 1.0.13 → 1.0.14

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
@@ -1,78 +1,80 @@
1
1
  # Etiquette
2
2
 
3
- Etiquette is a governance and evidence CLI for agent-assisted engineering work.
3
+ Etiquette is a local-first workflow and evidence CLI for teams that use coding
4
+ agents. Each task gets a named seat, bounded write scope, validation contract,
5
+ and receipt. Agents execute and report evidence; your human or Git gate still
6
+ decides what becomes canonical.
4
7
 
5
- It answers the questions that matter when agents help write code: who acted,
6
- under which seat, inside what scope, with what evidence, and who accepted the
7
- result.
8
+ ## Install
8
9
 
9
- Default loop: `setup -> handoff -> bootstrap local -> whereami -> doctor -> join -> pickup -> receipt -> promotion gate`.
10
+ Prerequisites: Git `>=2.40` and Bun `>=1.3`.
10
11
 
11
- Execution runtimes are optional; they return evidence and do not mint authority.
12
+ ```sh
13
+ npm install --global @etiquekit/etq
14
+ etq --version
15
+ ```
16
+
17
+ `etq` and `etiquette` are aliases for the same CLI.
12
18
 
13
- ## Start
19
+ ## Start A Governed Task
14
20
 
15
- Prereqs: Git and Bun `>=1.3.0`; the CLI shims require `bun` on `PATH`.
16
- For a clean repo or a new seat, read [QuickStart.md](QuickStart.md). For private team rollout, use [docs/TEAM_HANDOFF.md](docs/TEAM_HANDOFF.md), then:
21
+ Run inside a Git repository:
17
22
 
18
23
  ```sh
19
- ./bin/etiquette setup --target codex --project /tmp/etiquette-start
20
- ./bin/etiquette handoff --project /tmp/etiquette-start --task-prefix APP
21
- ./bin/etiquette bootstrap local --project /tmp/etiquette-start --task-prefix APP
22
- ./bin/etiquette whereami --project /tmp/etiquette-start --seat impl-seat --task APP-001
23
- ./bin/etiquette doctor --project /tmp/etiquette-start
24
- ./bin/etiquette status --project /tmp/etiquette-start
25
- ./bin/etiquette join --project /tmp/etiquette-start --seat impl-seat
26
- ./bin/etiquette pickup --project /tmp/etiquette-start --seat impl-seat --task APP-001 --allow-current-project --text
27
- ./bin/etiquette console board --project /tmp/etiquette-start --text
24
+ etq bootstrap local --project . --task-prefix APP --ask
25
+ etq doctor --project .
26
+ etq whereami --project . --seat impl-seat --task APP-001
27
+ etq join --project . --seat impl-seat
28
+ etq pickup --project . --seat impl-seat --task APP-001 --allow-current-project --text
28
29
  ```
29
30
 
30
- `handoff` is a read-only guide: it prints install, bootstrap, and pickup commands without installing, bootstrapping, pushing, closing, or granting.
31
+ The default is one implementation seat and one bounded task. Etiquette asks
32
+ before adding review seats and remains non-interactive in CI. A seat is a
33
+ durable role; Claude Code, Codex, Gemini, or another runtime is its temporary
34
+ occupant.
31
35
 
32
- `bootstrap local` creates repo-local seats, task envelopes, bounded runbooks,
33
- local packet events, and pickup docs without global bus writes or authority.
34
- `whereami` is the context readback: runner resume restores conversation
35
- history; `whereami` tells the seat which project, worktree, task, and runbook it
36
- is actually operating in.
36
+ ## Daily Loop
37
37
 
38
- Use repo-local commands for now. The hosted public installer is parked.
38
+ ```text
39
+ orient -> pick up bounded work -> implement -> validate -> return a receipt -> promote through the existing gate
40
+ ```
39
41
 
40
- ## Core Concepts
42
+ Use `whereami`, `status`, `console board`, and `return check` to inspect work.
43
+ Read commands do not initialize directories or rewrite event streams. Explicit
44
+ setup, bootstrap, post, and lifecycle commands own their writes.
41
45
 
42
- - Seat: durable work identity; the occupant or model can change.
43
- - Task envelope: scope, allowed writes, validation, and stop conditions.
44
- - Receipt: evidence of what happened; it proves but does not authorize.
45
- - Promotion gate: the human/Git gate where candidate work becomes truth.
46
- - Ledger: durable local record of sessions, receipts, and decisions.
46
+ ## Core Records
47
47
 
48
- Read next: [docs/README.md](docs/README.md), [docs/CONCEPTS.md](docs/CONCEPTS.md), [docs/CORE_PROFILE.md](docs/CORE_PROFILE.md), [docs/SEAT_PROVISIONING.md](docs/SEAT_PROVISIONING.md), and [docs/WORKTREE_QOL.md](docs/WORKTREE_QOL.md).
48
+ - **Seat:** durable work identity, independent of model or vendor.
49
+ - **Task envelope:** allowed writes, required validation, and stop conditions.
50
+ - **Receipt:** evidence of what ran and changed; evidence is not authority.
51
+ - **Local ledger:** Git-readable events and packet references.
52
+ - **Promotion gate:** the existing decision that accepts candidate work.
49
53
 
50
- Provision runtime seats only after assignment:
54
+ Etiquette is not a hosted executor, transport service, portal, or replacement
55
+ for source-control permissions. Consumers may bind packet profiles and their
56
+ own transport; the package does not silently write a global ledger.
51
57
 
52
- ```sh
53
- sh templates/seat-packs-v0/bin/provision-seat codex /tmp/dev-codex-seat
54
- sh templates/seat-packs-v0/bin/provision-seat claude-code /tmp/dev-claude-code-seat
55
- sh templates/seat-packs-v0/bin/provision-seat gemini /tmp/dev-gemini-seat
56
- sh templates/seat-packs-v0/bin/provision-seat ollama /tmp/dev-ollama-seat
57
- sh templates/seat-packs-v0/bin/provision-seat openrouter /tmp/dev-openrouter-seat
58
- ```
59
-
60
- ## Validate
58
+ ## Author A Draft Without Project State
61
59
 
62
60
  ```sh
63
- npm run release:scan
64
- bun test
65
- npm run typecheck
61
+ etq post --profile ./packet-profile.yaml --draft \
62
+ --out /tmp/review-return.md --kind review-return --work-id APP-001 \
63
+ --author impl-seat --recipient review-seat \
64
+ --summary "Implementation complete." --evidence "Checks passed." \
65
+ --next-step "Review the candidate."
66
66
  ```
67
67
 
68
- ## Release Status
68
+ Draft mode writes only the requested output. It does not create `.etiquette`,
69
+ emit an event, grant authority, merge, close, or publish.
69
70
 
70
- ```yaml
71
- default_surface: templates/etiquette-vanilla-v0
72
- optional_runtime_surface: templates/seat-packs-v0
73
- public_installer: held
74
- ```
71
+ Optional runtime packs: `provision-seat claude-code`, `provision-seat codex`, `provision-seat gemini`, `provision-seat ollama`, `provision-seat openrouter`.
72
+ ## Documentation
73
+
74
+ [Quick start](QuickStart.md) | [Concepts](docs/CONCEPTS.md) |
75
+ [Core profile](docs/CORE_PROFILE.md) | [Seats](docs/SEAT_PROVISIONING.md) |
76
+ [Worktrees](docs/WORKTREE_QOL.md)
75
77
 
76
78
  ## License
77
79
 
78
- Apache-2.0 see [LICENSE](./LICENSE) and [NOTICE](./NOTICE).
80
+ Apache-2.0. See [LICENSE](LICENSE) and [NOTICE](NOTICE).
@@ -94,7 +94,7 @@ package_surface:
94
94
  npm_pack_entries: 183
95
95
  max_budget: 190
96
96
  posture: local devkit package plus signed user-local installer
97
- distribution_origin: https://etiquette.example.com
97
+ distribution_origin: https://etiquekit.com
98
98
  excludes: workspace package metadata, workspace fixtures, internal, conformance, apps, site, dist, hosted parity docs, portal docs, root kernel manuals
99
99
  ```
100
100
 
@@ -142,7 +142,7 @@ npm run release:scan
142
142
 
143
143
  ## Package Readiness Check
144
144
 
145
- Use `etiquette release readiness --json` before changing distribution settings. It also binds the canonical package identity, version floor, source-shipping mode, required bins, source ancestry, and candidate verifier to `release/lineage.v0.json`; `release:candidate` preserves the exact verified tarball and checksum under ignored `dist/` for later signing without rebuilding.
145
+ Use `etiquette release readiness --json` before changing distribution settings. It also binds the canonical package identity, version floor, source-shipping mode, required bins, source ancestry, public distribution repository, and candidate verifier to `release/lineage.v0.json`; the source-of-record and public package home remain separate, explicit lineage fields. `release:candidate` preserves the exact verified tarball and checksum under ignored `dist/` for later signing without rebuilding.
146
146
  Codex-backed remote-runtime, plugin-MCP, and hook/action-auth canaries require CLI `0.141.0`+ (`docs/CODEX_CLIENT_COMPATIBILITY.md`). GA additionally runs `etiquette release ga-readiness --channel shell-installer,npm --tag-free-confirmed --json`; it requires a non-prerelease version, final tarball checksums, trusted RSA signature, private-compatible channels, and explicit remote-tag uniqueness confirmation, while still unable to publish, sign, tag, or create a channel. Git source archives must exclude maintainer-only `internal/`, `conformance/`, `site/`, `fixtures/`, `apps/`, local state, and `dist/`.
147
147
 
148
148
  ## Local-Thin Purge (v0.2.1)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@etiquekit/etq",
3
- "version": "1.0.13",
3
+ "version": "1.0.14",
4
4
  "description": "Governance and evidence CLI for AI-assisted engineering seats.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -51,9 +51,9 @@
51
51
  "homepage": "https://etiquekit.com",
52
52
  "repository": {
53
53
  "type": "git",
54
- "url": "git+https://github.com/leepickdev/etq-devkit.git"
54
+ "url": "git+https://github.com/leepickdev/etq.git"
55
55
  },
56
56
  "bugs": {
57
- "url": "https://github.com/leepickdev/etq-devkit/issues"
57
+ "url": "https://github.com/leepickdev/etq/issues"
58
58
  }
59
59
  }
@@ -550,6 +550,14 @@ function isInitializedLocalProject(root: string): boolean {
550
550
  return existsSync(projectBindingPath(root)) && existsSync(eventPath(root));
551
551
  }
552
552
 
553
+ async function requireInitializedProjectBinding(root: string, command: string): Promise<NonNullable<Awaited<ReturnType<typeof readProjectBinding>>>> {
554
+ const project = await readProjectBinding(root);
555
+ if (!project || !isInitializedLocalProject(root)) {
556
+ throw new Error(`${command} requires an initialized local Etiquette project; run etiquette init --project <path> first`);
557
+ }
558
+ return project;
559
+ }
560
+
553
561
  function relativeRef(root: string, path: string): string {
554
562
  const rel = relative(root, resolve(path));
555
563
  if (rel.length === 0 || rel.startsWith('..') || rel.includes('\\')) {
@@ -1368,8 +1376,8 @@ async function ensureLocalProject(root: string): Promise<void> {
1368
1376
  }
1369
1377
 
1370
1378
  async function readEvents(root: string): Promise<LocalEvent[]> {
1371
- await ensureLocalProject(root);
1372
- const raw = await readFile(eventPath(root), 'utf-8');
1379
+ const raw = await readTextIfExists(eventPath(root));
1380
+ if (!raw) return [];
1373
1381
  return raw
1374
1382
  .split('\n')
1375
1383
  .map((line) => line.trim())
@@ -1844,17 +1852,14 @@ async function validateSeatCard(path: string): Promise<string> {
1844
1852
  }
1845
1853
 
1846
1854
  async function enrollmentPath(root: string, seat: string): Promise<string> {
1847
- await ensureLocalProject(root);
1848
1855
  return join(root, '.etiquette', 'roster', 'enrollments', `${safeName(seat)}.json`);
1849
1856
  }
1850
1857
 
1851
1858
  async function subscriptionPath(root: string, seat: string): Promise<string> {
1852
- await ensureLocalProject(root);
1853
1859
  return join(root, '.etiquette', 'notify', 'subscriptions', `${safeName(seat)}.json`);
1854
1860
  }
1855
1861
 
1856
1862
  async function wakeQueuePath(root: string, seat: string): Promise<string> {
1857
- await ensureLocalProject(root);
1858
1863
  return join(root, '.etiquette', 'notify', 'queues', `${safeName(seat)}.json`);
1859
1864
  }
1860
1865
 
@@ -2492,7 +2497,6 @@ async function retirePath(
2492
2497
  async function dispatchTrash(positionals: string[], flags: Record<string, string | boolean>): Promise<void> {
2493
2498
  const verb = positionals[1];
2494
2499
  const root = projectRoot(flags);
2495
- await ensureLocalProject(root);
2496
2500
 
2497
2501
  if (verb === 'retire') {
2498
2502
  await retirePath(requirePositional(positionals, 2, 'path'), flags);
@@ -2521,6 +2525,7 @@ async function dispatchTrash(positionals: string[], flags: Record<string, string
2521
2525
  }
2522
2526
 
2523
2527
  if (verb === 'restore') {
2528
+ await ensureLocalProject(root);
2524
2529
  const trashId = requirePositional(positionals, 2, 'trash-id');
2525
2530
  const entries = await readTrashEntries(root);
2526
2531
  const entry = entries.find((candidate) => candidate.trash_id === trashId);
@@ -2550,6 +2555,7 @@ async function dispatchTrash(positionals: string[], flags: Record<string, string
2550
2555
  }
2551
2556
 
2552
2557
  if (verb === 'rebuild') {
2558
+ await ensureLocalProject(root);
2553
2559
  const events = await readEvents(root);
2554
2560
  const entries = events
2555
2561
  .map((event) => asRecord(event, 'local event').trash_entry)
@@ -3041,8 +3047,7 @@ async function dispatchHandoff(flags: Record<string, string | boolean>): Promise
3041
3047
 
3042
3048
  async function dispatchJoin(flags: Record<string, string | boolean>): Promise<void> {
3043
3049
  const root = projectRoot(flags);
3044
- await ensureLocalProject(root);
3045
- const project = await ensureProjectBinding(root, optionalString(flags, 'at'));
3050
+ const project = await requireInitializedProjectBinding(root, 'join');
3046
3051
  assertEngagementProject(project.project_id, optionalString(flags, 'expect-project-id'), 'current project');
3047
3052
  const seatInput = requireString(flags, 'seat');
3048
3053
  const resolvedSeat = resolveSeatFromEnrollments(await listEnrollments(root), seatInput);
@@ -3519,8 +3524,7 @@ async function dispatchWhereami(flags: Record<string, string | boolean>): Promis
3519
3524
 
3520
3525
  async function dispatchPickup(flags: Record<string, string | boolean>): Promise<void> {
3521
3526
  const root = projectRoot(flags);
3522
- await ensureLocalProject(root);
3523
- const project = await ensureProjectBinding(root, optionalString(flags, 'at'));
3527
+ const project = await requireInitializedProjectBinding(root, 'pickup');
3524
3528
  assertPickupProjectExpectation(project.project_id, flags);
3525
3529
  const seatInput = requireString(flags, 'seat');
3526
3530
  const task = requireString(flags, 'task');
@@ -3624,7 +3628,6 @@ async function dispatchAudit(positionals: string[], flags: Record<string, string
3624
3628
  async function dispatchRoster(positionals: string[], flags: Record<string, string | boolean>): Promise<void> {
3625
3629
  const verb = positionals[1];
3626
3630
  const root = projectRoot(flags);
3627
- await ensureLocalProject(root);
3628
3631
 
3629
3632
  if (verb === 'card') {
3630
3633
  const cardVerb = positionals[2];
@@ -3636,6 +3639,7 @@ async function dispatchRoster(positionals: string[], flags: Record<string, strin
3636
3639
  }
3637
3640
 
3638
3641
  if (verb === 'enroll') {
3642
+ await ensureLocalProject(root);
3639
3643
  const seat = requirePositional(positionals, 2, 'seat');
3640
3644
  const project = await ensureProjectBinding(root, optionalString(flags, 'at'));
3641
3645
  const card = requireString(flags, 'card');
@@ -3687,6 +3691,7 @@ async function dispatchRoster(positionals: string[], flags: Record<string, strin
3687
3691
  }
3688
3692
 
3689
3693
  if (verb === 'ready') {
3694
+ await ensureLocalProject(root);
3690
3695
  const seat = requirePositional(positionals, 2, 'seat');
3691
3696
  const project = await ensureProjectBinding(root, optionalString(flags, 'at'));
3692
3697
  const path = await enrollmentPath(root, seat);
@@ -3732,6 +3737,7 @@ async function dispatchRoster(positionals: string[], flags: Record<string, strin
3732
3737
  }
3733
3738
 
3734
3739
  if (verb === 'lifecycle') {
3740
+ await ensureLocalProject(root);
3735
3741
  const seat = requirePositional(positionals, 2, 'seat');
3736
3742
  const rawState = requireString(flags, 'state');
3737
3743
  assertLifecycleState(rawState, '--state');
@@ -3963,9 +3969,8 @@ async function dispatchNotify(positionals: string[], flags: Record<string, strin
3963
3969
  return;
3964
3970
  }
3965
3971
 
3966
- await ensureLocalProject(root);
3967
-
3968
3972
  if (verb === 'subscribe') {
3973
+ await ensureLocalProject(root);
3969
3974
  const seatInput = requireString(flags, 'seat');
3970
3975
  const resolvedSeat = resolveSeatFromEnrollments(await listEnrollments(root), seatInput);
3971
3976
  const seat = resolvedSeat.seat;
@@ -4004,6 +4009,7 @@ async function dispatchNotify(positionals: string[], flags: Record<string, strin
4004
4009
  }
4005
4010
 
4006
4011
  if (verb === 'poll') {
4012
+ await ensureLocalProject(root);
4007
4013
  const seatInput = requireString(flags, 'seat');
4008
4014
  const resolvedSeat = resolveSeatFromEnrollments(await listEnrollments(root), seatInput);
4009
4015
  const seat = resolvedSeat.seat;
@@ -4121,7 +4127,6 @@ async function dispatchRig(positionals: string[], flags: Record<string, string |
4121
4127
  const verb = positionals[1];
4122
4128
  if (verb !== 'doctor') throw new Error('rig verb must be: doctor');
4123
4129
  const root = projectRoot(flags);
4124
- await ensureLocalProject(root);
4125
4130
  const required = [
4126
4131
  projectBindingPath(root),
4127
4132
  eventPath(root),
@@ -4130,7 +4135,8 @@ async function dispatchRig(positionals: string[], flags: Record<string, string |
4130
4135
  ];
4131
4136
  const missing = required.filter((path) => !existsSync(path));
4132
4137
  if (missing.length > 0) throw new Error(`rig doctor failed; missing: ${missing.map((path) => relativeRef(root, path)).join(', ')}`);
4133
- const project = await ensureProjectBinding(root, optionalString(flags, 'at'));
4138
+ const project = await readProjectBinding(root);
4139
+ if (!project) throw new Error('rig doctor failed; project binding is missing');
4134
4140
  const sessions = await listSessions(root);
4135
4141
  const mismatched = sessions.filter((session) => session.project_id !== project.project_id);
4136
4142
  if (mismatched.length > 0) {
@@ -4236,7 +4242,6 @@ async function dispatchBus(positionals: string[], flags: Record<string, string |
4236
4242
  console.log(format === 'markdown' ? renderLocalLedgerViewMarkdown(view) : renderLocalLedgerViewText(view));
4237
4243
  return;
4238
4244
  }
4239
- await ensureLocalProject(root);
4240
4245
  const events = await readEvents(root);
4241
4246
 
4242
4247
  if (verb === 'status') {
@@ -4263,14 +4268,14 @@ async function dispatchBus(positionals: string[], flags: Record<string, string |
4263
4268
 
4264
4269
  async function dispatchStatus(flags: Record<string, string | boolean>): Promise<void> {
4265
4270
  const root = projectRoot(flags);
4266
- await ensureLocalProject(root);
4267
- const project = await ensureProjectBinding(root, optionalString(flags, 'at'));
4271
+ const project = await readProjectBinding(root);
4268
4272
  const events = await readEvents(root);
4269
4273
  const enrollments = await listJsonFiles(join(root, '.etiquette', 'roster', 'enrollments'));
4270
4274
  console.log([
4271
4275
  'workspace_status:',
4272
4276
  ` project: ${root}`,
4273
- ` project_id: ${project.project_id}`,
4277
+ ` project_id: ${project?.project_id ?? 'none'}`,
4278
+ ` initialized: ${String(isInitializedLocalProject(root))}`,
4274
4279
  ` events: ${events.length}`,
4275
4280
  ` enrolled_seats: ${enrollments.length}`,
4276
4281
  ' authority_boundary: read_only_without_grant',
@@ -4666,7 +4671,6 @@ export async function dispatchLocalWorkflow(
4666
4671
 
4667
4672
  export async function renderLocalBoard(projectRoot: string, seat?: string): Promise<string> {
4668
4673
  const root = resolve(projectRoot);
4669
- await ensureLocalProject(root);
4670
4674
  const resolvedSeat = seat ? resolveSeatFromEnrollments(await listEnrollments(root), seat) : null;
4671
4675
  const boardSeat = resolvedSeat?.seat ?? null;
4672
4676
  const board = await readTextIfExists(join(root, 'docs', 'work', 'BOARD.md'));
@@ -452,6 +452,7 @@ async function appendReleaseLineageChecks(
452
452
  const registryFloor = asString(lineage.registry_floor);
453
453
  const distributionMode = asString(lineage.distribution_mode);
454
454
  const sourceRepository = asString(lineage.source_repository);
455
+ const distributionRepository = asString(lineage.distribution_repository);
455
456
  const sourceBaseCommit = asString(lineage.source_base_commit);
456
457
  const requiredBins = asStringArray(lineage.required_bins);
457
458
  const packageRepository = repositoryUrl(packageJson.repository);
@@ -491,14 +492,24 @@ async function appendReleaseLineageChecks(
491
492
  'restore all reviewed CLI aliases before building the candidate',
492
493
  ));
493
494
 
494
- checks.push(sourceRepository && packageRepository === sourceRepository
495
- ? check('pass', 'release_source_repository', `package repository matches lineage: ${sourceRepository}`, RELEASE_LINEAGE_REF)
495
+ checks.push(sourceRepository
496
+ ? check('pass', 'release_source_repository', `source repository is declared: ${sourceRepository}`, RELEASE_LINEAGE_REF)
496
497
  : check(
497
498
  'block',
498
499
  'release_source_repository',
499
- `package repository does not match lineage: package=${packageRepository ?? 'unknown'} lineage=${sourceRepository ?? 'unknown'}`,
500
+ 'source repository is missing from release lineage',
500
501
  RELEASE_LINEAGE_REF,
501
- 'point package metadata at the reviewed public source repository',
502
+ 'declare the reviewed source-of-record repository',
503
+ ));
504
+
505
+ checks.push(distributionRepository && packageRepository === distributionRepository
506
+ ? check('pass', 'release_distribution_repository', `package repository matches public distribution home: ${distributionRepository}`, RELEASE_LINEAGE_REF)
507
+ : check(
508
+ 'block',
509
+ 'release_distribution_repository',
510
+ `package repository does not match public distribution home: package=${packageRepository ?? 'unknown'} lineage=${distributionRepository ?? 'unknown'}`,
511
+ RELEASE_LINEAGE_REF,
512
+ 'point package metadata at the reviewed public distribution repository',
502
513
  ));
503
514
 
504
515
  const shipsSource = packageInfo.files.includes('packages/control/src/')
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "schema": "etq-release-lineage.v0",
3
3
  "package_name": "@etiquekit/etq",
4
- "candidate_version": "1.0.13",
4
+ "candidate_version": "1.0.14",
5
5
  "distribution_mode": "bun-typescript-source",
6
6
  "source_repository": "git+https://github.com/leepickdev/etq-devkit.git",
7
- "source_base_commit": "7096242dcc47a3edbf87b9b61720b92cb01ba3aa",
8
- "registry_floor": "1.0.12",
7
+ "distribution_repository": "git+https://github.com/leepickdev/etq.git",
8
+ "source_base_commit": "b2384dc782548ce0d712da3bd33e108d977adb44",
9
+ "registry_floor": "1.0.13",
9
10
  "required_bins": [
10
11
  "etq",
11
12
  "etiquette",