synergyspec-selfevolving 1.1.12 → 1.1.13

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.
@@ -1,7 +1,7 @@
1
1
  import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
3
  import fastGlob from 'fast-glob';
4
- import { aggregateLearnEvolutionHints, applyCandidatePromotion, rollbackCandidatePromotion, shouldAutoPromote, isEvidenceComplete, generateEvolutionHints, persistLearnHints, readCandidateFitness, readHealthBaseline, writeHealthBaseline, readCandidatePackage, resolveTargetLocalFiles, CANONICAL_CANDIDATE_SOURCES, CANONICAL_TARGETS, collectArchiveExperiences, EVOLVABLE_PART_DESCRIPTIONS, EVOLVABLE_PARTS, evaluateTaskDecompositionForChange, evaluateToolEvolutionCandidate, generateCandidateId, generatePromotionReport, groupCandidatesByTarget, rankCandidatesForTarget, makeReplayRunChange, scoreCandidatesByReplay, isEvolutionPartEnabled, findSimilarArchiveExperiences, listCanonicalTargets, lookupCanonicalTarget, runCanonicalProposerAgent, validateCandidateEdits, renderUnifiedDiff, CanonicalProposerNoOp, resolveTargetEvolutionPolicy, isCanonicalTargetEvolvable, parseEvolutionSwitchOptions, readTemplateVariantManifest, renderAlignmentReport, renderArchiveExperienceBlock, renderStaticGateSummary, renderToolEvolutionGuardReport, renderEvolutionSwitches, requireCanonicalTarget, resolveCandidateRepo, runStaticCandidateGate, selectTemplateVariant, shouldTriggerCandidate, validateLearnEvolutionHint, writeCandidatePackage, verifySpecCodeAlignmentForChange, } from '../core/self-evolution/index.js';
4
+ import { aggregateLearnEvolutionHints, applyCandidatePromotion, rollbackCandidatePromotion, shouldAutoPromote, isEvidenceComplete, generateEvolutionHints, persistLearnHints, readCandidateFitness, readHealthBaseline, writeHealthBaseline, readCandidatePackage, resolveTargetLocalFiles, CANONICAL_CANDIDATE_SOURCES, CANONICAL_TARGETS, collectArchiveExperiences, EVOLVABLE_PART_DESCRIPTIONS, EVOLVABLE_PARTS, evaluateTaskDecompositionForChange, evaluateToolEvolutionCandidate, generateCandidateId, generatePromotionReport, groupCandidatesByTarget, rankCandidatesForTarget, makeReplayRunChange, scoreCandidatesByReplay, isEvolutionPartEnabled, findSimilarArchiveExperiences, listCanonicalTargets, lookupCanonicalTarget, runCanonicalProposerAgent, validateCandidateEdits, renderUnifiedDiff, CanonicalProposerNoOp, resolveTargetEvolutionPolicy, resolveKindOnlyPinTarget, detectUnbindableHintObservations, isCanonicalTargetEvolvable, parseEvolutionSwitchOptions, readTemplateVariantManifest, renderAlignmentReport, renderArchiveExperienceBlock, renderStaticGateSummary, renderToolEvolutionGuardReport, renderEvolutionSwitches, requireCanonicalTarget, resolveCandidateRepo, runStaticCandidateGate, selectTemplateVariant, shouldTriggerCandidate, validateLearnEvolutionHint, writeCandidatePackage, verifySpecCodeAlignmentForChange, } from '../core/self-evolution/index.js';
5
5
  import { generateLearnReport } from '../core/learn.js';
6
6
  import { resolveMetricSource } from '../core/fitness/index.js';
7
7
  import { validateChangeExists, validateSchemaExists } from './workflow/shared.js';
@@ -583,9 +583,34 @@ export async function runProposeCanonical(args, opts) {
583
583
  }
584
584
  return result;
585
585
  }
586
+ // Resolve the per-target evolution policy once (project config + CLI overrides),
587
+ // BEFORE aggregation so a kind-only hint can be re-pinned to the named
588
+ // --evolve-target. (issue #4 wired this pin into the learn path only; loading
589
+ // hints from disk in evolve-from-edits previously skipped it, so a legitimate
590
+ // kind-only hint could never bind to a concrete --evolve-target — the confusing
591
+ // "0 surviving hint group" refusal. See ses_15b0.)
592
+ const targetPolicy = resolveTargetEvolutionPolicy({
593
+ config: readProjectConfig(opts.repoRoot),
594
+ evolveTarget: args.evolveTarget,
595
+ freezeTarget: args.freezeTarget,
596
+ });
597
+ // Re-pin ONLY kind-only hints (no affectedTargetId) to the policy-resolved
598
+ // target, reusing the same pin logic + grouping-key formula as the learn path
599
+ // (so keys never double the kind prefix). Concrete hints are left UNTOUCHED —
600
+ // their evolvability is still enforced per-group below (frozen-target skip). A
601
+ // kind-only hint that cannot be uniquely pinned stays kind-only and surfaces as
602
+ // an unbindable defect rather than silently misbinding.
603
+ const scopedHints = hints.map((hint) => {
604
+ if (hint.affectedTargetId)
605
+ return hint;
606
+ const pinId = resolveKindOnlyPinTarget(hint, targetPolicy);
607
+ return typeof pinId === 'string'
608
+ ? { ...hint, affectedTargetId: pinId, thresholdKey: `${pinId}:${hint.proposedChangeType}` }
609
+ : hint;
610
+ });
586
611
  // 4) Aggregate. `aggregationOptions` lets auto-evolve act on a single change
587
612
  // (one forward pass = one loss); omitted = conservative cross-change defaults.
588
- const allGroups = aggregateLearnEvolutionHints(hints, args.aggregationOptions);
613
+ const allGroups = aggregateLearnEvolutionHints(scopedHints, args.aggregationOptions);
589
614
  // 5) Filter.
590
615
  const skipped = [];
591
616
  let surviving;
@@ -641,18 +666,27 @@ export async function runProposeCanonical(args, opts) {
641
666
  if (path.resolve(layout.baseDir) !== path.resolve(expectedBase)) {
642
667
  throw new Error(`Candidate sandbox boundary violated: resolved baseDir=${layout.baseDir} but expected ${expectedBase}`);
643
668
  }
644
- // Resolve the per-target evolution policy once (project config + CLI overrides).
645
- const targetPolicy = resolveTargetEvolutionPolicy({
646
- config: readProjectConfig(opts.repoRoot),
647
- evolveTarget: args.evolveTarget,
648
- freezeTarget: args.freezeTarget,
649
- });
650
669
  // `--from-edits` carries ONE payload, so it can only map to a single hint
651
670
  // group / target. Require the caller to have narrowed to exactly one surviving
652
671
  // group (via --target / --threshold-key) so the edits are never misattributed.
653
672
  if (args.editsInput && surviving.length !== 1) {
654
- stderr(`--from-edits requires exactly one surviving hint group, but ${surviving.length} survived. ` +
655
- `Narrow with --target <id> or --threshold-key <key>.`);
673
+ if (surviving.length === 0) {
674
+ // Distinguish a BINDING DEFECT (a kind-only hint that could not be pinned to
675
+ // a concrete target) from a plain over-broad result, so the caller does not
676
+ // record a binding bug as "the gate correctly refused". See ses_15b0.
677
+ const unbindable = detectUnbindableHintObservations(scopedHints, targetPolicy);
678
+ if (unbindable.length > 0) {
679
+ stderr(unbindable.map((o) => o.summary).join(' '));
680
+ }
681
+ else {
682
+ stderr(`--from-edits requires exactly one surviving hint group, but 0 survived. ` +
683
+ `Narrow with --target <id> or --threshold-key <key>.`);
684
+ }
685
+ }
686
+ else {
687
+ stderr(`--from-edits requires exactly one surviving hint group, but ${surviving.length} survived. ` +
688
+ `Narrow with --target <id> or --threshold-key <key>.`);
689
+ }
656
690
  return {
657
691
  exitCode: 2,
658
692
  proposed,
@@ -14,4 +14,16 @@ export interface AIToolOption {
14
14
  skillsDir?: string;
15
15
  }
16
16
  export declare const AI_TOOLS: AIToolOption[];
17
+ /**
18
+ * Skill namespaces an installed project may carry SynergySpec-SelfEvolving
19
+ * workflow-prompt / skill files under, e.g. `.claude`, `.codex`, `.opencode`.
20
+ *
21
+ * Derived from {@link AI_TOOLS} so it can never drift from what the installer
22
+ * actually writes: the ArtifactSyncEngine writes each skill to
23
+ * `<skillsDir>/skills/<name>/SKILL.md`. The AGENTS.md option carries no
24
+ * `skillsDir`, so it is (correctly) excluded. Self-evolution local-target
25
+ * resolution, the tool-evolution path allowlist, and the skill-instruction
26
+ * canonical globs all consume this single source of truth.
27
+ */
28
+ export declare const SKILL_NAMESPACES: readonly string[];
17
29
  //# sourceMappingURL=config.d.ts.map
@@ -29,4 +29,16 @@ export const AI_TOOLS = [
29
29
  { name: 'Windsurf', value: 'windsurf', available: true, successLabel: 'Windsurf', skillsDir: '.windsurf' },
30
30
  { name: 'AGENTS.md (works with Amp, VS Code, …)', value: 'agents', available: false, successLabel: 'your AGENTS.md-compatible assistant' }
31
31
  ];
32
+ /**
33
+ * Skill namespaces an installed project may carry SynergySpec-SelfEvolving
34
+ * workflow-prompt / skill files under, e.g. `.claude`, `.codex`, `.opencode`.
35
+ *
36
+ * Derived from {@link AI_TOOLS} so it can never drift from what the installer
37
+ * actually writes: the ArtifactSyncEngine writes each skill to
38
+ * `<skillsDir>/skills/<name>/SKILL.md`. The AGENTS.md option carries no
39
+ * `skillsDir`, so it is (correctly) excluded. Self-evolution local-target
40
+ * resolution, the tool-evolution path allowlist, and the skill-instruction
41
+ * canonical globs all consume this single source of truth.
42
+ */
43
+ export const SKILL_NAMESPACES = Object.freeze(Array.from(new Set(AI_TOOLS.map((tool) => tool.skillsDir).filter((dir) => typeof dir === 'string' && dir.length > 0))));
32
44
  //# sourceMappingURL=config.js.map
@@ -190,16 +190,6 @@ export declare function structuredMemoryBody(args: {
190
190
  }): string;
191
191
  export declare function defaultRetrievalQueries(title: string, tags: string[] | undefined): string[];
192
192
  export declare function classifyCandidate(candidate: LearnMemoryCandidate): LearnMemoryCandidate;
193
- /**
194
- * Find lines in a verification artifact that look like UNRESOLVED failure
195
- * evidence. The hazard (the same prose-keyword trap as the trajectory runner
196
- * detector) is mistaking a PASSED negative-path scenario for a failure: a result
197
- * row `| UC3-E7a | Cleanup failure propagates | PASS | … |`, a table header
198
- * naming a `Counterexample` / `Regression Test` column, or a list item
199
- * `- PASS UC1-E1a: Open fails because …` all merely MENTION failure words while
200
- * reporting success. We therefore decide pass-ness structurally (table outcome
201
- * cell / PASS-prefixed list item / header row) before keyword-scanning the rest.
202
- */
203
193
  export declare function extractFailureEvidence(file: ArtifactFile): Array<{
204
194
  file: string;
205
195
  line: string;
@@ -196,9 +196,12 @@ function buildLearnObservations(report) {
196
196
  // (a) Candidates carrying a raw template/workflow signal.
197
197
  for (const candidate of report.memoryCandidates) {
198
198
  const hasTemplateTag = candidate.tags.some((tag) => /^(template|workflow):/i.test(tag));
199
- const hasTemplateCode = candidate.sourceConclusionCodes.some((code) => code.startsWith('reuse.artifact-chain') ||
200
- code.startsWith('avoid.template-') ||
201
- code.startsWith('workflow.'));
199
+ // Only genuine `workflow.*` defects raise a template/workflow signal. `reuse.*`
200
+ // / `avoid.*` are COMPLIANCE signals (artifact hygiene), not defects, and must
201
+ // not emit a vacuous `template-signal` reflection (ses_15b0 category error). The
202
+ // explicit `template:` / `workflow:` tags (and `template-observation`) below
203
+ // remain the operator-intended path.
204
+ const hasTemplateCode = candidate.sourceConclusionCodes.some((code) => code.startsWith('workflow.'));
202
205
  if (candidate.tags.includes('template-observation') || hasTemplateTag || hasTemplateCode) {
203
206
  const files = candidate.links
204
207
  .map((link) => toPosix(link))
@@ -1446,6 +1449,61 @@ const FAIL_CELL_RE = /^(?:fail(?:ed|s|ing|ure)?|error(?:ed|s)?|blocked|incomplet
1446
1449
  * reporting success. We therefore decide pass-ness structurally (table outcome
1447
1450
  * cell / PASS-prefixed list item / header row) before keyword-scanning the rest.
1448
1451
  */
1452
+ // A failure-ish keyword group: matching one of these is NECESSARY but not
1453
+ // sufficient to call a prose line failure evidence (see isNegatedSuccessLine).
1454
+ const FAILURE_KEYWORD_RE = /\b(fail(?:ed|ing|s|ure)?|errors?|blocked|incomplete|missing|regress(?:ion|ed|ing)?)\b/i;
1455
+ // A concrete positive count next to a failure word — "2 tests failed", "3
1456
+ // errors", "1 missing requirement". A non-zero integer within a few words of a
1457
+ // failure keyword is a GENUINE failure even if a negator appears elsewhere on
1458
+ // the line, so such lines are never treated as negated successes.
1459
+ const POSITIVE_FAILURE_COUNT_RE = /\b[1-9]\d*\s+(?:\S+\s+){0,3}?(?:fail(?:ed|ing|s|ure)?|errors?|blocked|incomplete|missing|regress(?:ion|ed|ing)?)\b/i;
1460
+ // Leading negators that, when they govern a nearby failure word, scope it as a
1461
+ // NON-failure ("No tests failed", "no incomplete checkboxes", "0 errors").
1462
+ const NEGATOR_TOKEN_RE = /^(?:no|none|zero|0|without|never)$/i;
1463
+ /** Every failure keyword in `clause` has a negator within the preceding 4 tokens. */
1464
+ function everyFailureWordNegated(clause) {
1465
+ const tokens = clause.split(/\s+/).filter(Boolean);
1466
+ for (let i = 0; i < tokens.length; i++) {
1467
+ if (!FAILURE_KEYWORD_RE.test(tokens[i]))
1468
+ continue;
1469
+ let negated = false;
1470
+ for (let j = Math.max(0, i - 4); j < i; j++) {
1471
+ if (NEGATOR_TOKEN_RE.test(tokens[j])) {
1472
+ negated = true;
1473
+ break;
1474
+ }
1475
+ }
1476
+ if (!negated)
1477
+ return false;
1478
+ }
1479
+ return true;
1480
+ }
1481
+ /**
1482
+ * True when a line merely MENTIONS failure words while reporting a clean result,
1483
+ * e.g. "No tests failed.", "No missing or incomplete tests remain.", "has no
1484
+ * incomplete checkboxes." Such negated-success prose is common in PASSING
1485
+ * verification reports and must NOT be flagged as failure evidence (ses_1583).
1486
+ *
1487
+ * Rule: split the line into clauses (sentence/clause punctuation, contrast
1488
+ * conjunctions, and dashes) and require that EVERY failure keyword in EVERY
1489
+ * clause is governed by a negator within the preceding few tokens of the SAME
1490
+ * clause. A single un-negated failure word — or a concrete positive failure
1491
+ * count anywhere — means the line is genuine failure evidence. Requiring a
1492
+ * NEAR-LEFT negator per keyword (not just one negator before the first keyword)
1493
+ * is what keeps real prose like "the deploy failed" or "the failing UC4 test"
1494
+ * flagged, where a leading negator scopes a different word.
1495
+ */
1496
+ function isNegatedSuccessLine(line) {
1497
+ if (!FAILURE_KEYWORD_RE.test(line))
1498
+ return false;
1499
+ if (POSITIVE_FAILURE_COUNT_RE.test(line))
1500
+ return false;
1501
+ const clauses = line
1502
+ .split(/[.!?;:,]+|\s(?:but|however|though|although|yet|except)\s|\s[-–—]\s/i)
1503
+ .map((clause) => clause.trim())
1504
+ .filter(Boolean);
1505
+ return clauses.every((clause) => everyFailureWordNegated(clause));
1506
+ }
1449
1507
  export function extractFailureEvidence(file) {
1450
1508
  const lines = file.content.split(/\r?\n/);
1451
1509
  const nextNonEmpty = (from) => {
@@ -1498,6 +1556,12 @@ export function extractFailureEvidence(file) {
1498
1556
  if (/\bwithout\s+(failures?|failed|errors?)\b/i.test(trimmed)) {
1499
1557
  continue;
1500
1558
  }
1559
+ if (isNegatedSuccessLine(trimmed)) {
1560
+ // A negated SUCCESS statement ("No tests failed.", "no incomplete
1561
+ // checkboxes", "No missing or incomplete tests remain") merely mentions
1562
+ // failure words while reporting a clean result (ses_1583). Not evidence.
1563
+ continue;
1564
+ }
1501
1565
  if (/\bpass(?:ed|es)?\b/i.test(trimmed) && !/\bfail(?:ed|ing|s|ure)?\b/i.test(trimmed)) {
1502
1566
  continue;
1503
1567
  }
@@ -18,6 +18,7 @@
18
18
  * canonical targets: they define the functional-correctness oracle and must
19
19
  * not self-evolve. See `docs/decisions/2026-05-30-oracle-freeze.md`.
20
20
  */
21
+ import { SKILL_NAMESPACES } from '../config.js';
21
22
  const WORKFLOW_PROMPT_NAMES = [
22
23
  'apply-change',
23
24
  'archive-change',
@@ -37,11 +38,10 @@ const WORKFLOW_PROMPT_NAMES = [
37
38
  'verify-spec',
38
39
  ];
39
40
  const ARTIFACT_TEMPLATE_NAMES = ['proposal', 'spec', 'design', 'tasks'];
40
- const SKILL_INSTRUCTION_GLOBS = Object.freeze([
41
- '.codex/skills/**/SKILL.md',
42
- '.agents/skills/**/SKILL.md',
43
- '.claude/skills/**/SKILL.md',
44
- ]);
41
+ // Cover every installed host skill namespace (derived from AI_TOOLS via
42
+ // SKILL_NAMESPACES) so the skill-instruction surface is not blind to opencode,
43
+ // cursor, gemini, … the way a hand-curated list was.
44
+ const SKILL_INSTRUCTION_GLOBS = Object.freeze(SKILL_NAMESPACES.map((ns) => `${ns}/skills/**/SKILL.md`));
45
45
  const workflowPromptTargets = WORKFLOW_PROMPT_NAMES.map((name) => Object.freeze({
46
46
  id: `workflow-prompt:${name}`,
47
47
  kind: 'workflow-prompt',
@@ -111,7 +111,7 @@ export const CANONICAL_TARGETS = Object.freeze([
111
111
  promotionPolicy: 'human-required',
112
112
  rollbackPolicy: 'Regenerate SKILL.md files from previous canonical workflow prompts.',
113
113
  versioning: 'file-version',
114
- notes: 'Glob entries cover .codex, .agents, and .claude skill namespaces; regenerated from workflow prompts.',
114
+ notes: 'Glob entries cover every installed skill namespace (derived from AI_TOOLS skillsDir values); regenerated from workflow prompts.',
115
115
  }),
116
116
  ]);
117
117
  /**
@@ -40,6 +40,13 @@ export declare function resolveTargetLocalFilesReadonly(targetId: string, repoRo
40
40
  * `target-evolution.ts` and the `add-per-target-evolution-switch` change.
41
41
  */
42
42
  export declare function generateEvolutionHints(report: LearnSignals, policy?: TargetEvolutionPolicy): LearnEvolutionHint[];
43
+ /**
44
+ * Decide the concrete target a kind-only hint should be pinned to under the
45
+ * policy. Returns the target id to pin to, `undefined` to keep the hint
46
+ * kind-only (ambiguous — caller leaves it `unspecified`), or `null` to drop the
47
+ * hint entirely (no evolvable target of its kind exists).
48
+ */
49
+ export declare function resolveKindOnlyPinTarget(draft: LearnEvolutionHint, policy: TargetEvolutionPolicy): string | undefined | null;
43
50
  /**
44
51
  * Surface an UNBINDABLE kind-only evolution hint as an actionable DEFECT
45
52
  * observation. After {@link scopeHintsByPolicy} runs, a hint that still has no
@@ -25,12 +25,11 @@ import { findCanonicalTargetsByFile, listCanonicalTargets, lookupCanonicalTarget
25
25
  import { isCanonicalTargetEvolvable, explicitTargetIds, } from './target-evolution.js';
26
26
  import { detectRepoMode, resolveTargetLocalFiles } from './local-targets.js';
27
27
  import { getSchemaDir } from '../artifact-graph/resolver.js';
28
+ import { SKILL_NAMESPACES } from '../config.js';
28
29
  import { limitText, } from '../learn.js';
29
30
  function toPosix(value) {
30
31
  return value.replace(/\\/g, '/');
31
32
  }
32
- /** Skill namespaces an installed project may carry the workflow prompts under. */
33
- const SKILL_NAMESPACES = ['.codex', '.claude', '.agents'];
34
33
  /** The config root the dogfood/user project uses for project-local overrides. */
35
34
  const CONFIG_ROOT = 'synergyspec-selfevolving';
36
35
  /**
@@ -151,6 +150,13 @@ export function generateEvolutionHints(report, policy) {
151
150
  };
152
151
  // Heuristic (a): Template/workflow observations recurring in memory candidates.
153
152
  for (const candidate of report.memoryCandidates) {
153
+ // The learn quality gate's verdict also gates evolution: a candidate it
154
+ // REJECTED (qualityScore below the keep bar) must not drive a canonical
155
+ // template/prompt edit. (ses_15b0: a reject-disposition `reuse.artifact-chain`
156
+ // candidate still produced a `confidence: high` hint.) Scoped to (a) only —
157
+ // Heuristic (b) below intentionally consumes low-quality candidates.
158
+ if (candidate.disposition === 'reject')
159
+ continue;
154
160
  const observation = inferTemplateObservation(candidate);
155
161
  if (!observation)
156
162
  continue;
@@ -343,7 +349,7 @@ function scopeHintsByPolicy(drafts, policy) {
343
349
  * kind-only (ambiguous — caller leaves it `unspecified`), or `null` to drop the
344
350
  * hint entirely (no evolvable target of its kind exists).
345
351
  */
346
- function resolveKindOnlyPinTarget(draft, policy) {
352
+ export function resolveKindOnlyPinTarget(draft, policy) {
347
353
  // (1) Authoritative operator intent: a single registered, same-kind target
348
354
  // named on the CLI via `--evolve-target` pins the hint even when config leaves
349
355
  // other same-kind targets evolvable. `isCanonicalTargetEvolvable` honors
@@ -431,12 +437,13 @@ function inferTemplateObservation(candidate) {
431
437
  return { kind: 'artifact-template' };
432
438
  }
433
439
  for (const code of candidate.sourceConclusionCodes) {
434
- if (code.startsWith('reuse.artifact-chain')) {
435
- return { kind: 'artifact-template' };
436
- }
437
- if (code.startsWith('avoid.template-')) {
438
- return { kind: 'artifact-template' };
439
- }
440
+ // Only genuine workflow defects (e.g. `workflow.learn-before-archive`) infer a
441
+ // kind-only target. `reuse.*` / `avoid.*` are COMPLIANCE signals (artifact
442
+ // hygiene — "this change HAS all its artifacts"), NOT template defects, so they
443
+ // must not manufacture a template hint. (ses_15b0: a `reuse.artifact-chain`
444
+ // candidate produced a content-free, unbindable `artifact-template:unspecified`
445
+ // hint.) The explicit `template:` / `workflow:` / `template-observation` tag
446
+ // branches above remain the operator-intended path for a concrete template hint.
440
447
  if (code.startsWith('workflow.')) {
441
448
  return { kind: 'workflow-prompt' };
442
449
  }
@@ -8,7 +8,8 @@
8
8
  * exist only in the SS dev repo. In an installed USER repo the agent actually
9
9
  * reads its LOCAL artifacts:
10
10
  * - workflow prompts → `.codex/skills/synergyspec-selfevolving-<name>/SKILL.md`
11
- * (+ `.claude/…`, `.agents/…`) already on disk.
11
+ * (+ `.claude/…`, `.opencode/…`, and every other host
12
+ * namespace in SKILL_NAMESPACES) — already on disk.
12
13
  * - artifact templates→ `synergyspec-selfevolving/schemas/<schema>/templates/<name>.md`
13
14
  * — package-only until MATERIALIZED here (the resolver's
14
15
  * project-local tier then wins, per getSchemaDir).
@@ -23,8 +24,7 @@ import { existsSync } from 'node:fs';
23
24
  import * as path from 'node:path';
24
25
  import { lookupCanonicalTarget } from './canonical-targets.js';
25
26
  import { getSchemaDir } from '../artifact-graph/resolver.js';
26
- /** Skill namespaces an installed project may carry the workflow prompts under. */
27
- const SKILL_NAMESPACES = ['.codex', '.claude', '.agents'];
27
+ import { SKILL_NAMESPACES } from '../config.js';
28
28
  /** The config root the dogfood/user project uses for project-local overrides. */
29
29
  const CONFIG_ROOT = 'synergyspec-selfevolving';
30
30
  /**
@@ -89,8 +89,8 @@ async function resolveUserModeFile(registryFile, repoRoot) {
89
89
  return [{ relPath: rel, absPath: abs, content, materialized: true }];
90
90
  }
91
91
  // Skill-instruction glob, or any other surface that is package-internal:
92
- // expand a `.codex/.claude/.agents/skills/**/SKILL.md` glob to present files;
93
- // everything else has no user-editable local artifact.
92
+ // expand a `<ns>/skills/**/SKILL.md` glob to present files (any host namespace
93
+ // in SKILL_NAMESPACES); everything else has no user-editable local artifact.
94
94
  if (/skills\/\*\*\/SKILL\.md$/.test(file)) {
95
95
  // Caller (resolveTargetLocalFiles) handles glob targets separately.
96
96
  return [];
@@ -1,5 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { clamp01, scoreFromFindings } from './shared.js';
3
+ import { SKILL_NAMESPACES } from '../config.js';
3
4
  import { isEvolutionPartEnabled, resolveEvolutionSwitches, } from './evolution-switches.js';
4
5
  const ALLOWED_PREFIXES = [
5
6
  'schemas/',
@@ -12,9 +13,10 @@ const ALLOWED_PREFIXES = [
12
13
  'synergyspec-selfevolving/specs/',
13
14
  'synergyspec-selfevolving/changes/',
14
15
  'synergyspec-selfevolving/schemas/',
15
- '.codex/skills/',
16
- '.claude/skills/',
17
- '.agents/skills/',
16
+ // Installed skill prompts across every supported host namespace (derived from
17
+ // AI_TOOLS so a real consumer's `.opencode/skills/…`, `.cursor/skills/…`, etc.
18
+ // promote without being rejected as a disallowed path).
19
+ ...SKILL_NAMESPACES.map((ns) => `${ns}/skills/`),
18
20
  ];
19
21
  const GENERATED_OR_VENDOR_PREFIXES = [
20
22
  'dist/',
@@ -262,7 +264,8 @@ function isSchemaOrTemplate(filePath) {
262
264
  /**
263
265
  * An artifact-template (`…/templates/<name>.md`), workflow-prompt template
264
266
  * (`src/core/templates/workflows/<name>.ts`), or an installed workflow-prompt
265
- * skill (`.<ns>/skills/<skill>/SKILL.md` for ns in codex/claude/agents). These
267
+ * skill (`<ns>/skills/<skill>/SKILL.md` for any host namespace in
268
+ * SKILL_NAMESPACES, e.g. .codex/.claude/.opencode). These
266
269
  * canonical-target files ARE the user-facing artifact/prompt contract, so a
267
270
  * template-only edit is a complete change and should not warn for "missing
268
271
  * companion spec/tests". A machine contract (`schema.yaml`, CLI) is deliberately
@@ -274,13 +277,15 @@ function isArtifactOrPromptTemplate(filePath) {
274
277
  isInstalledSkillPrompt(filePath));
275
278
  }
276
279
  /**
277
- * An installed workflow-prompt skill file: `.codex|.claude|.agents/skills/<dir>/SKILL.md`.
280
+ * An installed workflow-prompt skill file: `<ns>/skills/<dir>/SKILL.md`, where
281
+ * `<ns>` is any supported host namespace (`.codex`, `.claude`, `.opencode`, …).
278
282
  * This is the user-facing prompt/contract on a user's local install, so it is
279
283
  * treated as a prompt/template contract for scoring and carve-out purposes.
280
284
  * Paths are POSIX-normalized (forward slashes) before this is called.
281
285
  */
286
+ const INSTALLED_SKILL_PROMPT_RE = new RegExp(`^(?:${SKILL_NAMESPACES.map((ns) => ns.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')})/skills/.+/SKILL\\.md$`);
282
287
  function isInstalledSkillPrompt(filePath) {
283
- return /^\.(codex|claude|agents)\/skills\/.+\/SKILL\.md$/.test(filePath);
288
+ return INSTALLED_SKILL_PROMPT_RE.test(filePath);
284
289
  }
285
290
  function isCli(filePath) {
286
291
  return /(^|\/)(cli|commands|bin)(\/|$)/.test(filePath) || filePath === 'bin/synergyspec-selfevolving.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synergyspec-selfevolving",
3
- "version": "1.1.12",
3
+ "version": "1.1.13",
4
4
  "description": "AI-native system for spec-driven development",
5
5
  "keywords": [
6
6
  "synergyspec-selfevolving",