@mrciphersmith/keryx 0.3.27 → 0.3.29
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/README.md +24 -3
- package/dist/cli.js +2409 -1523
- package/dist/core.js +188 -4
- package/package.json +1 -1
- package/src/gdskills/bundled/rules/core/model-selection.mdc +65 -18
- package/src/gdskills/bundled/rules/core/requirements-package-standard.mdc +37 -2
- package/src/gdskills/bundled/skills/planning/docpack-orchestrator/SKILL.md +4 -4
- package/src/gdskills/bundled/skills/review/review-orchestrator/reviewer-input.schema.json +2 -1
- package/src/gdskills/contracts/subagent-dispatch.schema.json +4 -4
package/dist/core.js
CHANGED
|
@@ -9178,12 +9178,19 @@ var init_export = __esm(() => {
|
|
|
9178
9178
|
SKILL_RUNTIMES = [...HARNESS_SKILL_RUNTIMES, "plugin"];
|
|
9179
9179
|
});
|
|
9180
9180
|
|
|
9181
|
+
// src/gdskills/model-version.ts
|
|
9182
|
+
var TRAILING_ALIAS_WORDS;
|
|
9183
|
+
var init_model_version = __esm(() => {
|
|
9184
|
+
TRAILING_ALIAS_WORDS = new Set(["latest", "preview"]);
|
|
9185
|
+
});
|
|
9186
|
+
|
|
9181
9187
|
// src/gdskills/model-tier.ts
|
|
9182
9188
|
function isModelTier(value) {
|
|
9183
9189
|
return typeof value === "string" && MODEL_TIERS.includes(value);
|
|
9184
9190
|
}
|
|
9185
9191
|
var MODEL_TIERS, TIER_ALIASES, SKILL_TIER_KEY = "model_tier", TIER_DECLARATION;
|
|
9186
9192
|
var init_model_tier = __esm(() => {
|
|
9193
|
+
init_model_version();
|
|
9187
9194
|
MODEL_TIERS = ["light", "standard", "deep"];
|
|
9188
9195
|
TIER_ALIASES = new Map([["cheap", "light"]]);
|
|
9189
9196
|
TIER_DECLARATION = new RegExp(`^\\s*(?:[-*]\\s*)?"?(?:${SKILL_TIER_KEY}|model-tier|modelTier)"?\\s*[:=]\\s*(.+?)\\s*,?\\s*$`, "i");
|
|
@@ -21620,6 +21627,7 @@ init_validator();
|
|
|
21620
21627
|
var exports_service2 = {};
|
|
21621
21628
|
__export(exports_service2, {
|
|
21622
21629
|
AC_CHECK_TOKEN_BUDGET: () => AC_CHECK_TOKEN_BUDGET,
|
|
21630
|
+
AC_KINDS: () => AC_KINDS,
|
|
21623
21631
|
CONFIRMATION_CAVEAT: () => CONFIRMATION_CAVEAT,
|
|
21624
21632
|
REVIEW_GATE_CONFIG_PATH: () => REVIEW_GATE_CONFIG_PATH,
|
|
21625
21633
|
acCheckCacheKey: () => acCheckCacheKey,
|
|
@@ -21629,10 +21637,13 @@ __export(exports_service2, {
|
|
|
21629
21637
|
assertAcIntact: () => assertAcIntact,
|
|
21630
21638
|
baseBranchCondition: () => baseBranchCondition,
|
|
21631
21639
|
batchAcCheckItems: () => batchAcCheckItems,
|
|
21640
|
+
buildAcKindReport: () => buildAcKindReport,
|
|
21641
|
+
classifyAcCriterionNotCheckable: () => classifyAcCriterionNotCheckable,
|
|
21632
21642
|
classifyNotCheckable: () => classifyNotCheckable,
|
|
21633
21643
|
computeAcFacts: () => computeAcFacts,
|
|
21634
21644
|
confirmPreconditionError: () => confirmPreconditionError,
|
|
21635
21645
|
createFlowService: () => createFlowService,
|
|
21646
|
+
describeAcKind: () => describeAcKind,
|
|
21636
21647
|
evaluatedVerdict: () => evaluatedVerdict,
|
|
21637
21648
|
extractCriterionTokens: () => extractCriterionTokens,
|
|
21638
21649
|
factsOnlyVerdict: () => factsOnlyVerdict,
|
|
@@ -21642,12 +21653,16 @@ __export(exports_service2, {
|
|
|
21642
21653
|
parseAcceptanceCriteria: () => parseAcceptanceCriteria,
|
|
21643
21654
|
readAcCheckCache: () => readAcCheckCache,
|
|
21644
21655
|
readAcCheckEnabled: () => readAcCheckEnabled,
|
|
21656
|
+
readAcKindRecords: () => readAcKindRecords,
|
|
21645
21657
|
readFlow: () => readFlow,
|
|
21646
21658
|
renderAcCheckAdvisoryNotice: () => renderAcCheckAdvisoryNotice,
|
|
21647
21659
|
renderAcCheckReport: () => renderAcCheckReport,
|
|
21660
|
+
renderAcKindDistribution: () => renderAcKindDistribution,
|
|
21661
|
+
reportFromRecords: () => reportFromRecords,
|
|
21648
21662
|
resolveFlowDir: () => resolveFlowDir,
|
|
21649
21663
|
selectMatchedHunks: () => selectMatchedHunks,
|
|
21650
21664
|
statusLabel: () => statusLabel,
|
|
21665
|
+
stripVerifyMarker: () => stripVerifyMarker,
|
|
21651
21666
|
summarizeVerdicts: () => summarizeVerdicts,
|
|
21652
21667
|
validateCriterionName: () => validateCriterionName,
|
|
21653
21668
|
writeAcCheckCache: () => writeAcCheckCache
|
|
@@ -23972,6 +23987,161 @@ async function readRequireConfirmationDefault(cwd) {
|
|
|
23972
23987
|
return false;
|
|
23973
23988
|
}
|
|
23974
23989
|
}
|
|
23990
|
+
// src/flow/ac-kinds.ts
|
|
23991
|
+
var AC_KINDS = ["exec", "invariant", "judged", "none", "unclassified"];
|
|
23992
|
+
var AC_LINE = /^\s*[-*]\s*(AC\d+)\s*:\s?(.*)$/i;
|
|
23993
|
+
var MARKER_OPEN = "[verify:";
|
|
23994
|
+
function maskCodeSpans(text) {
|
|
23995
|
+
return text.replace(/`[^`]*`/g, (span) => " ".repeat(span.length));
|
|
23996
|
+
}
|
|
23997
|
+
function scanMarkers(text) {
|
|
23998
|
+
const masked = maskCodeSpans(text);
|
|
23999
|
+
let occurrences = 0;
|
|
24000
|
+
let lastStart = -1;
|
|
24001
|
+
for (let at = masked.indexOf(MARKER_OPEN);at !== -1; at = masked.indexOf(MARKER_OPEN, at + MARKER_OPEN.length)) {
|
|
24002
|
+
occurrences += 1;
|
|
24003
|
+
lastStart = at;
|
|
24004
|
+
}
|
|
24005
|
+
const end = text.trimEnd().length;
|
|
24006
|
+
return { occurrences, lastStart, trailing: occurrences > 0 && end > lastStart && text[end - 1] === "]", end };
|
|
24007
|
+
}
|
|
24008
|
+
function stripVerifyMarker(text) {
|
|
24009
|
+
const scan = scanMarkers(text);
|
|
24010
|
+
if (!scan.trailing || scan.occurrences !== 1)
|
|
24011
|
+
return text;
|
|
24012
|
+
return text.slice(0, scan.lastStart).trimEnd();
|
|
24013
|
+
}
|
|
24014
|
+
var KIND_WORDS = "exec, invariant, judged or none";
|
|
24015
|
+
function unclassified(error) {
|
|
24016
|
+
return error === undefined ? { record: { kind: "unclassified" } } : { record: { kind: "unclassified" }, error };
|
|
24017
|
+
}
|
|
24018
|
+
function parseAcKindText(text) {
|
|
24019
|
+
const scan = scanMarkers(text);
|
|
24020
|
+
if (scan.occurrences === 0)
|
|
24021
|
+
return unclassified();
|
|
24022
|
+
if (scan.occurrences > 1) {
|
|
24023
|
+
return unclassified("a second [verify: \u2026] marker \u2014 a criterion carries exactly one, as the last thing on its line");
|
|
24024
|
+
}
|
|
24025
|
+
if (!scan.trailing) {
|
|
24026
|
+
return unclassified("the [verify: \u2026] marker must be the last thing on the line and close with `]`");
|
|
24027
|
+
}
|
|
24028
|
+
const payload = text.slice(scan.lastStart + MARKER_OPEN.length, scan.end - 1).trim();
|
|
24029
|
+
if (payload.length === 0)
|
|
24030
|
+
return unclassified(`an empty [verify: \u2026] marker \u2014 expected ${KIND_WORDS}`);
|
|
24031
|
+
const word = /^[A-Za-z]+/.exec(payload)?.[0] ?? "";
|
|
24032
|
+
const rest = payload.slice(word.length).trim();
|
|
24033
|
+
switch (word) {
|
|
24034
|
+
case "judged":
|
|
24035
|
+
return rest.length === 0 ? { record: { kind: "judged" } } : unclassified("`judged` takes no arguments \u2014 write [verify: judged]");
|
|
24036
|
+
case "exec":
|
|
24037
|
+
case "invariant": {
|
|
24038
|
+
if (rest.length === 0)
|
|
24039
|
+
return unclassified(`\`${word}\` requires a backticked command, e.g. [verify: ${word} \`bun test path/to.test.ts\`]`);
|
|
24040
|
+
const command = /^`([^`]*)`$/.exec(rest);
|
|
24041
|
+
if (command === null)
|
|
24042
|
+
return unclassified(`\`${word}\` requires exactly one backticked command after the kind, e.g. [verify: ${word} \`bun test path/to.test.ts\`]`);
|
|
24043
|
+
const check = (command[1] ?? "").trim();
|
|
24044
|
+
if (check.length === 0)
|
|
24045
|
+
return unclassified(`\`${word}\` has an empty backticked command`);
|
|
24046
|
+
return { record: { kind: word, check } };
|
|
24047
|
+
}
|
|
24048
|
+
case "none": {
|
|
24049
|
+
if (rest.length === 0)
|
|
24050
|
+
return unclassified("`none` requires a reason after an em dash, e.g. [verify: none \u2014 <reason>]");
|
|
24051
|
+
const reason = /^\u2014\s*([\s\S]*)$/.exec(rest);
|
|
24052
|
+
if (reason === null)
|
|
24053
|
+
return unclassified("`none` requires its reason after an em dash (\u2014), e.g. [verify: none \u2014 <reason>]");
|
|
24054
|
+
const why = (reason[1] ?? "").trim();
|
|
24055
|
+
if (why.length === 0)
|
|
24056
|
+
return unclassified("`none` has an empty reason after the em dash");
|
|
24057
|
+
return { record: { kind: "none", reason: why } };
|
|
24058
|
+
}
|
|
24059
|
+
default:
|
|
24060
|
+
return unclassified(`unknown verification kind "${word.length > 0 ? word : payload}" \u2014 expected ${KIND_WORDS}`);
|
|
24061
|
+
}
|
|
24062
|
+
}
|
|
24063
|
+
function parseAcKinds(content) {
|
|
24064
|
+
const criteria = [];
|
|
24065
|
+
const errors = [];
|
|
24066
|
+
for (const line of content.split(/\r?\n/)) {
|
|
24067
|
+
const match = AC_LINE.exec(line);
|
|
24068
|
+
if (!match?.[1])
|
|
24069
|
+
continue;
|
|
24070
|
+
const id = match[1].toUpperCase();
|
|
24071
|
+
const rawText = (match[2] ?? "").trim();
|
|
24072
|
+
const parsed = parseAcKindText(rawText);
|
|
24073
|
+
if (parsed.error !== undefined)
|
|
24074
|
+
errors.push({ id, message: parsed.error });
|
|
24075
|
+
criteria.push({ id, text: stripVerifyMarker(rawText), rawText, record: parsed.record });
|
|
24076
|
+
}
|
|
24077
|
+
return { criteria, errors };
|
|
24078
|
+
}
|
|
24079
|
+
function emptyCounts() {
|
|
24080
|
+
return { exec: 0, invariant: 0, judged: 0, none: 0, unclassified: 0 };
|
|
24081
|
+
}
|
|
24082
|
+
function buildAcKindReport(flowId, content) {
|
|
24083
|
+
const parsed = parseAcKinds(content);
|
|
24084
|
+
const criteria = {};
|
|
24085
|
+
for (const criterion of parsed.criteria) {
|
|
24086
|
+
if (!(criterion.id in criteria))
|
|
24087
|
+
criteria[criterion.id] = criterion.record;
|
|
24088
|
+
}
|
|
24089
|
+
return { report: reportFromRecords(flowId, criteria), errors: parsed.errors };
|
|
24090
|
+
}
|
|
24091
|
+
function reportFromRecords(flowId, criteria) {
|
|
24092
|
+
const counts = emptyCounts();
|
|
24093
|
+
for (const record of Object.values(criteria))
|
|
24094
|
+
counts[record.kind] += 1;
|
|
24095
|
+
return {
|
|
24096
|
+
flowId,
|
|
24097
|
+
criteria,
|
|
24098
|
+
counts,
|
|
24099
|
+
runnable: counts.exec + counts.invariant,
|
|
24100
|
+
total: Object.keys(criteria).length
|
|
24101
|
+
};
|
|
24102
|
+
}
|
|
24103
|
+
function renderAcKindDistribution(report) {
|
|
24104
|
+
const { counts } = report;
|
|
24105
|
+
const percent = report.total === 0 ? 0 : Math.round(report.runnable / report.total * 100);
|
|
24106
|
+
const tail = [`${counts.none} accepted as unverifiable`, `${counts.judged} judged`, ...counts.unclassified > 0 ? [`${counts.unclassified} unclassified`] : []];
|
|
24107
|
+
return [
|
|
24108
|
+
`acceptance kinds: exec ${counts.exec} invariant ${counts.invariant} judged ${counts.judged} none ${counts.none} unclassified ${counts.unclassified} (${report.total} criteria)`,
|
|
24109
|
+
`coverage: ${report.runnable}/${report.total} runnable (${percent}%) \xB7 ${tail.join(" \xB7 ")}`
|
|
24110
|
+
];
|
|
24111
|
+
}
|
|
24112
|
+
function describeAcKind(record) {
|
|
24113
|
+
switch (record.kind) {
|
|
24114
|
+
case "exec":
|
|
24115
|
+
case "invariant":
|
|
24116
|
+
return `${record.kind} \`${record.check}\``;
|
|
24117
|
+
case "none":
|
|
24118
|
+
return `none \u2014 ${record.reason}`;
|
|
24119
|
+
default:
|
|
24120
|
+
return record.kind;
|
|
24121
|
+
}
|
|
24122
|
+
}
|
|
24123
|
+
function readAcKindRecords(value) {
|
|
24124
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
24125
|
+
return;
|
|
24126
|
+
const out = {};
|
|
24127
|
+
for (const [id, raw] of Object.entries(value)) {
|
|
24128
|
+
if (typeof raw !== "object" || raw === null)
|
|
24129
|
+
return;
|
|
24130
|
+
const record = raw;
|
|
24131
|
+
const kind = record["kind"];
|
|
24132
|
+
if ((kind === "exec" || kind === "invariant") && typeof record["check"] === "string")
|
|
24133
|
+
out[id] = { kind, check: record["check"] };
|
|
24134
|
+
else if (kind === "judged")
|
|
24135
|
+
out[id] = { kind };
|
|
24136
|
+
else if (kind === "none" && typeof record["reason"] === "string")
|
|
24137
|
+
out[id] = { kind, reason: record["reason"] };
|
|
24138
|
+
else if (kind === "unclassified")
|
|
24139
|
+
out[id] = { kind };
|
|
24140
|
+
else
|
|
24141
|
+
return;
|
|
24142
|
+
}
|
|
24143
|
+
return out;
|
|
24144
|
+
}
|
|
23975
24145
|
// src/flow/check-ac.ts
|
|
23976
24146
|
import { createHash as createHash8 } from "crypto";
|
|
23977
24147
|
import { chmod as chmod4, mkdir as mkdir9, readFile as readFile21 } from "fs/promises";
|
|
@@ -23985,11 +24155,11 @@ function estimateTokens(text) {
|
|
|
23985
24155
|
// src/flow/check-ac.ts
|
|
23986
24156
|
init_service2();
|
|
23987
24157
|
init_fs();
|
|
23988
|
-
var
|
|
24158
|
+
var AC_LINE2 = /^\s*[-*]\s*(AC\d+)\s*:\s?(.*)$/i;
|
|
23989
24159
|
function parseAcceptanceCriteria(content) {
|
|
23990
24160
|
const out = [];
|
|
23991
24161
|
for (const line of content.split(/\r?\n/)) {
|
|
23992
|
-
const match =
|
|
24162
|
+
const match = AC_LINE2.exec(line);
|
|
23993
24163
|
if (match?.[1]) {
|
|
23994
24164
|
out.push({ id: match[1].toUpperCase(), text: (match[2] ?? "").trim() });
|
|
23995
24165
|
}
|
|
@@ -24036,6 +24206,9 @@ function classifyNotCheckable(criterionText) {
|
|
|
24036
24206
|
reason: `not-checkable: every clause of this criterion names a condition no diff records (${labels.join(", ")}) \u2014 assigned without asking Jev.`
|
|
24037
24207
|
};
|
|
24038
24208
|
}
|
|
24209
|
+
function classifyAcCriterionNotCheckable(criterion) {
|
|
24210
|
+
return classifyNotCheckable(stripVerifyMarker(criterion.text));
|
|
24211
|
+
}
|
|
24039
24212
|
function extractCriterionTokens(text) {
|
|
24040
24213
|
const tokens = new Set;
|
|
24041
24214
|
for (const match of text.matchAll(/`([^`]+)`/g)) {
|
|
@@ -24053,7 +24226,7 @@ function extractCriterionTokens(text) {
|
|
|
24053
24226
|
return [...tokens];
|
|
24054
24227
|
}
|
|
24055
24228
|
function computeAcFacts(criterion, diffText, changedFiles) {
|
|
24056
|
-
const tokens = extractCriterionTokens(criterion.text);
|
|
24229
|
+
const tokens = extractCriterionTokens(stripVerifyMarker(criterion.text));
|
|
24057
24230
|
const tokensPresent = tokens.filter((token) => diffText.includes(token) || changedFiles.some((file) => file.includes(token)));
|
|
24058
24231
|
const tokensAbsent = tokens.filter((token) => !tokensPresent.includes(token));
|
|
24059
24232
|
const testFiles = changedFiles.filter((file) => /\.(test|spec)\.[a-z]+$/i.test(file));
|
|
@@ -24079,7 +24252,7 @@ var AC_CHECK_TOKEN_BUDGET = 64000;
|
|
|
24079
24252
|
function questionFor(criterion) {
|
|
24080
24253
|
return {
|
|
24081
24254
|
type: "noul",
|
|
24082
|
-
instructions: `Given the deterministic facts and the redacted diff evidence above, does this change LIKELY SATISFY this frozen ` + `acceptance criterion? Criterion ${criterion.id}: ${redactSensitiveText(criterion.text)}`
|
|
24255
|
+
instructions: `Given the deterministic facts and the redacted diff evidence above, does this change LIKELY SATISFY this frozen ` + `acceptance criterion? Criterion ${criterion.id}: ${redactSensitiveText(stripVerifyMarker(criterion.text))}`
|
|
24083
24256
|
};
|
|
24084
24257
|
}
|
|
24085
24258
|
var MAX_MATCHED_HUNKS_PER_ITEM = 8;
|
|
@@ -24424,6 +24597,10 @@ function renderJournal(createdAt) {
|
|
|
24424
24597
|
}
|
|
24425
24598
|
|
|
24426
24599
|
// src/flow/service.ts
|
|
24600
|
+
async function deriveAcKinds(cwd, dir, flowId) {
|
|
24601
|
+
const { report } = buildAcKindReport(flowId, await readFile23(acPath(cwd, dir), "utf8"));
|
|
24602
|
+
return { ...report.criteria };
|
|
24603
|
+
}
|
|
24427
24604
|
function recordAttempt(task, outcome, at, detail) {
|
|
24428
24605
|
const attempts = task.attempts ?? { count: 0, log: [] };
|
|
24429
24606
|
attempts.count += 1;
|
|
@@ -24621,6 +24798,7 @@ function createFlowService(deps) {
|
|
|
24621
24798
|
throw new Error("Cannot freeze: acceptance-criteria.md must contain at least one real `- ACn:` criterion.");
|
|
24622
24799
|
}
|
|
24623
24800
|
flow.acChecksum = await acChecksum(cwd, dir);
|
|
24801
|
+
flow.acKinds = await deriveAcKinds(cwd, dir, flow.id);
|
|
24624
24802
|
flow.status = "ready";
|
|
24625
24803
|
return save(cwd, dir, flow, "frozen", `${criteria.length} criteria; checksum recorded`);
|
|
24626
24804
|
});
|
|
@@ -24765,10 +24943,15 @@ function createFlowService(deps) {
|
|
|
24765
24943
|
detail = `${normalizedCriterion}: "${previousText ?? "(new)"}" -> "${normalizedText}" (${reason.trim()})`;
|
|
24766
24944
|
}
|
|
24767
24945
|
flow.acChecksum = await acChecksum(cwd, dir);
|
|
24946
|
+
flow.acKinds = await deriveAcKinds(cwd, dir, flow.id);
|
|
24768
24947
|
flow.acConfirmed = {};
|
|
24769
24948
|
return save(cwd, dir, flow, "ac-updated", detail);
|
|
24770
24949
|
});
|
|
24771
24950
|
},
|
|
24951
|
+
async acKinds({ cwd, id }) {
|
|
24952
|
+
const { dir, flow } = await load(cwd, id);
|
|
24953
|
+
return buildAcKindReport(flow.id, await readFile23(acPath(cwd, dir), "utf8"));
|
|
24954
|
+
},
|
|
24772
24955
|
async acReseal({ cwd, id, reason }) {
|
|
24773
24956
|
if (!reason?.trim()) {
|
|
24774
24957
|
throw new Error('flow ac reseal requires --reason "<why the checksum is stale>"');
|
|
@@ -24787,6 +24970,7 @@ function createFlowService(deps) {
|
|
|
24787
24970
|
throw new Error(`flow ac reseal: refusing \u2014 ${evidence.reason}. ` + "Reseal only re-seals a stale checksum over an unchanged file; it is not a way to " + 'accept an edit. If the criteria really changed, use `keryx flow ac update <id> --reason "..."`, ' + "which re-seals AND voids the prior confirmations.");
|
|
24788
24971
|
}
|
|
24789
24972
|
flow.acChecksum = current;
|
|
24973
|
+
flow.acKinds = await deriveAcKinds(cwd, dir, flow.id);
|
|
24790
24974
|
return save(cwd, dir, flow, "ac-resealed", reason);
|
|
24791
24975
|
});
|
|
24792
24976
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrciphersmith/keryx",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.29",
|
|
4
4
|
"description": "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|
|
@@ -57,13 +57,14 @@ candidate set. `src/gdskills/model-tier.ts` then:
|
|
|
57
57
|
| Tier | Resolves to |
|
|
58
58
|
|---|---|
|
|
59
59
|
| `standard` | the session's model, always |
|
|
60
|
-
| `deep` | the
|
|
61
|
-
| `light` | the
|
|
60
|
+
| `deep` | the best discovered model **above** the session's — a larger size class, or a NEWER generation of the session model's own family — else the session's |
|
|
61
|
+
| `light` | the **next size step below** the session's (an Opus-class session takes the Sonnet class, not the Haiku class), else the session's; the smallest class only when nothing sits between |
|
|
62
62
|
|
|
63
63
|
Anchoring on the session model is what makes "never a downgrade" checkable
|
|
64
64
|
rather than hoped for: a tier can only move away from the session model in the
|
|
65
65
|
direction its own name points, and a candidate at the session's own rank is a
|
|
66
|
-
lateral move and never taken.
|
|
66
|
+
lateral move and never taken. An OLDER sibling of the session's own family is
|
|
67
|
+
never `light` or `deep` either.
|
|
67
68
|
|
|
68
69
|
## What the hints are, and what they are not
|
|
69
70
|
|
|
@@ -83,13 +84,24 @@ residue, and it is kept in the smallest shape that works:
|
|
|
83
84
|
A model whose name is a codename carrying no size is unrankable by design. That
|
|
84
85
|
is the honest outcome, not a gap to be patched with folklore.
|
|
85
86
|
|
|
86
|
-
**Version is
|
|
87
|
-
version number out of an id
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
87
|
+
**Version is a second axis, not a size hint (flow 358).** `rankModelId` still
|
|
88
|
+
never parses a version number out of an id: the size words order size CLASSES.
|
|
89
|
+
Generation is ordered separately, and only within **the same family and
|
|
90
|
+
vendor** — the id with its version tokens removed (`familyKey`) must match. A
|
|
91
|
+
newer release outranks an older one of its own family, so a session on the
|
|
92
|
+
older one resolves `deep` to the newer sibling, and a session on the newer one
|
|
93
|
+
never resolves `light` or `deep` to the older. A version number is **never**
|
|
94
|
+
compared across families: a higher number on one line says nothing about a
|
|
95
|
+
different line.
|
|
96
|
+
|
|
97
|
+
**A larger class of an older generation is ambiguous, not "above".** When the
|
|
98
|
+
only discovered models above the session are a larger size class whose version is
|
|
99
|
+
older than the session's, size words alone cannot call them stronger, and
|
|
100
|
+
`deep` must not move onto a model that is older-generation and — by the
|
|
101
|
+
profile price — the more expensive one. Without an agent the tier keeps the
|
|
102
|
+
session's model as `session-fallback`, with the reason recorded (reason id
|
|
103
|
+
`kept-older-generation`, or `kept-older-generation-pricier` when the profile
|
|
104
|
+
price shows it costs more). The agent fallback below is the only other way out.
|
|
93
105
|
|
|
94
106
|
The routing "derived default table" (`deriveDefaultTable`,
|
|
95
107
|
`src/harness/routing/derive-default-table.ts`, flow 327/PRD §6.3) is that
|
|
@@ -102,8 +114,9 @@ version ranks higher there — the family word (a size marker from this same
|
|
|
102
114
|
`MODEL_RANK_HINTS` list) already carries the size class, so version is free
|
|
103
115
|
to order generations within it. That table reuses
|
|
104
116
|
`rankModelId`/`MODEL_RANK_HINTS` UNMODIFIED for the size axis and adds its
|
|
105
|
-
own separate, conservative version parse on top of it
|
|
106
|
-
|
|
117
|
+
own separate, conservative version parse on top of it. Since flow 358 the tier
|
|
118
|
+
resolution reads the same version parse (`src/gdskills/model-version.ts`, shared
|
|
119
|
+
so neither module imports the other).
|
|
107
120
|
|
|
108
121
|
That version parse (`parseModelVersion`/`familyKey`, round 2) reads BOTH a
|
|
109
122
|
dotted id (`vendor-opus-5.5`, `gemini-3.8-flash`) and a real
|
|
@@ -152,6 +165,35 @@ Do not "fix" a fallback by adding model ids to the code. Add a size word to the
|
|
|
152
165
|
hints if one genuinely applies, or leave it: running on the session's model is a
|
|
153
166
|
correct answer.
|
|
154
167
|
|
|
168
|
+
## The agent fallback: comparing candidate models, nothing else
|
|
169
|
+
|
|
170
|
+
When the ranking is **refused** but the provider did report other models, or a
|
|
171
|
+
tier is **ambiguous** (an older-generation larger class, above), an injected
|
|
172
|
+
agent may be asked to put the candidate models in order. It is a port
|
|
173
|
+
(`TierRankAgent`): `src/gdskills/model-tier.ts` holds no network and no
|
|
174
|
+
filesystem, the caller supplies the real one, and a test supplies a fake.
|
|
175
|
+
|
|
176
|
+
Its limits are the point:
|
|
177
|
+
|
|
178
|
+
- **It compares candidate models and never rates a task's own difficulty.** It is
|
|
179
|
+
shown no task, no diff and no user text — only the discovered ids and the
|
|
180
|
+
profile prices — and it is asked one question: which of these models is more
|
|
181
|
+
capable than which. The tier is still computed by `assignTier` from signals
|
|
182
|
+
the orchestrator holds.
|
|
183
|
+
- It must answer JSON, `{"order": [...]}`, best first, and must place the session's
|
|
184
|
+
own model. An id that was not discovered is dropped. An agent error, a
|
|
185
|
+
timeout, a malformed answer, or one that leaves out the session's model
|
|
186
|
+
resolves to the session's model, exactly as without an agent.
|
|
187
|
+
- It never places `standard` or `deep` below the session: `deep` is only ever taken
|
|
188
|
+
from above the session in its order, `standard` is never asked. A larger
|
|
189
|
+
class of an older generation that costs more than the session's model is
|
|
190
|
+
refused whatever it says.
|
|
191
|
+
- The result is cached by a hash of the candidate catalogue with prices, so an
|
|
192
|
+
identical catalogue asks once.
|
|
193
|
+
- It runs on the **light** tier of the session's own provider — the cheap step
|
|
194
|
+
below the session, or the session model itself when nothing sits below.
|
|
195
|
+
- A resolution it produced is recorded `tier_resolution: agent-ranked`.
|
|
196
|
+
|
|
155
197
|
## Where this actually runs
|
|
156
198
|
|
|
157
199
|
Two places, and they are not the same shape:
|
|
@@ -236,8 +278,9 @@ arithmetic straight back in the caller's head.
|
|
|
236
278
|
|
|
237
279
|
## Adaptive: `inherit: true`
|
|
238
280
|
|
|
239
|
-
A model is named only when discovery
|
|
240
|
-
|
|
281
|
+
A model is named only when discovery — or, failing that, the agent fallback —
|
|
282
|
+
assigned one **other than** the session's (`tier_resolution: discovered` or
|
|
283
|
+
`agent-ranked`). In every other case — no session named, ranking
|
|
241
284
|
refused, or the tier resolving to the session's own model — the block carries the
|
|
242
285
|
tier and `inherit: true`, and the **host** resolves it against the models its own
|
|
243
286
|
dispatch tool offers:
|
|
@@ -255,18 +298,19 @@ fallback. Exit status is always 0.
|
|
|
255
298
|
## Recording the decision
|
|
256
299
|
|
|
257
300
|
The dispatch carries the tier, the ordered rule ids that produced it, which of
|
|
258
|
-
the
|
|
259
|
-
`model.tier`, `model.tier_reasons
|
|
301
|
+
the outcomes occurred, and what was on the table when it did —
|
|
302
|
+
`model.tier`, `model.tier_reasons` (the assignment rule ids, then `resolve:<reason id>` for the resolution), `model.tier_resolution` and
|
|
260
303
|
`model.model_discovery` in
|
|
261
304
|
`.metaproject/core/gdskills/contracts/subagent-dispatch.schema.json`.
|
|
262
305
|
|
|
263
|
-
`tier_resolution` distinguishes
|
|
306
|
+
`tier_resolution` distinguishes four facts that must not be flattened:
|
|
264
307
|
|
|
265
308
|
| Value | Means |
|
|
266
309
|
|---|---|
|
|
267
310
|
| `discovered` | a discovered model was assigned to this tier |
|
|
268
311
|
| `session-ranked` | ranking worked and placed the tier at the session's model |
|
|
269
|
-
| `session-fallback` | ranking was refused; the session's model is kept |
|
|
312
|
+
| `session-fallback` | ranking was refused or was ambiguous; the session's model is kept |
|
|
313
|
+
| `agent-ranked` | the agent fallback put the candidates in order and its answer was used |
|
|
270
314
|
|
|
271
315
|
"Fell back to the session model" and "assigned the session model because it
|
|
272
316
|
ranked there" are different facts. A run that cannot be explained afterwards is
|
|
@@ -286,6 +330,9 @@ recording them is one copy rather than four decisions.
|
|
|
286
330
|
the resolution code.
|
|
287
331
|
- Take the candidate set from runtime detection. Never from a literal list of
|
|
288
332
|
what exists.
|
|
333
|
+
- The agent fallback compares candidate models. Never ask any model to rate a
|
|
334
|
+
task's own difficulty, and never let the fallback place `standard` or `deep`
|
|
335
|
+
below the session's model.
|
|
289
336
|
- When the candidates cannot be ranked, the block is adaptive (`inherit: true`).
|
|
290
337
|
The host never goes below its session model for `standard` or `deep`, and takes
|
|
291
338
|
a lighter class only for a `light` tier.
|
|
@@ -62,11 +62,26 @@ document changes.
|
|
|
62
62
|
- problem;
|
|
63
63
|
- goal;
|
|
64
64
|
- users;
|
|
65
|
-
- requirements;
|
|
66
|
-
- success criteria;
|
|
65
|
+
- requirements, each carrying a verification field (see Verification Field below);
|
|
66
|
+
- success criteria, split into two lists (see Success Contract below);
|
|
67
67
|
- risks;
|
|
68
68
|
- recommendation.
|
|
69
69
|
|
|
70
|
+
### Success Contract
|
|
71
|
+
|
|
72
|
+
The PRD's success criteria are two lists, never one:
|
|
73
|
+
|
|
74
|
+
- **Release criteria** are checkable when the work ships: a test, a command, a
|
|
75
|
+
file, a count. Each is something a release can be blocked on.
|
|
76
|
+
- **Outcome criteria** are what should be true some time after release: adoption,
|
|
77
|
+
a measured change, a behavior seen in use. Each outcome criterion either names
|
|
78
|
+
the observation that would confirm it (what is looked at, where, and when) or
|
|
79
|
+
declares `not measured — <reason>`.
|
|
80
|
+
|
|
81
|
+
An outcome criterion with neither an observation nor a `not measured — <reason>`
|
|
82
|
+
is not a criterion. A list whose every entry is `not measured` is acceptable: it
|
|
83
|
+
is honest. A PRD with no outcome list at all is not.
|
|
84
|
+
|
|
70
85
|
## Specification Contract
|
|
71
86
|
|
|
72
87
|
`specification.md` must state:
|
|
@@ -79,6 +94,21 @@ document changes.
|
|
|
79
94
|
- integration points;
|
|
80
95
|
- acceptance criteria.
|
|
81
96
|
|
|
97
|
+
### Verification Field
|
|
98
|
+
|
|
99
|
+
Every requirement in the PRD (each `R<n>` under Requirements) carries one verification field, in the
|
|
100
|
+
same vocabulary a flow criterion's trailing marker uses:
|
|
101
|
+
|
|
102
|
+
```markdown
|
|
103
|
+
Verification: exec `<command>` # a command whose exit code decides it
|
|
104
|
+
Verification: invariant `<command>` # a command that must keep passing
|
|
105
|
+
Verification: judged # a person reads the evidence
|
|
106
|
+
Verification: none — <reason> # honestly unverifiable, with the reason
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`exec` and `invariant` name a backticked command; `none` names its reason after
|
|
110
|
+
an em dash. A requirement with no field is a defect, not a `none`.
|
|
111
|
+
|
|
82
112
|
## Verification Contract
|
|
83
113
|
|
|
84
114
|
Before finishing:
|
|
@@ -88,6 +118,11 @@ Before finishing:
|
|
|
88
118
|
- verify README links to all package files;
|
|
89
119
|
- verify schemas are referenced from specification when present;
|
|
90
120
|
- verify status is honest: `draft`, `spec ready`, `implemented`, or `future`;
|
|
121
|
+
- verify every PRD requirement carries a `Verification:` field, and
|
|
122
|
+
fail the package when one does not, naming that requirement;
|
|
123
|
+
- verify the PRD has both a release-criteria list and an outcome-criteria list,
|
|
124
|
+
and fail the package when the outcome list is missing (an outcome list whose
|
|
125
|
+
entries are all `not measured — <reason>` passes);
|
|
91
126
|
- update `docs/requirements/roadmap.md` when the package represents a module or
|
|
92
127
|
standard capability.
|
|
93
128
|
|
|
@@ -109,10 +109,8 @@ Add optional files only when justified:
|
|
|
109
109
|
Write concise Markdown with explicit headings. Required contracts:
|
|
110
110
|
|
|
111
111
|
- README: purpose, status, document index, scope, related modules.
|
|
112
|
-
- PRD: problem, goal, users, requirements, success criteria, risks,
|
|
113
|
-
|
|
114
|
-
- Specification: identity, structure, manifest/config, CLI/skill surface, data
|
|
115
|
-
contracts, integrations, acceptance criteria.
|
|
112
|
+
- PRD: problem, goal, users, requirements (each `R<n>` carries a `Verification:` field: `exec`/`invariant` with a backticked command, `judged`, or `none — <reason>`), success criteria (a release list and an outcome list; an outcome criterion names its observation or declares `not measured — <reason>`), risks, recommendation.
|
|
113
|
+
- Specification: identity, structure, manifest/config, CLI/skill surface, data contracts, integrations, acceptance criteria.
|
|
116
114
|
|
|
117
115
|
When updating existing docs, preserve useful content and bump versions.
|
|
118
116
|
|
|
@@ -125,6 +123,7 @@ Run a local verification pass:
|
|
|
125
123
|
- README links to every package file;
|
|
126
124
|
- schema files are valid JSON;
|
|
127
125
|
- specification references schemas when present;
|
|
126
|
+
- every PRD requirement has a `Verification:` field (a package with a requirement that omits it FAILS, naming that requirement); the PRD has a release list and an outcome list (no outcome list FAILS; an outcome list whose entries are all `not measured — <reason>` passes);
|
|
128
127
|
- roadmap is updated for new module/standard capabilities;
|
|
129
128
|
- no implementation status is overstated.
|
|
130
129
|
|
|
@@ -154,6 +153,7 @@ Do not emit the Final Response until all of these hold:
|
|
|
154
153
|
- Every Markdown file in the package carries a `Version`, and every file you changed has a bumped one.
|
|
155
154
|
- `README.md` links to every file in the package; the specification links to each schema it defines.
|
|
156
155
|
- Every `schemas/*.json` parses as valid JSON.
|
|
156
|
+
- Every specification requirement carries a `Verification:` field, and the PRD carries both a release list and an outcome list; a package missing either was reported as `verification: fail`, naming the requirement.
|
|
157
157
|
- `docs/requirements/roadmap.md` is updated when the package represents a new module or capability, or the report states why it does not.
|
|
158
158
|
- No implementation claim appears that code does not support; anything unbuilt is marked planned or future.
|
|
159
159
|
- `docpack-review` has been run in Phase 5 and reports zero blockers. Remaining warnings appear verbatim in `remaining_gaps`.
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
}
|
|
84
84
|
},
|
|
85
85
|
"model": {
|
|
86
|
-
"description": "Which model runs this dispatch. Skills declare a `tier`; at dispatch time src/gdskills/model-tier.ts DISCOVERS the models runtime provider detection reported for the active provider, ranks them by size markers in their names, and places the tier relative to the session's own model — `standard` is the session model, `deep` a discovered model
|
|
86
|
+
"description": "Which model runs this dispatch. Skills declare a `tier`; at dispatch time src/gdskills/model-tier.ts DISCOVERS the models runtime provider detection reported for the active provider, ranks them by size markers in their names, and places the tier relative to the session's own model — `standard` is the session model, `deep` a discovered model above it (a larger size class, or a newer generation of the session model's own family), `light` the next size step below it. When the ranking is refused or ambiguous an injected agent may order the candidate models instead (it compares models, never rates a task); its answer is recorded as `agent-ranked`. No table of model ids exists anywhere in the resolution. When the candidates cannot be ranked, every tier keeps the session's model. `provider`/`model` remain available for an explicit lateral selection, and an omitted block inherits the parent verbatim. Two things fill this block. The interactive `spawn_subagent` tool does it in code: it builds the tier map with `buildTierMap` from its host's detection result, hands it to `resolveChildModel`, and records the outcome on the dispatch's run trace. An orchestrator authoring a dispatch document RUNS `keryx review tier`, which takes the §4.4 signals as flags and prints exactly the fields below, ready to paste (`--json` prints this object alone). It is a command rather than a documented function call because an orchestrator is an agent following prose and cannot call a TypeScript function: told to `decideDispatchModel`, it computed the tier in its head from a table, which is the mechanical step that belongs in code. When nothing can be worked out the command emits `inherit: true` instead of `provider`/`model` — the dispatch runs on the caller's own model, never a downgrade. Neither path is enforced by a reader — nothing validates a recorded resolution against the model that actually ran — so these fields are a record, not a gate.",
|
|
87
87
|
"type": "object",
|
|
88
88
|
"additionalProperties": false,
|
|
89
89
|
"properties": {
|
|
@@ -95,14 +95,14 @@
|
|
|
95
95
|
"enum": ["light", "standard", "deep"]
|
|
96
96
|
},
|
|
97
97
|
"tier_reasons": {
|
|
98
|
-
"description": "The ordered rule ids `assignTier` applied, e.g. [\"base:standard\", \"floor:blast-radius\"]. Produced by `keryx review tier` from the signals passed as flags, never written by hand: a reason list an author composes records the story they tell about the tier rather than the rules that produced it. Recorded so a tier can be explained after the run instead of re-derived from a state nobody kept. Ids rather than prose, because they are compared across runs.",
|
|
98
|
+
"description": "The ordered rule ids `assignTier` applied, e.g. [\"base:standard\", \"floor:blast-radius\"], followed by one `resolve:<reason-id>` entry from the model resolution (e.g. `resolve:picked-above`, `resolve:kept-older-generation-pricier`, `resolve:agent-picked-below`). Produced by `keryx review tier` from the signals passed as flags, never written by hand: a reason list an author composes records the story they tell about the tier rather than the rules that produced it. Recorded so a tier can be explained after the run instead of re-derived from a state nobody kept. Ids rather than prose, because they are compared across runs.",
|
|
99
99
|
"type": "array",
|
|
100
100
|
"items": { "type": "string", "minLength": 1 }
|
|
101
101
|
},
|
|
102
102
|
"tier_resolution": {
|
|
103
|
-
"description": "Which of
|
|
103
|
+
"description": "Which of four outcomes produced the model, as `resolveTierModel` reports it on `TierResolution.source`. `discovered` — a model reported by runtime provider detection was assigned to this tier. `session-ranked` — ranking WORKED and placed the tier at the session's own model (always so for `standard`; for `light`/`deep` when nothing discovered ranks below/above the session). `session-fallback` — ranking was refused or was ambiguous (a larger size class that is an older generation), or the ranking carried no rank for the session model, so the session's model is used because nothing could be worked out. `agent-ranked` — the ranking was refused or ambiguous and an injected agent, shown only the discovered ids and profile prices, put the candidate models in order and its validated answer picked the model; it compares models and never rates a task. Collapsing these would hide whether the environment was understood. Recorded, never read back: it explains a finished run and gates nothing.",
|
|
104
104
|
"type": "string",
|
|
105
|
-
"enum": ["discovered", "session-ranked", "session-fallback"]
|
|
105
|
+
"enum": ["discovered", "session-ranked", "session-fallback", "agent-ranked"]
|
|
106
106
|
},
|
|
107
107
|
"model_discovery": {
|
|
108
108
|
"description": "What was on the table when the tier was resolved: which models runtime detection reported for the active provider, how the size-marker hints ordered them, and — when ranking was refused — why. `tier_resolution` alone says a fallback happened and never says what it fell back FROM.",
|