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/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
|
+
}
|