phronesis 1.0.0
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 +66 -0
- package/package.json +34 -0
- package/src/act.js +61 -0
- package/src/active-context.js +56 -0
- package/src/adapter.js +386 -0
- package/src/cli.js +1893 -0
- package/src/codex-hooks-schema.js +92 -0
- package/src/compile.js +513 -0
- package/src/doctor.js +303 -0
- package/src/due.js +271 -0
- package/src/eval.js +467 -0
- package/src/event.js +212 -0
- package/src/export.js +136 -0
- package/src/guard.js +89 -0
- package/src/hooks.js +230 -0
- package/src/ingest.js +965 -0
- package/src/init.js +150 -0
- package/src/layout.js +394 -0
- package/src/links.js +83 -0
- package/src/lint.js +58 -0
- package/src/profile.js +116 -0
- package/src/prompts.js +26 -0
- package/src/registry.js +190 -0
- package/src/scaffold.js +134 -0
- package/src/search.js +139 -0
- package/src/skill-discovery.js +117 -0
- package/src/skills-bump.js +885 -0
- package/src/skills.js +355 -0
- package/src/subscription.js +5 -0
- package/src/validate.js +152 -0
- package/templates/claude/settings.json +86 -0
- package/templates/codex/INDEX.md +68 -0
- package/templates/codex/seed/ai-practice/compile-dont-just-capture.md +42 -0
- package/templates/codex/seed/ai-practice/design-for-the-distribution.md +42 -0
- package/templates/codex/seed/ai-practice/from-spec-author-to-outcome-curator.md +43 -0
- package/templates/codex/seed/ai-practice/recall-builds-trust-when-calibrated.md +70 -0
- package/templates/codex/seed/ai-practice/the-eval-is-the-spec.md +42 -0
- package/templates/codex/seed/ai-practice/the-gold-dust-leaves-no-trace.md +72 -0
- package/templates/codex/seed/ai-practice/the-model-is-not-the-moat.md +40 -0
- package/templates/codex/seed/ai-practice/when-generation-is-cheap-judgment-is-the-work.md +39 -0
- package/templates/codex/seed/judgment/contextual-stewardship-is-the-senior-skill.md +50 -0
- package/templates/codex/seed/judgment/decision-quality-is-distinct-from-outcome-quality.md +39 -0
- package/templates/codex/seed/judgment/embrace-reality-ego-and-blind-spots.md +38 -0
- package/templates/codex/seed/judgment/groups-seek-consensus-individuals-seek-truth.md +44 -0
- package/templates/codex/seed/judgment/heresies-are-where-the-edge-lives.md +42 -0
- package/templates/codex/seed/judgment/judgment-compounds-through-honest-reflection.md +38 -0
- package/templates/codex/seed/judgment/phronesis-is-judgment-in-the-particular.md +67 -0
- package/templates/codex/seed/judgment/show-me-the-apparatus.md +40 -0
- package/templates/codex/seed/judgment/small-judgment-gaps-compound-under-leverage.md +37 -0
- package/templates/codex/seed/judgment/taste-is-compressed-judgment.md +38 -0
- package/templates/codex/seed/judgment/trust-taste-only-where-feedback-is-valid.md +42 -0
- package/templates/codex/seed/judgment/weigh-the-reasoning-not-the-recommendation.md +43 -0
- package/templates/codex/seed/leadership/a-managers-output-is-the-teams-output.md +35 -0
- package/templates/codex/seed/leadership/most-team-problems-are-structural.md +40 -0
- package/templates/codex/seed/leadership/next-play.md +36 -0
- package/templates/codex/seed/leadership/psychological-safety.md +39 -0
- package/templates/codex/seed/leadership/right-size-your-teams.md +38 -0
- package/templates/codex/seed/leadership/spend-time-on-high-leverage-activities.md +36 -0
- package/templates/codex/seed/leadership/talent-density-is-the-master-variable.md +39 -0
- package/templates/codex/seed/leadership/under-pressure-calm-is-contagious.md +36 -0
- package/templates/codex/seed/leadership/your-best-alternative-is-your-leverage.md +37 -0
- package/templates/codex/seed/pm/a-problem-well-stated-is-half-solved.md +39 -0
- package/templates/codex/seed/pm/a-real-strategy-makes-planning-trivial.md +40 -0
- package/templates/codex/seed/pm/barrels-and-ammunition.md +39 -0
- package/templates/codex/seed/pm/develop-taste-in-your-beliefs.md +38 -0
- package/templates/codex/seed/pm/hire-for-progress.md +44 -0
- package/templates/codex/seed/pm/jobs-are-stable-solutions-churn.md +39 -0
- package/templates/codex/seed/pm/level-by-the-ambiguity-someone-can-own.md +39 -0
- package/templates/codex/seed/pm/listen-before-you-prescribe.md +38 -0
- package/templates/codex/seed/pm/live-in-the-future-and-build-whats-missing.md +38 -0
- package/templates/codex/seed/pm/match-effort-to-leverage.md +37 -0
- package/templates/codex/seed/pm/not-my-fault-but-my-problem.md +40 -0
- package/templates/codex/seed/pm/pick-the-job-then-refuse-everything-outside-it.md +37 -0
- package/templates/codex/seed/pm/retention-sets-the-ceiling.md +38 -0
- package/templates/codex/seed/pm/scale-with-leaders-not-process.md +38 -0
- package/templates/codex/seed/pm/solution-discovery-over-problem-validation.md +38 -0
- package/templates/codex/seed/pm/tenfold-better-on-a-painful-problem.md +40 -0
- package/templates/codex-surface/README.md +73 -0
- package/templates/codex-surface/hooks/_resolve.sh +36 -0
- package/templates/codex-surface/hooks/recall-guard.sh +30 -0
- package/templates/codex-surface/hooks/session-start.sh +18 -0
- package/templates/codex-surface/hooks/session-sweep-precompact.sh +20 -0
- package/templates/codex-surface/hooks/session-sweep-subagent.sh +23 -0
- package/templates/codex-surface/hooks/session-sweep.sh +19 -0
- package/templates/codex-surface/hooks.json +69 -0
- package/templates/domains/pm/AGENTS.md +111 -0
- package/templates/domains/pm/MEMORY.md +7 -0
- package/templates/domains/pm/USER.md +8 -0
- package/templates/phronesis-hooks/compile-active-context.sh +38 -0
- package/templates/phronesis-hooks/recall-guard.sh +103 -0
- package/templates/phronesis-hooks/session-start.sh +90 -0
- package/templates/phronesis-hooks/session-sweep.sh +140 -0
- package/templates/phronesis-hooks/skill-lifecycle.sh +37 -0
- package/templates/skills/compile/SKILL.md +279 -0
- package/templates/skills/compile/evals/rubric.md +75 -0
- package/templates/skills/decision-log/SKILL.md +156 -0
- package/templates/skills/decision-log/evals/rubric.md +97 -0
- package/templates/skills/extract-gold-dust/SKILL.md +311 -0
- package/templates/skills/extract-gold-dust/evals/rubric.md +52 -0
- package/templates/skills/ingest/SKILL.md +204 -0
- package/templates/skills/ingest/evals/rubric.md +117 -0
- package/templates/skills/lint/SKILL.md +246 -0
- package/templates/skills/lint/evals/rubric.md +139 -0
- package/templates/skills/meeting-prep/SKILL.md +195 -0
- package/templates/skills/meeting-prep/evals/rubric.md +93 -0
- package/templates/skills/onboard/SKILL.md +258 -0
- package/templates/skills/onboard/evals/rubric.md +60 -0
- package/templates/skills/prd-draft/SKILL.md +193 -0
- package/templates/skills/prd-draft/evals/rubric.md +38 -0
- package/templates/skills/prep-extraction/SKILL.md +217 -0
- package/templates/skills/prep-extraction/evals/rubric.md +45 -0
- package/templates/skills/recall/SKILL.md +155 -0
- package/templates/skills/recall/evals/rubric.md +37 -0
- package/templates/skills/research/SKILL.md +247 -0
- package/templates/skills/research/evals/rubric.md +139 -0
- package/templates/skills/stakeholder-update/SKILL.md +163 -0
- package/templates/skills/stakeholder-update/evals/rubric.md +111 -0
- package/vendor/core/package.json +25 -0
- package/vendor/core/src/action-registry.js +459 -0
- package/vendor/core/src/action-writers.js +201 -0
- package/vendor/core/src/actions.js +2140 -0
- package/vendor/core/src/active-context.js +167 -0
- package/vendor/core/src/artifacts.js +301 -0
- package/vendor/core/src/content-digest.js +21 -0
- package/vendor/core/src/contract.js +245 -0
- package/vendor/core/src/event-log.js +338 -0
- package/vendor/core/src/guard.js +398 -0
- package/vendor/core/src/hook-executor.js +527 -0
- package/vendor/core/src/index.js +180 -0
- package/vendor/core/src/ingest.js +286 -0
- package/vendor/core/src/install-root.js +42 -0
- package/vendor/core/src/layout.js +13 -0
- package/vendor/core/src/link-index.js +198 -0
- package/vendor/core/src/lint.js +368 -0
- package/vendor/core/src/schedule.js +535 -0
- package/vendor/core/src/search.js +534 -0
- package/vendor/core/src/session-hooks.js +129 -0
- package/vendor/core/src/skill-lifecycle.js +479 -0
- package/vendor/core/src/skill-registry.js +524 -0
- package/vendor/core/src/subscription.js +84 -0
- package/vendor/core/src/validate-object.js +62 -0
- package/vendor/core/src/workspace-scan.js +125 -0
package/src/skills.js
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
// `phronesis skills status` (core-0015 / T3.1) — the diagnosis surface for the
|
|
2
|
+
// install's skill registry, the registry-status parity surface one layer up: which
|
|
3
|
+
// skills are registered, each one's resolved contract (version, weight, tier,
|
|
4
|
+
// prompt_pin, triggers, reads, via_action_types, degrade target), and — on an invalid
|
|
5
|
+
// layer — the SAME load errors the loader refuses with, exit non-zero. The skills
|
|
6
|
+
// layer is a separate failure domain from the action layer (no skill execution exists
|
|
7
|
+
// in the act pipeline), but it RESOLVES against it: via_action_types must name
|
|
8
|
+
// registered action types, so an invalid action layer blocks skill resolution too.
|
|
9
|
+
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
12
|
+
import { existsSync } from 'node:fs';
|
|
13
|
+
import { createHash } from 'node:crypto';
|
|
14
|
+
import { pathToFileURL } from 'node:url';
|
|
15
|
+
import fse from 'fs-extra';
|
|
16
|
+
import {
|
|
17
|
+
loadSkills,
|
|
18
|
+
resolveActionTypes,
|
|
19
|
+
resolveInstall,
|
|
20
|
+
skillFilePath,
|
|
21
|
+
} from '@phronesis/core';
|
|
22
|
+
import { DEFAULT_K, bumpSkill, resolveRepoRoot } from './skills-bump.js';
|
|
23
|
+
|
|
24
|
+
// skills/ dirs that carry a SKILL.md but no registry entry — informational, never an
|
|
25
|
+
// error (an unregistered skill is inert, the action-type-removal posture: absence
|
|
26
|
+
// refuses to load, and that is a tightening).
|
|
27
|
+
async function unregisteredSkillDirs(root, loaded) {
|
|
28
|
+
let entries;
|
|
29
|
+
try {
|
|
30
|
+
entries = await readdir(join(root, 'skills'), { withFileTypes: true });
|
|
31
|
+
} catch {
|
|
32
|
+
// ENOENT (no skills/ dir yet) is the by-design empty case; anything else
|
|
33
|
+
// (EACCES…) only suppresses this informational listing — the load-bearing
|
|
34
|
+
// checks above never depend on it. Do not widen this catch into the loader.
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
const out = [];
|
|
38
|
+
for (const e of entries) {
|
|
39
|
+
if (!e.isDirectory()) continue;
|
|
40
|
+
if (Object.hasOwn(loaded, e.name)) continue;
|
|
41
|
+
if (await fse.pathExists(join(root, skillFilePath(e.name)))) out.push(e.name);
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// PURE skill-registry resolution (core-0038 D2/D3): resolve the install, read the registry,
|
|
47
|
+
// resolve action types, load the registered skills — returning a STRUCTURED verdict with NO
|
|
48
|
+
// console output and NO process.exitCode side-effect. `runSkillsStatus` (the CLI wrapper) and
|
|
49
|
+
// `phronesis doctor` both consume this, so the diagnosis can never disagree between the two
|
|
50
|
+
// surfaces (the "two consumers → one matcher" rule). `kind` lets the wrapper reproduce its exact
|
|
51
|
+
// per-failure-mode message; `status` + `errors` + `skills`/`unregistered` are what doctor needs.
|
|
52
|
+
export async function skillsStatusReport({ dir } = {}) {
|
|
53
|
+
const installed = resolveInstall({ dir });
|
|
54
|
+
if (installed.error) {
|
|
55
|
+
return { status: 'fail', kind: 'no-install', root: null, errors: [installed.error], skills: {}, unregistered: [], schema_version: null };
|
|
56
|
+
}
|
|
57
|
+
const root = installed.root;
|
|
58
|
+
let registry;
|
|
59
|
+
try {
|
|
60
|
+
registry = await fse.readJson(join(root, '.phronesis', 'registry.json'));
|
|
61
|
+
} catch (err) {
|
|
62
|
+
return {
|
|
63
|
+
status: 'fail', kind: 'registry-unreadable', root,
|
|
64
|
+
errors: [`could not read the install registry (.phronesis/registry.json): ${err.message}`],
|
|
65
|
+
skills: {}, unregistered: [], schema_version: null,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
// via_action_types resolves against the EFFECTIVE action-type table — same resolver the action
|
|
69
|
+
// core consumes (the act/status parity lesson). An invalid action layer means there is no
|
|
70
|
+
// registered set to resolve against: fail closed, naming why.
|
|
71
|
+
const action = resolveActionTypes(registry);
|
|
72
|
+
if (action.errors.length) {
|
|
73
|
+
return { status: 'fail', kind: 'action-registry-invalid', root, errors: action.errors, skills: {}, unregistered: [], schema_version: registry.schema_version };
|
|
74
|
+
}
|
|
75
|
+
const { errors, skills } = await loadSkills({
|
|
76
|
+
installRoot: root,
|
|
77
|
+
registry,
|
|
78
|
+
registeredActionTypes: Object.keys(action.effective),
|
|
79
|
+
});
|
|
80
|
+
if (errors.length) {
|
|
81
|
+
return { status: 'fail', kind: 'skills-invalid', root, errors, skills: {}, unregistered: [], schema_version: registry.schema_version };
|
|
82
|
+
}
|
|
83
|
+
// Membership universe = the LOADED map (on success its keys are exactly the validated section's),
|
|
84
|
+
// never a re-read of registry.skills — one validated source.
|
|
85
|
+
const unregistered = await unregisteredSkillDirs(root, skills);
|
|
86
|
+
return { status: 'ok', kind: 'ok', root, errors: [], skills, unregistered, schema_version: registry.schema_version };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function runSkillsStatus({ dir, json = false } = {}) {
|
|
90
|
+
const report = await skillsStatusReport({ dir });
|
|
91
|
+
if (report.status === 'fail') {
|
|
92
|
+
if (report.kind === 'no-install' || report.kind === 'registry-unreadable') {
|
|
93
|
+
console.error(report.errors[0]);
|
|
94
|
+
} else if (report.kind === 'action-registry-invalid') {
|
|
95
|
+
console.error(
|
|
96
|
+
'invalid .phronesis/registry.json (action-type registry) — skills cannot resolve via_action_types until it is fixed (see: phronesis registry status):',
|
|
97
|
+
);
|
|
98
|
+
for (const e of report.errors) console.error(` ✗ ${e}`);
|
|
99
|
+
} else {
|
|
100
|
+
console.error(
|
|
101
|
+
'invalid skill registry — the skills layer is disabled until every registered skill is valid (removing a skills entry unregisters that skill):',
|
|
102
|
+
);
|
|
103
|
+
for (const e of report.errors) console.error(` ✗ ${e}`);
|
|
104
|
+
}
|
|
105
|
+
process.exitCode = 1;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const { root, skills, unregistered, schema_version } = report;
|
|
109
|
+
|
|
110
|
+
if (json) {
|
|
111
|
+
console.log(JSON.stringify({ root, schema_version, skills, unregistered }, null, 2));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.log(`Phronesis skill registry — ${root}`);
|
|
116
|
+
const names = Object.keys(skills);
|
|
117
|
+
if (!names.length) {
|
|
118
|
+
console.log(
|
|
119
|
+
'skills: none registered (.phronesis/registry.json skills section is empty; add skills.{name} entries to register skill dirs)',
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
for (const name of names) {
|
|
123
|
+
const s = skills[name];
|
|
124
|
+
console.log(`\n${name} @ ${s.version}`);
|
|
125
|
+
console.log(` file: ${s.path}`);
|
|
126
|
+
console.log(` weight: ${s.weight} · tier: ${s.requires_model_tier} · prompt_pin: ${s.prompt_pin.raw}`);
|
|
127
|
+
const triggers = [
|
|
128
|
+
s.invocation.explicit.length ? `explicit(${s.invocation.explicit.length})` : null,
|
|
129
|
+
s.invocation.suggested.length ? `suggested(${s.invocation.suggested.length})` : null,
|
|
130
|
+
s.invocation.woven ? 'woven' : null,
|
|
131
|
+
].filter(Boolean);
|
|
132
|
+
console.log(` invocation: ${triggers.join(', ') || 'none declared'}`);
|
|
133
|
+
console.log(` reads: ${s.reads.length} declaration(s)`);
|
|
134
|
+
console.log(
|
|
135
|
+
` writes: via ${s.writes.via_action_types.length ? s.writes.via_action_types.join(', ') : '(read-only)'}${s.writes.paths.length ? ` · direct: ${s.writes.paths.join(', ')}` : ''}`,
|
|
136
|
+
);
|
|
137
|
+
if (s.sub_agents.length) console.log(` sub_agents: ${s.sub_agents.join(', ')}`);
|
|
138
|
+
console.log(` degrades: ${s.degrade_target}`);
|
|
139
|
+
}
|
|
140
|
+
if (unregistered.length) {
|
|
141
|
+
console.log(
|
|
142
|
+
`\nunregistered — skills/ dirs with a SKILL.md but no registry entry; these do not load (add a skills.{name} entry to register): ${unregistered.join(', ')}`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// --- `phronesis skills bump` (core-0031 / T7.4 + T7.2 coverage floor) -----------------
|
|
148
|
+
// The deployment-unit gate. This module wires the REAL deps (the candidate-execution
|
|
149
|
+
// substrate, the install corpus, the registry, the judge pin) and hands them to the pure
|
|
150
|
+
// orchestrator in skills-bump.js. The substrate is dynamic-imported FROM THE RESOLVED REPO
|
|
151
|
+
// ROOT — bump is a dev/release op on git-tracked source (B6), so the substrate it runs is
|
|
152
|
+
// that repo's, and a packaged CLI without harness/ still loads (the import is lazy).
|
|
153
|
+
|
|
154
|
+
function sha256(value) {
|
|
155
|
+
return createHash('sha256').update(String(value), 'utf8').digest('hex');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Resolve the skill's golden corpus the same way run_skill_goldens.py does:
|
|
159
|
+
// --goldens-dir → $PHRONESIS_INSTALL/corpus/skill-goldens/<skill> (goldens are real
|
|
160
|
+
// operator material; they live in the install, never the repo — change 0023). Builds the
|
|
161
|
+
// name-free corpus identity (sorted id:hash + manifest hash) and reads signedness from an
|
|
162
|
+
// explicit marker only (signing authority is T7.0/T7.6; absent ⇒ provisional, never a
|
|
163
|
+
// false green).
|
|
164
|
+
export async function loadCorpusFromInstall({ skill, goldensDir }) {
|
|
165
|
+
const dir = goldensDir
|
|
166
|
+
|| (process.env.PHRONESIS_INSTALL
|
|
167
|
+
? join(process.env.PHRONESIS_INSTALL, 'corpus', 'skill-goldens', skill)
|
|
168
|
+
: null);
|
|
169
|
+
if (!dir || !existsSync(dir)) {
|
|
170
|
+
return { goldens: [], signed: false, identity: '', source: dir || '(no $PHRONESIS_INSTALL set)' };
|
|
171
|
+
}
|
|
172
|
+
const files = (await readdir(dir)).filter((f) => /^golden-.*\.json$/.test(f)).sort();
|
|
173
|
+
const goldens = [];
|
|
174
|
+
const idParts = [];
|
|
175
|
+
for (const f of files) {
|
|
176
|
+
const p = join(dir, f);
|
|
177
|
+
const raw = await readFile(p, 'utf8');
|
|
178
|
+
let d;
|
|
179
|
+
// FAIL CLOSED on a malformed golden — a release gate must never SILENTLY shrink its
|
|
180
|
+
// corpus by skipping a bad file (a dropped hard case is the most dangerous loss). A
|
|
181
|
+
// file named golden-*.json is asserted to be a golden; if it isn't a well-formed one,
|
|
182
|
+
// the operator fixes or removes it before the gate can run.
|
|
183
|
+
try {
|
|
184
|
+
d = JSON.parse(raw);
|
|
185
|
+
} catch (err) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
`corpus golden ${f} is not valid JSON (${err.message}) — fix or remove ${p}; ` +
|
|
188
|
+
'the bump gate fails closed rather than silently drop a golden.',
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
if (!('input' in d && 'expected' in d && 'model_default' in d)) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
`corpus golden ${f} is missing a required field (needs input, expected, model_default) — ` +
|
|
194
|
+
`fix or remove ${p}; the bump gate fails closed rather than silently skip a golden.`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
const id = d.id || f.replace(/\.json$/, '');
|
|
198
|
+
goldens.push({ ...d, id, path: p });
|
|
199
|
+
idParts.push(`${id}:${sha256(raw)}`);
|
|
200
|
+
}
|
|
201
|
+
let manifest = '';
|
|
202
|
+
for (const mp of [join(dir, 'MANIFEST.md'), join(dir, '..', 'MANIFEST.md')]) {
|
|
203
|
+
if (existsSync(mp)) {
|
|
204
|
+
manifest = await readFile(mp, 'utf8');
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const identity = `${idParts.sort().join('\n')}\n--manifest--\n${sha256(manifest)}`;
|
|
209
|
+
let signed = false;
|
|
210
|
+
const signingPath = join(dir, 'signing.json');
|
|
211
|
+
if (existsSync(signingPath)) {
|
|
212
|
+
try {
|
|
213
|
+
signed = JSON.parse(await readFile(signingPath, 'utf8')).signed === true;
|
|
214
|
+
} catch {
|
|
215
|
+
signed = false;
|
|
216
|
+
}
|
|
217
|
+
} else if (existsSync(join(dir, 'SIGNED'))) {
|
|
218
|
+
signed = true;
|
|
219
|
+
}
|
|
220
|
+
return { goldens, signed, identity, source: dir };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export async function runSkillsBump({
|
|
224
|
+
skill,
|
|
225
|
+
to,
|
|
226
|
+
repo,
|
|
227
|
+
k = DEFAULT_K,
|
|
228
|
+
acceptRegression = false,
|
|
229
|
+
rebaseline = false,
|
|
230
|
+
shapeChange = false,
|
|
231
|
+
qualityFloor,
|
|
232
|
+
coverageFloor,
|
|
233
|
+
rationale,
|
|
234
|
+
acceptedBy = 'operator',
|
|
235
|
+
judgeModel = 'sonnet',
|
|
236
|
+
goldensDir,
|
|
237
|
+
domain,
|
|
238
|
+
json = false,
|
|
239
|
+
} = {}) {
|
|
240
|
+
const resolved = resolveRepoRoot({ repo });
|
|
241
|
+
if (resolved.error) {
|
|
242
|
+
console.error(resolved.error);
|
|
243
|
+
process.exitCode = 1;
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const root = resolved.root;
|
|
247
|
+
|
|
248
|
+
// Dynamic-import the substrate from the resolved repo root (B1/B6).
|
|
249
|
+
const substratePath = join(root, 'harness', 'evals', 'skill-goldens', 'run_candidates.mjs');
|
|
250
|
+
if (!existsSync(substratePath)) {
|
|
251
|
+
console.error(
|
|
252
|
+
`candidate-execution substrate not found at ${substratePath} — skills bump rides core-0028; ` +
|
|
253
|
+
'run from a repo that carries it.',
|
|
254
|
+
);
|
|
255
|
+
process.exitCode = 1;
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const substrate = await import(pathToFileURL(substratePath).href);
|
|
259
|
+
|
|
260
|
+
// The eval-identity registry component MUST be the action-type contract the SUBSTRATE
|
|
261
|
+
// actually runs against — the substrate resolves core from the repo root (B6), which can
|
|
262
|
+
// differ from this CLI's bundled @phronesis/core. Hashing the CLI's defaultActionTypesSection()
|
|
263
|
+
// would let registry/schema drift in the repo's core look "stable" and pass an invalid
|
|
264
|
+
// comparison. So we read the contract from the substrate's own exported accessor.
|
|
265
|
+
if (typeof substrate.actionTypeContract !== 'function') {
|
|
266
|
+
console.error(
|
|
267
|
+
'the candidate-execution substrate does not export actionTypeContract() — it predates the ' +
|
|
268
|
+
'eval-identity registry fix (core-0031). Update the substrate (core-0028) before bumping.',
|
|
269
|
+
);
|
|
270
|
+
process.exitCode = 1;
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const judgePyPath = join(root, 'harness', 'evals', 'judge', 'judge.py');
|
|
275
|
+
const judgePin = `${judgeModel}|${existsSync(judgePyPath) ? sha256(await readFile(judgePyPath, 'utf8')) : 'absent'}`;
|
|
276
|
+
|
|
277
|
+
const deps = {
|
|
278
|
+
runCandidates: substrate.runCandidates,
|
|
279
|
+
parseSkillSource: substrate.parseSkillSource,
|
|
280
|
+
runnerProtocolVersion: substrate.RUNNER_PROTOCOL_VERSION,
|
|
281
|
+
registrySection: JSON.stringify(substrate.actionTypeContract()),
|
|
282
|
+
judgePin,
|
|
283
|
+
judgeModel,
|
|
284
|
+
judge: 'judge.py',
|
|
285
|
+
loadCorpus: ({ skill: s }) => loadCorpusFromInstall({ skill: s, goldensDir }),
|
|
286
|
+
now: new Date(),
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
let result;
|
|
290
|
+
try {
|
|
291
|
+
result = await bumpSkill(
|
|
292
|
+
{
|
|
293
|
+
skill,
|
|
294
|
+
toPin: to,
|
|
295
|
+
repoRoot: root,
|
|
296
|
+
k,
|
|
297
|
+
rebaseline,
|
|
298
|
+
acceptRegression,
|
|
299
|
+
shapeChange,
|
|
300
|
+
qualityFloor,
|
|
301
|
+
coverageFloor,
|
|
302
|
+
rationale,
|
|
303
|
+
acceptedBy,
|
|
304
|
+
activeDomain: domain,
|
|
305
|
+
},
|
|
306
|
+
deps,
|
|
307
|
+
);
|
|
308
|
+
} catch (err) {
|
|
309
|
+
// A malformed SKILL.md (no version/prompt_pin), an inconsistent rubric, a corrupt
|
|
310
|
+
// PINS.md block, or a judge.py crash throws — surface it as a clean failure, not a raw
|
|
311
|
+
// unhandled-rejection stack trace. Every throw site runs before the PINS write, so no
|
|
312
|
+
// partial record is left behind.
|
|
313
|
+
console.error(`✗ skills bump failed: ${err.message}`);
|
|
314
|
+
process.exitCode = 1;
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (json) {
|
|
319
|
+
console.log(JSON.stringify(result, null, 2));
|
|
320
|
+
} else {
|
|
321
|
+
printBumpResult(result);
|
|
322
|
+
}
|
|
323
|
+
if (result.status !== 'ok') process.exitCode = 1;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function printBumpResult(result) {
|
|
327
|
+
const axes = result.aggregate
|
|
328
|
+
? `quality floors {${Object.entries(result.aggregate.quality.criteria)
|
|
329
|
+
.map(([c, v]) => `${c}:${v.min}`)
|
|
330
|
+
.join(', ')}} · coverage floor ${result.aggregate.coverage.hit_rate_min} (fabrications ${result.aggregate.coverage.fabrications})`
|
|
331
|
+
: '';
|
|
332
|
+
if (result.status === 'ok') {
|
|
333
|
+
console.log(`✓ ${result.message}`);
|
|
334
|
+
if (axes) console.log(` ${axes}`);
|
|
335
|
+
console.log(` gate: ${result.gate_status}${result.corpus_signed ? '' : ' (corpus unsigned)'}${result.coverage_asserted === false ? ' · coverage not exercised' : ''}`);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (result.status === 'refused') {
|
|
339
|
+
console.error(`⚠ refused — ${result.reason}`);
|
|
340
|
+
console.error(` ${result.message}`);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (result.status === 'invalid') {
|
|
344
|
+
console.error(`✗ ${result.error}`);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (result.status === 'error') {
|
|
348
|
+
console.error(`✗ error — ${result.reason}`);
|
|
349
|
+
console.error(` ${result.message}`);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
console.error(`✗ blocked — ${result.reason}`);
|
|
353
|
+
console.error(` ${result.message}`);
|
|
354
|
+
if (axes) console.error(` ${axes}`);
|
|
355
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Subscription matching moved to @phronesis/core in core-0013, when the in-pipeline
|
|
2
|
+
// hook executor became its second consumer — one shared matcher (rules.md two-matchers
|
|
3
|
+
// drift), reachable from the only legal import direction (apps→packages). This shim
|
|
4
|
+
// keeps the CLI surface's import site stable.
|
|
5
|
+
export { validateSubscription, matchesSubscription } from '@phronesis/core';
|
package/src/validate.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { resolve, join } from 'node:path';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import fse from 'fs-extra';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import {
|
|
6
|
+
checkObjectContent,
|
|
7
|
+
contextFor,
|
|
8
|
+
checkArtifactContent,
|
|
9
|
+
listArtifactFiles,
|
|
10
|
+
listObjectFiles,
|
|
11
|
+
checkActiveContextStaleness,
|
|
12
|
+
validateScheduleRegistry,
|
|
13
|
+
pruneOrphanScheduleState,
|
|
14
|
+
} from '@phronesis/core';
|
|
15
|
+
import { checkSkillDiscoveryStaleness } from './skill-discovery.js';
|
|
16
|
+
|
|
17
|
+
// Validate every typed object in domains/*/compiled/ and codex/ against the locked
|
|
18
|
+
// ontology contract (specs/ontology.md, change 0009). Returns { ok, checked, fileErrors }.
|
|
19
|
+
// The per-object check (frontmatter contract + link existence) lives in @phronesis/core's
|
|
20
|
+
// checkObjectContent — the SAME check the action spine's staged writer enforces
|
|
21
|
+
// (core-0008), so an action can never commit what validate would reject. This module is
|
|
22
|
+
// the reporting surface: discovery, iteration, and the per-file error report.
|
|
23
|
+
export async function validateInstallation({ dir } = {}) {
|
|
24
|
+
const installRoot = resolve(process.cwd(), dir || '.');
|
|
25
|
+
|
|
26
|
+
const hasDomains = await fse.pathExists(join(installRoot, 'domains'));
|
|
27
|
+
const hasCodex = await fse.pathExists(join(installRoot, 'codex'));
|
|
28
|
+
if (!hasDomains && !hasCodex) {
|
|
29
|
+
console.error(`Nothing to validate: ${installRoot} has no domains/ or codex/ (not a Phronesis installation).`);
|
|
30
|
+
process.exitCode = 1;
|
|
31
|
+
return { ok: false, checked: 0, fileErrors: [] };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const files = await listObjectFiles(installRoot);
|
|
35
|
+
|
|
36
|
+
const fileErrors = [];
|
|
37
|
+
let checked = 0;
|
|
38
|
+
|
|
39
|
+
for (const rel of files) {
|
|
40
|
+
if (!contextFor(rel)) continue; // not a typed object (e.g. codex/INDEX.md)
|
|
41
|
+
checked += 1;
|
|
42
|
+
|
|
43
|
+
let errors;
|
|
44
|
+
try {
|
|
45
|
+
const content = await readFile(join(installRoot, rel), 'utf8');
|
|
46
|
+
errors = await checkObjectContent({ installRoot, relPath: rel, content });
|
|
47
|
+
} catch (err) {
|
|
48
|
+
errors = [`could not read object: ${err.message}`];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (errors.length) fileErrors.push({ file: rel, errors });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Durable artifacts are intentionally not typed ontology objects, so they get
|
|
55
|
+
// their own narrow structural pass rather than being smuggled through contextFor.
|
|
56
|
+
// Keeping it in `phronesis validate` means an exported workspace has one familiar
|
|
57
|
+
// integrity command even though PRDs remain non-canonical.
|
|
58
|
+
const artifactFiles = await listArtifactFiles(installRoot);
|
|
59
|
+
for (const rel of artifactFiles) {
|
|
60
|
+
checked += 1;
|
|
61
|
+
let errors;
|
|
62
|
+
try {
|
|
63
|
+
const content = await readFile(join(installRoot, rel), 'utf8');
|
|
64
|
+
errors = await checkArtifactContent({ installRoot, relPath: rel, content });
|
|
65
|
+
} catch (err) {
|
|
66
|
+
errors = [`could not read artifact: ${err.message}`];
|
|
67
|
+
}
|
|
68
|
+
if (errors.length) fileErrors.push({ file: rel, errors });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Cross-file generated-state hygiene (core-0017 H5): a domain's active-context
|
|
72
|
+
// projection must not be staler than the compiled objects it summarizes. This is a
|
|
73
|
+
// CROSS-file check (not per-object checkObjectContent, which sees one file), and it runs
|
|
74
|
+
// at validate time — not as a post-commit gate hook, which fires after the write lands
|
|
75
|
+
// and could not "fail validation until refreshed."
|
|
76
|
+
const staleFindings = await checkActiveContextStaleness({ installRoot });
|
|
77
|
+
const skillFindings = await checkSkillDiscoveryStaleness({ installRoot });
|
|
78
|
+
|
|
79
|
+
// Schedule registry (core-0051 / change 0042): a HARD contract check (shape / unknown field /
|
|
80
|
+
// bad cadence / duplicate id / connector_pull target), plus the D1b warn-and-prune of orphan
|
|
81
|
+
// runtime-state entries (a `last_completed` for an id no longer declared). The prune is a
|
|
82
|
+
// safe GC of gitignored derived state — informational, never a failure. Runs BEFORE the hard
|
|
83
|
+
// check reads nothing it depends on; both are independent.
|
|
84
|
+
const scheduleResult = await validateScheduleRegistry({ installRoot });
|
|
85
|
+
const scheduleErrors = scheduleResult.errors;
|
|
86
|
+
// Prune orphan runtime-state ONLY when declarations are valid — never against an unreliable
|
|
87
|
+
// declared set (Codex round-2 F2; pruneOrphanScheduleState is also self-guarded). The prune is
|
|
88
|
+
// a NICETY: a write failure (EACCES on a read-only state dir) must WARN, never crash validation
|
|
89
|
+
// — the core call is guarded and this try/catch is belt-and-suspenders (PR #138 review).
|
|
90
|
+
let pruned = [];
|
|
91
|
+
let pruneError = null;
|
|
92
|
+
if (scheduleErrors.length === 0) {
|
|
93
|
+
try {
|
|
94
|
+
const r = await pruneOrphanScheduleState({ installRoot });
|
|
95
|
+
pruned = r.pruned;
|
|
96
|
+
pruneError = r.error || null;
|
|
97
|
+
} catch (err) {
|
|
98
|
+
pruneError = `could not prune orphan schedule state: ${err.message}`;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const ok =
|
|
103
|
+
fileErrors.length === 0 &&
|
|
104
|
+
staleFindings.length === 0 &&
|
|
105
|
+
skillFindings.length === 0 &&
|
|
106
|
+
scheduleErrors.length === 0;
|
|
107
|
+
if (pruned.length) {
|
|
108
|
+
console.error(
|
|
109
|
+
chalk.yellow(`! pruned ${pruned.length} orphan schedule-state entr(ies) (no matching declaration): ${pruned.join(', ')}`),
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
if (pruneError) {
|
|
113
|
+
// A prune write failure is a WARNING, not a validation failure — `ok` is unaffected.
|
|
114
|
+
console.error(chalk.yellow(`! ${pruneError} — validation is unaffected; re-run once .phronesis/state/ is writable to clean them`));
|
|
115
|
+
}
|
|
116
|
+
report(ok, checked, fileErrors, staleFindings, skillFindings, scheduleErrors);
|
|
117
|
+
process.exitCode = ok ? 0 : 1;
|
|
118
|
+
return { ok, checked, fileErrors, staleFindings, skillFindings, scheduleErrors, prunedScheduleState: pruned, scheduleStatePruneError: pruneError };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function report(ok, checked, fileErrors, staleFindings = [], skillFindings = [], scheduleErrors = []) {
|
|
122
|
+
if (ok) {
|
|
123
|
+
console.log(chalk.dim(`validate: ${checked} object(s) checked.`));
|
|
124
|
+
console.log(`${chalk.green('✓')} valid — every object satisfies the ontology contract.`);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
for (const { file, errors } of fileErrors) {
|
|
128
|
+
console.error(chalk.bold(file));
|
|
129
|
+
for (const e of errors) console.error(` ${chalk.red('✗')} ${e}`);
|
|
130
|
+
}
|
|
131
|
+
for (const f of staleFindings) {
|
|
132
|
+
console.error(chalk.bold(`domains/${f.domain}/ACTIVE_CONTEXT.md`));
|
|
133
|
+
console.error(
|
|
134
|
+
` ${chalk.red('✗')} stale: ${f.key} is updated ${f.objectDate} but the projection records ${f.projectionDate} — run \`phronesis active-context refresh --domain ${f.domain}\``,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
for (const f of skillFindings) {
|
|
138
|
+
console.error(chalk.bold(f.domain ? `domains/${f.domain}/AGENTS.md` : 'AGENTS.md skill discovery'));
|
|
139
|
+
console.error(` ${chalk.red('✗')} ${f.message}`);
|
|
140
|
+
}
|
|
141
|
+
if (scheduleErrors.length) {
|
|
142
|
+
console.error(chalk.bold('.phronesis/schedule.json'));
|
|
143
|
+
for (const e of scheduleErrors) console.error(` ${chalk.red('✗')} ${e}`);
|
|
144
|
+
}
|
|
145
|
+
const issues = fileErrors.reduce((n, f) => n + f.errors.length, 0);
|
|
146
|
+
const parts = [];
|
|
147
|
+
if (fileErrors.length) parts.push(`${issues} issue(s) across ${fileErrors.length} object(s)`);
|
|
148
|
+
if (staleFindings.length) parts.push(`${staleFindings.length} stale projection entr(ies)`);
|
|
149
|
+
if (skillFindings.length) parts.push(`${skillFindings.length} stale skill-discovery entr(ies)`);
|
|
150
|
+
if (scheduleErrors.length) parts.push(`${scheduleErrors.length} schedule-registry issue(s)`);
|
|
151
|
+
console.error(`${chalk.red('✗')} ${parts.join(' + ')} (${checked} object(s) checked).`);
|
|
152
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "Phronesis hooks (Tier 1, Claude Code — specs/hooks.md; changes 0010 + 0005 + 0021). UserPromptSubmit: run the pre-answer recall guard and add context only. PreToolUse(Skill) + UserPromptExpansion: mint adapter-owned skill invocation ids and emit skill.invoked. SessionStart: clear stale invocation state, emit session.start, and surface due decision reviews. Stop/SubagentStop: emit skill.completed at the real boundary after the sweep is not blocking. StopFailure: clear errored-turn invocation state without synthetic completion. PreCompact: best-effort sweep reminder before context is lost. Scripts degrade to a clean no-op when no phronesis runtime is on PATH.",
|
|
3
|
+
"hooks": {
|
|
4
|
+
"UserPromptSubmit": [
|
|
5
|
+
{
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "\"$CLAUDE_PROJECT_DIR/.phronesis/hooks/recall-guard.sh\""
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"PreToolUse": [
|
|
15
|
+
{
|
|
16
|
+
"matcher": "Skill",
|
|
17
|
+
"hooks": [
|
|
18
|
+
{
|
|
19
|
+
"type": "command",
|
|
20
|
+
"command": "\"$CLAUDE_PROJECT_DIR/.phronesis/hooks/skill-lifecycle.sh\" start"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"UserPromptExpansion": [
|
|
26
|
+
{
|
|
27
|
+
"hooks": [
|
|
28
|
+
{
|
|
29
|
+
"type": "command",
|
|
30
|
+
"command": "\"$CLAUDE_PROJECT_DIR/.phronesis/hooks/skill-lifecycle.sh\" start"
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
"SessionStart": [
|
|
36
|
+
{
|
|
37
|
+
"hooks": [
|
|
38
|
+
{
|
|
39
|
+
"type": "command",
|
|
40
|
+
"command": "\"$CLAUDE_PROJECT_DIR/.phronesis/hooks/session-start.sh\""
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
"Stop": [
|
|
46
|
+
{
|
|
47
|
+
"hooks": [
|
|
48
|
+
{
|
|
49
|
+
"type": "command",
|
|
50
|
+
"command": "\"$CLAUDE_PROJECT_DIR/.phronesis/hooks/session-sweep.sh\""
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
],
|
|
55
|
+
"SubagentStop": [
|
|
56
|
+
{
|
|
57
|
+
"hooks": [
|
|
58
|
+
{
|
|
59
|
+
"type": "command",
|
|
60
|
+
"command": "\"$CLAUDE_PROJECT_DIR/.phronesis/hooks/session-sweep.sh\" --lifecycle-only"
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
"StopFailure": [
|
|
66
|
+
{
|
|
67
|
+
"hooks": [
|
|
68
|
+
{
|
|
69
|
+
"type": "command",
|
|
70
|
+
"command": "\"$CLAUDE_PROJECT_DIR/.phronesis/hooks/skill-lifecycle.sh\" abandon"
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
],
|
|
75
|
+
"PreCompact": [
|
|
76
|
+
{
|
|
77
|
+
"hooks": [
|
|
78
|
+
{
|
|
79
|
+
"type": "command",
|
|
80
|
+
"command": "\"$CLAUDE_PROJECT_DIR/.phronesis/hooks/session-sweep.sh\" --advisory"
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
}
|