synergyspec-selfevolving 1.1.11 → 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.
- package/dist/commands/self-evolution.js +44 -10
- package/dist/core/config.d.ts +12 -0
- package/dist/core/config.js +12 -0
- package/dist/core/learn.d.ts +0 -10
- package/dist/core/learn.js +67 -3
- package/dist/core/self-evolution/canonical-targets.js +6 -6
- package/dist/core/self-evolution/learn-observation-adapter.d.ts +7 -0
- package/dist/core/self-evolution/learn-observation-adapter.js +16 -9
- package/dist/core/self-evolution/local-targets.js +5 -5
- package/dist/core/self-evolution/tool-evolution.js +11 -6
- package/package.json +1 -1
|
@@ -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(
|
|
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
|
-
|
|
655
|
-
|
|
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,
|
package/dist/core/config.d.ts
CHANGED
|
@@ -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
|
package/dist/core/config.js
CHANGED
|
@@ -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
|
package/dist/core/learn.d.ts
CHANGED
|
@@ -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;
|
package/dist/core/learn.js
CHANGED
|
@@ -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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
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
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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/…`, `.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
16
|
-
'
|
|
17
|
-
|
|
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 (
|
|
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:
|
|
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
|
|
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';
|