infinity-harness 2.0.3 → 2.0.4

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/CHANGELOG.md CHANGED
@@ -4,6 +4,19 @@ All notable changes to this project are documented here.
4
4
  Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow
5
5
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [2.0.4] — 2026-08-23
8
+
9
+ ### Changed
10
+
11
+ - **A domain skill now has to be pulled in by the task's own vocabulary.** Run against a real project
12
+ the matcher offered `cli-design` for *"serialise plan writes so two workers cannot race on the
13
+ lock"* — because the goal above it read "ship the payments rewrite behind a flag", and `flags` is
14
+ on that skill's tag list. One incidental word is not vocabulary. A `domain` or `meta` skill must
15
+ now clear the bar on tag hits alone; the phase ranks it but never qualifies it. `process` skills
16
+ are unchanged — belonging to the phase is the whole point of them.
17
+
18
+ ---
19
+
7
20
  ## [2.0.3] — 2026-08-23
8
21
 
9
22
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infinity-harness",
3
- "version": "2.0.3",
3
+ "version": "2.0.4",
4
4
  "description": "A pi agent extension that runs a gated build pipeline unattended \u2014 enforces phases, validates with deterministic gates, and keeps working for hours or days without losing the plan.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -330,25 +330,26 @@ export function matchSkills(skills: SkillMeta[], options: MatchOptions = {}): Sk
330
330
  if (hit && !hits.includes(t)) hits.push(t);
331
331
  }
332
332
 
333
- const position = phase ? skill.phases.indexOf(phase) : -1;
334
- const phaseCounts = position >= 0 && (skill.kind === "process" || hits.length > 0);
335
- if (phaseCounts) {
336
- score += Math.max(1, PHASE_WEIGHT - position);
337
- reasons.push(`${phase} phase`);
333
+ // `meta` is on every meta skill's tag list, so counting it like any other
334
+ // hit would surface all of them constantly. They have to be asked for.
335
+ const counted = skill.kind === "meta" ? hits.filter((h) => h !== "meta") : hits;
336
+ const tagScore = TAG_WEIGHT * Math.min(counted.length, MAX_TAG_HITS);
337
+
338
+ // A domain skill has to be pulled in by the task's own vocabulary, and one
339
+ // incidental word is not vocabulary. "Ship the payments rewrite behind a
340
+ // flag" should not summon the CLI-design skill because `flags` is on its
341
+ // tag list. The phase ranks a domain skill; it never qualifies one.
342
+ if (skill.kind !== "process" && tagScore < MIN_SCORE) continue;
343
+
344
+ score += tagScore;
345
+ if (counted.length) {
346
+ reasons.push(`matches ${counted.slice(0, MAX_TAG_HITS).map((h) => `"${h}"`).join(", ")}`);
338
347
  }
339
348
 
340
- if (hits.length && skill.kind !== "meta") {
341
- score += TAG_WEIGHT * Math.min(hits.length, MAX_TAG_HITS);
342
- } else if (hits.length) {
343
- // A meta skill has to be asked for: `meta` is on all of their tag lists,
344
- // so counting it like any other hit would surface all four constantly.
345
- const real = hits.filter((h) => h !== "meta");
346
- if (real.length === 0) continue;
347
- score += TAG_WEIGHT * Math.min(real.length, MAX_TAG_HITS);
348
- }
349
- if (hits.length) {
350
- const shown = hits.filter((h) => h !== "meta" || skill.kind !== "meta").slice(0, MAX_TAG_HITS);
351
- if (shown.length) reasons.push(`matches ${shown.map((h) => `"${h}"`).join(", ")}`);
349
+ const position = phase ? skill.phases.indexOf(phase) : -1;
350
+ if (position >= 0 && (skill.kind === "process" || counted.length > 0)) {
351
+ score += Math.max(1, PHASE_WEIGHT - position);
352
+ reasons.unshift(`${phase} phase`);
352
353
  }
353
354
 
354
355
  // Naming the skill in the task means it, and outranks any guess.