@1aboveio/skills 1.0.14 → 1.0.15

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 (86) hide show
  1. package/.codex-plugin/plugin.json +1 -1
  2. package/.codex-plugin/resources.json +147 -147
  3. package/package.json +4 -4
  4. package/skills/app-dev/airflow/SKILL.md +1 -1
  5. package/skills/app-dev/android-development/SKILL.md +3 -2
  6. package/skills/app-dev/android-development/references/diagnosis.md +5 -0
  7. package/skills/app-dev/android-development/references/feature-work.md +15 -0
  8. package/skills/app-dev/android-development/references/init.md +26 -4
  9. package/skills/app-dev/android-development/scripts/init.mjs +105 -8
  10. package/skills/app-dev/app-debug/SKILL.md +1 -1
  11. package/skills/app-dev/better-auth/SKILL.md +1 -1
  12. package/skills/app-dev/data-catalog/SKILL.md +53 -16
  13. package/skills/app-dev/data-catalog/scripts/persist-conventions-core.mjs +231 -21
  14. package/skills/app-dev/data-catalog/scripts/persist-conventions.mjs +12 -1
  15. package/skills/app-dev/data-quality/SKILL.md +1 -1
  16. package/skills/app-dev/ios-foundation/SKILL.md +1 -1
  17. package/skills/app-dev/monorepo/SKILL.md +1 -1
  18. package/skills/app-dev/nextjs-fullstack/SKILL.md +1 -1
  19. package/skills/app-dev/prisma-setup/SKILL.md +1 -1
  20. package/skills/app-dev/pyspark/SKILL.md +1 -1
  21. package/skills/app-dev/python-backend/SKILL.md +1 -1
  22. package/skills/app-dev/shadcn/SKILL.md +1 -1
  23. package/skills/app-dev/zod-v4/SKILL.md +1 -1
  24. package/skills/cicd/cloud-build/SKILL.md +1 -1
  25. package/skills/cicd/cloud-debug/SKILL.md +1 -1
  26. package/skills/cicd/cloud-deploy/SKILL.md +1 -1
  27. package/skills/cicd/docker/SKILL.md +1 -1
  28. package/skills/cicd/github-actions/SKILL.md +1 -1
  29. package/skills/cicd/github-flow/SKILL.md +10 -1
  30. package/skills/cicd/github-flow/scripts/persist-conventions-core.mjs +231 -21
  31. package/skills/cicd/github-flow/scripts/persist-conventions.mjs +18 -1
  32. package/skills/cicd/google-cloud/SKILL.md +1 -1
  33. package/skills/cicd/mergify/SKILL.md +1 -1
  34. package/skills/cicd/podman/SKILL.md +1 -1
  35. package/skills/content/article-review/SKILL.md +1 -1
  36. package/skills/content/article-to-image/SKILL.md +1 -1
  37. package/skills/content/flowchart-svg/SKILL.md +1 -1
  38. package/skills/content/image-gen/SKILL.md +1 -1
  39. package/skills/content/mermaid/SKILL.md +1 -1
  40. package/skills/content/wechat-article-writer/SKILL.md +1 -1
  41. package/skills/content/wechat-publisher/SKILL.md +1 -1
  42. package/skills/content/wechat-search/SKILL.md +1 -1
  43. package/skills/content/xai-search/SKILL.md +1 -1
  44. package/skills/core/distill-writing/SKILL.md +7 -1
  45. package/skills/core/doc-to-md/SKILL.md +1 -1
  46. package/skills/core/editorial-review/SKILL.md +1 -1
  47. package/skills/core/house-style/SKILL.md +1 -1
  48. package/skills/core/house-style/references/persistence.md +8 -3
  49. package/skills/core/house-style/scripts/init-guidance.mjs +1 -1
  50. package/skills/core/plain-language/SKILL.md +1 -1
  51. package/skills/core/polish-cn/SKILL.md +1 -1
  52. package/skills/core/reg-parser/SKILL.md +1 -1
  53. package/skills/core/terminology/SKILL.md +1 -1
  54. package/skills/core/terminology/references/init.md +11 -0
  55. package/skills/core/write-doc/SKILL.md +1 -1
  56. package/skills/core/write-report/SKILL.md +1 -1
  57. package/skills/engineering/better-review/SKILL.md +1 -1
  58. package/skills/engineering/e2e-test/SKILL.md +1 -1
  59. package/skills/engineering/engineering/SKILL.md +19 -4
  60. package/skills/engineering/engineering/references/init-scope.md +5 -2
  61. package/skills/engineering/engineering/references/scope-management.md +4 -2
  62. package/skills/engineering/engineering/scripts/init.mjs +34 -9
  63. package/skills/engineering/engineering/scripts/persist-conventions-core.mjs +231 -21
  64. package/skills/engineering/engineering/scripts/persist-conventions.mjs +48 -21
  65. package/skills/engineering/engineering-runtime/SKILL.md +1 -1
  66. package/skills/engineering/engineering-runtime/coherence/workflow.json +13 -13
  67. package/skills/engineering/ensure-coverage/SKILL.md +1 -1
  68. package/skills/engineering/golden-path/SKILL.md +1 -1
  69. package/skills/engineering/harness-runtime/SKILL.md +1 -1
  70. package/skills/engineering/resolve-issues/SKILL.md +1 -1
  71. package/skills/engineering/resolve-issues/references/review.md +27 -4
  72. package/skills/engineering/resolve-release/SKILL.md +1 -1
  73. package/skills/engineering/rush-issues/SKILL.md +1 -1
  74. package/skills/engineering/rush-release/SKILL.md +1 -1
  75. package/skills/engineering/test-quality/SKILL.md +3 -1
  76. package/skills/engineering/test-quality/scripts/persist-conventions-core.mjs +231 -21
  77. package/skills/engineering/test-quality/scripts/persist-conventions.mjs +13 -1
  78. package/skills/risk-compliance/compliance-audit/SKILL.md +1 -1
  79. package/skills/risk-compliance/contract-compare/SKILL.md +1 -1
  80. package/skills/risk-compliance/contract-review/SKILL.md +1 -1
  81. package/skills/risk-compliance/doc-authenticity/SKILL.md +1 -1
  82. package/skills/risk-compliance/fraud-detection/SKILL.md +1 -1
  83. package/skills/risk-compliance/payment-analysis/SKILL.md +1 -1
  84. package/skills/risk-compliance/portfolio-payment-analysis/SKILL.md +1 -1
  85. package/skills/risk-compliance/similar-domain/SKILL.md +1 -1
  86. package/skills/risk-compliance/tld-detection/SKILL.md +1 -1
@@ -369,7 +369,7 @@
369
369
  "repository": "1aboveio/ai",
370
370
  "revision": "main"
371
371
  },
372
- "contentDigest": "d50381c5d34f05b3c4ac60bd0d1a5a56fdeb7112caf3a58303e2c8d5b9cd73d4",
372
+ "contentDigest": "a91694b84a0cfd014714c53a7b91e3a470fd76daccf85582c21620a4d91523fb",
373
373
  "digestExcludes": []
374
374
  },
375
375
  {
@@ -382,7 +382,7 @@
382
382
  "repository": "1aboveio/ai",
383
383
  "revision": "main"
384
384
  },
385
- "contentDigest": "7fb81756e33e2582cc02b2a964af1361a474ddae64f1304dfdff680162e5f35e",
385
+ "contentDigest": "8c4c6f21ea637ce24245cf6f20d0aa46d811c35cb8f4b81a78d07907a8c3e8af",
386
386
  "digestExcludes": []
387
387
  },
388
388
  {
@@ -395,7 +395,7 @@
395
395
  "repository": "1aboveio/ai",
396
396
  "revision": "main"
397
397
  },
398
- "contentDigest": "08e07649d8ee91496a4275c9417cd679932d798c91cdbb483ca45979733dd0c3",
398
+ "contentDigest": "f66eee222aef4ca965a2e9fbfbfc16a95a69213fa30472485519799e9dc99487",
399
399
  "digestExcludes": []
400
400
  },
401
401
  {
@@ -408,7 +408,7 @@
408
408
  "repository": "1aboveio/ai",
409
409
  "revision": "main"
410
410
  },
411
- "contentDigest": "45e72502bf9d1f707caf2990ba8a9bda7fbda4e748b5f798731a1d0d992b0b02",
411
+ "contentDigest": "647fb4c57154f3e84d4adff0dd44f32a09c8fd6eb0145afbe1141d674e7965db",
412
412
  "digestExcludes": []
413
413
  },
414
414
  {
@@ -421,7 +421,7 @@
421
421
  "repository": "1aboveio/ai",
422
422
  "revision": "main"
423
423
  },
424
- "contentDigest": "5cf9c65216eda37ed0acebcaa97be9eea9239fad96397cdd858b4694cae9d906",
424
+ "contentDigest": "a9085e135bfbe535f96c4915f8402303e98e663f182b3508376799bff1013763",
425
425
  "digestExcludes": []
426
426
  },
427
427
  {
@@ -434,7 +434,7 @@
434
434
  "repository": "1aboveio/ai",
435
435
  "revision": "main"
436
436
  },
437
- "contentDigest": "6e1c15f7d61a3bb2af78df82b57674152d9c321cc17b5a83d32379cc0a9104ef",
437
+ "contentDigest": "e21ef0dbc6f4d423b310f78ca342a20aeb672a5fb517a27f3d318e14dacd3f45",
438
438
  "digestExcludes": []
439
439
  },
440
440
  {
@@ -447,7 +447,7 @@
447
447
  "repository": "1aboveio/ai",
448
448
  "revision": "main"
449
449
  },
450
- "contentDigest": "303941d679cacbafc5594a6b8369e016cef3cdf3c065ae919edc8695cbce8a69",
450
+ "contentDigest": "435361dd2f0aecda06c751825f75d918520203c53ba245b63630eeaa390ae0c5",
451
451
  "digestExcludes": []
452
452
  },
453
453
  {
@@ -460,7 +460,7 @@
460
460
  "repository": "1aboveio/ai",
461
461
  "revision": "main"
462
462
  },
463
- "contentDigest": "d8de3acaf463033d55e5bedf17b5aeac8e9a1d77267c5960af18bdebbaf18c7d",
463
+ "contentDigest": "fb0eadc4b382a5e6b863d76638949052d24f8c378a74971a0a28ccf43de49e0f",
464
464
  "digestExcludes": []
465
465
  },
466
466
  {
@@ -473,7 +473,7 @@
473
473
  "repository": "1aboveio/ai",
474
474
  "revision": "main"
475
475
  },
476
- "contentDigest": "8b8a149b90292ae4b0c3dd8b34a387ea7fa8c154610c232c8c55ce9781cca03d",
476
+ "contentDigest": "86660760773ae4869968f5e85efdab6d23e179a02f8c7066bf497f9cc64275c5",
477
477
  "digestExcludes": []
478
478
  },
479
479
  {
@@ -486,7 +486,7 @@
486
486
  "repository": "1aboveio/ai",
487
487
  "revision": "main"
488
488
  },
489
- "contentDigest": "553b2748b6357b92caa30ee29351add044d10cc6ad28e4cdb809f4f23bb5a9da",
489
+ "contentDigest": "35fc60a6348784ca9b269516a0afddcc8f8e6a2dbb2f281afa657b3abe459450",
490
490
  "digestExcludes": []
491
491
  },
492
492
  {
@@ -499,7 +499,7 @@
499
499
  "repository": "1aboveio/ai",
500
500
  "revision": "main"
501
501
  },
502
- "contentDigest": "65bb8314e2fe1f2ebbdca60f2dcd433af44a8eeba080adf30f0682ef5c0a2acd",
502
+ "contentDigest": "826e64ed07b75f1ba24da7cac6c6e67b4f8152f5c34e067548f1af24038a3fa0",
503
503
  "digestExcludes": []
504
504
  },
505
505
  {
@@ -512,13 +512,13 @@
512
512
  "repository": "1aboveio/ai",
513
513
  "revision": "main"
514
514
  },
515
- "contentDigest": "9fb61a9e59fee73ff803113e9d33e47bc19a2ef5ce92c06f63a4d5cca8a72189",
515
+ "contentDigest": "0fc35665e627e1c1698ba10a1c9d00a3745df10c22562b7a3ef72267e2309c4b",
516
516
  "digestExcludes": [
517
517
  "coherence/workflow.json"
518
518
  ]
519
519
  }
520
520
  ],
521
- "releaseIdentity": "d0f4c3cc3c13714b4d4be17f6b8b1f56aff7fe1901be7c7823f4faba348baeb6",
521
+ "releaseIdentity": "30ddab5b3c21191385d102158a2eb2bd3dcbd6e82d350777722543931e01ad44",
522
522
  "lifecycleAuthority": "native-skills-cli",
523
523
  "repairRecipe": {
524
524
  "id": "engineering-workflow-dependency-first",
@@ -2,7 +2,7 @@
2
2
  name: ensure-coverage
3
3
  description: "Define or audit behavior-first test criteria — ACs, required test levels, mock/integration/E2E policy — across three axes: breadth (Coverage Ledger), depth (over-mocking), presentation (perceivability of browser surfaces). NOT judging whether a single test is any good (test-quality), authoring browser tests (e2e-test), reviewing a PR (better-review), or delivering issues (resolve-issues)."
4
4
  disable-model-invocation: true
5
- release-version: 0.46.0
5
+ release-version: 0.47.0
6
6
  ---
7
7
  **Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
8
8
  # Ensure Coverage
@@ -3,7 +3,7 @@ name: golden-path
3
3
  description: "Golden-path E2E: the short list of user journeys whose failure blocks a release, kept in a repo as one contract (golden-paths.json) with a generated GOLDEN_PATHS.md and a generated GitHub Actions workflow, and run against a local build or a deployed URL for an ALIVE/DEAD verdict. Use to set golden paths up in a repo ('init golden paths', 'add golden-path CI'), to migrate an older smoke.manifest.json or a hand-built smoke setup ('audit', 'migrate smoke'), to add, change, or remove a golden path, to ask 'is the build alive?' or run them before a PR or release, or when a change touches a flow listed in GOLDEN_PATHS.md. NOT for writing the Playwright specs themselves (e2e-test), deciding what a change must prove (ensure-coverage), or general workflow YAML (github-actions)."
4
4
  dependencies:
5
5
  - e2e-test
6
- release-version: 0.46.0
6
+ release-version: 0.47.0
7
7
  ---
8
8
  **Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
9
9
  # Golden path
@@ -3,7 +3,7 @@ name: harness-runtime
3
3
  description: Internal import-only library shared by first-party engineering packages. Not a workflow skill; never select or invoke it as a user capability.
4
4
  disable-model-invocation: true
5
5
  user-invocable: false
6
- release-version: 0.46.0
6
+ release-version: 0.47.0
7
7
  ---
8
8
  **Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
9
9
  # Harness Runtime
@@ -8,7 +8,7 @@ dependencies:
8
8
  - better-review
9
9
  - golden-path
10
10
  - harness-runtime
11
- release-version: 0.46.0
11
+ release-version: 0.47.0
12
12
  ---
13
13
  **Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
14
14
  # Resolve issues
@@ -9,7 +9,10 @@ restate its rules in the brief beyond the two below.
9
9
  ## Brief
10
10
 
11
11
  - Reviewer model: a different id from the implementer's
12
- ([preflight.md](preflight.md#1-model-slots)).
12
+ ([preflight.md](preflight.md#1-model-slots)). Take its model id and effort from
13
+ the plan's recorded reviewer slot, a model or effort the human named included,
14
+ and pass them as structured launch arguments (the spawn's model and effort
15
+ fields), never as brief text and never a model id hardcoded here.
13
16
  - Context: spec, repository standards, target branch, unit branch and HEAD,
14
17
  parent PR when stacked or chained, exploration path, and the contracts the unit
15
18
  provides and builds on.
@@ -92,6 +95,25 @@ pass (incomplete, an open P0/P1, a GitHub re-review awaited): pick the tier
92
95
  instead. Example: round 2 passed on
93
96
  `5f63348`; CI failed and the fix pushed `2035c74`: carry, no reviewer.
94
97
 
98
+ ### Resume an incomplete review
99
+
100
+ An incomplete review is unfinished work, not evidence of a bad patch: start no
101
+ fix round. Before another launch, read the retained record for unfinished passes,
102
+ completed commands and findings, and why the review stopped. Resolve an open
103
+ `executionIncident` first; ask the reviewer to record missing continuation details.
104
+
105
+ For the same valid HEAD and scope, resume the same reviewer in the same round
106
+ using its result file, review clone and retained context. Give it the unfinished
107
+ work rather than repeating the whole assignment; preserve completed valid passes.
108
+ A wrong HEAD or invalid scope must be corrected, not carried as completed proof.
109
+
110
+ If that reviewer cannot continue, state why replacement is necessary, which
111
+ retained evidence the replacement reads, and what it redoes before launching it.
112
+ The replacement reviews every pass with `tier: "full"`. Keep the round number,
113
+ `fixRound`, unit `attempt` and earlier round files; incompleteness neither resets
114
+ history nor counts as progress. Use the plan's recorded model and effort in
115
+ structured launch arguments as [the brief](#brief) requires.
116
+
95
117
  ## GitHub reviews
96
118
 
97
119
  Count reviews posted on the PR by any account with write access other than
@@ -135,8 +157,9 @@ the PR author; `github-reviews.mjs` reads them and decides what blocks.
135
157
  completed review left a P0/P1 open, so skips, and any review that follows
136
158
  a passing one, never consume a fix cycle; another attempt after a
137
159
  completed blocker does, even with an unchanged patch, so fix first.
138
- Resume an incomplete review in the same round with the same reviewer when
139
- it can continue; a new reviewer that replaces it reviews in full and keeps
160
+ Resume an incomplete review in the same round
161
+ ([Resume an incomplete review](#resume-an-incomplete-review)); a new
162
+ reviewer that replaces it reviews in full and keeps
140
163
  the prior counter. Legacy records without
141
164
  `fixRound` use their attempt number; never reset history. The verdict checks the counter against the immediate prior, given
142
165
  with `--prior` when the review's filename is not `<unit>-r<N>.json`. The
@@ -178,7 +201,7 @@ the PR author; `github-reviews.mjs` reads them and decides what blocks.
178
201
  | `handback` | `not-converging`: stop fixing; mark the unit `blocked` (`blockedReason: review-handback`) and defer the PR and its open findings on its issue |
179
202
  | `decide` | `spec-question`: the printed `decide` file holds the question. Answer it ([expire.md](expire.md#decide-before-you-defer)) as a `decisions` entry whose `answers` names that file, then rerun this step with `--decision <its id>`: it writes a `test-first` fix brief that carries the decision |
180
203
  | `wait` | No P0/P1 is open, but `awaiting` names a reviewer whose changes request a fix already answered: request their re-review (`gh pr edit <N> --add-reviewer <login>`) and keep the other units shipping; rerun step 3 on each completion notice, or every 5 minutes when nothing else runs; after 30 minutes, mark the unit `blocked` (`blockedReason: review-handback`) and defer it on its issue |
181
- | `review-again` | Review incomplete (`complete` not true, passes, unresolved incident, no checks, P2 with neither a URL nor a dropbox id, wrong HEAD, or a skip the rerun no longer confirms): resolve any incident, then resume or spawn the reviewer for the same round; start no fix round |
204
+ | `review-again` | Review incomplete (`complete` not true, passes, unresolved incident, no checks, P2 with neither a URL nor a dropbox id, wrong HEAD, or a skip the rerun no longer confirms): resolve any incident, then [resume the same reviewer, or replace it and say what changes](#resume-an-incomplete-review), for the same round; start no fix round |
182
205
  | (exit 2, no verdict) | The script could not run: fix the command or path |
183
206
 
184
207
  ## What blocks
@@ -4,7 +4,7 @@ description: "Slash/explicit-only (/resolve-release). Full GitHub Flow release f
4
4
  disable-model-invocation: true
5
5
  metadata:
6
6
  workflow-role: coordinator
7
- release-version: 0.46.0
7
+ release-version: 0.47.0
8
8
  ---
9
9
  **Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
10
10
  # Resolve release
@@ -6,7 +6,7 @@ metadata:
6
6
  workflow-role: coordinator
7
7
  dependencies:
8
8
  - resolve-issues
9
- release-version: 0.46.0
9
+ release-version: 0.47.0
10
10
  ---
11
11
  **Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
12
12
  # Rush issues (shortcut of resolve-issues)
@@ -6,7 +6,7 @@ metadata:
6
6
  workflow-role: coordinator
7
7
  dependencies:
8
8
  - resolve-release
9
- release-version: 0.46.0
9
+ release-version: 0.47.0
10
10
  ---
11
11
  **Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
12
12
  # Rush release (shortcut of resolve-release)
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: test-quality
3
3
  description: "Seven principles for tests that catch real bugs, and an audit that reports weak, redundant, and not-coverage tests without fixing them. Use when writing, changing, or reviewing tests, or when asked whether tests are useful, redundant, over-mocked, flaky, only assert static text, or test config files, or asked to make a slow test suite faster without losing coverage, or when the user asks to write the testing conventions into a repo's CONTRIBUTING.md. NOT defining what a spec must prove (ensure-coverage) or authoring browser tests (e2e-test)."
4
- release-version: 0.46.0
4
+ release-version: 0.47.0
5
5
  ---
6
6
  **Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
7
7
  # Test Quality
@@ -82,6 +82,8 @@ node "$SKILL_DIR/scripts/persist-conventions.mjs" --write # write, once the us
82
82
 
83
83
  It writes the seven principles ([references/testing-conventions.md](references/testing-conventions.md)) as the `## Testing conventions` section of `CONTRIBUTING.md`, between `conventions:testing` markers: a re-run replaces only that section and keeps everything else, including the `## CI conventions` and `## Delivery` sections `github-flow` writes. It adds a pointer to `AGENTS.md` unless that already names `CONTRIBUTING.md`, and creates a missing `CLAUDE.md` as a symlink to `AGENTS.md` (a regular `CLAUDE.md` that names neither file gets the pointer); missing files are created. `CONTRIBUTING.md` is the file `code-review` reads as a standards source, so the principles then apply in review too. Report each file's result line; committing is the user's call.
84
84
 
85
+ The same run also adds one `## Testing task entry` rule to `AGENTS.md`, between `conventions:testing-task-entry` markers: when writing, changing, or reviewing tests, read and apply this skill. It is a separate check from the guide pointer — a repo whose `AGENTS.md`, or whose regular `CLAUDE.md`, already names `CONTRIBUTING.md` still gets it, because a link says where the rules live, not that the skill is loaded. The rule routes to this skill and states no new requirements; the principles stay in `CONTRIBUTING.md`. An entrypoint that already loads `test-quality`, an unmanaged section with that heading, or malformed markers is preserved and reported instead of duplicated, and a `CLAUDE.md` that points at `AGENTS.md` inherits the rule rather than a copy. Repeating the run changes nothing.
86
+
85
87
  ## The lint
86
88
 
87
89
  Scripts live under **this skill's directory**: set `SKILL_DIR` to the directory you read this file from, never your CWD. `node "$SKILL_DIR/scripts/lint-tests.mjs"` scans the tree; `--diff <base>` only lines changed since the merge-base. A tool-local **gate** rule fails the command's exit status; it is not automatically a repository merge requirement. Assess optional hits in context and preserve existing adopted gates. Rules, flags, and what a clean run proves (little, for Python and Go): [references/lint.md](references/lint.md).
@@ -13,6 +13,12 @@
13
13
  // A section sits between `conventions:<id>` markers, so a re-run replaces only
14
14
  // that block and keeps everything else, including other skills' sections.
15
15
  // Nothing is written without --write: the user triggers the write, not an agent.
16
+ //
17
+ // The guide pointer and a skill's task-entry loader are independent. A link to
18
+ // CONTRIBUTING.md says where the policy lives; it does not load the skill that applies
19
+ // it. So a repo whose AGENTS.md (or regular CLAUDE.md) already names the guide still
20
+ // gets the loader, and a repo that already loads the skill keeps its own rule.
21
+ // engineering's init.mjs reuses `withTaskEntries` for its own two loaders.
16
22
 
17
23
  import fs from 'node:fs'
18
24
  import path from 'node:path'
@@ -20,6 +26,8 @@ import path from 'node:path'
20
26
  export const CONTRIBUTING_FILE = 'CONTRIBUTING.md'
21
27
  const MENTIONS_CONTRIBUTING = /(?:^|[^\w-])CONTRIBUTING\.md/
22
28
 
29
+ const TASK_ENTRY = 'task-entry'
30
+
23
31
  export const AGENTS_SECTION = [
24
32
  '## Contributing',
25
33
  '',
@@ -69,26 +77,218 @@ export function renderClaude(current) {
69
77
  return `${current.replace(/\s*$/, '')}\n\n${AGENTS_SECTION}`
70
78
  }
71
79
 
72
- export function persist(repoRoot, section, { write = false } = {}) {
73
- const block = sectionBlock(section)
74
- const plan = [
75
- [CONTRIBUTING_FILE, (text) => renderConventions(text, section.id, block)],
76
- ['AGENTS.md', renderAgents],
77
- ['CLAUDE.md', renderClaude],
80
+ export function taskEntryMarkers(id) {
81
+ return [`<!-- conventions:${id}-${TASK_ENTRY}:start -->`, `<!-- conventions:${id}-${TASK_ENTRY}:end -->`]
82
+ }
83
+
84
+ // Pure: the managed loader block. A short rule in an entrypoint; the conventions it
85
+ // points at stay in CONTRIBUTING.md.
86
+ export function taskEntryBlock({ id, title, body }) {
87
+ const [start, end] = taskEntryMarkers(id)
88
+ const recovery = body.includes('After context loss') ? '' : '\nAfter context loss, reload this skill\'s rules before continuing.'
89
+ return `${start}\n## ${title}\n\n${body.trim()}${recovery}\n${end}\n`
90
+ }
91
+
92
+ const escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
93
+ function withoutExamples(text) {
94
+ let fence = null
95
+ let comment = false
96
+ const active = text.split('\n').map(line => {
97
+ if (fence) {
98
+ const match = /^ {0,3}(`{3,}|~{3,})/.exec(line)
99
+ if (match && match[1][0] === fence[0] && match[1].length >= fence.length
100
+ && line.slice(match[0].length).trim() === '') fence = null
101
+ return ''
102
+ }
103
+ let masked = ''
104
+ let cursor = 0
105
+ while (cursor < line.length) {
106
+ const marker = line.indexOf(comment ? '-->' : '<!--', cursor)
107
+ const end = marker < 0 ? line.length : marker
108
+ masked += comment ? ' '.repeat(end - cursor) : line.slice(cursor, end)
109
+ if (marker < 0) break
110
+ const length = comment ? 3 : 4
111
+ masked += ' '.repeat(length)
112
+ cursor = marker + length
113
+ comment = !comment
114
+ }
115
+ const match = /^ {0,3}(`{3,}|~{3,})/.exec(masked)
116
+ if (match) { fence = match[1]; return '' }
117
+ return /^(?: {4}|\t|\s*>)/.test(line) ? '' : line
118
+ }).join('\n')
119
+ if (fence) throw new Error('unclosed Markdown code fence; repair guidance before setup')
120
+ if (comment) {
121
+ throw new Error('unclosed HTML comment; repair guidance before setup')
122
+ }
123
+ return active
124
+ }
125
+
126
+ // Keep code-form skill names, but not inline examples containing whole instructions.
127
+ const visible = (text) => withoutExamples(text).replace(/<!--[\s\S]*?-->/g, '')
128
+ .replace(/(`+)([^`]*?)\1/g, (span, _ticks, content) => /\s/.test(content) ? '' : span)
129
+
130
+ function hasHeading(text, title) {
131
+ return new RegExp(`^#{1,6}\\s+${escape(title)}\\s*$`, 'm').test(visible(text))
132
+ }
133
+
134
+ // A hand-written equivalent ("read and apply the `test-quality` skill") already loads
135
+ // the skill, so the managed block would repeat it. A guide link never matches.
136
+ function loadsSkill(text, skill, contexts = []) {
137
+ const name = escape(skill)
138
+ const instruction = new RegExp(`\\b(?:read|load|apply|use|follow)\\s+(?:and\\s+(?:apply|read)\\s+)?(?:(?:the|installed)\\s+){0,2}(?:\`${name}\`|${name}\\s+skills?\\b)`, 'i')
139
+ return visible(text).split(/\n\s*\n/).some(paragraph =>
140
+ instruction.test(paragraph)
141
+ && (!contexts.length || contexts.every(word => new RegExp(`\\b${escape(word)}\\b`, 'i').test(paragraph))))
142
+ }
143
+
144
+ // Pure: `{ text, conflicts }` — each loader's block added once, where it is missing.
145
+ // An equivalent existing rule is preserved; malformed markers and an unmanaged section
146
+ // under the same heading are reported instead of written over.
147
+ export function withTaskEntries(current, loaders = []) {
148
+ let text = current
149
+ const conflicts = []
150
+ for (const loader of loaders) {
151
+ const body = visible(text)
152
+ const denied = new RegExp(`\\b(?:do not|don't|never|must not)\\s+(?:(?:automatically|ever)\\s+)?(?:read|load|apply|use|follow)\\b[^.\\n]{0,120}\\b${escape(loader.skill)}\\b`, 'i')
153
+ if (denied.test(body)) {
154
+ conflicts.push(`an existing rule forbids loading "${loader.skill}"; reconcile it before setup`)
155
+ continue
156
+ }
157
+ const restricted = new RegExp(`\\b${escape(loader.skill)}\\b[^.\\n]{0,100}\\bonly\\s+(?:when|if)\\s+(?:explicitly|the user)`, 'i')
158
+ if (restricted.test(body)) {
159
+ conflicts.push(`an existing explicit-only rule restricts "${loader.skill}"; reconcile it before setup`)
160
+ continue
161
+ }
162
+ if (loader.contexts?.length && visible(text).split(/\n\s*\n/).some(paragraph =>
163
+ loadsSkill(paragraph, loader.skill) && /\b(?:for|during)\b[^.\n]{0,80}\bonly\s*[,;:]/i.test(paragraph))) {
164
+ conflicts.push(`an existing task-only rule restricts "${loader.skill}"; reconcile it before setup`)
165
+ continue
166
+ }
167
+ const [start, end] = taskEntryMarkers(loader.id)
168
+ const active = withoutExamples(text)
169
+ const starts = active.split(start).length - 1
170
+ const ends = active.split(end).length - 1
171
+ if (starts || ends) {
172
+ // Already written: a re-run leaves the block as it is rather than refreshing it.
173
+ if (starts !== 1 || ends !== 1 || active.indexOf(start) > active.indexOf(end)) {
174
+ conflicts.push(`malformed "${loader.title}" markers; repair them by hand`)
175
+ } else if (!loadsSkill(active.slice(active.indexOf(start), active.indexOf(end)), loader.skill, loader.contexts)) {
176
+ conflicts.push(`the preserved "${loader.title}" block has no applicable skill loader; reconcile it before setup`)
177
+ }
178
+ continue
179
+ }
180
+ if (loadsSkill(text, loader.skill, loader.contexts)) continue
181
+ if (hasHeading(text, loader.title)) {
182
+ conflicts.push(`an existing "${loader.title}" section needs manual reconciliation; no competing block added`)
183
+ continue
184
+ }
185
+ text = `${text.replace(/\s*$/, '')}\n\n${taskEntryBlock(loader)}`
186
+ }
187
+ return { text, conflicts }
188
+ }
189
+
190
+ function indexEntries(section) {
191
+ const loaders = section.loader ? [section.loader] : []
192
+ return [
193
+ { name: 'AGENTS.md', render: renderAgents, loaders },
194
+ // A regular CLAUDE.md that already points at AGENTS.md inherits that file's loaders.
195
+ { name: 'CLAUDE.md', render: renderClaude, loaders, reachesAgents: true },
78
196
  ]
79
- return plan.map(([name, render]) => {
80
- const file = path.join(repoRoot, name)
81
- if (fs.lstatSync(file, { throwIfNoEntry: false })?.isSymbolicLink()) return { file: name, action: 'unchanged' }
82
- const current = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null
83
- const next = render(current)
84
- const action = current == null ? 'create' : next === current ? 'unchanged' : 'update'
197
+ }
198
+
199
+ function reachesSelectedAgents(text, repoRoot, file) {
200
+ const body = visible(text)
201
+ const expected = path.join(repoRoot, 'AGENTS.md')
202
+ const canonical = name => fs.existsSync(name) ? fs.realpathSync(name) : path.resolve(name)
203
+ const affirmative = body.split('\n').filter(line => !/\b(?:do not|don't|never|must not)\b/i.test(line)).join('\n')
204
+ const references = [...affirmative.matchAll(/\[[^\]]*\]\((?:<([^>]+)>|([^\s)]+))\)/g)]
205
+ .map(match => match[1] ?? match[2])
206
+ for (const match of affirmative.matchAll(/\b(?:read|load|follow|see|consult)\s+`?((?:[^\s`]+\/)?AGENTS\.md)`?/gi)) {
207
+ references.push(match[1])
208
+ }
209
+ return references.some(ref => {
210
+ if (/^[a-z][a-z\d+.-]*:|^\/\//i.test(ref)) return false
211
+ try {
212
+ const target = path.resolve(path.dirname(file), decodeURIComponent(ref.split(/[?#]/)[0]))
213
+ return canonical(target) === canonical(expected)
214
+ } catch { return false }
215
+ })
216
+ }
217
+
218
+ function apply(repoRoot, entries, write) {
219
+ repoRoot = fs.realpathSync(repoRoot)
220
+ const readText = file => new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(fs.readFileSync(file))
221
+ const plannedContent = new Map()
222
+ const plan = entries.map((entry) => {
223
+ const file = path.join(repoRoot, entry.name)
224
+ if (fs.lstatSync(file, { throwIfNoEntry: false })?.isSymbolicLink()) {
225
+ let conflicts = []
226
+ try {
227
+ const target = fs.realpathSync(file)
228
+ const relative = path.relative(repoRoot, target)
229
+ if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
230
+ conflicts = ['linked entrypoint is outside the selected project; verify its task loader separately']
231
+ } else {
232
+ const current = plannedContent.get(target) ?? readText(file)
233
+ const checked = withTaskEntries(current, entry.loaders)
234
+ conflicts = checked.conflicts
235
+ if (checked.text !== current && !(entry.reachesAgents && reachesSelectedAgents(current, repoRoot, target))) {
236
+ conflicts.push('linked entrypoint lacks the task loader; explicit reconciliation is required')
237
+ }
238
+ }
239
+ } catch { conflicts = ['linked entrypoint could not be verified; setup is partial'] }
240
+ return { file: entry.name, action: 'unchanged', conflicts }
241
+ }
242
+ const current = fs.existsSync(file) ? readText(file) : null
243
+ const base = entry.render(current)
244
+ let next = base
245
+ let conflicts = []
246
+ if (base !== LINK_TO_AGENTS) {
247
+ const applied = withTaskEntries(base, entry.loaders)
248
+ const inherits = entry.reachesAgents && reachesSelectedAgents(current ?? '', repoRoot, file)
249
+ next = inherits ? base : applied.text
250
+ conflicts = applied.conflicts
251
+ }
85
252
  if (next === LINK_TO_AGENTS) {
86
- if (write) fs.symlinkSync('AGENTS.md', file)
87
- return { file: name, action: 'link' }
253
+ return { file: entry.name, action: 'link', conflicts, next }
254
+ }
255
+ const action = current == null ? 'create' : next === current ? 'unchanged' : 'update'
256
+ if (current !== null && action !== 'unchanged' && fs.statSync(file).nlink > 1) {
257
+ conflicts.push('hard-linked guidance cannot be changed without exclusive ownership')
88
258
  }
89
- if (write && action !== 'unchanged') fs.writeFileSync(file, next)
90
- return { file: name, action }
259
+ plannedContent.set(file, next)
260
+ return { file: entry.name, action, conflicts, next }
91
261
  })
262
+ const blocked = plan.some(entry => entry.conflicts.length)
263
+ if (write && !blocked) {
264
+ for (const entry of plan.filter(entry => entry.action !== 'unchanged')) {
265
+ const file = path.join(repoRoot, entry.file)
266
+ fs.accessSync(entry.action === 'create' || entry.action === 'link' ? repoRoot : file, fs.constants.W_OK)
267
+ }
268
+ for (const entry of plan) {
269
+ const file = path.join(repoRoot, entry.file)
270
+ if (entry.next === LINK_TO_AGENTS) fs.symlinkSync('AGENTS.md', file)
271
+ else if (entry.action !== 'unchanged') fs.writeFileSync(file, entry.next)
272
+ }
273
+ }
274
+ return plan.map(({ next: _next, ...entry }) => ({
275
+ ...entry, action: blocked && entry.action !== 'unchanged' ? 'blocked' : entry.action,
276
+ }))
277
+ }
278
+
279
+ export function persist(repoRoot, section, { write = false } = {}) {
280
+ const block = sectionBlock(section)
281
+ const entries = [
282
+ { name: CONTRIBUTING_FILE, render: (text) => renderConventions(text, section.id, block), loaders: [] },
283
+ ...indexEntries(section),
284
+ ]
285
+ return apply(repoRoot, entries, write)
286
+ }
287
+
288
+ // The index half alone: for a section that is already present, only the guide pointer
289
+ // and the task-entry loader are due.
290
+ export function persistIndex(repoRoot, section, { write = false } = {}) {
291
+ return apply(repoRoot, indexEntries(section), write)
92
292
  }
93
293
 
94
294
  // `section` = { id, owner, title, body }. Exit 0 on success, 2 on a usage error.
@@ -107,12 +307,22 @@ export function runCli(argv, section, { cwd = process.cwd(), log = console.log }
107
307
  log(`persist-conventions: not a directory: ${repoRoot}`)
108
308
  return 2
109
309
  }
110
- const results = persist(repoRoot, section, { write })
111
- const done = { create: 'created', link: 'linked to AGENTS.md', update: 'updated', unchanged: 'unchanged' }
112
- for (const r of results) log(` ${r.file}: ${write || r.action === 'unchanged' ? done[r.action] : `would ${r.action}`}`)
113
- const changed = results.filter((r) => r.action !== 'unchanged').length
310
+ let results
311
+ try { results = persist(repoRoot, section, { write }) }
312
+ catch (error) { log(`persist-conventions: setup refused: ${error.message}`); return 2 }
313
+ const done = { create: 'created', link: 'linked to AGENTS.md', update: 'updated', unchanged: 'unchanged', blocked: 'blocked' }
314
+ const conflicts = []
315
+ for (const r of results) {
316
+ for (const conflict of r.conflicts ?? []) conflicts.push(`${r.file}: ${conflict}`)
317
+ log(` ${r.file}: ${write || r.action === 'unchanged' ? done[r.action] : `would ${r.action}`}`)
318
+ }
319
+ const changed = results.filter((r) => !['unchanged', 'blocked'].includes(r.action)).length
114
320
  log(write
115
321
  ? `persist-conventions: ${section.title}: ${changed} file(s) written, ${results.length - changed} unchanged`
116
322
  : `persist-conventions: ${section.title}: preview only, ${changed} file(s) would change; re-run with --write to write`)
117
- return 0
323
+ if (conflicts.length) {
324
+ for (const conflict of conflicts) log(` conflict: ${conflict}`)
325
+ log(`persist-conventions: ${section.title}: ${conflicts.length} conflict(s); no files written; reconcile before setup`)
326
+ }
327
+ return conflicts.length ? 2 : 0
118
328
  }
@@ -1,6 +1,7 @@
1
1
  // persist-conventions — the test principles as the `## Testing conventions`
2
2
  // section of a repo's CONTRIBUTING.md (../SKILL.md, Persist mode). Run only when
3
- // the user asks; previews unless --write.
3
+ // the user asks; previews unless --write. --write also adds the skill's task-entry
4
+ // loader to AGENTS.md (and to a regular CLAUDE.md that does not reach AGENTS.md).
4
5
  //
5
6
  // node persist-conventions.mjs [--repo-root <dir>] [--write]
6
7
  import fs from 'node:fs'
@@ -12,6 +13,17 @@ const SECTION = {
12
13
  owner: 'test-quality',
13
14
  title: 'Testing conventions',
14
15
  body: fs.readFileSync(fileURLToPath(new URL('../references/testing-conventions.md', import.meta.url)), 'utf8'),
16
+ loader: {
17
+ id: 'testing',
18
+ skill: 'test-quality',
19
+ title: 'Testing task entry',
20
+ body: [
21
+ 'When writing, changing, or reviewing tests, read and apply the installed',
22
+ '`test-quality` skill before choosing test shape or adding a gate. The',
23
+ "repository's testing rules stay in its contributor guide; loading the skill",
24
+ 'adds no new requirement.',
25
+ ].join('\n'),
26
+ },
15
27
  }
16
28
 
17
29
  process.exitCode = runCli(process.argv.slice(2), SECTION)
@@ -2,7 +2,7 @@
2
2
  name: compliance-audit
3
3
  version: 2.9.0
4
4
  description: "Audit operations against financial-compliance checklists (AML/KYC, VARS, PCI-DSS, TPA oversight, onboarding, transaction monitoring). Produces tracker.md (with a tracker.xlsx export) and phase-trail.md with layered validation; cites evidence by original filename. Input: reg-parser or the built-in VARS checklist."
5
- release-version: 0.46.0
5
+ release-version: 0.47.0
6
6
  ---
7
7
  **Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
8
8
  # Compliance Audit
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: contract-compare
3
3
  description: "Compare two or more contracts side-by-side for structured diff, gap analysis, and benchmarking. Use when: comparing contracts or agreements, contract diff, compare terms, vendor-vs-vendor, compare our template vs theirs, compliance gap between contracts. NOT for single-contract risk review — use contract-review for that."
4
- release-version: 0.46.0
4
+ release-version: 0.47.0
5
5
  ---
6
6
  **Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
7
7
  # Contract Compare
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: contract-review
3
3
  description: "Review contracts for legal, commercial, and compliance risks via convert → chunk review → merge → validate (+ optional mandate preprocessing). Use for reviewing, auditing, or checking any contract, agreement, NDA, MSA, or SLA; input a file path (PDF/DOCX/TXT) or pasted text."
4
- release-version: 0.46.0
4
+ release-version: 0.47.0
5
5
  ---
6
6
  **Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
7
7
  # Contract Review
@@ -3,7 +3,7 @@ name: doc-authenticity
3
3
  version: 0.4.1
4
4
  description: "Screen PDF documents and raster images for fabrication, tampering, Photoshop or other image-editor use, and content validity. Detects file type, runs format-specific authenticity forensics, classifies bank statements, conversation logs, invoices, agreements, and other documents, then applies type-specific validity checks. Produces separate authenticity (NO_EVIDENCE_OF_MODIFICATION / SUSPICIOUS / LIKELY_MODIFIED / INCONCLUSIVE) and validity (VALID / INVALID / INCONCLUSIVE) verdicts. Use whenever a user asks whether a document, screenshot, scan, statement, invoice, contract, chat log, receipt, or payment evidence is genuine, original, edited, Photoshopped, manipulated, fake, or valid."
5
5
  disable-model-invocation: true
6
- release-version: 0.46.0
6
+ release-version: 0.47.0
7
7
  ---
8
8
  **Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
9
9
  # Document and Image Authenticity
@@ -12,7 +12,7 @@ description: >
12
12
  impact, and a business report. Use for fraud, scam, mule, account-takeover,
13
13
  chargeback, dispute, transfer, collection, or payout risk analysis even when the
14
14
  user does not name this skill.
15
- release-version: 0.46.0
15
+ release-version: 0.47.0
16
16
  ---
17
17
  **Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
18
18
  # Fraud Detection
@@ -14,7 +14,7 @@ description: >
14
14
  transactions" — even if they do not say payment-analysis. Do NOT use for
15
15
  fraud rule discovery (fraud-detection) or TLD / MCC-miscoding
16
16
  clusters (tld-detection).
17
- release-version: 0.46.0
17
+ release-version: 0.47.0
18
18
  ---
19
19
  **Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
20
20
  # Payment Analysis