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,479 @@
1
+ // Claude Code skill lifecycle producer (core-0032; change 0021 live surface adapter).
2
+ //
3
+ // The active invocation record is the adapter-owned channel between Claude Code skill
4
+ // start hooks and later `phronesis act` calls in the same session. It is transient
5
+ // state, not canonical workspace data: the record mints and carries the live
6
+ // invocation_id, while the action core remains the only writer of action_type.* events.
7
+
8
+ import { createHash, randomUUID } from 'node:crypto';
9
+ import { join } from 'node:path';
10
+ import { readFile } from 'node:fs/promises';
11
+ import yaml from 'js-yaml';
12
+ import fse from 'fs-extra';
13
+ import { appendEvent, readEvents } from './event-log.js';
14
+ import { appendAuditEntry } from './hook-executor.js';
15
+ import { matchFrontmatterBlock } from './workspace-scan.js';
16
+ import { resolveInstall } from './install-root.js';
17
+ import { advanceSchedulesForSkill } from './schedule.js';
18
+
19
+ const RECORD_VERSION = 1;
20
+ const SURFACE = 'claude-code';
21
+ const ACTOR = 'agent';
22
+ const DEDUPE_WINDOW_MS = 30_000;
23
+
24
+ function isPlainObject(v) {
25
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
26
+ }
27
+
28
+ function nonEmptyString(v) {
29
+ return typeof v === 'string' && v.trim() !== '' ? v.trim() : undefined;
30
+ }
31
+
32
+ function recordKey(session) {
33
+ return createHash('sha256').update(session).digest('hex').slice(0, 32);
34
+ }
35
+
36
+ export function invocationsDir(installRoot) {
37
+ return join(installRoot, '.phronesis', 'invocations');
38
+ }
39
+
40
+ export function activeInvocationPath(installRoot, session) {
41
+ return join(invocationsDir(installRoot), `${recordKey(session)}.json`);
42
+ }
43
+
44
+ export async function readActiveInvocation({ installRoot, session } = {}) {
45
+ if (!nonEmptyString(session)) return null;
46
+ let parsed;
47
+ try {
48
+ parsed = await fse.readJson(activeInvocationPath(installRoot, session));
49
+ } catch {
50
+ return null;
51
+ }
52
+ if (!isPlainObject(parsed) || parsed.schema_version !== RECORD_VERSION) return null;
53
+ if (parsed.session_id !== session) return null;
54
+ if (!nonEmptyString(parsed.invocation_id) || !nonEmptyString(parsed.skill_name)) return null;
55
+ return parsed;
56
+ }
57
+
58
+ async function writeActiveInvocation({ installRoot, session, record }) {
59
+ await fse.ensureDir(invocationsDir(installRoot));
60
+ const path = activeInvocationPath(installRoot, session);
61
+ const tmp = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
62
+ await fse.writeJson(tmp, record, { spaces: 2 });
63
+ await fse.move(tmp, path, { overwrite: true });
64
+ return record;
65
+ }
66
+
67
+ export async function clearActiveInvocation({ installRoot, session } = {}) {
68
+ if (!nonEmptyString(session)) return false;
69
+ const path = activeInvocationPath(installRoot, session);
70
+ const existed = await fse.pathExists(path);
71
+ await fse.remove(path);
72
+ return existed;
73
+ }
74
+
75
+ function hookSession(hookInput) {
76
+ return nonEmptyString(hookInput?.session_id);
77
+ }
78
+
79
+ function toolInput(hookInput) {
80
+ return isPlainObject(hookInput?.tool_input) ? hookInput.tool_input : {};
81
+ }
82
+
83
+ function skillNameFromTool(hookInput) {
84
+ const input = toolInput(hookInput);
85
+ return (
86
+ nonEmptyString(input.skill_name) ||
87
+ nonEmptyString(input.skill) ||
88
+ nonEmptyString(input.name) ||
89
+ nonEmptyString(hookInput?.skill_name)
90
+ );
91
+ }
92
+
93
+ function skillNameFromSlash(hookInput) {
94
+ if (hookInput?.expansion_type && hookInput.expansion_type !== 'slash_command') return undefined;
95
+ return nonEmptyString(hookInput?.command_name);
96
+ }
97
+
98
+ async function registrySkillVersion(installRoot, name) {
99
+ try {
100
+ const registry = await fse.readJson(join(installRoot, '.phronesis', 'registry.json'));
101
+ const entry = registry?.skills?.[name];
102
+ return nonEmptyString(entry?.version);
103
+ } catch {
104
+ return undefined;
105
+ }
106
+ }
107
+
108
+ async function readSkillFrontmatter(installRoot, name) {
109
+ try {
110
+ const source = await readFile(join(installRoot, 'skills', name, 'SKILL.md'), 'utf8');
111
+ const block = matchFrontmatterBlock(source);
112
+ if (!block) return null;
113
+ const fm = yaml.load(block[1], { schema: yaml.JSON_SCHEMA });
114
+ if (!isPlainObject(fm) || fm.name !== name) return null;
115
+ return fm;
116
+ } catch {
117
+ return null;
118
+ }
119
+ }
120
+
121
+ async function fileSkillVersion(installRoot, name) {
122
+ const fm = await readSkillFrontmatter(installRoot, name);
123
+ return fm ? nonEmptyString(fm.version) : undefined;
124
+ }
125
+
126
+ // The registered skill's declared weight (skills.md §"Frontmatter contract"): 'heavy'
127
+ // marks a SUBSTANTIVE skill — one whose cognitive load crowds out ambient capture, so it
128
+ // embeds structural checkpoints. An unreadable / unregistered / missing weight resolves to
129
+ // undefined ⇒ NOT substantive: the on_skill_complete warning never fires on a skill the
130
+ // workspace cannot class (the safe default — better a missed warning than a noisy one).
131
+ async function fileSkillWeight(installRoot, name) {
132
+ const fm = await readSkillFrontmatter(installRoot, name);
133
+ return fm ? nonEmptyString(fm.weight) : undefined;
134
+ }
135
+
136
+ // on_skill_complete warning POLICY (core-0027 J1; specs/hooks.md:79 — "checks
137
+ // mesh_enrichments; flags if zero on a substantive skill"). J1 owns this policy; core-0032
138
+ // owns the one Stop command that sequences emit → evaluate (this runs in-process right
139
+ // after the skill.completed emit). SUBSTANTIVE = weight 'heavy' (skills.md §"weight"). The
140
+ // signal is mesh_enrichments === 0, NOT action_types_committed === 0: action-zero ⇒
141
+ // mesh-zero but never the reverse, so the action-zero marker MISSES the case J1 exists for
142
+ // — a heavy skill that committed actions yet enriched no links (mesh-zero with
143
+ // action_types_committed > 0). WARNING-CLASS only, never a gate: mesh_enrichments is a
144
+ // links_updated sum (a coarse enrichment heuristic, change 0021), the producer DERIVES it
145
+ // from the matching committed events — it is not an agent self-report, but a sum is still
146
+ // only a heuristic for "this run enriched the mesh."
147
+ export function meshZeroWarning({ skillName, weight, meshEnrichments, actionTypesCommitted } = {}) {
148
+ if (weight !== 'heavy') return null;
149
+ if (!Number.isInteger(meshEnrichments) || meshEnrichments !== 0) return null;
150
+ return {
151
+ skill_name: skillName,
152
+ weight,
153
+ mesh_enrichments: meshEnrichments,
154
+ action_types_committed: actionTypesCommitted,
155
+ };
156
+ }
157
+
158
+ async function resolveSkillMetadata({ installRoot, hookInput }) {
159
+ const event = hookInput?.hook_event_name;
160
+ if (event === 'PreToolUse' && hookInput?.tool_name === 'Skill') {
161
+ const name = skillNameFromTool(hookInput);
162
+ if (!name) return null;
163
+ const version = (await registrySkillVersion(installRoot, name)) || (await fileSkillVersion(installRoot, name));
164
+ // A model-visible Skill tool input is not enough to mint a dogfood denominator
165
+ // entry. The lifecycle stream only starts for skills the workspace actually
166
+ // knows about, matching the direct slash path below.
167
+ if (!version) return null;
168
+ return {
169
+ skill_name: name,
170
+ version,
171
+ mode: nonEmptyString(toolInput(hookInput).mode) || 'suggested',
172
+ start_path: 'model',
173
+ tool_use_id: nonEmptyString(hookInput.tool_use_id),
174
+ boundary_key: nonEmptyString(hookInput.transcript_path),
175
+ };
176
+ }
177
+
178
+ if (event === 'UserPromptExpansion') {
179
+ const name = skillNameFromSlash(hookInput);
180
+ if (!name) return null;
181
+ const version = (await registrySkillVersion(installRoot, name)) || (await fileSkillVersion(installRoot, name));
182
+ // UserPromptExpansion also covers custom slash commands. Only emit for commands
183
+ // that map to a Phronesis skill in the workspace.
184
+ if (!version) return null;
185
+ return {
186
+ skill_name: name,
187
+ version,
188
+ mode: 'explicit',
189
+ start_path: 'slash',
190
+ command_name: name,
191
+ boundary_key: nonEmptyString(hookInput.transcript_path),
192
+ };
193
+ }
194
+
195
+ return null;
196
+ }
197
+
198
+ function mintInvocationId() {
199
+ return `inv_${randomUUID()}`;
200
+ }
201
+
202
+ async function auditLifecycle(installRoot, entry) {
203
+ await appendAuditEntry(installRoot, {
204
+ hook: 'skill-lifecycle',
205
+ ...entry,
206
+ });
207
+ }
208
+
209
+ async function committedEventsForInvocation({ installRoot, record }) {
210
+ const events = await readEvents({ installRoot, type: 'action_type.committed' });
211
+ return events.filter(
212
+ (ev) =>
213
+ ev.session_id === record.session_id &&
214
+ ev.payload?.invocation_id === record.invocation_id &&
215
+ ev.payload?.no_op !== true &&
216
+ (!record.started_at || ev.ts >= record.started_at),
217
+ );
218
+ }
219
+
220
+ function meshEnrichmentCount(commits) {
221
+ return commits.reduce((sum, ev) => {
222
+ const n = ev.payload?.links_updated;
223
+ return Number.isInteger(n) && n > 0 ? sum + n : sum;
224
+ }, 0);
225
+ }
226
+
227
+ export async function completeActiveSkillInvocation({
228
+ installRoot,
229
+ session,
230
+ hookInput,
231
+ reason = 'stop',
232
+ now = new Date(),
233
+ } = {}) {
234
+ const record = await readActiveInvocation({ installRoot, session });
235
+ if (!record) return { completed: false };
236
+ const boundaryEvent = nonEmptyString(hookInput?.hook_event_name) || reason;
237
+ const boundaryKey = nonEmptyString(hookInput?.transcript_path);
238
+ if (boundaryEvent === 'SubagentStop' && (!boundaryKey || boundaryKey !== record.boundary_key)) {
239
+ await auditLifecycle(installRoot, {
240
+ session_id: session,
241
+ event_type: 'SubagentStop',
242
+ invocation_id: record.invocation_id,
243
+ skill_name: record.skill_name,
244
+ outcome: 'skill-lifecycle-subagent-boundary-skipped',
245
+ reason: boundaryKey ? 'boundary-mismatch' : 'missing-boundary-key',
246
+ });
247
+ return { completed: false, skipped: true, reason: 'subagent-boundary-mismatch', record };
248
+ }
249
+
250
+ const commits = await committedEventsForInvocation({ installRoot, record });
251
+ const count = commits.length;
252
+ const meshEnrichments = meshEnrichmentCount(commits);
253
+ const event = await appendEvent({
254
+ installRoot,
255
+ type: 'skill.completed',
256
+ session_id: session,
257
+ actor: ACTOR,
258
+ surface: SURFACE,
259
+ payload: {
260
+ skill_name: record.skill_name,
261
+ action_types_committed: count,
262
+ mesh_enrichments: meshEnrichments,
263
+ invocation_id: record.invocation_id,
264
+ },
265
+ now,
266
+ });
267
+ await clearActiveInvocation({ installRoot, session });
268
+
269
+ // Schedule sugar (change 0042 / core-0051): advance any `kind: skill` schedule targeting this
270
+ // skill, HERE at the single authoritative skill.completed boundary — so EVERY completion path
271
+ // advances it (the Stop hook AND the next-start-closes-prior path via beginSkillInvocation;
272
+ // Codex round-3 F1), keyed on this event's own day. Best-effort: the completion has already
273
+ // committed (event emitted, invocation cleared), so a schedule-state write failure must NOT
274
+ // fail the lifecycle close — it degrades to the schedule staying due (safe over-surface).
275
+ try {
276
+ await advanceSchedulesForSkill({ installRoot, skillName: record.skill_name, now: new Date(event.ts) });
277
+ } catch {
278
+ // over-surface, never break the completion
279
+ }
280
+
281
+ if (count === 0) {
282
+ await auditLifecycle(installRoot, {
283
+ session_id: session,
284
+ event_type: 'skill.completed',
285
+ event_id: event.id,
286
+ invocation_id: record.invocation_id,
287
+ skill_name: record.skill_name,
288
+ outcome: 'skill-lifecycle-zero-capture',
289
+ reason,
290
+ });
291
+ }
292
+
293
+ // J1 zero-capture warning (core-0027): phase (2), in-process after the emit (NOT a
294
+ // second Stop hook — parallel Stop hooks could scan before phase 1, Codex round 9). A
295
+ // substantive (heavy) skill with mesh_enrichments === 0 banked nothing — the change-0010
296
+ // misfire class. This is the only signal that catches a heavy skill which committed
297
+ // actions yet enriched zero links (mesh-zero with action_types_committed > 0).
298
+ //
299
+ // It does NOT replace the action-zero `skill-lifecycle-zero-capture` audit above: the
300
+ // packet keeps action-zero as "a complementary/coarser audit detail, not a substitute".
301
+ // The two carry distinct `outcome`s for distinct machine consumers (action-zero is
302
+ // core-0032's coarse capture marker; mesh-zero-warning is J1's heavy-skill warning), so
303
+ // a heavy run that is BOTH action-zero and mesh-zero is intentionally recorded by both —
304
+ // each consumer filters by its own outcome; neither double-counts the other.
305
+ //
306
+ // SURFACING is the caller's job and only at the Stop boundary: runHooksSkillComplete
307
+ // turns this warning into an operator-visible systemMessage. A sequential/next-start
308
+ // completion (a heavy skill closed because the NEXT skill started — beginSkillInvocation)
309
+ // records this audit entry but surfaces no systemMessage (a skill-start is not a Stop
310
+ // boundary). That misfire is then audit-only, by design.
311
+ const weight = await fileSkillWeight(installRoot, record.skill_name);
312
+ const warning = meshZeroWarning({
313
+ skillName: record.skill_name,
314
+ weight,
315
+ meshEnrichments,
316
+ actionTypesCommitted: count,
317
+ });
318
+ if (warning) {
319
+ await auditLifecycle(installRoot, {
320
+ session_id: session,
321
+ event_type: 'skill.completed',
322
+ event_id: event.id,
323
+ invocation_id: record.invocation_id,
324
+ skill_name: record.skill_name,
325
+ weight,
326
+ mesh_enrichments: meshEnrichments,
327
+ action_types_committed: count,
328
+ outcome: 'skill-lifecycle-mesh-zero-warning',
329
+ reason,
330
+ });
331
+ }
332
+
333
+ return {
334
+ completed: true,
335
+ event,
336
+ record,
337
+ action_types_committed: count,
338
+ mesh_enrichments: meshEnrichments,
339
+ warning,
340
+ };
341
+ }
342
+
343
+ function shouldDedupeStart(prior, meta, now) {
344
+ if (!prior || prior.start_path !== 'slash' || meta.start_path !== 'model') return false;
345
+ if (prior.skill_name !== meta.skill_name) return false;
346
+ const started = Date.parse(prior.started_at || '');
347
+ if (Number.isNaN(started)) return false;
348
+ return Math.abs(now.getTime() - started) <= DEDUPE_WINDOW_MS;
349
+ }
350
+
351
+ export async function beginSkillInvocation({ installRoot, hookInput, now = new Date() } = {}) {
352
+ const session = hookSession(hookInput);
353
+ if (!session) return { started: false, reason: 'missing-session' };
354
+
355
+ const meta = await resolveSkillMetadata({ installRoot, hookInput });
356
+ if (!meta) return { started: false, reason: 'not-a-skill-start', session };
357
+
358
+ const prior = await readActiveInvocation({ installRoot, session });
359
+ if (shouldDedupeStart(prior, meta, now)) {
360
+ return { started: false, deduped: true, record: prior, session };
361
+ }
362
+ let completedPrior = null;
363
+ if (prior) {
364
+ completedPrior = await completeActiveSkillInvocation({
365
+ installRoot,
366
+ session,
367
+ reason: 'next-start',
368
+ now,
369
+ });
370
+ }
371
+
372
+ const record = {
373
+ schema_version: RECORD_VERSION,
374
+ session_id: session,
375
+ invocation_id: mintInvocationId(),
376
+ skill_name: meta.skill_name,
377
+ version: meta.version,
378
+ mode: meta.mode,
379
+ started_at: now.toISOString(),
380
+ start_event: hookInput.hook_event_name,
381
+ start_path: meta.start_path,
382
+ ...(meta.tool_use_id ? { tool_use_id: meta.tool_use_id } : {}),
383
+ ...(meta.command_name ? { command_name: meta.command_name } : {}),
384
+ ...(meta.boundary_key ? { boundary_key: meta.boundary_key } : {}),
385
+ };
386
+ const event = await appendEvent({
387
+ installRoot,
388
+ type: 'skill.invoked',
389
+ session_id: session,
390
+ actor: ACTOR,
391
+ surface: SURFACE,
392
+ payload: {
393
+ skill_name: record.skill_name,
394
+ version: record.version,
395
+ mode: record.mode,
396
+ invocation_id: record.invocation_id,
397
+ },
398
+ now,
399
+ });
400
+ await writeActiveInvocation({ installRoot, session, record });
401
+
402
+ return { started: true, event, record, completedPrior, session };
403
+ }
404
+
405
+ export async function abandonActiveSkillInvocation({
406
+ installRoot,
407
+ hookInput,
408
+ reason = 'StopFailure',
409
+ now = new Date(),
410
+ } = {}) {
411
+ const session = hookSession(hookInput);
412
+ if (!session) return { abandoned: false, reason: 'missing-session' };
413
+ const record = await readActiveInvocation({ installRoot, session });
414
+ if (!record) return { abandoned: false, reason: 'no-active-record', session };
415
+ await clearActiveInvocation({ installRoot, session });
416
+ await auditLifecycle(installRoot, {
417
+ session_id: session,
418
+ invocation_id: record.invocation_id,
419
+ skill_name: record.skill_name,
420
+ event_type: hookInput?.hook_event_name || reason,
421
+ outcome: 'skill-lifecycle-abandoned',
422
+ reason: nonEmptyString(hookInput?.error_type) || nonEmptyString(hookInput?.error) || reason,
423
+ ts_observed: now.toISOString(),
424
+ });
425
+ return { abandoned: true, record, session };
426
+ }
427
+
428
+ export async function sweepStaleSkillInvocation({ installRoot, session, now = new Date() } = {}) {
429
+ const record = await readActiveInvocation({ installRoot, session });
430
+ if (!record) return { cleared: false };
431
+ await clearActiveInvocation({ installRoot, session });
432
+ await auditLifecycle(installRoot, {
433
+ session_id: session,
434
+ invocation_id: record.invocation_id,
435
+ skill_name: record.skill_name,
436
+ event_type: 'SessionStart',
437
+ outcome: 'skill-lifecycle-stale-cleared',
438
+ reason: 'SessionStart',
439
+ ts_observed: now.toISOString(),
440
+ });
441
+ return { cleared: true, record };
442
+ }
443
+
444
+ export async function resolveInvocationIdForAct({
445
+ dir,
446
+ cwd = process.cwd(),
447
+ session,
448
+ callerInvocationId,
449
+ envInvocationId,
450
+ surface = 'cli',
451
+ } = {}) {
452
+ const installed = resolveInstall({ dir, cwd });
453
+ const caller = nonEmptyString(callerInvocationId);
454
+ const env = nonEmptyString(envInvocationId);
455
+ if (installed.error || !nonEmptyString(session)) {
456
+ return { invocation_id: caller || env, installRoot: installed.root, active: null };
457
+ }
458
+
459
+ const installRoot = installed.root;
460
+ const active = await readActiveInvocation({ installRoot, session });
461
+ if (!active) return { invocation_id: caller || env, installRoot, active: null };
462
+
463
+ const attempted = [];
464
+ if (caller && caller !== active.invocation_id) attempted.push({ source: 'flag', value: caller });
465
+ if (env && env !== active.invocation_id) attempted.push({ source: 'env', value: env });
466
+ if (attempted.length) {
467
+ await auditLifecycle(installRoot, {
468
+ session_id: session,
469
+ invocation_id: active.invocation_id,
470
+ skill_name: active.skill_name,
471
+ event_type: 'action_type.requested',
472
+ outcome: 'skill-lifecycle-override-refused',
473
+ surface,
474
+ attempted_sources: attempted,
475
+ });
476
+ }
477
+
478
+ return { invocation_id: active.invocation_id, installRoot, active };
479
+ }