wingfoil 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +12 -6
  2. package/dist/cli/index.d.ts +1 -1
  3. package/dist/cli/index.js +1 -1
  4. package/dist/cli/init-command.d.ts.map +1 -1
  5. package/dist/cli/init-command.js +2 -1
  6. package/dist/cli/init-command.js.map +1 -1
  7. package/dist/cli/program.d.ts.map +1 -1
  8. package/dist/cli/program.js +117 -29
  9. package/dist/cli/program.js.map +1 -1
  10. package/dist/cli/registrar.d.ts +15 -6
  11. package/dist/cli/registrar.d.ts.map +1 -1
  12. package/dist/cli/registrar.js +4 -0
  13. package/dist/cli/registrar.js.map +1 -1
  14. package/dist/core/exit-code.d.ts.map +1 -1
  15. package/dist/core/exit-code.js +7 -6
  16. package/dist/core/exit-code.js.map +1 -1
  17. package/dist/core/index.d.ts +5 -4
  18. package/dist/core/index.d.ts.map +1 -1
  19. package/dist/core/index.js +232 -89
  20. package/dist/core/index.js.map +1 -1
  21. package/dist/core/init.d.ts +5 -3
  22. package/dist/core/init.d.ts.map +1 -1
  23. package/dist/core/init.js +5 -3
  24. package/dist/core/init.js.map +1 -1
  25. package/dist/core/registry.d.ts +69 -17
  26. package/dist/core/registry.d.ts.map +1 -1
  27. package/dist/core/registry.js.map +1 -1
  28. package/dist/directives/schema.d.ts +3 -3
  29. package/dist/directives/schema.js +3 -3
  30. package/dist/mcp/index.d.ts +1 -1
  31. package/dist/memory/add.d.ts +62 -5
  32. package/dist/memory/add.d.ts.map +1 -1
  33. package/dist/memory/add.js +130 -7
  34. package/dist/memory/add.js.map +1 -1
  35. package/dist/memory/audit.d.ts +26 -6
  36. package/dist/memory/audit.d.ts.map +1 -1
  37. package/dist/memory/audit.js +27 -11
  38. package/dist/memory/audit.js.map +1 -1
  39. package/dist/memory/commit-message.js +1 -1
  40. package/dist/memory/commit-message.js.map +1 -1
  41. package/dist/memory/history.d.ts +1 -1
  42. package/dist/memory/history.js +1 -1
  43. package/dist/memory/index.d.ts +3 -3
  44. package/dist/memory/index.d.ts.map +1 -1
  45. package/dist/memory/index.js +6 -2
  46. package/dist/memory/index.js.map +1 -1
  47. package/dist/memory/state-machine.js +2 -2
  48. package/dist/storage/templates.d.ts.map +1 -1
  49. package/dist/storage/templates.js +12 -1
  50. package/dist/storage/templates.js.map +1 -1
  51. package/dist/validation/id.d.ts +26 -0
  52. package/dist/validation/id.d.ts.map +1 -1
  53. package/dist/validation/id.js +64 -1
  54. package/dist/validation/id.js.map +1 -1
  55. package/dist/validation/index.d.ts +1 -1
  56. package/dist/validation/index.d.ts.map +1 -1
  57. package/dist/validation/index.js +4 -1
  58. package/dist/validation/index.js.map +1 -1
  59. package/dist/validation/secret-scan.d.ts +7 -5
  60. package/dist/validation/secret-scan.d.ts.map +1 -1
  61. package/dist/validation/secret-scan.js +7 -5
  62. package/dist/validation/secret-scan.js.map +1 -1
  63. package/dist/workflow/schema.d.ts.map +1 -1
  64. package/dist/workflow/schema.js +27 -1
  65. package/dist/workflow/schema.js.map +1 -1
  66. package/package.json +23 -9
@@ -211,13 +211,15 @@ const dnaShowFn = async (params) => {
211
211
  * task-093-dna-mutation-surface-add-remove-update) — the mutating-op template `dna set` established
212
212
  * (P2.1, task-025), now with one structure-aware traversal underneath all four verbs:
213
213
  *
214
- * 1. **`requireGitIdentity` pre-flight** (REQ-SEC-01, task-014) — refuse before any read/write when
214
+ * 1. **Argument validation** — done by the callers before this body runs (`dnaPathPositional`,
215
+ * `dnaMutationRequest`, `dnaSetFn`'s `--value` check): a missing required argument, or a dotted
216
+ * path `splitDnaPath` cannot parse (an empty segment, the BDD's `..language`; an unterminated
217
+ * quote; a `"` no delimiter can account for), is a usage error: `throw new UsageError(...)` → exit
218
+ * **2** (mapped by `exitCodeForThrow`), per spec-005-cli-command-contract §1, whatever the git
219
+ * identity (task-125, `bug-172`). A path that is well-formed but names nothing the schema declares
220
+ * is a different failure and exits **1** — see step 5.
221
+ * 2. **`requireGitIdentity` pre-flight** (REQ-SEC-01, task-014) — refuse before any read/write when
215
222
  * `user.name`/`user.email` are unset, returning its `CoreResult.error` unchanged (exit 1).
216
- * 2. **Argument validation** — a missing required argument, or a dotted path `splitDnaPath` cannot
217
- * parse (an empty segment, the BDD's `..language`; an unterminated quote; a `"` no delimiter can
218
- * account for), is a usage error: `throw new UsageError(...)` → exit **2**
219
- * (mapped by `exitCodeForThrow`), per spec-005-cli-command-contract §1. A path that is well-formed
220
- * but names nothing the schema declares is a different failure and exits **1** — see step 4.
221
223
  * 3. **`requireUnmodifiedTarget` pre-flight** (`dl-080`(B) / `bug-078`, task-092) — refuse, before
222
224
  * anything is read or written, while `dna.yaml` carries modifications this operation does not own.
223
225
  * The plain "refuse a dirty target" rule applies here unaltered: all four verbs **edit an existing
@@ -445,23 +447,30 @@ const DNA_VALUE_OPTION = {
445
447
  const DNA_SET_VALUE_OPTION = {
446
448
  name: 'value',
447
449
  required: true,
448
- description: 'the new value for <path> (scalar fields only — use `dna update` for collections and lists)',
450
+ description: 'the new value for <path> (scalar fields only — use dna update for collections and lists)',
449
451
  };
450
452
  const DNA_ENTRY_OPTIONS = (0, path_2.dnaEntryOptionNames)().map((field) => ({
451
453
  name: (0, path_2.dnaEntryOptionName)(field),
452
454
  description: `'${field}' of the entry <path> names, where that collection declares it`,
453
455
  }));
456
+ /** A single-valued option of {@link MemoryAddParams.options}: a repeated occurrence keeps the last. */
457
+ function singleOption(options, name) {
458
+ const value = options?.[name];
459
+ return typeof value === 'string' || value === undefined ? value : value[value.length - 1];
460
+ }
454
461
  /**
455
462
  * `memory add` `CoreOperation.fn` (P1.3, `mutates: true` — the first Memory-document mutation and the
456
463
  * pattern for all Memory CRUD; spec-006-core-domain-api §3 memory table). Follows `dna set`'s
457
464
  * mutating-op template (task-025) exactly, over the Memory store instead of `dna.yaml`:
458
465
  *
459
- * 1. **`requireGitIdentity` pre-flight** (REQ-SEC-01, task-014) — refuse before any read/write when
460
- * the git identity is unset, returning its `CoreResult.error` unchanged (exit 1).
461
- * 2. **Argument validation** — a missing `--type`/`--title` is a usage error: `throw new UsageError`
466
+ * 1. **Argument validation** — a missing `--type`/`--title` is a usage error: `throw new UsageError`
462
467
  * → exit **2** with the exact `missing required argument: --<name>` message
463
468
  * (spec-008-cli-grammar §5, mapped by `exitCodeForThrow`), same classification as `dna set`'s
464
- * missing positional.
469
+ * missing positional. So is every spelling fault of `--set` (`spec-008` §10: no `=`, a name that
470
+ * is not a field name, a reserved name, a blank value, a name given twice — task-110). It runs
471
+ * first, so a malformed invocation exits 2 whatever the git identity (task-125, `bug-172`).
472
+ * 2. **`requireGitIdentity` pre-flight** (REQ-SEC-01, task-014) — refuse before any read/write when
473
+ * the git identity is unset, returning its `CoreResult.error` unchanged (exit 1).
465
474
  * 3. **Resolve `--type` against the `memory.yaml` committed at `HEAD`** (spec-001-memory-yaml-schema)
466
475
  * via {@link resolveAddType} — the type registry, the type's `path` and its `template` scaffold
467
476
  * all resolve against the repository as committed, never against the working tree (task-095,
@@ -470,13 +479,17 @@ const DNA_ENTRY_OPTIONS = (0, path_2.dnaEntryOptionNames)().map((field) => ({
470
479
  * domain `NOT_FOUND` (exit 1) with the exact P1.3 message
471
480
  * `unknown memory type '<t>' (not defined in memory.yaml)`, returned BEFORE any write.
472
481
  * 4. **Generate the id** deterministically from the type's committed `id_pattern` (task-002's
473
- * `generateId`, REQ-SYS-07): the `{slug}` from the title, and — only for a `{n}`-token pattern — a
474
- * sequence counter over the type's directory (`src/memory/add.ts`; no wall-clock/random). That
482
+ * `generateId`, REQ-SYS-07), in `spec-001`'s fixed order (task-110, `dl-107` S2): first every
483
+ * frontmatter/context token from `--set` (a `--set` name the committed `id_pattern` and `path` do
484
+ * not contain is refused, exit 1; a token with no value names the option that supplies it), then
485
+ * the `{slug}` from the title, and — only for a `{n}`-token pattern — a sequence counter over the
486
+ * type's directory, matched against the already-materialized pattern (`src/memory/add.ts`; no wall-clock/random). That
475
487
  * counter still reads the WORKING TREE: it is `bug-087` (`release: v0.3`), a different read in
476
488
  * this verb and deliberately not task-095's. What changed is only that the directory it counts in
477
489
  * is now derived from the committed `path` pattern.
478
490
  * 5. **Fill the committed scaffold's bytes** with only the `id`/`status: draft`/`--title`/`--tags`
479
- * skeleton (P1.3; spec-010-memory-frontmatter-schema), then **write + commit** through task-022's
491
+ * skeleton (P1.3; spec-010-memory-frontmatter-schema) plus the `--set` fields (a context token only
492
+ * where the scaffold declares it), then **write + commit** through task-022's
480
493
  * confined `writeMemoryEntry` (REQ-SEC-06 refuse-before-write + one scoped commit
481
494
  * `wf(<type>): add <id>`); the returned sha rides `CoreResult.commit`.
482
495
  *
@@ -487,16 +500,19 @@ const DNA_ENTRY_OPTIONS = (0, path_2.dnaEntryOptionNames)().map((field) => ({
487
500
  */
488
501
  const memoryAddFn = async (params) => {
489
502
  const { root, options } = params;
490
- const identity = (0, git_identity_1.requireGitIdentity)(root);
491
- if (!identity.ok)
492
- return identity;
493
- const type = options?.type;
503
+ const type = singleOption(options, 'type');
494
504
  if (type === undefined)
495
505
  throw new usage_error_1.UsageError('missing required argument: --type');
496
- const title = options?.title;
506
+ const title = singleOption(options, 'title');
497
507
  if (title === undefined)
498
508
  throw new usage_error_1.UsageError('missing required argument: --title');
499
- const tags = (0, memory_1.parseTags)(options?.tags);
509
+ const tags = (0, memory_1.parseTags)(singleOption(options, 'tags'));
510
+ const set = (0, memory_1.parseSetOptions)(options?.set);
511
+ if (!set.ok)
512
+ throw new usage_error_1.UsageError(set.message);
513
+ const identity = (0, git_identity_1.requireGitIdentity)(root);
514
+ if (!identity.ok)
515
+ return identity;
500
516
  // task-095 / `bug-085`: the registry, the `path` and the scaffold all resolve at HEAD, inside a
501
517
  // function that accepts no parsed `MemoryYaml` — so this verb cannot decide any of the three from
502
518
  // a working-tree copy, in the same way task-091 made that true of the four transition verbs.
@@ -504,12 +520,24 @@ const memoryAddFn = async (params) => {
504
520
  if (!resolved.ok)
505
521
  return resolved;
506
522
  const { pathPattern, idPattern, scaffold } = resolved.value;
523
+ const unknown = (0, memory_1.unknownSetNames)(set.values, idPattern, pathPattern);
524
+ if (unknown.length > 0) {
525
+ return (0, types_1.coreErr)({
526
+ code: 'VALIDATION',
527
+ message: unknown
528
+ .map((name) => `--set ${name}: memory type '${type}' has no token {${name}} in its id_pattern or path`)
529
+ .join('; '),
530
+ });
531
+ }
507
532
  try {
508
- const sequence = (0, memory_1.hasNumericToken)(idPattern)
509
- ? (0, memory_1.nextSequenceNumber)((0, memory_1.resolveTypeDirectory)(root, pathPattern), idPattern)
533
+ // spec-001's order: field/context tokens → {slug} → {n}, so the counter sees the materialized prefix.
534
+ const materialized = (0, memory_1.expandFieldTokens)(idPattern, set.values);
535
+ const sequence = (0, memory_1.hasNumericToken)(materialized)
536
+ ? (0, memory_1.nextSequenceNumber)((0, memory_1.resolveTypeDirectory)(root, pathPattern, set.values), materialized)
510
537
  : 0;
511
- const id = (0, validation_1.generateId)(idPattern, { slug: (0, memory_1.slugifyTitle)(title), n: sequence });
512
- const content = (0, memory_1.renderAddDocument)(scaffold, { id, title, tags });
538
+ const id = (0, validation_1.generateId)(materialized, { slug: (0, memory_1.slugifyTitle)(title), n: sequence });
539
+ const content = (0, memory_1.renderAddDocument)(scaffold, { id, title, tags, fields: (0, memory_1.writtenFields)(scaffold, set.values) });
540
+ const pathValues = { ...set.values, id };
513
541
  const message = `wf(${type}): add ${id}`;
514
542
  // dl-080 (B) / bug-078, AC3: this verb CREATES, so the rule is absence rather than cleanliness.
515
543
  // `nextSequenceNumber` counts the WORKING TREE, so a tree that disagrees with `HEAD` about how
@@ -519,11 +547,11 @@ const memoryAddFn = async (params) => {
519
547
  // `writeMemoryEntry`'s return value because the guard must run BEFORE the write; the resolution
520
548
  // is pure, so doing it twice is free of side effects and keeps that throwing storage primitive's
521
549
  // contract untouched. A confinement escape still throws `StorageError` from this same call.
522
- const targetPath = (0, path_1.relative)(root, (0, memory_path_1.resolveConfinedMemoryPath)(root, pathPattern, { id })).split(path_1.sep).join('/');
550
+ const targetPath = (0, path_1.relative)(root, (0, memory_path_1.resolveConfinedMemoryPath)(root, pathPattern, pathValues)).split(path_1.sep).join('/');
523
551
  const absent = (0, write_guard_1.requireAbsentTarget)(root, targetPath);
524
552
  if (!absent.ok)
525
553
  return absent;
526
- const { path, sha } = (0, memory_1.writeMemoryEntry)(root, pathPattern, { id }, content, message);
554
+ const { path, sha } = (0, memory_1.writeMemoryEntry)(root, pathPattern, pathValues, content, message);
527
555
  const leaked = (0, write_guard_1.committedScopeError)(root, sha, targetPath, content);
528
556
  if (leaked)
529
557
  return leaked;
@@ -662,8 +690,9 @@ const memoryHistoryFn = async (params) => {
662
690
  * "the content is written — move it forward": it commits the document as the author left it, with
663
691
  * `status` advanced. Order, every refusal before the single write:
664
692
  *
665
- * 1. **`requireGitIdentity`** (REQ-SEC-01) — exit 1.
666
- * 2. **`<id>`** absent or blank → `UsageError` (exit 2), as `memory history`.
693
+ * 1. **`<id>`** absent or blank → `UsageError` (exit 2), as `memory history` — before the identity
694
+ * check, so the exit code does not depend on the machine (task-125, `bug-172`).
695
+ * 2. **`requireGitIdentity`** (REQ-SEC-01) — exit 1.
667
696
  * 3. **{@link prepareMemoryTransition}** — it resolves the `memory.yaml` committed at `HEAD` itself
668
697
  * (task-091, `dl-080` (B)); no committed machine, an unreadable one, not found, unknown type,
669
698
  * invalid state, or an illegal transition, each a `CoreResult.error` (exit 1); an illegal one
@@ -679,12 +708,12 @@ const memoryHistoryFn = async (params) => {
679
708
  */
680
709
  const memorySubmitFn = async (params) => {
681
710
  const { root, positional: id } = params;
682
- const identity = (0, git_identity_1.requireGitIdentity)(root);
683
- if (!identity.ok)
684
- return identity;
685
711
  if (id === undefined || id.trim().length === 0) {
686
712
  throw new usage_error_1.UsageError('missing required argument: memory submit <id>');
687
713
  }
714
+ const identity = (0, git_identity_1.requireGitIdentity)(root);
715
+ if (!identity.ok)
716
+ return identity;
688
717
  const prepared = (0, memory_transition_1.prepareMemoryTransition)(root, id, 'submit');
689
718
  if (!prepared.ok)
690
719
  return prepared;
@@ -711,11 +740,12 @@ const memorySubmitFn = async (params) => {
711
740
  * (P1.2/P1.10), never written into the message. Order, every refusal before the single write, so
712
741
  * "the state is unchanged" (P1.7 sc.2/sc.3) holds by construction:
713
742
  *
714
- * 1. **`requireGitIdentity`** (REQ-SEC-01) — exit 1. It is also the identity the `Approver:` line and
743
+ * 1. **`<id>`** absent or blank → `UsageError` (exit 2), as `memory submit`/`memory history`.
744
+ * 2. **`requireReason`** (REQ-SEC-04, task-041) → `UsageError` `missing required argument: --reason`
745
+ * (exit 2, P1.7 sc.2). Placed before any file is read, so an omitted reason touches nothing. Both
746
+ * usage checks precede the identity check (task-125, `bug-172`).
747
+ * 3. **`requireGitIdentity`** (REQ-SEC-01) — exit 1. It is also the identity the `Approver:` line and
715
748
  * the commit's own author record, so the two can never name different principals.
716
- * 2. **`<id>`** absent or blank → `UsageError` (exit 2), as `memory submit`/`memory history`.
717
- * 3. **`requireReason`** (REQ-SEC-04, task-041) → `UsageError` `missing required argument: --reason`
718
- * (exit 2, P1.7 sc.2). Placed before any file is read, so an omitted reason touches nothing.
719
749
  * 4. **{@link prepareMemoryTransition}** — it resolves the `memory.yaml` committed at `HEAD` itself
720
750
  * (task-091, `dl-080` (B)); no committed machine, an unreadable one, not found, unknown type,
721
751
  * invalid state, or an illegal transition, each a `CoreResult.error` (exit 1); an illegal one
@@ -742,13 +772,13 @@ const memorySubmitFn = async (params) => {
742
772
  */
743
773
  const memoryApproveFn = async (params) => {
744
774
  const { root, positional: id, options } = params;
745
- const identity = (0, git_identity_1.requireGitIdentity)(root);
746
- if (!identity.ok)
747
- return identity;
748
775
  if (id === undefined || id.trim().length === 0) {
749
776
  throw new usage_error_1.UsageError('missing required argument: memory approve <id>');
750
777
  }
751
778
  const reason = (0, require_reason_1.requireReason)(options);
779
+ const identity = (0, git_identity_1.requireGitIdentity)(root);
780
+ if (!identity.ok)
781
+ return identity;
752
782
  const prepared = (0, memory_transition_1.prepareMemoryTransition)(root, id, 'approve');
753
783
  if (!prepared.ok)
754
784
  return prepared;
@@ -777,10 +807,11 @@ const memoryApproveFn = async (params) => {
777
807
  * `Approver:`/`Reason:` body (`dl-054-submit-commit-subject-bracket`, CLAUDE.md §5.1). Order, every
778
808
  * refusal before the single write:
779
809
  *
780
- * 1. **`requireGitIdentity`** (REQ-SEC-01) — exit `1`.
781
- * 2. **`<id>`** absent or blank → `UsageError` (exit `2`), as `memory submit`.
782
- * 3. **`requireReason`** (REQ-SEC-04, task-041-mandatory-reason-on-verbs) → `UsageError`
783
- * `missing required argument: --reason` (exit `2`, P1.8 sc.3).
810
+ * 1. **`<id>`** absent or blank → `UsageError` (exit `2`), as `memory submit`.
811
+ * 2. **`requireReason`** (REQ-SEC-04, task-041-mandatory-reason-on-verbs) → `UsageError`
812
+ * `missing required argument: --reason` (exit `2`, P1.8 sc.3). Both usage checks precede the
813
+ * identity check (task-125, `bug-172`).
814
+ * 3. **`requireGitIdentity`** (REQ-SEC-01) — exit `1`.
784
815
  * 4. **{@link prepareMemoryTransition}** with op `reject`, against the `memory.yaml` committed at
785
816
  * `HEAD` (task-091, `dl-080` (B)) — no committed machine, an unreadable one, not found,
786
817
  * unknown type, invalid state, or an illegal transition (the document is in no `gates`
@@ -806,13 +837,13 @@ const memoryApproveFn = async (params) => {
806
837
  */
807
838
  const memoryRejectFn = async (params) => {
808
839
  const { root, positional: id, options } = params;
809
- const identity = (0, git_identity_1.requireGitIdentity)(root);
810
- if (!identity.ok)
811
- return identity;
812
840
  if (id === undefined || id.trim().length === 0) {
813
841
  throw new usage_error_1.UsageError('missing required argument: memory reject <id>');
814
842
  }
815
843
  const reason = (0, require_reason_1.requireReason)(options);
844
+ const identity = (0, git_identity_1.requireGitIdentity)(root);
845
+ if (!identity.ok)
846
+ return identity;
816
847
  const prepared = (0, memory_transition_1.prepareMemoryTransition)(root, id, 'reject');
817
848
  if (!prepared.ok)
818
849
  return prepared;
@@ -849,8 +880,10 @@ const memoryRejectFn = async (params) => {
849
880
  *
850
881
  * Order, every refusal before the single write:
851
882
  *
852
- * 1. **`requireGitIdentity`** (REQ-SEC-01) — exit `1`.
853
- * 2. **`<id>`** absent or blank → `UsageError` (exit `2`), as `memory submit`/`memory reject`.
883
+ * 1. **`<id>`** absent or blank → `UsageError` (exit `2`), as `memory submit`/`memory reject`; so is a
884
+ * `--reason` that is given but blank or trailer-shaped (`optionalReason`). Both precede the
885
+ * identity check (task-125, `bug-172`).
886
+ * 2. **`requireGitIdentity`** (REQ-SEC-01) — exit `1`.
854
887
  * 3. **{@link prepareMemoryTransition}** with op `deprecate`, against the `memory.yaml` committed at
855
888
  * `HEAD` (task-091, `dl-080` (B)) — no committed machine, an unreadable one, not found,
856
889
  * unknown type, or a `status` that is not a state of the type, each a
@@ -876,9 +909,6 @@ const memoryRejectFn = async (params) => {
876
909
  */
877
910
  const memoryDeprecateFn = async (params) => {
878
911
  const { root, positional: id, options } = params;
879
- const identity = (0, git_identity_1.requireGitIdentity)(root);
880
- if (!identity.ok)
881
- return identity;
882
912
  if (id === undefined || id.trim().length === 0) {
883
913
  throw new usage_error_1.UsageError('missing required argument: memory deprecate <id>');
884
914
  }
@@ -886,6 +916,9 @@ const memoryDeprecateFn = async (params) => {
886
916
  // trailer — blank or trailer-shaped is a usage error at exit 2, before anything is read or written
887
917
  // (`dl-067` clauses 1 and 4; `bug-042` F2/F3, whose amendment makes THIS verb the exploitable one).
888
918
  const reason = (0, require_reason_1.optionalReason)(options);
919
+ const identity = (0, git_identity_1.requireGitIdentity)(root);
920
+ if (!identity.ok)
921
+ return identity;
889
922
  const prepared = (0, memory_transition_1.prepareMemoryTransition)(root, id, 'deprecate');
890
923
  if (!prepared.ok)
891
924
  return prepared;
@@ -905,14 +938,16 @@ const memoryDeprecateFn = async (params) => {
905
938
  * and the pillar's first CLI verb; spec-006-core-domain-api §3 directives table). Follows `dna set`'s
906
939
  * mutating-op template (task-025) exactly, over the Directives pillar instead of `dna.yaml`:
907
940
  *
908
- * 1. **`requireGitIdentity` pre-flight** (REQ-SEC-01, task-014) — refuse before any read/write when
909
- * the git identity is unset, returning its `CoreResult.error` unchanged (exit 1).
910
- * 2. **Argument validation** — both failures are usage errors (`throw new UsageError` → exit **2**,
941
+ * 1. **Argument validation** — both failures are usage errors (`throw new UsageError` → exit **2**,
911
942
  * mapped by `exitCodeForThrow`): an absent `--name` uses spec-008-cli-grammar §4's exact
912
943
  * `missing required argument: --<name>` wording (same as `memoryAdd`), and a non-kebab-case name
913
944
  * uses P3.1's exact `invalid directive name (use kebab-case)` message
914
945
  * ({@link INVALID_DIRECTIVE_NAME_MESSAGE}). Because {@link isValidDirectiveName} runs before any
915
946
  * path is built, a traversal-shaped name (`../x`, `a/b`, `/etc/passwd`) can never reach the write.
947
+ * It runs before the identity check, so the exit code does not depend on the machine (task-125,
948
+ * `bug-172`).
949
+ * 2. **`requireGitIdentity` pre-flight** (REQ-SEC-01, task-014) — refuse before any read/write when
950
+ * the git identity is unset, returning its `CoreResult.error` unchanged (exit 1).
916
951
  * 3. **Already-exists check** — a file at `.wingfoil/directives/custom/<name>.md` makes this a domain
917
952
  * `CONFLICT` (the input is well-formed; the target is taken), returned NOT thrown, with P3.1's exact
918
953
  * `directive already exists: <name>` message → exit 1 via `exitCodeForError`. Nothing is written, so
@@ -928,14 +963,14 @@ const memoryDeprecateFn = async (params) => {
928
963
  */
929
964
  const directiveCreateFn = async (params) => {
930
965
  const { root, options } = params;
931
- const identity = (0, git_identity_1.requireGitIdentity)(root);
932
- if (!identity.ok)
933
- return identity;
934
966
  const name = options?.name;
935
967
  if (name === undefined)
936
968
  throw new usage_error_1.UsageError('missing required argument: --name');
937
969
  if (!(0, create_1.isValidDirectiveName)(name))
938
970
  throw new usage_error_1.UsageError(create_1.INVALID_DIRECTIVE_NAME_MESSAGE);
971
+ const identity = (0, git_identity_1.requireGitIdentity)(root);
972
+ if (!identity.ok)
973
+ return identity;
939
974
  // One spelling of the location, reused for the existence check, the write and the commit scope, so
940
975
  // the three can never drift apart. `join` normalizes the separators, so the POSIX form is also the
941
976
  // correct absolute path on Windows; the root-relative form is what `commitPaths` stages.
@@ -966,11 +1001,11 @@ const directiveCreateFn = async (params) => {
966
1001
  * it, and `spec-008` §1 / `X_cli-cmds.md` list no fourth directive verb — so it is this operation
967
1002
  * with a list-valued `--directive`. Same mutating-op order as {@link directiveCreateFn}:
968
1003
  *
969
- * 1. `requireGitIdentity` pre-flight (REQ-SEC-01).
970
- * 2. `--directive` then `--role` presence — `UsageError`, spec-008 §4 wording, exit 2. A `--directive`
1004
+ * 1. `--directive` then `--role` presence — `UsageError`, spec-008 §4 wording, exit 2. A `--directive`
971
1005
  * value that parses to **no ids** (`""`, `" "`, `","`) counts as absent, the same reading
972
1006
  * `parseTags` takes of an empty `--tags` (`[AUTHORING]`, task-056 D3: before P3.7 such a value
973
1007
  * reached step 3 and failed with an empty id in the message, `unknown directive: `).
1008
+ * 2. `requireGitIdentity` pre-flight (REQ-SEC-01), after the usage checks (task-125, `bug-172`).
974
1009
  * 3. `checkAssignable` — which resolves **both** of its baselines from the repository as committed at
975
1010
  * `HEAD` itself, the role catalogue (task-091, `bug-082`) and the directive inventory (task-096,
976
1011
  * `bug-086`), per `dl-080` (B) — returns P3.2's exact
@@ -988,9 +1023,6 @@ const directiveCreateFn = async (params) => {
988
1023
  */
989
1024
  const directiveAssignFn = async (params) => {
990
1025
  const { root, options } = params;
991
- const identity = (0, git_identity_1.requireGitIdentity)(root);
992
- if (!identity.ok)
993
- return identity;
994
1026
  const directive = options?.directive;
995
1027
  if (directive === undefined)
996
1028
  throw new usage_error_1.UsageError('missing required argument: --directive');
@@ -1000,6 +1032,9 @@ const directiveAssignFn = async (params) => {
1000
1032
  const role = options?.role;
1001
1033
  if (role === undefined)
1002
1034
  throw new usage_error_1.UsageError('missing required argument: --role');
1035
+ const identity = (0, git_identity_1.requireGitIdentity)(root);
1036
+ if (!identity.ok)
1037
+ return identity;
1003
1038
  const invalid = (0, directive_assign_1.checkAssignable)(root, role, directives);
1004
1039
  if (invalid)
1005
1040
  return (0, types_1.coreErr)(invalid);
@@ -1017,9 +1052,9 @@ const directiveAssignFn = async (params) => {
1017
1052
  * option (b)) assigns clause (b)'s directive half here. Both are enforced, in this order, and both
1018
1053
  * strictly **before** anything on disk changes:
1019
1054
  *
1020
- * 1. `requireGitIdentity` pre-flight (REQ-SEC-01).
1021
- * 2. `<name>` presence — a missing or blank positional is a `UsageError` → exit **2**
1055
+ * 1. `<name>` presence — a missing or blank positional is a `UsageError` → exit **2**
1022
1056
  * (`missing required argument: directive remove <name>`, the `memory submit <id>` precedent).
1057
+ * 2. `requireGitIdentity` pre-flight (REQ-SEC-01), after the usage check (task-125, `bug-172`).
1023
1058
  * 3. **Resolve the name to a real file**, never to a constructed path. `loadDirectives` reads every
1024
1059
  * installed directive and {@link selectDirectivesById} picks the winner for that id under
1025
1060
  * `dl-037`'s custom-wins precedence (`src/core/context.ts` — the same primitive `directives list`
@@ -1067,12 +1102,12 @@ const directiveAssignFn = async (params) => {
1067
1102
  */
1068
1103
  const directiveRemoveFn = async (params) => {
1069
1104
  const { root, positional: name } = params;
1070
- const identity = (0, git_identity_1.requireGitIdentity)(root);
1071
- if (!identity.ok)
1072
- return identity;
1073
1105
  if (name === undefined || name.trim().length === 0) {
1074
1106
  throw new usage_error_1.UsageError('missing required argument: directive remove <name>');
1075
1107
  }
1108
+ const identity = (0, git_identity_1.requireGitIdentity)(root);
1109
+ if (!identity.ok)
1110
+ return identity;
1076
1111
  const directiveFiles = loadOrError(() => (0, loaders_1.loadDirectives)(root));
1077
1112
  if (!directiveFiles.ok)
1078
1113
  return directiveFiles;
@@ -1158,28 +1193,69 @@ const directivesListFn = async (params) => {
1158
1193
  exports.CORE_MODULES = [
1159
1194
  {
1160
1195
  name: 'dna',
1196
+ description: "read and change dna.yaml, the project's structural map",
1161
1197
  operations: {
1162
1198
  // The DNA mutation surface (task-093, `dl-081-dna-mutation-surface-shape` option (E)): three
1163
1199
  // verbs carrying the collection in their argument rather than in the verb name, so the verb
1164
1200
  // count stays constant as `spec-002`'s schema grows and spec-006 §3's one-Tool-per-function rule
1165
1201
  // costs three Tools (`dna.add`, `dna.remove`, `dna.update`) instead of the dozen a
1166
1202
  // per-collection verb set would need. The path itself is the POSITIONAL `<path>` every command
1167
- // in this CLI uses for its target (`dl-082-cli-parameter-shape`), registered generically by
1168
- // `src/cli/program.ts`, which is why no option declares it here.
1169
- dnaAdd: { name: 'dnaAdd', mutates: true, options: [DNA_VALUE_OPTION, ...DNA_ENTRY_OPTIONS], fn: dnaAddFn },
1203
+ // in this CLI uses for its target (`dl-082-cli-parameter-shape`): each verb declares it as its
1204
+ // `positional` (task-120, what `--help` names), which is why no option declares it here.
1205
+ dnaAdd: {
1206
+ name: 'dnaAdd',
1207
+ mutates: true,
1208
+ description: 'add an entry to a collection, or values to a list',
1209
+ positional: { name: 'path', required: true, description: 'the collection (e.g. team.members) or list (e.g. paths.sources) to add to' },
1210
+ options: [DNA_VALUE_OPTION, ...DNA_ENTRY_OPTIONS],
1211
+ example: 'wingfoil dna add team.members --value "Ada Lovelace" --entry-email ada@example.com --entry-roles approver',
1212
+ fn: dnaAddFn,
1213
+ },
1170
1214
  // `remove` declares no entry-field options: it takes what to drop, never what to write.
1171
- dnaRemove: { name: 'dnaRemove', mutates: true, options: [DNA_VALUE_OPTION], fn: dnaRemoveFn },
1215
+ dnaRemove: {
1216
+ name: 'dnaRemove',
1217
+ mutates: true,
1218
+ description: 'remove a collection entry, or values from a list',
1219
+ positional: { name: 'path', required: true, description: 'the entry to remove (<collection>.<name>), or the list to remove the --value values from' },
1220
+ options: [DNA_VALUE_OPTION],
1221
+ example: 'wingfoil dna remove paths.docs --value README.md',
1222
+ fn: dnaRemoveFn,
1223
+ },
1172
1224
  // The FIRST `mutates: true` operation in production (spec-006 §3 dna table) — by construction an
1173
1225
  // MCP Tool (`dna.set`) + CLI command (`wingfoil dna set`), and the op the REQ-SYS-05 parity test
1174
1226
  // now actually guards (task-025-implement-dna-set). Its second positional became `--value` in
1175
1227
  // task-093 (`dl-082`) — a breaking change to a shipped command, made before `minor-v0.2` ships.
1176
- dnaSet: { name: 'dnaSet', mutates: true, options: [DNA_SET_VALUE_OPTION], fn: dnaSetFn },
1177
- dnaShow: { name: 'dnaShow', mutates: false, fn: dnaShowFn },
1178
- dnaUpdate: { name: 'dnaUpdate', mutates: true, options: [DNA_VALUE_OPTION, ...DNA_ENTRY_OPTIONS], fn: dnaUpdateFn },
1228
+ dnaSet: {
1229
+ name: 'dnaSet',
1230
+ mutates: true,
1231
+ description: 'set one scalar field',
1232
+ positional: { name: 'path', required: true, description: 'the dotted path of the scalar field, e.g. project.name (double-quote a segment that contains a dot)' },
1233
+ options: [DNA_SET_VALUE_OPTION],
1234
+ example: 'wingfoil dna set project.name --value "My Project"',
1235
+ fn: dnaSetFn,
1236
+ },
1237
+ dnaShow: {
1238
+ name: 'dnaShow',
1239
+ mutates: false,
1240
+ description: 'print dna.yaml, or one top-level section of it',
1241
+ positional: { name: 'section', description: 'a top-level key: project, modules, stacks, team or paths (omit it for the whole file)' },
1242
+ example: 'wingfoil dna show project',
1243
+ fn: dnaShowFn,
1244
+ },
1245
+ dnaUpdate: {
1246
+ name: 'dnaUpdate',
1247
+ mutates: true,
1248
+ description: 'change fields of an existing collection entry',
1249
+ positional: { name: 'path', required: true, description: 'the entry (<collection>.<name>), or one of its fields (<collection>.<name>.<field>) with --value' },
1250
+ options: [DNA_VALUE_OPTION, ...DNA_ENTRY_OPTIONS],
1251
+ example: 'wingfoil dna update modules.api --entry-description "Public HTTP API"',
1252
+ fn: dnaUpdateFn,
1253
+ },
1179
1254
  },
1180
1255
  },
1181
1256
  {
1182
1257
  name: 'memory',
1258
+ description: 'create Memory documents, move them through their state machine, and read them back',
1183
1259
  operations: {
1184
1260
  // The FIRST Memory-document mutation (P1.3, spec-006 §3 memory table) — `mutates: true`, so by
1185
1261
  // construction an MCP Tool (`memory.add`) + CLI command (`wingfoil memory add`), and the second
@@ -1190,18 +1266,36 @@ exports.CORE_MODULES = [
1190
1266
  memoryAdd: {
1191
1267
  name: 'memoryAdd',
1192
1268
  mutates: true,
1269
+ description: "create a document in its type's initial state (draft) from the type's template",
1193
1270
  options: [
1194
- { name: 'type', required: true },
1195
- { name: 'title', required: true },
1196
- { name: 'tags' },
1271
+ { name: 'type', required: true, valueName: 'type', description: 'the Memory type, as the committed memory.yaml declares it' },
1272
+ { name: 'title', required: true, valueName: 'title', description: 'the document title; also the source of the {slug} token' },
1273
+ { name: 'tags', valueName: 't1,t2', description: 'comma-separated tags, written as the tags list' },
1274
+ // task-110, `spec-008-cli-grammar` §10 (`dl-107` S2): the value of an id_pattern/path token,
1275
+ // written into the frontmatter field of that name. One declared option — not one derived
1276
+ // option per field, whose first member, `--version`, is a global action flag.
1277
+ {
1278
+ name: 'set',
1279
+ repeatable: true,
1280
+ valueName: 'name=value',
1281
+ description: 'fill the id_pattern/path token {name} and write the frontmatter field of that name (repeatable)',
1282
+ },
1197
1283
  ],
1284
+ example: 'wingfoil memory add --type task --title "My first task" --tags demo,quickstart',
1198
1285
  fn: memoryAddFn,
1199
1286
  },
1200
1287
  // P1.10 (task-049-memory-history) — `mutates: false`, so by construction an MCP Resource +
1201
1288
  // CLI command (`wingfoil memory history <id>`). It declares no flags and no value options: the
1202
1289
  // document id rides the generic bare `positional` seam (task-026), per spec-008-cli-grammar §7's
1203
1290
  // bare-`<id>` rule for a command whose noun already scopes the type.
1204
- memoryHistory: { name: 'memoryHistory', mutates: false, fn: memoryHistoryFn },
1291
+ memoryHistory: {
1292
+ name: 'memoryHistory',
1293
+ mutates: false,
1294
+ description: "print a document's audit trail, reconstructed from git",
1295
+ positional: { name: 'id', required: true, description: 'the document id, e.g. task-001-my-first-task' },
1296
+ example: 'wingfoil memory history task-001-my-first-task',
1297
+ fn: memoryHistoryFn,
1298
+ },
1205
1299
  // The FIRST Memory-document READ operation (P1.5, spec-006 §3 memory table) — `mutates: false`,
1206
1300
  // so by construction an MCP Resource (`wingfoil://memory/search`, the mechanical zero-argument
1207
1301
  // form spec-006 §3 pins verbatim) + CLI command (`wingfoil memory search`). Its keyword rides the
@@ -1211,12 +1305,26 @@ exports.CORE_MODULES = [
1211
1305
  memorySearch: {
1212
1306
  name: 'memorySearch',
1213
1307
  mutates: false,
1214
- options: [{ name: 'tag' }, { name: 'status' }, { name: 'type' }],
1308
+ description: 'find documents by keyword and/or metadata',
1309
+ positional: { name: 'keyword', description: 'a case-insensitive substring matched against title, id, tags and body (omit it to filter by metadata alone)' },
1310
+ options: [
1311
+ { name: 'tag', valueName: 'tag', description: 'keep documents carrying this tag' },
1312
+ { name: 'status', valueName: 'status', description: 'keep documents in this state' },
1313
+ { name: 'type', valueName: 'type', description: 'keep documents of this Memory type' },
1314
+ ],
1315
+ example: 'wingfoil memory search --status approved --type task',
1215
1316
  fn: memorySearchFn,
1216
1317
  },
1217
1318
  // P1.6 (task-045-memory-submit) — `mutates: true`: CLI `wingfoil memory submit <id>` + MCP Tool
1218
1319
  // `memory.submit`. The id rides the bare `positional` seam (spec-008 §7); no flags, no options.
1219
- memorySubmit: { name: 'memorySubmit', mutates: true, fn: memorySubmitFn },
1320
+ memorySubmit: {
1321
+ name: 'memorySubmit',
1322
+ mutates: true,
1323
+ description: "move a document one step forward along its type's sequence — for the default machine, draft → pending",
1324
+ positional: { name: 'id', required: true, description: 'the document id, e.g. task-001-my-first-task' },
1325
+ example: 'wingfoil memory submit task-001-my-first-task',
1326
+ fn: memorySubmitFn,
1327
+ },
1220
1328
  // P1.7 (task-046-memory-approve) — `mutates: true`: CLI `wingfoil memory approve <id> --reason
1221
1329
  // <text>` + MCP Tool `memory.approve`. The id rides the bare `positional` seam (spec-008 §7);
1222
1330
  // `--reason` is the one declared value option, `required` per spec-008 §2 / REQ-SEC-04 (the
@@ -1224,7 +1332,10 @@ exports.CORE_MODULES = [
1224
1332
  memoryApprove: {
1225
1333
  name: 'memoryApprove',
1226
1334
  mutates: true,
1227
- options: [{ name: 'reason', required: true }],
1335
+ description: 'pass a gate: move a document forward from a gated state (default machine: pending → approved)',
1336
+ positional: { name: 'id', required: true, description: 'the document id, e.g. task-001-my-first-task' },
1337
+ options: [{ name: 'reason', required: true, valueName: 'text', description: "why you approve it, recorded as the commit's Reason:; needs the approver role" }],
1338
+ example: 'wingfoil memory approve task-001-my-first-task --reason "Scope and acceptance criteria are clear."',
1228
1339
  fn: memoryApproveFn,
1229
1340
  },
1230
1341
  // P1.8 (task-047-memory-reject) — `mutates: true`: CLI `wingfoil memory reject <id> --reason
@@ -1235,7 +1346,10 @@ exports.CORE_MODULES = [
1235
1346
  memoryReject: {
1236
1347
  name: 'memoryReject',
1237
1348
  mutates: true,
1238
- options: [{ name: 'reason', required: true }],
1349
+ description: "send a gated document back to its gate's reject target (default machine: pending → draft)",
1350
+ positional: { name: 'id', required: true, description: 'the document id, e.g. task-001-my-first-task' },
1351
+ options: [{ name: 'reason', required: true, valueName: 'text', description: "what must change before resubmitting, recorded as the commit's Reason: and as rejection_reason; needs the approver role" }],
1352
+ example: 'wingfoil memory reject task-001-my-first-task --reason "Add acceptance criteria before resubmitting."',
1239
1353
  fn: memoryRejectFn,
1240
1354
  },
1241
1355
  // P1.9 (task-048-memory-deprecate) — `mutates: true`: CLI `wingfoil memory deprecate <id>
@@ -1246,18 +1360,24 @@ exports.CORE_MODULES = [
1246
1360
  memoryDeprecate: {
1247
1361
  name: 'memoryDeprecate',
1248
1362
  mutates: true,
1249
- options: [{ name: 'reason' }],
1363
+ description: 'retire a document, from any state, to deprecated',
1364
+ positional: { name: 'id', required: true, description: 'the document id, e.g. task-001-my-first-task' },
1365
+ options: [{ name: 'reason', valueName: 'text', description: "why the document is retired, recorded as the commit's Reason: (not blank when given)" }],
1366
+ example: 'wingfoil memory deprecate dl-001-use-postgresql --reason "Superseded by the hosted-DB decision."',
1250
1367
  fn: memoryDeprecateFn,
1251
1368
  },
1252
1369
  },
1253
1370
  },
1254
1371
  {
1255
1372
  name: 'directives',
1373
+ description: 'list the directives and the roles they apply to',
1256
1374
  operations: {
1257
1375
  directivesList: {
1258
1376
  name: 'directivesList',
1259
1377
  mutates: false,
1260
- options: [{ name: 'role' }],
1378
+ description: 'list directives with the roles each is assigned to',
1379
+ options: [{ name: 'role', valueName: 'role', description: 'keep only the directives that apply to this role, globals included' }],
1380
+ example: 'wingfoil directives list --role developer',
1261
1381
  fn: directivesListFn,
1262
1382
  },
1263
1383
  },
@@ -1267,16 +1387,27 @@ exports.CORE_MODULES = [
1267
1387
  operations: {
1268
1388
  // Self-named (operation name === module name) — the flat/no-verb `wingfoil paths [category]`
1269
1389
  // form (see `deriveVerb`, `./registry.ts`), not `wingfoil paths paths`. `category` rides the
1270
- // generic bare positional (task-026's seam); only `--list` is declared here.
1271
- paths: { name: 'paths', mutates: false, flags: ['list'], fn: pathsFn },
1390
+ // generic bare positional (task-026's seam), declared as `positional` so `--help` names it.
1391
+ paths: {
1392
+ name: 'paths',
1393
+ mutates: false,
1394
+ description: 'print the resource paths declared in dna.yaml paths:',
1395
+ positional: { name: 'category', description: 'sources, tests, docs, config or governance (omit it for the whole map)' },
1396
+ flags: [{ name: 'list', description: 'accepted for the planned drill-down view; it does not change the output yet' }],
1397
+ example: 'wingfoil paths sources',
1398
+ fn: pathsFn,
1399
+ },
1272
1400
  },
1273
1401
  },
1274
1402
  {
1275
1403
  name: 'workflow',
1404
+ description: 'read the workflows the project declares (there is no workflow engine yet)',
1276
1405
  operations: {
1277
1406
  workflowList: {
1278
1407
  name: 'workflowList',
1279
1408
  mutates: false,
1409
+ description: 'print the workflow manifest (workflows.yaml) and every workflow it includes, with their phases',
1410
+ example: 'wingfoil workflow list',
1280
1411
  fn: wrapReadOnly(loaders_1.loadWorkflowsYaml),
1281
1412
  },
1282
1413
  },
@@ -1296,6 +1427,7 @@ exports.CORE_MODULES = [
1296
1427
  // the concurrently-edited `directives` block; `enumerateOperations` sorts, so position is inert.
1297
1428
  {
1298
1429
  name: 'directive',
1430
+ description: 'create, assign and remove custom directives',
1299
1431
  operations: {
1300
1432
  // The FIRST Directives-pillar mutation (P3.1) — `mutates: true`, so by construction an MCP Tool
1301
1433
  // (`directive.create`) + CLI command (`wingfoil directive create`), and the third op the
@@ -1305,7 +1437,9 @@ exports.CORE_MODULES = [
1305
1437
  directiveCreate: {
1306
1438
  name: 'directiveCreate',
1307
1439
  mutates: true,
1308
- options: [{ name: 'name', required: true }],
1440
+ description: 'create a custom directive from a scaffold',
1441
+ options: [{ name: 'name', required: true, valueName: 'name', description: 'the new directive, written to .wingfoil/directives/custom/<name>.md' }],
1442
+ example: 'wingfoil directive create --name api-style',
1309
1443
  fn: directiveCreateFn,
1310
1444
  },
1311
1445
  // P3.2 (task-051-directive-assign) — on this SINGULAR module per dl-041 B, so `deriveVerb` yields
@@ -1313,10 +1447,12 @@ exports.CORE_MODULES = [
1313
1447
  directiveAssign: {
1314
1448
  name: 'directiveAssign',
1315
1449
  mutates: true,
1450
+ description: 'assign one or more directives to a role in roles.yaml',
1316
1451
  options: [
1317
- { name: 'directive', required: true },
1318
- { name: 'role', required: true },
1452
+ { name: 'directive', required: true, valueName: 'name[,name...]', description: 'the directive(s) to assign, comma-separated' },
1453
+ { name: 'role', required: true, valueName: 'role', description: 'the role to assign them to, as the committed dna.yaml declares it' },
1319
1454
  ],
1455
+ example: 'wingfoil directive assign --directive api-style --role developer',
1320
1456
  fn: directiveAssignFn,
1321
1457
  },
1322
1458
  // P3.3 (task-052-directive-remove) — on this SINGULAR module per dl-041 B, so `deriveVerb`
@@ -1324,7 +1460,14 @@ exports.CORE_MODULES = [
1324
1460
  // no flags and no value options: the name rides the bare `positional` seam (spec-008 §7), like
1325
1461
  // `memory submit <id>`. Completes REQ-SEC-07 for the directive surface (clause (a) via
1326
1462
  // task-042's `requireCustomAsset`, clause (b) via `checkUnreferenced` — dl-030).
1327
- directiveRemove: { name: 'directiveRemove', mutates: true, fn: directiveRemoveFn },
1463
+ directiveRemove: {
1464
+ name: 'directiveRemove',
1465
+ mutates: true,
1466
+ description: 'delete a custom directive',
1467
+ positional: { name: 'name', required: true, description: 'the custom directive to delete (built-in directives cannot be removed)' },
1468
+ example: 'wingfoil directive remove api-style',
1469
+ fn: directiveRemoveFn,
1470
+ },
1328
1471
  },
1329
1472
  },
1330
1473
  ];