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
@@ -0,0 +1,180 @@
1
+ // @phronesis/core — the shared ontology/event substrate (ADR-0003; changes/0008).
2
+ // One package, one entry point: every surface (CLI today; daemon + GUI later) imports
3
+ // these primitives from here, so the validator, the link index, and the action types
4
+ // can never disagree about what counts as an object, a link, or an event.
5
+
6
+ export {
7
+ OBJECT_TYPES,
8
+ DOMAIN_TYPES,
9
+ CODEX_PRINCIPLE_TYPES,
10
+ ENTITY_TYPES,
11
+ TYPE_DIR,
12
+ COMPILED_DIRS,
13
+ ENUMS,
14
+ LINK_RELS,
15
+ RE_SLUG,
16
+ DATE_RE,
17
+ classifyTarget,
18
+ validateFrontmatter,
19
+ } from './contract.js';
20
+
21
+ export { checkObjectContent } from './validate-object.js';
22
+
23
+ export {
24
+ ARTIFACT_KINDS,
25
+ artifactContextFor,
26
+ listArtifactFiles,
27
+ checkArtifactContent,
28
+ readArtifactRecord,
29
+ artifactPath,
30
+ writeArtifact,
31
+ } from './artifacts.js';
32
+
33
+ export { assertRawDestination, stageRawSource } from './ingest.js';
34
+
35
+ export { contentDigest, digestFile } from './content-digest.js';
36
+
37
+ export {
38
+ slugify,
39
+ serializeFrontmatter,
40
+ formatLogEntry,
41
+ appendLogBlock,
42
+ splitObjectSource,
43
+ stageValidateRename,
44
+ stageValidateRenameAll,
45
+ } from './action-writers.js';
46
+
47
+ export {
48
+ ACTION_REGISTRY,
49
+ listActionTypes,
50
+ permissionGate,
51
+ runAction,
52
+ compileAccept,
53
+ compileReject,
54
+ resumeCompileAccept,
55
+ CONVERGENT_COMMITTED_HOOKS,
56
+ defaultActionTypesSection,
57
+ resolveActionTypes,
58
+ resolveDomain,
59
+ } from './actions.js';
60
+
61
+ export {
62
+ buildActiveContextProjection,
63
+ refreshActiveContext,
64
+ checkActiveContextStaleness,
65
+ } from './active-context.js';
66
+
67
+ export { PERMISSION_ORDER, layoutPattern } from './action-registry.js';
68
+
69
+ export {
70
+ HOOK_KINDS,
71
+ HOOK_CLASSIFICATIONS,
72
+ HOOK_TIMEOUT_MS,
73
+ resolveHooks,
74
+ matchingHooks,
75
+ executeHooksFor,
76
+ auditLogPath,
77
+ auditArchivePath,
78
+ readAuditLog,
79
+ appendAuditEntry,
80
+ enforceAuditRetention,
81
+ purgeAuditLog,
82
+ } from './hook-executor.js';
83
+
84
+ export { computeDueDecisions, runDueCheck, sweepStatus } from './session-hooks.js';
85
+
86
+ export {
87
+ SCHEDULE_REGISTRY_REL,
88
+ SCHEDULE_STATE_REL,
89
+ SCHEDULE_KINDS,
90
+ parseCadenceDays,
91
+ loadScheduleRegistry,
92
+ loadScheduleState,
93
+ writeScheduleState,
94
+ validateScheduleRegistry,
95
+ computeScheduleDue,
96
+ markScheduleCompleted,
97
+ advanceSchedulesForSkill,
98
+ runScheduleDueCheck,
99
+ pruneOrphanScheduleState,
100
+ scheduleStaleness,
101
+ } from './schedule.js';
102
+
103
+ export {
104
+ invocationsDir,
105
+ activeInvocationPath,
106
+ readActiveInvocation,
107
+ clearActiveInvocation,
108
+ beginSkillInvocation,
109
+ completeActiveSkillInvocation,
110
+ abandonActiveSkillInvocation,
111
+ sweepStaleSkillInvocation,
112
+ resolveInvocationIdForAct,
113
+ meshZeroWarning,
114
+ } from './skill-lifecycle.js';
115
+
116
+ export {
117
+ SKILL_WEIGHTS,
118
+ MODEL_TIERS,
119
+ skillFilePath,
120
+ parsePromptPin,
121
+ resolveSkillsSection,
122
+ validateSkillSource,
123
+ loadSkills,
124
+ } from './skill-registry.js';
125
+
126
+ export { validateSubscription, matchesSubscription } from './subscription.js';
127
+
128
+ export {
129
+ splitFrontmatter,
130
+ walkMarkdown,
131
+ contextFor,
132
+ listObjectFiles,
133
+ listRetrievableObjectFiles,
134
+ } from './workspace-scan.js';
135
+
136
+ export { LinkIndex, buildLinkIndex, buildLinkIndexFromRecords } from './link-index.js';
137
+
138
+ export {
139
+ SearchIndex,
140
+ loadSearchIndex,
141
+ rebuildSearchIndex,
142
+ indexDir,
143
+ snapshotPath,
144
+ } from './search.js';
145
+
146
+ export {
147
+ DEFAULT_LINT_STALE_MONTHS,
148
+ LINT_BYPASS_WINDOW_DAYS,
149
+ LINT_BYPASS_THRESHOLD,
150
+ runWorkspaceLint,
151
+ } from './lint.js';
152
+
153
+ export {
154
+ GUARD_ID,
155
+ GUARD_RULESET_VERSION,
156
+ detectContextRelevance,
157
+ runContextRelevanceGuard,
158
+ } from './guard.js';
159
+
160
+ export {
161
+ EVENT_TYPES,
162
+ isKnownType,
163
+ isValidType,
164
+ isReservedType,
165
+ eventsDir,
166
+ localDateStamp,
167
+ makeEventId,
168
+ readEventLogConfig,
169
+ validateEventInput,
170
+ applyCaptureGate,
171
+ appendEvent,
172
+ listDayFiles,
173
+ readEvents,
174
+ enforceRetention,
175
+ purgeEvents,
176
+ } from './event-log.js';
177
+
178
+ export { findInstallRoot, resolveInstall } from './install-root.js';
179
+
180
+ export { DEFAULT_DOMAIN, RAW_SUBDIRS } from './layout.js';
@@ -0,0 +1,286 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import { link, readFile, readdir, writeFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import fse from 'fs-extra';
5
+ import yaml from 'js-yaml';
6
+ import { RE_SLUG } from './contract.js';
7
+ import { localDateStamp } from './event-log.js';
8
+ import { RAW_SUBDIRS } from './layout.js';
9
+ import { serializeFrontmatter, slugify } from './action-writers.js';
10
+ import { matchFrontmatterBlock } from './workspace-scan.js';
11
+
12
+ const EXT_RE = /^[A-Za-z0-9]+$/;
13
+ const CONNECTOR_BRIDGE_PROVENANCE_FIELDS = new Set(['source_connector', 'source_external_id', 'source_timestamp']);
14
+ const CONNECTOR_BRIDGE_IGNORED_FIELDS = new Set([
15
+ 'trust_tier',
16
+ 'fetched_at',
17
+ 'fetched_by',
18
+ 'content_hash',
19
+ 'source_file',
20
+ 'source_path',
21
+ 'source_url',
22
+ 'content_type',
23
+ 'retrieval_tool',
24
+ 'operator_affirmed_trusted',
25
+ ]);
26
+
27
+ function plainObject(value) {
28
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
29
+ }
30
+
31
+ function normalizeExt(ext = 'md') {
32
+ const normalized = String(ext || 'md').replace(/^\./, '').toLowerCase();
33
+ if (!EXT_RE.test(normalized)) {
34
+ throw new Error(`raw source ext "${ext}" is invalid (expected letters/numbers only).`);
35
+ }
36
+ return normalized;
37
+ }
38
+
39
+ function clippedSlug(input) {
40
+ const base = slugify(input);
41
+ if (!base) return '';
42
+ return base.slice(0, 80).replace(/-+$/g, '');
43
+ }
44
+
45
+ function deriveSlug({ slug, provenance, body }) {
46
+ const candidates = [
47
+ slug,
48
+ provenance.slug,
49
+ provenance.source_title,
50
+ provenance.title,
51
+ provenance.source_external_id,
52
+ String(body || '').split(/\r?\n/).find((line) => line.trim()),
53
+ 'source',
54
+ ];
55
+ for (const candidate of candidates) {
56
+ const derived = clippedSlug(candidate);
57
+ if (derived) return derived;
58
+ }
59
+ return 'source';
60
+ }
61
+
62
+ function rawSourcePath({ domain, lane, dateStamp, slug, ext }) {
63
+ if (!RE_SLUG.test(domain)) throw new Error(`domain "${domain}" is not a valid slug.`);
64
+ assertRawLane(lane);
65
+ if (!RE_SLUG.test(slug)) throw new Error(`raw source slug "${slug}" is not a valid slug.`);
66
+ return `domains/${domain}/raw/${lane}/${dateStamp}-${slug}.${ext}`;
67
+ }
68
+
69
+ function assertRawLane(lane) {
70
+ if (!RAW_SUBDIRS.includes(lane)) {
71
+ throw new Error(`lane "${lane}" is not a raw lane (allowed: ${RAW_SUBDIRS.join(', ')}).`);
72
+ }
73
+ }
74
+
75
+ function rawSlugCandidates({ baseSlug, provenance, body }) {
76
+ const hash = createHash('sha256')
77
+ .update(`${provenance.source_connector}\0${provenance.source_external_id}\0${body}`)
78
+ .digest('hex')
79
+ .slice(0, 8);
80
+ const prefix = baseSlug.slice(0, Math.max(1, 80 - hash.length - 1)).replace(/-+$/g, '') || 'source';
81
+ return [...new Set([baseSlug, `${prefix}-${hash}`])];
82
+ }
83
+
84
+ function normalizedBody(body) {
85
+ return body.endsWith('\n') ? body : `${body}\n`;
86
+ }
87
+
88
+ function bodyContentHash(body) {
89
+ return `sha256:${createHash('sha256').update(normalizedBody(body)).digest('hex')}`;
90
+ }
91
+
92
+ function legacyBodyContentHashes(body) {
93
+ const hashes = new Set([bodyContentHash(body)]);
94
+ if (body.startsWith('\n')) hashes.add(bodyContentHash(body.slice(1)));
95
+ if (body.startsWith('\r\n')) hashes.add(bodyContentHash(body.slice(2)));
96
+ return hashes;
97
+ }
98
+
99
+ function isPartialRawWriteName(name) {
100
+ return /\.partial-\d+-[a-f0-9]{8}$/i.test(name);
101
+ }
102
+
103
+ async function assertRegisteredDomain(installRoot, domain) {
104
+ let registry;
105
+ try {
106
+ registry = JSON.parse(await readFile(join(installRoot, '.phronesis', 'registry.json'), 'utf8'));
107
+ } catch (error) {
108
+ throw new Error(`could not read the install registry (.phronesis/registry.json): ${error.message}`);
109
+ }
110
+ const registered = Array.isArray(registry.domains) ? registry.domains : [];
111
+ if (!registered.includes(domain)) {
112
+ throw new Error(
113
+ `domain "${domain}" is not registered in this installation (registered: ${registered.join(', ') || 'none'}). Refusing — ingest never creates a domain as a side effect.`,
114
+ );
115
+ }
116
+ }
117
+
118
+ function parseRawSource(content) {
119
+ const block = matchFrontmatterBlock(content);
120
+ if (!block) return null;
121
+ try {
122
+ const fm = yaml.load(block[1], { schema: yaml.JSON_SCHEMA });
123
+ return plainObject(fm) ? { fm, body: content.slice(block[0].length) } : null;
124
+ } catch {
125
+ return null;
126
+ }
127
+ }
128
+
129
+ async function findExistingSource({ installRoot, domain, lanes = RAW_SUBDIRS, sourceExternalId, sourceConnector, contentHash }) {
130
+ if (!sourceExternalId) return null;
131
+ for (const lane of lanes) {
132
+ const laneRoot = join(installRoot, 'domains', domain, 'raw', lane);
133
+ let entries;
134
+ try {
135
+ entries = await readdir(laneRoot, { withFileTypes: true });
136
+ } catch {
137
+ continue;
138
+ }
139
+ for (const entry of entries) {
140
+ if (!entry.isFile() || isPartialRawWriteName(entry.name)) continue;
141
+ const relPath = `domains/${domain}/raw/${lane}/${entry.name}`;
142
+ const content = await readFile(join(installRoot, relPath), 'utf8').catch(() => null);
143
+ if (content === null) continue;
144
+ const parsed = parseRawSource(content);
145
+ if (!parsed) continue;
146
+ const { fm, body: existingBody } = parsed;
147
+ if (fm.source_external_id !== sourceExternalId) continue;
148
+ if (sourceConnector && fm.source_connector !== sourceConnector) continue;
149
+ if (contentHash) {
150
+ if (typeof fm.content_hash === 'string' && fm.content_hash.trim()) {
151
+ if (fm.content_hash !== contentHash) continue;
152
+ } else if (!legacyBodyContentHashes(existingBody).has(contentHash)) {
153
+ continue;
154
+ }
155
+ }
156
+ return relPath;
157
+ }
158
+ }
159
+ return null;
160
+ }
161
+
162
+ function connectorBridgeFrontmatter({ provenance, now, body }) {
163
+ for (const key of Object.keys(provenance)) {
164
+ if (!CONNECTOR_BRIDGE_PROVENANCE_FIELDS.has(key) && !CONNECTOR_BRIDGE_IGNORED_FIELDS.has(key)) {
165
+ throw new Error(`connector bridge provenance field "${key}" is not allowed.`);
166
+ }
167
+ }
168
+
169
+ const fm = {
170
+ source_connector: provenance.source_connector,
171
+ source_external_id: provenance.source_external_id,
172
+ source_timestamp: provenance.source_timestamp,
173
+ trust_tier: 'untrusted',
174
+ fetched_at: now.toISOString(),
175
+ fetched_by: 'phronesis-ingest-cli@0.0.0',
176
+ content_hash: bodyContentHash(body),
177
+ };
178
+ for (const field of ['source_connector', 'source_external_id', 'source_timestamp', 'fetched_at', 'fetched_by', 'content_hash']) {
179
+ if (typeof fm[field] !== 'string' || fm[field].trim() === '') {
180
+ throw new Error(`raw source provenance requires non-empty ${field}.`);
181
+ }
182
+ }
183
+ return fm;
184
+ }
185
+
186
+ function provenanceFrontmatter({ provenance, now, body, provenanceMode }) {
187
+ if (!plainObject(provenance)) throw new Error('raw source provenance must be a mapping.');
188
+ if (provenanceMode === 'connector-bridge') return connectorBridgeFrontmatter({ provenance, now, body });
189
+ if (provenanceMode !== 'manual') throw new Error(`raw source provenance mode "${provenanceMode}" is invalid.`);
190
+
191
+ const fetchedAt = provenance.fetched_at || now.toISOString();
192
+ const trustTier = provenance.trust_tier || 'untrusted';
193
+ if (!['trusted', 'untrusted'].includes(trustTier)) {
194
+ throw new Error(`trust_tier "${trustTier}" is invalid (expected trusted | untrusted).`);
195
+ }
196
+
197
+ const fm = {
198
+ source_connector: provenance.source_connector || 'manual',
199
+ source_external_id: provenance.source_external_id,
200
+ source_timestamp: provenance.source_timestamp || fetchedAt,
201
+ trust_tier: trustTier,
202
+ fetched_at: fetchedAt,
203
+ fetched_by: provenance.fetched_by || 'phronesis-ingest-cli@0.0.0',
204
+ };
205
+ for (const [key, value] of Object.entries(provenance)) {
206
+ if (value !== undefined && fm[key] === undefined) fm[key] = value;
207
+ }
208
+ for (const field of ['source_connector', 'source_external_id', 'source_timestamp', 'fetched_at', 'fetched_by']) {
209
+ if (typeof fm[field] !== 'string' || fm[field].trim() === '') {
210
+ throw new Error(`raw source provenance requires non-empty ${field}.`);
211
+ }
212
+ }
213
+ return fm;
214
+ }
215
+
216
+ export async function assertRawDestination({ installRoot, domain, lane = 'inbox' } = {}) {
217
+ if (typeof installRoot !== 'string' || installRoot.trim() === '') {
218
+ throw new Error('stageRawSource requires installRoot.');
219
+ }
220
+ if (typeof domain !== 'string' || domain.trim() === '') {
221
+ throw new Error('stageRawSource requires a domain.');
222
+ }
223
+ await assertRegisteredDomain(installRoot, domain);
224
+ assertRawLane(lane);
225
+ }
226
+
227
+ export async function stageRawSource({
228
+ installRoot,
229
+ domain,
230
+ lane = 'inbox',
231
+ slug,
232
+ ext = 'md',
233
+ body,
234
+ provenance,
235
+ provenanceMode = 'manual',
236
+ now = new Date(),
237
+ } = {}) {
238
+ if (typeof body !== 'string' || body.trim() === '') {
239
+ throw new Error('stageRawSource requires a non-empty body.');
240
+ }
241
+ await assertRawDestination({ installRoot, domain, lane });
242
+
243
+ const fm = provenanceFrontmatter({ provenance, now, body, provenanceMode });
244
+ const existing = await findExistingSource({
245
+ installRoot,
246
+ domain,
247
+ sourceExternalId: fm.source_external_id,
248
+ sourceConnector: fm.source_connector,
249
+ contentHash: fm.content_hash,
250
+ });
251
+ if (existing) return { ok: true, path: existing, deduped: true, written: false };
252
+
253
+ const rawSlug = deriveSlug({ slug, provenance: fm, body });
254
+ const normalizedExt = normalizeExt(ext);
255
+ const content = `${serializeFrontmatter(fm)}\n${normalizedBody(body)}`;
256
+ let lastCollision = null;
257
+
258
+ await fse.ensureDir(join(installRoot, 'domains', domain, 'raw', lane));
259
+ for (const candidateSlug of rawSlugCandidates({ baseSlug: rawSlug, provenance: fm, body })) {
260
+ const relPath = rawSourcePath({
261
+ domain,
262
+ lane,
263
+ dateStamp: localDateStamp(now),
264
+ slug: candidateSlug,
265
+ ext: normalizedExt,
266
+ });
267
+ const absolute = join(installRoot, relPath);
268
+ const temporary = `${absolute}.partial-${process.pid}-${randomBytes(4).toString('hex')}`;
269
+ try {
270
+ await writeFile(temporary, content, { encoding: 'utf8', flag: 'wx' });
271
+ try {
272
+ await link(temporary, absolute);
273
+ } catch (error) {
274
+ if (error?.code === 'EEXIST') {
275
+ lastCollision = relPath;
276
+ continue;
277
+ }
278
+ throw error;
279
+ }
280
+ return { ok: true, path: relPath, deduped: false, written: true };
281
+ } finally {
282
+ await fse.remove(temporary).catch(() => {});
283
+ }
284
+ }
285
+ throw new Error(`${lastCollision || rawSlug} already exists — refusing to overwrite raw source material.`);
286
+ }
@@ -0,0 +1,42 @@
1
+ // findInstallRoot — resolve the installation root from any working directory inside it.
2
+ //
3
+ // Surfaces run from domains/{slug}/ (the active door's cwd), but the event log, registry,
4
+ // and config live at the INSTALLATION root. Every install-scoped command needs to discover
5
+ // that root: walk up from cwd to the nearest dir holding .phronesis/registry.json — the
6
+ // ownership marker init writes last (rules.md: the marker is the final write). Shared so the
7
+ // daemon (T4.1) and future commands resolve the install the same way the event CLI does.
8
+
9
+ import { resolve, dirname, join } from 'node:path';
10
+ import { existsSync } from 'node:fs';
11
+
12
+ // Returns the absolute install root, or null if cwd is not inside a Phronesis installation.
13
+ // Stops at the filesystem root — never an unbounded walk.
14
+ export function findInstallRoot(startDir = process.cwd()) {
15
+ let dir = resolve(startDir);
16
+ for (;;) {
17
+ if (existsSync(join(dir, '.phronesis', 'registry.json'))) return dir;
18
+ const parent = dirname(dir);
19
+ if (parent === dir) return null; // reached the filesystem root with no marker
20
+ dir = parent;
21
+ }
22
+ }
23
+
24
+ // Resolve the installation an install-scoped command should act on: an explicit `dir`
25
+ // wins; otherwise walk up from cwd to the marker. BOTH paths require the
26
+ // .phronesis/registry.json ownership marker — `dir` is an override of WHERE to look,
27
+ // not a waiver of the check (rules.md: the explicit path must fail closed like the
28
+ // implicit one; a typo'd --dir must never scatter a phantom tree outside any install).
29
+ // Shared by the event CLI and the action spine (core-0008). Returns { root } or { error }.
30
+ export function resolveInstall({ dir, cwd = process.cwd() } = {}) {
31
+ if (dir) {
32
+ const root = resolve(cwd, dir);
33
+ if (existsSync(join(root, '.phronesis', 'registry.json'))) return { root };
34
+ return { error: `--dir ${dir} is not a Phronesis installation (no .phronesis/registry.json).` };
35
+ }
36
+ const root = findInstallRoot(cwd);
37
+ if (root) return { root };
38
+ return {
39
+ error:
40
+ 'Not inside a Phronesis installation (no .phronesis/registry.json found walking up from the current directory). Pass --dir <install>.',
41
+ };
42
+ }
@@ -0,0 +1,13 @@
1
+ // The shared path-layout subset (specs/workspace-substrate.md). Only what every
2
+ // surface needs to resolve paths in an installation lives here — init/scaffold
3
+ // concerns (registry/config templates, .gitignore, export excludes) are CLI-init
4
+ // territory and stay in apps/cli/src/layout.js, which re-exports this constant.
5
+ // TYPE_DIR / COMPILED_DIRS already live in contract.js (one source — rules.md);
6
+ // localDateStamp lives in event-log.js.
7
+
8
+ // Default V1 configuration: a single PM door plus the shared codex.
9
+ export const DEFAULT_DOMAIN = 'pm';
10
+
11
+ // Raw content-type lanes (change 0024): source identity lives in provenance,
12
+ // never in the folder name.
13
+ export const RAW_SUBDIRS = ['transcripts', 'threads', 'inbox', 'meetings', 'research', 'degraded'];