phronesis 1.0.0 → 1.1.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/package.json +1 -1
- package/src/act.js +62 -1
- package/src/adapter.js +366 -61
- package/src/cli.js +527 -32
- package/src/compile.js +141 -10
- package/src/doctor.js +63 -2
- package/src/hook.js +312 -0
- package/src/hooks-refresh.js +1363 -0
- package/src/hooks.js +48 -33
- package/src/init.js +37 -4
- package/src/launcher.js +2105 -0
- package/src/layout.js +51 -7
- package/src/profile.js +169 -0
- package/src/prompts.js +88 -0
- package/src/skills-bump.js +88 -20
- package/src/skills.js +103 -10
- package/templates/codex/INDEX.md +4 -2
- package/templates/codex/seed/ai-practice/the-workspace-is-part-of-the-thinking.md +96 -0
- package/templates/codex-surface/README.md +15 -16
- package/templates/codex-surface/hooks/_resolve.sh +25 -24
- package/templates/codex-surface/hooks/recall-guard.sh +5 -30
- package/templates/codex-surface/hooks/session-start.sh +5 -18
- package/templates/codex-surface/hooks/session-sweep-precompact.sh +5 -20
- package/templates/codex-surface/hooks/session-sweep-subagent.sh +5 -23
- package/templates/codex-surface/hooks/session-sweep.sh +5 -19
- package/templates/launcher/phronesis +95 -0
- package/templates/phronesis-hooks/compile-active-context.sh +12 -38
- package/templates/phronesis-hooks/recall-guard.sh +12 -103
- package/templates/phronesis-hooks/session-start.sh +10 -88
- package/templates/phronesis-hooks/session-sweep.sh +16 -139
- package/templates/phronesis-hooks/skill-lifecycle.sh +12 -37
- package/templates/skills/lint/SKILL.md +7 -2
- package/templates/skills/onboard/SKILL.md +47 -9
- package/templates/skills/onboard/evals/rubric.md +5 -3
- package/templates/skills/prd-draft/SKILL.md +34 -26
- package/templates/skills/prd-draft/evals/rubric.md +1 -1
- package/vendor/core/src/action-registry.js +6 -3
- package/vendor/core/src/actions.js +120 -16
- package/vendor/core/src/index.js +3 -0
- package/vendor/core/src/lint.js +22 -5
- package/vendor/core/src/skill-lifecycle.js +67 -7
package/vendor/core/src/lint.js
CHANGED
|
@@ -80,9 +80,9 @@ function seedRefFromPath(path) {
|
|
|
80
80
|
|
|
81
81
|
function orphanDetail(scope) {
|
|
82
82
|
if (!scope.all_domains) {
|
|
83
|
-
return 'domain compiled object has no
|
|
83
|
+
return 'domain compiled object has no typed link in either direction to another object in the scoped graph (links to raw sources do not count as connectivity)';
|
|
84
84
|
}
|
|
85
|
-
return 'domain compiled object has no
|
|
85
|
+
return 'domain compiled object has no typed link in either direction to another object that resolves within its source domain; links to raw sources and bare targets from other domains do not count as connectivity';
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
function lintRecords(records, scope) {
|
|
@@ -204,11 +204,28 @@ export async function runWorkspaceLint({
|
|
|
204
204
|
const findings = [];
|
|
205
205
|
const adoptions = adoptedSeedMap(records);
|
|
206
206
|
|
|
207
|
-
// 1. Orphans: domain
|
|
207
|
+
// 1. Orphans: an in-scope domain object with NO typed edge in either direction to another
|
|
208
|
+
// in-scope object (core-0061 D1 — direction-agnostic). The engine's own action types write
|
|
209
|
+
// hub-and-spoke (`log_decision` links `enables → projects/*` outbound), so inbound-only
|
|
210
|
+
// counting flags every spoke and reports a maximally healthy young workspace as ~90%
|
|
211
|
+
// orphans — pure noise. Connectivity counts only edges whose OTHER endpoint is an in-scope
|
|
212
|
+
// node: `index.has(edge.to)` for outbound drops raw-source + unresolved forward targets
|
|
213
|
+
// (never nodes); every edge's `from` is an in-scope node by construction. Self-edges never
|
|
214
|
+
// count. Cross-domain scoping (D3) is structural: an edge's `to` resolves within its source
|
|
215
|
+
// domain, so it can only land on a node when that domain is itself in scope.
|
|
208
216
|
for (const record of records) {
|
|
209
217
|
if (record.ctx.layer !== 'domain') continue;
|
|
210
|
-
|
|
211
|
-
|
|
218
|
+
// Connectivity counts edges to/from another in-scope object that is *earned mesh* — a
|
|
219
|
+
// domain compiled object or ADOPTED codex (codex/personal). A quarantined codex/seed node
|
|
220
|
+
// is discoverability, not connectivity (D1 / change 0053): a domain object linked only to
|
|
221
|
+
// an unadopted seed, or pointed at only by one, is still an orphan. `seedRefFromPath` is
|
|
222
|
+
// non-null exactly for codex/seed/ paths, so it excludes seed endpoints in both directions.
|
|
223
|
+
const outbound = index.outEdges.get(record.rel) ?? [];
|
|
224
|
+
const inbound = index.inEdges.get(record.rel) ?? [];
|
|
225
|
+
const connected =
|
|
226
|
+
outbound.some((edge) => edge.to !== record.rel && index.has(edge.to) && seedRefFromPath(edge.to) === null) ||
|
|
227
|
+
inbound.some((edge) => edge.from !== record.rel && seedRefFromPath(edge.from) === null);
|
|
228
|
+
if (!connected) {
|
|
212
229
|
findings.push({
|
|
213
230
|
surface: 'orphan',
|
|
214
231
|
kind: 'orphan',
|
|
@@ -15,8 +15,13 @@ import { appendAuditEntry } from './hook-executor.js';
|
|
|
15
15
|
import { matchFrontmatterBlock } from './workspace-scan.js';
|
|
16
16
|
import { resolveInstall } from './install-root.js';
|
|
17
17
|
import { advanceSchedulesForSkill } from './schedule.js';
|
|
18
|
+
import { RE_SLUG } from './contract.js';
|
|
18
19
|
|
|
19
20
|
const RECORD_VERSION = 1;
|
|
21
|
+
// The DEFAULT surface for a minted lifecycle event. Claude Code was the sole minter until change
|
|
22
|
+
// 0056; the producer now takes a `surface` parameter (default this value) so a first-party in-process
|
|
23
|
+
// surface — Craft Phronesis Desktop — mints `skill.invoked`/`skill.completed` truthfully with its own
|
|
24
|
+
// `surface: 'desktop'`. Every existing Claude Code caller omits the parameter and is unchanged.
|
|
20
25
|
const SURFACE = 'claude-code';
|
|
21
26
|
const ACTOR = 'agent';
|
|
22
27
|
const DEDUPE_WINDOW_MS = 30_000;
|
|
@@ -195,6 +200,34 @@ async function resolveSkillMetadata({ installRoot, hookInput }) {
|
|
|
195
200
|
return null;
|
|
196
201
|
}
|
|
197
202
|
|
|
203
|
+
// Surface-neutral skill metadata for a FIRST-PARTY model-invocation (change 0056). Unlike a Claude
|
|
204
|
+
// Code hook (which carries a `PreToolUse`+`Skill` or slash envelope), an in-process surface's
|
|
205
|
+
// `invoke_skill(name)` already holds the skill name. The descriptor is `{ name, version?, mode?,
|
|
206
|
+
// start_path? }`; the version resolves from the install registry / SKILL.md when omitted, exactly as
|
|
207
|
+
// the hook path resolves it — and, mirroring the hook path, an unregistered/unknown skill returns
|
|
208
|
+
// null (the lifecycle stream only starts for skills the workspace actually knows about). The name is
|
|
209
|
+
// slug-validated before it reaches a `skills/{name}/SKILL.md` read (path-traversal fail-closed).
|
|
210
|
+
async function resolveExplicitSkill({ installRoot, skill }) {
|
|
211
|
+
if (!isPlainObject(skill)) return null;
|
|
212
|
+
const name = nonEmptyString(skill.name);
|
|
213
|
+
if (!name || !RE_SLUG.test(name)) return null;
|
|
214
|
+
// The skill MUST exist in the workspace (registry entry or a readable SKILL.md) before a lifecycle is
|
|
215
|
+
// minted (Codex R3 F5): a caller-supplied `version` for an unknown skill would fabricate a
|
|
216
|
+
// skill.invoked/completed pair for a skill that cannot run. Prove existence FIRST, then a caller
|
|
217
|
+
// version may pin it; otherwise fall to the resolved one.
|
|
218
|
+
const existingVersion =
|
|
219
|
+
(await registrySkillVersion(installRoot, name)) || (await fileSkillVersion(installRoot, name));
|
|
220
|
+
if (!existingVersion) return null;
|
|
221
|
+
const version = nonEmptyString(skill.version) || existingVersion;
|
|
222
|
+
return {
|
|
223
|
+
skill_name: name,
|
|
224
|
+
version,
|
|
225
|
+
mode: nonEmptyString(skill.mode) || 'suggested',
|
|
226
|
+
start_path: nonEmptyString(skill.start_path) || 'model',
|
|
227
|
+
start_event: 'model-invocation',
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
198
231
|
function mintInvocationId() {
|
|
199
232
|
return `inv_${randomUUID()}`;
|
|
200
233
|
}
|
|
@@ -229,6 +262,7 @@ export async function completeActiveSkillInvocation({
|
|
|
229
262
|
session,
|
|
230
263
|
hookInput,
|
|
231
264
|
reason = 'stop',
|
|
265
|
+
surface = SURFACE,
|
|
232
266
|
now = new Date(),
|
|
233
267
|
} = {}) {
|
|
234
268
|
const record = await readActiveInvocation({ installRoot, session });
|
|
@@ -255,7 +289,7 @@ export async function completeActiveSkillInvocation({
|
|
|
255
289
|
type: 'skill.completed',
|
|
256
290
|
session_id: session,
|
|
257
291
|
actor: ACTOR,
|
|
258
|
-
surface
|
|
292
|
+
surface,
|
|
259
293
|
payload: {
|
|
260
294
|
skill_name: record.skill_name,
|
|
261
295
|
action_types_committed: count,
|
|
@@ -348,11 +382,25 @@ function shouldDedupeStart(prior, meta, now) {
|
|
|
348
382
|
return Math.abs(now.getTime() - started) <= DEDUPE_WINDOW_MS;
|
|
349
383
|
}
|
|
350
384
|
|
|
351
|
-
export async function beginSkillInvocation({
|
|
352
|
-
|
|
385
|
+
export async function beginSkillInvocation({
|
|
386
|
+
installRoot,
|
|
387
|
+
hookInput,
|
|
388
|
+
skill,
|
|
389
|
+
session: sessionArg,
|
|
390
|
+
surface = SURFACE,
|
|
391
|
+
pinDeviation,
|
|
392
|
+
resolvedModel,
|
|
393
|
+
now = new Date(),
|
|
394
|
+
} = {}) {
|
|
395
|
+
// Session: a Claude hook carries it in `hookInput.session_id`; a first-party surface passes it
|
|
396
|
+
// explicitly (`session`), since its `skill` descriptor is not a hook envelope (change 0056).
|
|
397
|
+
const session = hookSession(hookInput) || nonEmptyString(sessionArg);
|
|
353
398
|
if (!session) return { started: false, reason: 'missing-session' };
|
|
354
399
|
|
|
355
|
-
|
|
400
|
+
// A `skill` descriptor is the first-party model-invocation path; `hookInput` is the Claude path.
|
|
401
|
+
const meta = skill
|
|
402
|
+
? await resolveExplicitSkill({ installRoot, skill })
|
|
403
|
+
: await resolveSkillMetadata({ installRoot, hookInput });
|
|
356
404
|
if (!meta) return { started: false, reason: 'not-a-skill-start', session };
|
|
357
405
|
|
|
358
406
|
const prior = await readActiveInvocation({ installRoot, session });
|
|
@@ -365,6 +413,7 @@ export async function beginSkillInvocation({ installRoot, hookInput, now = new D
|
|
|
365
413
|
installRoot,
|
|
366
414
|
session,
|
|
367
415
|
reason: 'next-start',
|
|
416
|
+
surface,
|
|
368
417
|
now,
|
|
369
418
|
});
|
|
370
419
|
}
|
|
@@ -377,7 +426,7 @@ export async function beginSkillInvocation({ installRoot, hookInput, now = new D
|
|
|
377
426
|
version: meta.version,
|
|
378
427
|
mode: meta.mode,
|
|
379
428
|
started_at: now.toISOString(),
|
|
380
|
-
start_event: hookInput.
|
|
429
|
+
start_event: hookInput?.hook_event_name || meta.start_event || 'model-invocation',
|
|
381
430
|
start_path: meta.start_path,
|
|
382
431
|
...(meta.tool_use_id ? { tool_use_id: meta.tool_use_id } : {}),
|
|
383
432
|
...(meta.command_name ? { command_name: meta.command_name } : {}),
|
|
@@ -388,12 +437,17 @@ export async function beginSkillInvocation({ installRoot, hookInput, now = new D
|
|
|
388
437
|
type: 'skill.invoked',
|
|
389
438
|
session_id: session,
|
|
390
439
|
actor: ACTOR,
|
|
391
|
-
surface
|
|
440
|
+
surface,
|
|
392
441
|
payload: {
|
|
393
442
|
skill_name: record.skill_name,
|
|
394
443
|
version: record.version,
|
|
395
444
|
mode: record.mode,
|
|
396
445
|
invocation_id: record.invocation_id,
|
|
446
|
+
// Pin-deviation (change 0051 / CR3): an additive optional field a ROUTING surface (the desktop)
|
|
447
|
+
// stamps when it substitutes a tier-equivalent for the skill's pinned model. Shape:
|
|
448
|
+
// { pinned_model, substituted_model, provider, tier }. No new event type; skill.invoked carries it.
|
|
449
|
+
...(isPlainObject(pinDeviation) ? { pin_deviation: pinDeviation } : {}),
|
|
450
|
+
...(nonEmptyString(resolvedModel) ? { resolved_model: resolvedModel } : {}),
|
|
397
451
|
},
|
|
398
452
|
now,
|
|
399
453
|
});
|
|
@@ -405,18 +459,24 @@ export async function beginSkillInvocation({ installRoot, hookInput, now = new D
|
|
|
405
459
|
export async function abandonActiveSkillInvocation({
|
|
406
460
|
installRoot,
|
|
407
461
|
hookInput,
|
|
462
|
+
session: sessionArg,
|
|
408
463
|
reason = 'StopFailure',
|
|
464
|
+
surface = SURFACE,
|
|
409
465
|
now = new Date(),
|
|
410
466
|
} = {}) {
|
|
411
|
-
const session = hookSession(hookInput);
|
|
467
|
+
const session = hookSession(hookInput) || nonEmptyString(sessionArg);
|
|
412
468
|
if (!session) return { abandoned: false, reason: 'missing-session' };
|
|
413
469
|
const record = await readActiveInvocation({ installRoot, session });
|
|
414
470
|
if (!record) return { abandoned: false, reason: 'no-active-record', session };
|
|
415
471
|
await clearActiveInvocation({ installRoot, session });
|
|
472
|
+
// Honest gap (StopFailure-equivalent): clear the active record and audit the abandonment; NEVER
|
|
473
|
+
// mint a synthetic skill.completed to fake a clean close (emission honesty). The desktop calls this
|
|
474
|
+
// on an errored/cancelled turn with surface: 'desktop'.
|
|
416
475
|
await auditLifecycle(installRoot, {
|
|
417
476
|
session_id: session,
|
|
418
477
|
invocation_id: record.invocation_id,
|
|
419
478
|
skill_name: record.skill_name,
|
|
479
|
+
surface,
|
|
420
480
|
event_type: hookInput?.hook_event_name || reason,
|
|
421
481
|
outcome: 'skill-lifecycle-abandoned',
|
|
422
482
|
reason: nonEmptyString(hookInput?.error_type) || nonEmptyString(hookInput?.error) || reason,
|