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.
Files changed (142) hide show
  1. package/README.md +66 -0
  2. package/package.json +34 -0
  3. package/src/act.js +61 -0
  4. package/src/active-context.js +56 -0
  5. package/src/adapter.js +386 -0
  6. package/src/cli.js +1893 -0
  7. package/src/codex-hooks-schema.js +92 -0
  8. package/src/compile.js +513 -0
  9. package/src/doctor.js +303 -0
  10. package/src/due.js +271 -0
  11. package/src/eval.js +467 -0
  12. package/src/event.js +212 -0
  13. package/src/export.js +136 -0
  14. package/src/guard.js +89 -0
  15. package/src/hooks.js +230 -0
  16. package/src/ingest.js +965 -0
  17. package/src/init.js +150 -0
  18. package/src/layout.js +394 -0
  19. package/src/links.js +83 -0
  20. package/src/lint.js +58 -0
  21. package/src/profile.js +116 -0
  22. package/src/prompts.js +26 -0
  23. package/src/registry.js +190 -0
  24. package/src/scaffold.js +134 -0
  25. package/src/search.js +139 -0
  26. package/src/skill-discovery.js +117 -0
  27. package/src/skills-bump.js +885 -0
  28. package/src/skills.js +355 -0
  29. package/src/subscription.js +5 -0
  30. package/src/validate.js +152 -0
  31. package/templates/claude/settings.json +86 -0
  32. package/templates/codex/INDEX.md +68 -0
  33. package/templates/codex/seed/ai-practice/compile-dont-just-capture.md +42 -0
  34. package/templates/codex/seed/ai-practice/design-for-the-distribution.md +42 -0
  35. package/templates/codex/seed/ai-practice/from-spec-author-to-outcome-curator.md +43 -0
  36. package/templates/codex/seed/ai-practice/recall-builds-trust-when-calibrated.md +70 -0
  37. package/templates/codex/seed/ai-practice/the-eval-is-the-spec.md +42 -0
  38. package/templates/codex/seed/ai-practice/the-gold-dust-leaves-no-trace.md +72 -0
  39. package/templates/codex/seed/ai-practice/the-model-is-not-the-moat.md +40 -0
  40. package/templates/codex/seed/ai-practice/when-generation-is-cheap-judgment-is-the-work.md +39 -0
  41. package/templates/codex/seed/judgment/contextual-stewardship-is-the-senior-skill.md +50 -0
  42. package/templates/codex/seed/judgment/decision-quality-is-distinct-from-outcome-quality.md +39 -0
  43. package/templates/codex/seed/judgment/embrace-reality-ego-and-blind-spots.md +38 -0
  44. package/templates/codex/seed/judgment/groups-seek-consensus-individuals-seek-truth.md +44 -0
  45. package/templates/codex/seed/judgment/heresies-are-where-the-edge-lives.md +42 -0
  46. package/templates/codex/seed/judgment/judgment-compounds-through-honest-reflection.md +38 -0
  47. package/templates/codex/seed/judgment/phronesis-is-judgment-in-the-particular.md +67 -0
  48. package/templates/codex/seed/judgment/show-me-the-apparatus.md +40 -0
  49. package/templates/codex/seed/judgment/small-judgment-gaps-compound-under-leverage.md +37 -0
  50. package/templates/codex/seed/judgment/taste-is-compressed-judgment.md +38 -0
  51. package/templates/codex/seed/judgment/trust-taste-only-where-feedback-is-valid.md +42 -0
  52. package/templates/codex/seed/judgment/weigh-the-reasoning-not-the-recommendation.md +43 -0
  53. package/templates/codex/seed/leadership/a-managers-output-is-the-teams-output.md +35 -0
  54. package/templates/codex/seed/leadership/most-team-problems-are-structural.md +40 -0
  55. package/templates/codex/seed/leadership/next-play.md +36 -0
  56. package/templates/codex/seed/leadership/psychological-safety.md +39 -0
  57. package/templates/codex/seed/leadership/right-size-your-teams.md +38 -0
  58. package/templates/codex/seed/leadership/spend-time-on-high-leverage-activities.md +36 -0
  59. package/templates/codex/seed/leadership/talent-density-is-the-master-variable.md +39 -0
  60. package/templates/codex/seed/leadership/under-pressure-calm-is-contagious.md +36 -0
  61. package/templates/codex/seed/leadership/your-best-alternative-is-your-leverage.md +37 -0
  62. package/templates/codex/seed/pm/a-problem-well-stated-is-half-solved.md +39 -0
  63. package/templates/codex/seed/pm/a-real-strategy-makes-planning-trivial.md +40 -0
  64. package/templates/codex/seed/pm/barrels-and-ammunition.md +39 -0
  65. package/templates/codex/seed/pm/develop-taste-in-your-beliefs.md +38 -0
  66. package/templates/codex/seed/pm/hire-for-progress.md +44 -0
  67. package/templates/codex/seed/pm/jobs-are-stable-solutions-churn.md +39 -0
  68. package/templates/codex/seed/pm/level-by-the-ambiguity-someone-can-own.md +39 -0
  69. package/templates/codex/seed/pm/listen-before-you-prescribe.md +38 -0
  70. package/templates/codex/seed/pm/live-in-the-future-and-build-whats-missing.md +38 -0
  71. package/templates/codex/seed/pm/match-effort-to-leverage.md +37 -0
  72. package/templates/codex/seed/pm/not-my-fault-but-my-problem.md +40 -0
  73. package/templates/codex/seed/pm/pick-the-job-then-refuse-everything-outside-it.md +37 -0
  74. package/templates/codex/seed/pm/retention-sets-the-ceiling.md +38 -0
  75. package/templates/codex/seed/pm/scale-with-leaders-not-process.md +38 -0
  76. package/templates/codex/seed/pm/solution-discovery-over-problem-validation.md +38 -0
  77. package/templates/codex/seed/pm/tenfold-better-on-a-painful-problem.md +40 -0
  78. package/templates/codex-surface/README.md +73 -0
  79. package/templates/codex-surface/hooks/_resolve.sh +36 -0
  80. package/templates/codex-surface/hooks/recall-guard.sh +30 -0
  81. package/templates/codex-surface/hooks/session-start.sh +18 -0
  82. package/templates/codex-surface/hooks/session-sweep-precompact.sh +20 -0
  83. package/templates/codex-surface/hooks/session-sweep-subagent.sh +23 -0
  84. package/templates/codex-surface/hooks/session-sweep.sh +19 -0
  85. package/templates/codex-surface/hooks.json +69 -0
  86. package/templates/domains/pm/AGENTS.md +111 -0
  87. package/templates/domains/pm/MEMORY.md +7 -0
  88. package/templates/domains/pm/USER.md +8 -0
  89. package/templates/phronesis-hooks/compile-active-context.sh +38 -0
  90. package/templates/phronesis-hooks/recall-guard.sh +103 -0
  91. package/templates/phronesis-hooks/session-start.sh +90 -0
  92. package/templates/phronesis-hooks/session-sweep.sh +140 -0
  93. package/templates/phronesis-hooks/skill-lifecycle.sh +37 -0
  94. package/templates/skills/compile/SKILL.md +279 -0
  95. package/templates/skills/compile/evals/rubric.md +75 -0
  96. package/templates/skills/decision-log/SKILL.md +156 -0
  97. package/templates/skills/decision-log/evals/rubric.md +97 -0
  98. package/templates/skills/extract-gold-dust/SKILL.md +311 -0
  99. package/templates/skills/extract-gold-dust/evals/rubric.md +52 -0
  100. package/templates/skills/ingest/SKILL.md +204 -0
  101. package/templates/skills/ingest/evals/rubric.md +117 -0
  102. package/templates/skills/lint/SKILL.md +246 -0
  103. package/templates/skills/lint/evals/rubric.md +139 -0
  104. package/templates/skills/meeting-prep/SKILL.md +195 -0
  105. package/templates/skills/meeting-prep/evals/rubric.md +93 -0
  106. package/templates/skills/onboard/SKILL.md +258 -0
  107. package/templates/skills/onboard/evals/rubric.md +60 -0
  108. package/templates/skills/prd-draft/SKILL.md +193 -0
  109. package/templates/skills/prd-draft/evals/rubric.md +38 -0
  110. package/templates/skills/prep-extraction/SKILL.md +217 -0
  111. package/templates/skills/prep-extraction/evals/rubric.md +45 -0
  112. package/templates/skills/recall/SKILL.md +155 -0
  113. package/templates/skills/recall/evals/rubric.md +37 -0
  114. package/templates/skills/research/SKILL.md +247 -0
  115. package/templates/skills/research/evals/rubric.md +139 -0
  116. package/templates/skills/stakeholder-update/SKILL.md +163 -0
  117. package/templates/skills/stakeholder-update/evals/rubric.md +111 -0
  118. package/vendor/core/package.json +25 -0
  119. package/vendor/core/src/action-registry.js +459 -0
  120. package/vendor/core/src/action-writers.js +201 -0
  121. package/vendor/core/src/actions.js +2140 -0
  122. package/vendor/core/src/active-context.js +167 -0
  123. package/vendor/core/src/artifacts.js +301 -0
  124. package/vendor/core/src/content-digest.js +21 -0
  125. package/vendor/core/src/contract.js +245 -0
  126. package/vendor/core/src/event-log.js +338 -0
  127. package/vendor/core/src/guard.js +398 -0
  128. package/vendor/core/src/hook-executor.js +527 -0
  129. package/vendor/core/src/index.js +180 -0
  130. package/vendor/core/src/ingest.js +286 -0
  131. package/vendor/core/src/install-root.js +42 -0
  132. package/vendor/core/src/layout.js +13 -0
  133. package/vendor/core/src/link-index.js +198 -0
  134. package/vendor/core/src/lint.js +368 -0
  135. package/vendor/core/src/schedule.js +535 -0
  136. package/vendor/core/src/search.js +534 -0
  137. package/vendor/core/src/session-hooks.js +129 -0
  138. package/vendor/core/src/skill-lifecycle.js +479 -0
  139. package/vendor/core/src/skill-registry.js +524 -0
  140. package/vendor/core/src/subscription.js +84 -0
  141. package/vendor/core/src/validate-object.js +62 -0
  142. 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';
@@ -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
+ }