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/init.js ADDED
@@ -0,0 +1,150 @@
1
+ import { resolve, relative, isAbsolute, join } from 'node:path';
2
+ import fse from 'fs-extra';
3
+ import chalk from 'chalk';
4
+ import slugify from 'slugify';
5
+ import { createInstallation } from './scaffold.js';
6
+ import { confirmOverwrite, promptProfile } from './prompts.js';
7
+ import { resolveProfile } from './profile.js';
8
+ import { DEFAULT_DOMAIN } from './layout.js';
9
+
10
+ const DEFAULT_DIR = 'phronesis-installation';
11
+
12
+ // True if `child` is `parent` or nested under it. Used to refuse deleting the
13
+ // directory the process is standing in (or any ancestor of it).
14
+ function isInsideOrEqual(child, parent) {
15
+ const rel = relative(parent, child);
16
+ return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
17
+ }
18
+
19
+ // Recognize a prior Phronesis installation by its registry. Only a directory we
20
+ // can identify as our own is eligible for destructive replacement — Phronesis
21
+ // never deletes data it did not create (specs/security.md: agents do not delete).
22
+ async function isPhronesisInstall(dir) {
23
+ return fse.pathExists(join(dir, '.phronesis', 'registry.json'));
24
+ }
25
+
26
+ export async function init({ dir, force = false, domain, name, role, company, workingStyle } = {}) {
27
+ const targetDir = resolve(process.cwd(), dir || DEFAULT_DIR);
28
+ const domainSlug = slugify(domain || DEFAULT_DOMAIN, { lower: true, strict: true }) || DEFAULT_DOMAIN;
29
+ if (domain && domainSlug !== domain) {
30
+ console.error(`Note: --domain "${domain}" normalized to "${domainSlug}".`);
31
+ }
32
+ if (domainSlug !== DEFAULT_DOMAIN) {
33
+ console.error(
34
+ `Note: V1 ships PM-shaped templates — the "${domainSlug}" domain gets PM-door ` +
35
+ 'content until its own door exists.',
36
+ );
37
+ }
38
+
39
+ // Never delete the ground we stand on (covers `init .`, `init ..`, `init ~`).
40
+ if (isInsideOrEqual(process.cwd(), targetDir)) {
41
+ console.error(
42
+ `Refusing to initialize ${targetDir}: it is the current directory (or an ancestor). ` +
43
+ 'Run init from the parent and pass a subdirectory name.',
44
+ );
45
+ process.exitCode = 1;
46
+ return { cancelled: true };
47
+ }
48
+
49
+ let mustRemove = false;
50
+ if (await fse.pathExists(targetDir)) {
51
+ const entries = await fse.readdir(targetDir);
52
+ if (entries.length > 0) {
53
+ if (!(await isPhronesisInstall(targetDir))) {
54
+ console.error(
55
+ `Refusing to overwrite ${targetDir}: it is not empty and is not a Phronesis ` +
56
+ 'installation. Phronesis does not delete data it did not create — remove it ' +
57
+ 'yourself or choose an empty target.',
58
+ );
59
+ process.exitCode = 1;
60
+ return { cancelled: true };
61
+ }
62
+ // A prior Phronesis install — replacement is in-bounds, but still confirm.
63
+ if (!force) {
64
+ if (process.stdin.isTTY) {
65
+ const ok = await confirmOverwrite(targetDir);
66
+ if (!ok) {
67
+ console.log('Setup cancelled.');
68
+ return { cancelled: true };
69
+ }
70
+ } else {
71
+ console.error(
72
+ `${targetDir} already holds a Phronesis installation. Re-run with --force to replace it.`,
73
+ );
74
+ process.exitCode = 1;
75
+ return { cancelled: true };
76
+ }
77
+ }
78
+ // Replace it — but DEFER the destructive remove until the profile is validated below, so a bad
79
+ // --name/--role never deletes an existing install without a replacement (Codex round-1 F1).
80
+ mustRemove = true;
81
+ }
82
+ // An existing empty dir is fine to scaffold into; nothing to remove.
83
+ }
84
+
85
+ // Resolve the minimal operator profile (flags + a TTY-only prompt for missing name/role), AFTER the
86
+ // re-init refusals (never prompt then refuse) but BEFORE the destructive remove below. An over-long
87
+ // value fails closed HERE — before any install is removed (F1). Off a TTY this NEVER prompts (the
88
+ // load-bearing no-hang invariant, D2).
89
+ let profile;
90
+ try {
91
+ profile = await resolveProfile({
92
+ flags: { name, role, company, workingStyle },
93
+ isTTY: Boolean(process.stdin.isTTY),
94
+ prompt: promptProfile,
95
+ });
96
+ } catch (err) {
97
+ console.error(`Initialization failed: ${err.message}`);
98
+ process.exitCode = 1;
99
+ return { cancelled: true, error: err.message };
100
+ }
101
+
102
+ // --force replaces a prior install: the ORIGINAL remove-then-build, UNCHANGED (D4). 0039 only adds
103
+ // profile capture, and its operator-triggerable failure mode is already closed BEFORE this point —
104
+ // resolveProfile validated the flags above (so a bad --name never reaches here), and the fill itself
105
+ // is literal + atomic + budget-checked, so it cannot throw on operator input. (The general --force
106
+ // remove-then-build window — a *build* failure after the remove — is the pre-existing core-0001/0002
107
+ // behavior, out of this packet's scope; a crash-safe atomic --force is flagged as a follow-up.)
108
+ if (mustRemove) await fse.remove(targetDir);
109
+
110
+ let result;
111
+ try {
112
+ result = await createInstallation(targetDir, { domain: domainSlug, profile });
113
+ } catch (err) {
114
+ // Roll back the partial install so a failed init never leaves a half-built directory
115
+ // (registry.json is written last, so the marker won't survive either).
116
+ await fse.remove(targetDir).catch(() => {});
117
+ console.error(`Initialization failed; removed the partial install at ${targetDir}. ${err.message}`);
118
+ process.exitCode = 1;
119
+ return { cancelled: true, error: err.message };
120
+ }
121
+
122
+ printSuccess(result);
123
+ return result;
124
+ }
125
+
126
+ function printSuccess({ targetDir, domain }) {
127
+ console.log();
128
+ console.log('Phronesis installation ready.');
129
+ console.log();
130
+ console.log(` ${chalk.bold(targetDir)}`);
131
+ console.log(chalk.dim(' codex/ shared principles, citable from every domain'));
132
+ console.log(chalk.dim(` domains/${domain}/ your ${domain} workspace (raw + compiled)`));
133
+ console.log(chalk.dim(' skills/ installed engine + door skills'));
134
+ console.log(chalk.dim(' .phronesis/ runtime and configuration'));
135
+ console.log();
136
+ console.log(chalk.dim(' Source of truth is the folder. Plain Markdown, yours to keep.'));
137
+ console.log();
138
+ // Surface adapters: Claude Code reads .claude/settings.json at the install root
139
+ // automatically. Codex (change 0028 / core-0034) needs a trust step the operator must
140
+ // take, and a specific open target — so it earns an explicit note here.
141
+ console.log(' Codex (CLI + desktop app):');
142
+ console.log(chalk.dim(` Open Codex AT ${join(targetDir, 'domains', domain)} — its .codex/ hooks load only`));
143
+ console.log(chalk.dim(' from the opened directory (no parent-walk), and the session spec lives there too.'));
144
+ console.log(chalk.dim(' Trust the project + enable its hooks: CLI `/hooks`, or the app Settings → Coding →'));
145
+ console.log(chalk.dim(' Hooks. Then verify: `phronesis adapter test codex`. (Claude Code needs no trust step.)'));
146
+ console.log();
147
+ console.log(' First run: ask your agent to run `onboard me`.');
148
+ console.log(chalk.dim(' The onboard skill maps your world, compiles the first anchors, and guides seed adoption.'));
149
+ console.log();
150
+ }
package/src/layout.js ADDED
@@ -0,0 +1,394 @@
1
+ // Canonical three-layer installation layout (specs/workspace-substrate.md).
2
+ //
3
+ // Single source of truth for what `phronesis init` creates and what the fitness
4
+ // checks assert. STRUCTURE ONLY — session-file content (AGENTS.md, MEMORY.md,
5
+ // USER.md, codex/INDEX.md, the CLAUDE.md/.cursorrules symlinks, seed principles)
6
+ // is authored by T1.2 / T3.3, not here. core-0001 lays the skeleton.
7
+
8
+ // The compiled/ type-dirs come from the ontology contract's TYPE_DIR (one source —
9
+ // the scaffold and the validator must never disagree about what a type-dir is named;
10
+ // the 0012 rename made the second hand-list a live drift risk).
11
+ export const COMPILED_SUBDIRS = COMPILED_DIRS;
12
+ export const PHRONESIS_SUBDIRS = ['hooks', 'events', 'evals', 'connectors', 'invocations'];
13
+
14
+ // Default V1 configuration: a single PM door plus the shared codex. The constants
15
+ // live in @phronesis/core (the shared path-layout subset, core-0007); re-exported
16
+ // here so init/scaffold callers keep one import site. No duplication — one source.
17
+ import { DEFAULT_DOMAIN, RAW_SUBDIRS, COMPILED_DIRS, defaultActionTypesSection } from '@phronesis/core';
18
+ export { DEFAULT_DOMAIN, RAW_SUBDIRS };
19
+
20
+ // The V1 default skill set (specs/skills.md §"V1 skill set"). SINGLE SOURCE for
21
+ // both scaffold copies and .phronesis/registry.json's skills section: if this
22
+ // list drifts from skills/<name>/SKILL.md frontmatter, validate-workspace fails.
23
+ export const DEFAULT_SKILLS = Object.freeze([
24
+ Object.freeze({ name: 'ingest', version: '0.1.0' }),
25
+ Object.freeze({ name: 'compile', version: '0.1.0' }),
26
+ Object.freeze({ name: 'research', version: '0.1.0' }),
27
+ Object.freeze({ name: 'recall', version: '0.1.0' }),
28
+ Object.freeze({ name: 'lint', version: '0.1.0' }),
29
+ Object.freeze({ name: 'onboard', version: '0.2.0' }),
30
+ Object.freeze({ name: 'prep-extraction', version: '0.1.0' }),
31
+ Object.freeze({ name: 'extract-gold-dust', version: '0.1.0' }),
32
+ Object.freeze({ name: 'meeting-prep', version: '0.1.1' }),
33
+ Object.freeze({ name: 'stakeholder-update', version: '0.1.0' }),
34
+ Object.freeze({ name: 'decision-log', version: '0.1.0' }),
35
+ Object.freeze({ name: 'prd-draft', version: '0.1.0' }),
36
+ ]);
37
+
38
+ export function defaultSkillsSection() {
39
+ return Object.fromEntries(DEFAULT_SKILLS.map(({ name, version }) => [name, { version }]));
40
+ }
41
+
42
+ // Legacy two-layer artifact that must NEVER appear in a V1 installation. The
43
+ // old pming scaffold rooted everything under context/; the three-layer engine
44
+ // replaces it with codex/ (Layer 3) + domains/{slug}/{raw,compiled} (Layers 1-2).
45
+ export const FORBIDDEN_DIRS = ['context'];
46
+
47
+ // Installation-level dirs (Layer 3 codex + shared), relative to the install root.
48
+ // The codex is NOT nested under a domain: it is unaffiliated and citable by all.
49
+ export function installationDirs() {
50
+ return [
51
+ 'codex',
52
+ 'codex/seed', // immutable seeded principles (content: T5.4)
53
+ 'codex/personal', // operator-adopted/promoted principles
54
+ 'skills',
55
+ 'domains',
56
+ '.phronesis',
57
+ ...PHRONESIS_SUBDIRS.map((d) => `.phronesis/${d}`),
58
+ ];
59
+ }
60
+
61
+ // One domain workspace (Layer 1 raw + Layer 2 compiled), relative to install root.
62
+ export function domainDirs(slug) {
63
+ return [
64
+ `domains/${slug}`,
65
+ `domains/${slug}/raw`,
66
+ ...RAW_SUBDIRS.map((d) => `domains/${slug}/raw/${d}`),
67
+ `domains/${slug}/compiled`,
68
+ ...COMPILED_SUBDIRS.map((d) => `domains/${slug}/compiled/${d}`),
69
+ `domains/${slug}/memory`,
70
+ `domains/${slug}/artifacts`,
71
+ `domains/${slug}/artifacts/prd`,
72
+ `domains/${slug}/exports`,
73
+ ];
74
+ }
75
+
76
+ // The installation registry: what init created (domains) PLUS the action-type
77
+ // registry (core-0011 / T2.4) — schema, permission, target layout per type,
78
+ // GENERATED from the @phronesis/core code table so the scaffold and the compiled
79
+ // defaults can never drift (one source). The operator edits permissions here within
80
+ // the code-resident safety lattice (loosening a floor action fails closed at load);
81
+ // `phronesis registry status` shows the effective state. Deterministic (no
82
+ // timestamps) so smoke tests can assert on it.
83
+ export function registryJson(domains = [DEFAULT_DOMAIN]) {
84
+ return {
85
+ // schema_version 2 (core-0017): the compile handler landed, so the action_types
86
+ // section carries the REAL compile entry (the H2b per-object_type layout + a runnable
87
+ // schema), not the pre-T5.1 deferred stub. A pre-2 install with the deferred compile
88
+ // entry upgrades via `node harness/migrations/0017-compile-handler.mjs <install>`.
89
+ schema_version: 2,
90
+ created_by: 'phronesis-cli',
91
+ domains,
92
+ action_types: defaultActionTypesSection(),
93
+ // Hook subscriptions (core-0013 / T4.2): the V1 in-pipeline executor fires these at
94
+ // the act pipeline's requested/committed emit points; the V1.5 daemon consumes the
95
+ // same section. V1 ships ONE default: the post-compile active-context projection
96
+ // refresh (core-0017 H5) — subscribes to action_type.committed filtered to compile, so
97
+ // it fires on a trusted compile or an accept (canonical mutation) and NOT on any other
98
+ // action type. A mutation hook: a failure is audited, never blocking.
99
+ hooks: [
100
+ {
101
+ id: 'compile-active-context',
102
+ kind: 'mutation',
103
+ subscribes_to: ['action_type.committed'],
104
+ filter: { 'payload.action_type': 'compile' },
105
+ command: '.phronesis/hooks/compile-active-context.sh',
106
+ quota: { max_per_session: 500 },
107
+ classification: 'silent',
108
+ kill_switch: 'PHRONESIS_SKIP=compile-active-context',
109
+ },
110
+ ],
111
+ // Skill registry (core-0015 / T3.1): name → { version }, loaded from
112
+ // skills/{name}/SKILL.md and validated against skills.md §"Frontmatter contract"
113
+ // + the runtime-free degrade contract. Generated from DEFAULT_SKILLS so init
114
+ // installs and registers the V1 set as one deterministic unit.
115
+ skills: defaultSkillsSection(),
116
+ };
117
+ }
118
+
119
+ // Installation config. The model-swap emancipation invariant (specs/security.md)
120
+ // requires that pointing the workspace at a different LLM provider is a SINGLE
121
+ // config-value change — never an edit to operator data. `provider` is that single
122
+ // swap-point: change `id` (and `model`) here and nowhere else. No API key lives
123
+ // here — keys stay in the OS keychain (security.md). The provider id deliberately
124
+ // appears in exactly one file so swapping providers leaves domains/ and codex/
125
+ // untouched; validate-emancipation enforces that.
126
+ //
127
+ // No LLM-calling code reads this yet. Consumption (and any schema refinement) is
128
+ // Phase 5's compile/retrieve loop; this establishes the swap-point the invariant
129
+ // gates on. Deterministic (no timestamps) so smoke tests can assert on it.
130
+ //
131
+ // The event_log section (core-0006 / T4.0) is ADDITIVE — a sibling of `provider`, never a
132
+ // change to it — so the model-swap emancipation gate (which checks the provider swap-point)
133
+ // is undisturbed. `capture_message_text` is the privacy gate (default OFF: message bodies
134
+ // and recall queries stay out of the log, per security.md + change 0005);
135
+ // `retention_days` is the rotation window (`events rotate` archives older day files).
136
+ export function configJson() {
137
+ return {
138
+ schema_version: 0,
139
+ provider: {
140
+ id: 'anthropic',
141
+ model: 'claude-opus-4-8',
142
+ },
143
+ event_log: {
144
+ capture_message_text: false,
145
+ retention_days: 90,
146
+ },
147
+ };
148
+ }
149
+
150
+ // Schedule registry (core-0051 / change 0042): the operator-authored cadence DECLARATIONS,
151
+ // a separate failure domain from registry.json (ratification (a) — a malformed schedule must
152
+ // never disable the action layer). Scaffolded EMPTY: nothing fires unasked on a fresh install.
153
+ // JSON has no comments and the registry's own unknown-field refusal forbids a `$comment`
154
+ // workaround (change 0041 / F2), so the worked example lives in a SIBLING file
155
+ // (`.phronesis/schedule.example.json`) + the docs, never as commented-out JSON. TRACKED +
156
+ // exported (durable operator intent); the mutable runtime state is the separate file below.
157
+ export function scheduleJson() {
158
+ return { schedules: [] };
159
+ }
160
+
161
+ // The sibling worked example (`.phronesis/schedule.example.json`) — a VALID entry the operator
162
+ // copies into schedule.json and edits. Deterministic; passes validateScheduleRegistry as-is (a
163
+ // fitness assertion). Deliberately DOMAIN-AGNOSTIC: a `kind: skill` example must demonstrate the
164
+ // auto-advance path (it completes when `lint` runs), and only domain-agnostic skill schedules
165
+ // auto-advance — `skill.completed` carries no domain, so a domain-scoped one is confirmed via
166
+ // `phronesis due complete` (Codex round-6/7). `domain` stays an optional, spec-documented field.
167
+ export function scheduleExampleJson() {
168
+ return {
169
+ schedules: [
170
+ {
171
+ id: 'lint-weekly',
172
+ kind: 'skill',
173
+ target: 'lint',
174
+ cadence: '7d',
175
+ unattended: false,
176
+ classification: 'acknowledged',
177
+ },
178
+ ],
179
+ };
180
+ }
181
+
182
+ // Transient / non-canonical installation state: rebuildable, disposable, or
183
+ // operational. SINGLE SOURCE OF TRUTH for what the install .gitignore omits AND
184
+ // what `phronesis export` strips — the two must never drift. A backup that keeps
185
+ // privacy-sensitive event logs, or a .gitignore that tracks disposable renders,
186
+ // breaks the emancipation promise. Canonical state (compiled/, codex/, raw/, the
187
+ // session files, config) is never listed here.
188
+ //
189
+ // `gitignore` entries are .gitignore lines; `exclude` entries are tar globs.
190
+ //
191
+ // Path-specific globs are given in BOTH a bare and a `*/`-prefixed form because
192
+ // GNU tar and bsdtar disagree on `--exclude` matching: GNU defaults to
193
+ // non-anchored matching (the bare `.phronesis/events` matches inside the
194
+ // `install/...` member path), while bsdtar anchors at the start (so it needs the
195
+ // leading `*/` to skip the top-level `install/` dir). Listing both covers both
196
+ // families; a pattern that matches nothing is harmless. validate-emancipation
197
+ // asserts the strip actually bit on whatever tar ran.
198
+ const TRANSIENT_STATE = [
199
+ {
200
+ comment: 'OS cruft',
201
+ gitignore: ['.DS_Store', 'Thumbs.db'],
202
+ exclude: ['.DS_Store', 'Thumbs.db'],
203
+ },
204
+ {
205
+ comment: 'Disposable renders (exports are disposable per workspace-substrate)',
206
+ gitignore: ['domains/*/exports/'],
207
+ exclude: ['domains/*/exports', '*/domains/*/exports'],
208
+ },
209
+ {
210
+ comment: 'Operational event stream (local logs; privacy-sensitive, retained locally)',
211
+ gitignore: ['.phronesis/events/'],
212
+ exclude: ['.phronesis/events', '*/.phronesis/events'],
213
+ },
214
+ {
215
+ comment: 'Hook execution audit log (operational, local — same class as the event stream)',
216
+ gitignore: ['.phronesis/audit.log'],
217
+ exclude: ['.phronesis/audit.log', '*/.phronesis/audit.log'],
218
+ },
219
+ {
220
+ // audit.log retention archive (core-0027 J5): `events rotate`/`purge --archive` moves
221
+ // audit lines past retention here. Same operational/local class as audit.log itself —
222
+ // gitignore + export globs are per-entry explicit lists (never inherited from a sibling),
223
+ // so the archive needs its own row or it would ship in export while audit.log is stripped.
224
+ comment: 'Hook audit retention archive (operational, local — rotated audit.log lines)',
225
+ gitignore: ['.phronesis/audit.log.archive'],
226
+ exclude: ['.phronesis/audit.log.archive', '*/.phronesis/audit.log.archive'],
227
+ },
228
+ {
229
+ comment: 'Derived machine search index — rebuildable from compiled/ + codex/ (phronesis index rebuild)',
230
+ gitignore: ['.phronesis/index/'],
231
+ exclude: ['.phronesis/index', '*/.phronesis/index'],
232
+ },
233
+ {
234
+ // Compile draft staging (core-0017): UNAPPROVED candidates for untrusted compiles,
235
+ // pending operator accept/reject. Transient-but-NOT-derived (distinct from the index):
236
+ // excluded because a draft is unapproved, not because it is rebuildable. The exclusion
237
+ // is NOT inherited — gitignore + exportExcludeGlobs are per-subdir explicit lists, so
238
+ // without this entry a draft is git-committed AND shipped in `phronesis export`,
239
+ // reintroducing the exact "unapproved draft visible as canonical" failure 0015 prevents.
240
+ comment: 'Compile draft staging — unapproved candidates pending operator review (security.md draft lifecycle)',
241
+ gitignore: ['.phronesis/drafts/'],
242
+ exclude: ['.phronesis/drafts', '*/.phronesis/drafts'],
243
+ },
244
+ {
245
+ // Compile-review batch journal (core-0029 PR2): the durable receipt a `compile review
246
+ // --approve-all` writes so a mid-batch crash is recoverable via --resume. Transient
247
+ // operational state — its intent/cursor reference local paths + event ids and it is
248
+ // deleted once the batch completes; it must never be committed or shipped in export.
249
+ comment: 'Compile-review batch journal — transient resume receipt (core-0029 compile review)',
250
+ gitignore: ['.phronesis/compile-review-journal.json'],
251
+ exclude: ['.phronesis/compile-review-journal.json', '*/.phronesis/compile-review-journal.json'],
252
+ },
253
+ {
254
+ // Claude Code skill lifecycle active records (core-0032): adapter-minted
255
+ // invocation_id state scoped to the current session. Transient-but-NOT-derived,
256
+ // and privacy-sensitive operational state: a stale export or commit would turn a
257
+ // live attribution record into portable data. The exclusion is explicit because
258
+ // gitignore/export globs are per-subdir lists, not inherited from .phronesis/.
259
+ comment: 'Skill lifecycle active records — adapter-owned session state (never canonical)',
260
+ gitignore: ['.phronesis/invocations/'],
261
+ exclude: ['.phronesis/invocations', '*/.phronesis/invocations'],
262
+ },
263
+ {
264
+ // Schedule runtime state (core-0051 / change 0042): mutable `last_completed` per schedule
265
+ // id. The DECLARATIONS (.phronesis/schedule.json, scaffolded above) are tracked + exported;
266
+ // this DERIVED completion state is gitignored + export-excluded (change 0008 tracked-vs-
267
+ // derived line). Losing it degrades to one-time over-surfacing, never a silent skip — so it
268
+ // must never ship in a backup or a commit and read back as authored intent. Per-subdir
269
+ // explicit lists (not inherited from .phronesis/), like every row here.
270
+ comment: 'Schedule runtime state — derived last_completed per schedule id (never canonical)',
271
+ gitignore: ['.phronesis/state/'],
272
+ exclude: ['.phronesis/state', '*/.phronesis/state'],
273
+ },
274
+ ];
275
+
276
+ // Tar exclude globs for `phronesis export`: strips the transient set above so the
277
+ // archive carries canonical state only — no privacy-sensitive logs, no disposable
278
+ // renders. "Strip nothing canonical" means exactly this set and no more.
279
+ export function exportExcludeGlobs() {
280
+ return TRANSIENT_STATE.flatMap((e) => e.exclude);
281
+ }
282
+
283
+ // Installation .gitignore. The operator may version their install for backup or
284
+ // portability — this ignores ONLY the transient set above. Canonical workspace
285
+ // state (compiled/, codex/, raw/, the session files, config) stays tracked. The
286
+ // machine search index landed with core-0014, so its entry is a real TRANSIENT_STATE
287
+ // row (the placeholder comment this block once carried is retired).
288
+ export function installationGitignore() {
289
+ const lines = [
290
+ '# Phronesis installation — generated and transient state only.',
291
+ '# Your canonical workspace (compiled/, codex/, raw/, session files) IS tracked.',
292
+ ];
293
+ for (const { comment, gitignore } of TRANSIENT_STATE) {
294
+ lines.push('', `# ${comment}`, ...gitignore);
295
+ }
296
+ lines.push('');
297
+ return lines.join('\n');
298
+ }
299
+
300
+ // Session-file + codex templates copied into a fresh installation. `from` is
301
+ // relative to apps/cli/templates/; `to` is relative to the install root. V1 ships
302
+ // the PM door's templates; other doors get PM-shaped content until their own exist.
303
+ // The session-boundary hook set (core-0013 / T4.2, change 0010 D4) ships with every
304
+ // install: Claude Code wiring in .claude/settings.json plus hook scripts — all degrade
305
+ // to a clean no-op when no Phronesis runtime is on PATH (change 0013).
306
+ //
307
+ // Surface adapters (specs/hooks.md §"Surface adapters") wire a hook-capable AI surface's
308
+ // native lifecycle events to the SAME .phronesis/hooks/ scripts. Claude Code is the first
309
+ // (.claude/settings.json @ install root, via $CLAUDE_PROJECT_DIR). Codex is the second
310
+ // (change 0028 / core-0034): its .codex/ hook config + launchers are emitted at the DOMAIN
311
+ // WORKSPACE dir (domains/{slug}/), because Codex loads .codex/ ONLY from the opened
312
+ // directory (no parent-walk, spike 2026-06-21) and the session spec (AGENTS.md/CLAUDE.md)
313
+ // already lives there — so the operator opens one directory and gets both. The launchers
314
+ // carry no slug-specific content (they walk up to the install's .phronesis/), so the
315
+ // templates are static → a re-init is byte-identical and does not re-trigger Codex's
316
+ // per-hook trust (core-0034 P4). See apps/cli/templates/codex-surface/.
317
+ // The Codex surface adapter contract (change 0028 / core-0034): Codex native event →
318
+ // the .codex/ launcher that handles it → the shared .phronesis/hooks/ script it execs →
319
+ // the reinforcement role + compliance tier (event-log.md §"Compliance levels"). SINGLE
320
+ // SOURCE: init emits hooks.json wiring exactly these; `phronesis adapter test codex` and
321
+ // harness/fitness/validate-codex-adapter assert the emitted config against it, so the
322
+ // emission and its checks can never disagree. `reinforcement` events are the Level-1 loop
323
+ // (recall / due-check / sweep, + the compaction reminder that supports it); `lifecycle` is
324
+ // the Level-2 skill-lifecycle close, PARTIAL on Codex (no invocation_id minted — no
325
+ // fabricated events, change 0021).
326
+ export const CODEX_ADAPTER_HOOKS = [
327
+ { event: 'SessionStart', launcher: 'session-start.sh', script: 'session-start.sh', role: 'due-check', tier: 'reinforcement' },
328
+ { event: 'UserPromptSubmit', launcher: 'recall-guard.sh', script: 'recall-guard.sh', role: 'recall-guard', tier: 'reinforcement' },
329
+ { event: 'Stop', launcher: 'session-sweep.sh', script: 'session-sweep.sh', role: 'capture-sweep', tier: 'reinforcement' },
330
+ { event: 'SubagentStop', launcher: 'session-sweep-subagent.sh', script: 'session-sweep.sh', role: 'subagent-lifecycle-close', tier: 'lifecycle' },
331
+ { event: 'PreCompact', launcher: 'session-sweep-precompact.sh', script: 'session-sweep.sh', role: 'compaction-reminder', tier: 'reinforcement' },
332
+ ];
333
+ const CODEX_SURFACE_HOOKS = CODEX_ADAPTER_HOOKS.map((h) => h.launcher);
334
+ // Every file under codex-surface/hooks/ that init emits: the per-event launchers plus the
335
+ // shared _resolve.sh they source for cwd-independent, decoy-immune install-root resolution
336
+ // (core-0034 adversarial review #1). _resolve.sh is infrastructure, not an event hook, so it
337
+ // is NOT in CODEX_ADAPTER_HOOKS — but it must ship, or every launcher fails to source it.
338
+ export const CODEX_SURFACE_RESOLVER = '_resolve.sh';
339
+ const CODEX_SURFACE_FILES = [CODEX_SURFACE_RESOLVER, ...CODEX_SURFACE_HOOKS];
340
+ export function templateCopies(slug = DEFAULT_DOMAIN) {
341
+ return [
342
+ { from: 'domains/pm/AGENTS.md', to: `domains/${slug}/AGENTS.md` },
343
+ { from: 'domains/pm/MEMORY.md', to: `domains/${slug}/MEMORY.md` },
344
+ { from: 'domains/pm/USER.md', to: `domains/${slug}/USER.md` },
345
+ { from: 'codex/INDEX.md', to: 'codex/INDEX.md' },
346
+ // Immutable PM-shaped seed principle library (T5.4): the whole codex/seed/ template
347
+ // tree (collections of codex-principle-seed objects) copied verbatim into the install.
348
+ // Ships INSIDE the public phronesis package (package.json files: ["templates"]) — no
349
+ // separate repo-root mirror, unlike the skills/ tree. Quarantined out of retrieval until
350
+ // adopted via `promote_to_codex --from-seed`; discoverable through codex/INDEX.md
351
+ // (specs/compile-retrieve-loop.md §"Seed-vs-personal quarantine").
352
+ { from: 'codex/seed', to: 'codex/seed' },
353
+ { from: 'claude/settings.json', to: '.claude/settings.json' },
354
+ { from: 'phronesis-hooks/recall-guard.sh', to: '.phronesis/hooks/recall-guard.sh', executable: true },
355
+ { from: 'phronesis-hooks/skill-lifecycle.sh', to: '.phronesis/hooks/skill-lifecycle.sh', executable: true },
356
+ { from: 'phronesis-hooks/session-start.sh', to: '.phronesis/hooks/session-start.sh', executable: true },
357
+ { from: 'phronesis-hooks/session-sweep.sh', to: '.phronesis/hooks/session-sweep.sh', executable: true },
358
+ // The post-compile active-context projection refresh (core-0017 H5), registered by
359
+ // default in registryJson's hooks array — both must ship together or the subscription
360
+ // names a command that is not there.
361
+ { from: 'phronesis-hooks/compile-active-context.sh', to: '.phronesis/hooks/compile-active-context.sh', executable: true },
362
+ // Codex surface adapter (change 0028 / core-0034): hook config + launchers at the
363
+ // domain workspace open target. NOTE the template source dir is `codex-surface/`, not
364
+ // `codex/` — the latter is the unrelated shared-codex INDEX (Layer 3 principles).
365
+ { from: 'codex-surface/hooks.json', to: `domains/${slug}/.codex/hooks.json` },
366
+ { from: 'codex-surface/README.md', to: `domains/${slug}/.codex/README.md` },
367
+ ...CODEX_SURFACE_FILES.map((name) => ({
368
+ from: `codex-surface/hooks/${name}`,
369
+ to: `domains/${slug}/.codex/hooks/${name}`,
370
+ executable: true,
371
+ })),
372
+ ];
373
+ }
374
+
375
+ // Default V1 skills copied from repo-root skills/ into operator installs. The
376
+ // rubric is runtime-referenced by every SKILL.md body; golden cases and dev notes
377
+ // deliberately stay out of installs.
378
+ export function defaultSkillCopies() {
379
+ return DEFAULT_SKILLS.flatMap(({ name }) => [
380
+ { from: `skills/${name}/SKILL.md`, to: `skills/${name}/SKILL.md` },
381
+ { from: `skills/${name}/evals/rubric.md`, to: `skills/${name}/evals/rubric.md` },
382
+ ]);
383
+ }
384
+
385
+ // Cross-surface entry points: vendor filenames symlink to the canonical AGENTS.md
386
+ // (specs/canonical-session-spec.md). Target is RELATIVE ('AGENTS.md', same dir) so
387
+ // the installation stays portable when moved or tarred, and resolves with no
388
+ // Phronesis runtime present (the skill-portability invariant).
389
+ export function domainSymlinks(slug = DEFAULT_DOMAIN) {
390
+ return [
391
+ { link: `domains/${slug}/CLAUDE.md`, target: 'AGENTS.md' },
392
+ { link: `domains/${slug}/.cursorrules`, target: 'AGENTS.md' },
393
+ ];
394
+ }
package/src/links.js ADDED
@@ -0,0 +1,83 @@
1
+ // `phronesis links` — the substrate/debug surface over the Stage 2 link walk
2
+ // (core-0005 / T2.3). Plumbing, not porcelain (cf. `phronesis act`, ontology.md:163):
3
+ // it makes the walk exercisable + gate-able on the real binary. T5.2 composes the same
4
+ // link index (@phronesis/core) into the operator-facing `phronesis query`; this command
5
+ // does not claim the search/query surfaces.
6
+
7
+ import { resolve } from 'node:path';
8
+ import fse from 'fs-extra';
9
+ import chalk from 'chalk';
10
+ import { buildLinkIndex } from '@phronesis/core';
11
+
12
+ export async function runLinks({ dir, object, hops = 2, direction = 'both', json = false } = {}) {
13
+ const installRoot = resolve(process.cwd(), dir || '.');
14
+
15
+ const hasDomains = await fse.pathExists(resolve(installRoot, 'domains'));
16
+ const hasCodex = await fse.pathExists(resolve(installRoot, 'codex'));
17
+ if (!hasDomains && !hasCodex) {
18
+ console.error(`Not a Phronesis installation: ${installRoot} has no domains/ or codex/.`);
19
+ process.exitCode = 1;
20
+ return null;
21
+ }
22
+
23
+ if (!object) {
24
+ console.error('links: name an object to walk from (a path, a name, or {type-dir}/{name}).');
25
+ process.exitCode = 1;
26
+ return null;
27
+ }
28
+
29
+ const index = await buildLinkIndex({ installRoot });
30
+ const found = index.find(object);
31
+ if (found.error) {
32
+ console.error(found.error);
33
+ process.exitCode = 1;
34
+ return null;
35
+ }
36
+
37
+ const subgraph = index.walk(found.path, { hops, direction });
38
+
39
+ if (json) {
40
+ console.log(JSON.stringify(subgraph));
41
+ } else {
42
+ console.log(formatHuman(subgraph, index));
43
+ }
44
+ process.exitCode = 0;
45
+ return subgraph;
46
+ }
47
+
48
+ function formatHuman(sub, index) {
49
+ const root = index.node(sub.root);
50
+ const lines = [
51
+ chalk.bold(root?.title || sub.root),
52
+ chalk.dim(sub.root),
53
+ chalk.dim(
54
+ `${sub.hops}-hop subgraph · direction ${sub.direction} · ${sub.nodes.length} node(s), ${sub.edges.length} edge(s)`,
55
+ ),
56
+ ];
57
+
58
+ const byHop = new Map();
59
+ for (const n of sub.nodes) {
60
+ if (n.hop === 0) continue;
61
+ if (!byHop.has(n.hop)) byHop.set(n.hop, []);
62
+ byHop.get(n.hop).push(n);
63
+ }
64
+
65
+ if (byHop.size === 0) {
66
+ lines.push('', chalk.dim('no linked objects within range.'));
67
+ return lines.join('\n');
68
+ }
69
+
70
+ for (const hop of [...byHop.keys()].sort((a, b) => a - b)) {
71
+ lines.push('', chalk.dim(`hop ${hop}`));
72
+ for (const n of byHop.get(hop)) {
73
+ const label = n.kind === 'object' ? `${n.type} ${n.name}` : `${n.kind} ${n.path}`;
74
+ lines.push(` ${label}${n.title ? ` ${n.title}` : ''}`);
75
+ }
76
+ }
77
+
78
+ lines.push('', chalk.dim('edges'));
79
+ for (const e of sub.edges) {
80
+ lines.push(` ${e.from} --[${e.rel || '?'}${e.forward ? ' forward' : ''}]--> ${e.to}`);
81
+ }
82
+ return lines.join('\n');
83
+ }