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/export.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { resolve, relative, isAbsolute, dirname, basename, join } from 'node:path';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
import fse from 'fs-extra';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { exportExcludeGlobs } from './layout.js';
|
|
7
|
+
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
|
|
10
|
+
// True if `child` is `parent` or nested under it. Used to refuse writing the
|
|
11
|
+
// tarball inside the directory being tarred (self-inclusion).
|
|
12
|
+
function isInsideOrEqual(child, parent) {
|
|
13
|
+
const rel = relative(parent, child);
|
|
14
|
+
return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Recognize a Phronesis installation by its registry marker — same contract init
|
|
18
|
+
// uses. Export is read-only; it never writes into the install it tars.
|
|
19
|
+
async function isPhronesisInstall(dir) {
|
|
20
|
+
return fse.pathExists(join(dir, '.phronesis', 'registry.json'));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Tar (gzip) a Phronesis installation into a portable archive. Strips nothing
|
|
24
|
+
// canonical: the export is a faithful snapshot of plain Markdown + config, with
|
|
25
|
+
// relative symlinks preserved as symlinks (tar does not dereference by default),
|
|
26
|
+
// so the extracted tree opens in Obsidian/VS Code with zero Phronesis runtime —
|
|
27
|
+
// the export emancipation invariant (specs/security.md).
|
|
28
|
+
//
|
|
29
|
+
// The ONLY things stripped are the transient/non-canonical set (privacy-sensitive
|
|
30
|
+
// event logs, disposable renders, OS cruft) — the same set the install .gitignore
|
|
31
|
+
// omits, from a single source of truth (layout.js). No API keys are present to
|
|
32
|
+
// strip: keys live in the OS keychain, never in installation files (security.md).
|
|
33
|
+
export async function exportInstallation({ dir, out } = {}) {
|
|
34
|
+
const installDir = resolve(process.cwd(), dir || '.');
|
|
35
|
+
|
|
36
|
+
if (!(await fse.pathExists(installDir))) {
|
|
37
|
+
console.error(`Nothing to export: ${installDir} does not exist.`);
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
return { cancelled: true };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!(await isPhronesisInstall(installDir))) {
|
|
43
|
+
console.error(
|
|
44
|
+
`Refusing to export ${installDir}: it is not a Phronesis installation ` +
|
|
45
|
+
'(.phronesis/registry.json not found). Pass the installation directory.',
|
|
46
|
+
);
|
|
47
|
+
process.exitCode = 1;
|
|
48
|
+
return { cancelled: true };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const outPath = out
|
|
52
|
+
? resolve(process.cwd(), out)
|
|
53
|
+
: join(dirname(installDir), `${basename(installDir)}.tar.gz`);
|
|
54
|
+
|
|
55
|
+
// Never write the archive inside the tree being tarred — it would race against
|
|
56
|
+
// its own inclusion and grow the install with a disposable artifact. Lexical
|
|
57
|
+
// check first (catches the obvious case without touching the filesystem).
|
|
58
|
+
if (isInsideOrEqual(outPath, installDir)) {
|
|
59
|
+
return refuseSelfInclusion(outPath);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
await fse.ensureDir(dirname(outPath));
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.error(`Cannot create the export destination ${dirname(outPath)}: ${err.message}`);
|
|
66
|
+
process.exitCode = 1;
|
|
67
|
+
return { cancelled: true, error: err.message };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Defense in depth: re-check after resolving symlinks, so an --out path that
|
|
71
|
+
// points into the install via a symlinked parent can't slip past the lexical
|
|
72
|
+
// guard. ensureDir above is a no-op when the parent is a symlink to an existing
|
|
73
|
+
// dir, so nothing was written into the install before this check.
|
|
74
|
+
const installReal = await fse.realpath(installDir);
|
|
75
|
+
const outReal = join(await fse.realpath(dirname(outPath)), basename(outPath));
|
|
76
|
+
if (isInsideOrEqual(outReal, installReal)) {
|
|
77
|
+
return refuseSelfInclusion(outPath);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Write to a unique temp path and rename on success, so an interrupted or failed
|
|
81
|
+
// tar never leaves a corrupt-but-valid-looking archive in place of a good one
|
|
82
|
+
// (the export's promise is a faithful, trustworthy snapshot).
|
|
83
|
+
const tmpOut = `${outPath}.partial-${process.pid}`;
|
|
84
|
+
const excludes = exportExcludeGlobs().map((g) => `--exclude=${g}`);
|
|
85
|
+
|
|
86
|
+
// -C <parent> <name>: archive the install as a single top-level dir. No -h, so
|
|
87
|
+
// symlinks are stored as symlinks and survive the round-trip. Args passed as an
|
|
88
|
+
// array (not a shell string) so paths can't be interpreted as shell.
|
|
89
|
+
try {
|
|
90
|
+
await execFileAsync('tar', [
|
|
91
|
+
'-czf',
|
|
92
|
+
tmpOut,
|
|
93
|
+
...excludes,
|
|
94
|
+
'-C',
|
|
95
|
+
dirname(installDir),
|
|
96
|
+
basename(installDir),
|
|
97
|
+
]);
|
|
98
|
+
await fse.move(tmpOut, outPath, { overwrite: true });
|
|
99
|
+
} catch (err) {
|
|
100
|
+
await fse.remove(tmpOut).catch(() => {});
|
|
101
|
+
if (err.code === 'ENOENT') {
|
|
102
|
+
console.error(
|
|
103
|
+
'Export needs the `tar` command, which was not found. It ships with macOS ' +
|
|
104
|
+
'and Linux (the V1 supported platforms).',
|
|
105
|
+
);
|
|
106
|
+
} else {
|
|
107
|
+
console.error(`Export failed: ${err.message}`);
|
|
108
|
+
}
|
|
109
|
+
process.exitCode = 1;
|
|
110
|
+
return { cancelled: true, error: err.message };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
printSuccess(installDir, outPath);
|
|
114
|
+
return { installDir, out: outPath };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function refuseSelfInclusion(outPath) {
|
|
118
|
+
console.error(
|
|
119
|
+
`Refusing to write the export inside the installation (${outPath}). ` +
|
|
120
|
+
'Choose an --out path outside the installation directory.',
|
|
121
|
+
);
|
|
122
|
+
process.exitCode = 1;
|
|
123
|
+
return { cancelled: true };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function printSuccess(installDir, outPath) {
|
|
127
|
+
console.log();
|
|
128
|
+
console.log('Installation exported.');
|
|
129
|
+
console.log();
|
|
130
|
+
console.log(` ${chalk.bold(outPath)}`);
|
|
131
|
+
console.log(chalk.dim(` a faithful snapshot of ${basename(installDir)}`));
|
|
132
|
+
console.log(chalk.dim(' plain Markdown plus config, relative symlinks preserved'));
|
|
133
|
+
console.log();
|
|
134
|
+
console.log(chalk.dim(' Opens anywhere, no Phronesis required. Yours to keep.'));
|
|
135
|
+
console.log();
|
|
136
|
+
}
|
package/src/guard.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// `phronesis guard` — stdin-only pre-answer adapter glue. The guard brain lives in
|
|
2
|
+
// @phronesis/core; this file reads the prompt from stdin, validates execution context,
|
|
3
|
+
// and formats the surface-agnostic result.
|
|
4
|
+
|
|
5
|
+
import { runContextRelevanceGuard } from '@phronesis/core';
|
|
6
|
+
|
|
7
|
+
async function readStdin() {
|
|
8
|
+
let data = '';
|
|
9
|
+
for await (const chunk of process.stdin) data += chunk;
|
|
10
|
+
return data;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function parseGuardInput(raw) {
|
|
14
|
+
const text = String(raw ?? '');
|
|
15
|
+
const trimmed = text.trim();
|
|
16
|
+
if (trimmed.startsWith('{')) {
|
|
17
|
+
try {
|
|
18
|
+
const parsed = JSON.parse(trimmed);
|
|
19
|
+
if (parsed && typeof parsed === 'object' && typeof parsed.prompt === 'string') {
|
|
20
|
+
return { prompt: parsed.prompt, session_id: parsed.session_id };
|
|
21
|
+
}
|
|
22
|
+
return { error: 'JSON stdin must include a string prompt field.' };
|
|
23
|
+
} catch {
|
|
24
|
+
return { error: 'malformed JSON stdin; pass raw text or a JSON envelope with a string prompt field.' };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return { prompt: text };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function printableResult(result) {
|
|
31
|
+
if (!result.fired) return '';
|
|
32
|
+
return result.packet || '';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function runGuard({ dir, session, surface, actor, domain, json = false } = {}) {
|
|
36
|
+
if (!dir) {
|
|
37
|
+
console.error('guard: --dir <install> is required.');
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const resolvedActor = actor || process.env.PHRONESIS_ACTOR || 'agent';
|
|
42
|
+
const resolvedSurface = surface || 'cli';
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const input = parseGuardInput(await readStdin());
|
|
46
|
+
if (input.error) {
|
|
47
|
+
console.error(`guard: ${input.error}`);
|
|
48
|
+
process.exitCode = 1;
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
if (!input.prompt || !input.prompt.trim()) {
|
|
52
|
+
console.error('guard: prompt is required on stdin (raw text or {"prompt": "..."} JSON).');
|
|
53
|
+
process.exitCode = 1;
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
if (input.session_id && session && input.session_id !== session) {
|
|
57
|
+
console.error('guard: stdin session_id does not match --session.');
|
|
58
|
+
process.exitCode = 1;
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
if (!session) {
|
|
62
|
+
console.error('guard: --session <id> is required.');
|
|
63
|
+
process.exitCode = 1;
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const result = await runContextRelevanceGuard({
|
|
68
|
+
dir,
|
|
69
|
+
cwd: process.cwd(),
|
|
70
|
+
prompt: input.prompt,
|
|
71
|
+
session,
|
|
72
|
+
surface: resolvedSurface,
|
|
73
|
+
actor: resolvedActor,
|
|
74
|
+
domain,
|
|
75
|
+
env: process.env,
|
|
76
|
+
});
|
|
77
|
+
if (json) console.log(JSON.stringify(result));
|
|
78
|
+
else {
|
|
79
|
+
const text = printableResult(result);
|
|
80
|
+
if (text) console.log(text);
|
|
81
|
+
}
|
|
82
|
+
process.exitCode = 0;
|
|
83
|
+
return result;
|
|
84
|
+
} catch (err) {
|
|
85
|
+
console.error(`guard: ${err.message}`);
|
|
86
|
+
process.exitCode = 1;
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
package/src/hooks.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// `phronesis hooks` — the session-boundary hook support surface (core-0013 / T4.2;
|
|
2
|
+
// change 0010 D4; packet decisions H7/H8). CLI glue only (D7): the due computation,
|
|
3
|
+
// the authoritative `outcome.due` emission, and the sweep-marker read live in
|
|
4
|
+
// @phronesis/core (session-hooks.js). The scaffolded Claude Code hook scripts
|
|
5
|
+
// (.phronesis/hooks/session-start.sh / session-sweep.sh) shell out to these.
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
ENUMS,
|
|
9
|
+
resolveInstall,
|
|
10
|
+
runDueCheck,
|
|
11
|
+
runScheduleDueCheck,
|
|
12
|
+
sweepStatus,
|
|
13
|
+
beginSkillInvocation,
|
|
14
|
+
completeActiveSkillInvocation,
|
|
15
|
+
abandonActiveSkillInvocation,
|
|
16
|
+
sweepStaleSkillInvocation,
|
|
17
|
+
} from '@phronesis/core';
|
|
18
|
+
|
|
19
|
+
function installOrFail(dir) {
|
|
20
|
+
const r = resolveInstall({ dir });
|
|
21
|
+
if (r.error) {
|
|
22
|
+
console.error(r.error);
|
|
23
|
+
process.exitCode = 1;
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return r.root;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Session-start due-check: emits one `outcome.due` per due decision AND one `schedule.due`
|
|
30
|
+
// per due schedule-registry entry (the V1 authoritative emitters — event-log.md rows;
|
|
31
|
+
// change 0042 folds the schedule half into this same command so an existing install's
|
|
32
|
+
// scaffolded session-start.sh — which already calls `phronesis hooks due-check` — surfaces
|
|
33
|
+
// schedules with NO shell-script change, just a CLI update: the Claude hook path stays
|
|
34
|
+
// byte-compatible). The print IS the surface integration: a Claude Code SessionStart hook's
|
|
35
|
+
// stdout is injected into the session context (Codex wraps the same stdout into its
|
|
36
|
+
// additionalContext envelope), so this output is what surfaces both the decision artifacts
|
|
37
|
+
// (hooks.md `on_review_date_due`) and the operator's own committed cadence
|
|
38
|
+
// (`on_schedule_due`, classification `acknowledged`, the 0038 posture).
|
|
39
|
+
export async function runHooksDueCheck({ session, dir, actor, surface } = {}) {
|
|
40
|
+
const installRoot = installOrFail(dir);
|
|
41
|
+
if (!installRoot) return null;
|
|
42
|
+
if (!session) {
|
|
43
|
+
console.error('hooks due-check: --session <id> is required.');
|
|
44
|
+
process.exitCode = 1;
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
const resolvedActor = actor || process.env.PHRONESIS_ACTOR || 'agent';
|
|
48
|
+
const resolvedSurface = surface || 'cli';
|
|
49
|
+
try {
|
|
50
|
+
const { due } = await runDueCheck({ installRoot, session, actor: resolvedActor, surface: resolvedSurface });
|
|
51
|
+
if (!due.length) {
|
|
52
|
+
console.log('No decisions due for review.');
|
|
53
|
+
} else {
|
|
54
|
+
console.log(`Phronesis due-check: ${due.length} decision(s) due for review.`);
|
|
55
|
+
for (const d of due) {
|
|
56
|
+
const parts = d.decision_path.split('/');
|
|
57
|
+
const domain = parts[1];
|
|
58
|
+
const name = parts[parts.length - 1].replace(/\.md$/, '');
|
|
59
|
+
const overdue = d.days_overdue === 0 ? 'due today' : `${d.days_overdue} day(s) overdue`;
|
|
60
|
+
console.log(`\n ${d.decision_path} — ${overdue} (review_date ${d.review_date})`);
|
|
61
|
+
console.log(
|
|
62
|
+
` record the outcome: phronesis act record_outcome --domain ${domain} --session ${session} --json '{"target":"decisions/${name}","outcome_status":"<${ENUMS.outcome_status.join('|')}>","lesson":"<what you learned>"}'`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Schedule half (change 0042): a malformed schedule.json is a SEPARATE failure domain —
|
|
67
|
+
// it degrades to zero due entries here (validate/doctor own the fault), never breaking the
|
|
68
|
+
// decision due-check above.
|
|
69
|
+
const { due: scheduleDue } = await runScheduleDueCheck({ installRoot, session, actor: resolvedActor, surface: resolvedSurface });
|
|
70
|
+
if (scheduleDue.length) {
|
|
71
|
+
console.log(`\nPhronesis schedule: ${scheduleDue.length} item(s) due.`);
|
|
72
|
+
for (const s of scheduleDue) {
|
|
73
|
+
const overdue = s.days_overdue === 0 ? 'due now' : `${s.days_overdue} day(s) overdue`;
|
|
74
|
+
console.log(`\n ${s.schedule_id} — ${s.kind}:${s.target} — ${overdue}`);
|
|
75
|
+
if (s.kind === 'cli_verb' || s.kind === 'connector_pull') {
|
|
76
|
+
console.log(` run now: phronesis due run ${s.schedule_id}`);
|
|
77
|
+
}
|
|
78
|
+
console.log(` mark done: phronesis due complete ${s.schedule_id}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
process.exitCode = 0;
|
|
82
|
+
return due;
|
|
83
|
+
} catch (err) {
|
|
84
|
+
console.error(`hooks due-check: ${err.message}`);
|
|
85
|
+
process.exitCode = 1;
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Sweep status: has this session emitted its capture-sweep marker (`session.end` with a
|
|
91
|
+
// `sweep` payload — `sweep.candidates: 0` is the honest negative)? Prints `swept` or
|
|
92
|
+
// `pending`; exits 0 when swept, 1 when pending — the scaffolded Stop-hook script keys
|
|
93
|
+
// its block-once decision on the stdout word.
|
|
94
|
+
export async function runHooksSweepStatus({ session, dir, json = false } = {}) {
|
|
95
|
+
const installRoot = installOrFail(dir);
|
|
96
|
+
if (!installRoot) return null;
|
|
97
|
+
if (!session) {
|
|
98
|
+
console.error('hooks sweep-status: --session <id> is required.');
|
|
99
|
+
process.exitCode = 1;
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const status = await sweepStatus({ installRoot, session });
|
|
104
|
+
if (json) {
|
|
105
|
+
console.log(JSON.stringify(status));
|
|
106
|
+
} else {
|
|
107
|
+
console.log(status.swept ? 'swept' : 'pending');
|
|
108
|
+
}
|
|
109
|
+
process.exitCode = status.swept ? 0 : 1;
|
|
110
|
+
return status;
|
|
111
|
+
} catch (err) {
|
|
112
|
+
console.error(`hooks sweep-status: ${err.message}`);
|
|
113
|
+
process.exitCode = 1;
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function parseHookPayload(label, payload) {
|
|
119
|
+
try {
|
|
120
|
+
return payload === undefined ? {} : JSON.parse(payload);
|
|
121
|
+
} catch {
|
|
122
|
+
console.error(`${label}: --payload must be valid JSON.`);
|
|
123
|
+
process.exitCode = 1;
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// The operator-facing copy for the J1 zero-capture warning (core-0027). The POLICY (when
|
|
129
|
+
// to warn) is @phronesis/core's meshZeroWarning; this is presentation only.
|
|
130
|
+
function meshZeroWarningMessage(w) {
|
|
131
|
+
return (
|
|
132
|
+
`Phronesis: the heavy skill "${w.skill_name}" completed with 0 mesh enrichments ` +
|
|
133
|
+
`(no links_updated across ${w.action_types_committed} committed action(s)). A substantive ` +
|
|
134
|
+
'skill that banks nothing is usually a missed capture — intended? (specs/hooks.md on_skill_complete)'
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function runHooksSkillStart({ dir, payload } = {}) {
|
|
139
|
+
const installRoot = installOrFail(dir);
|
|
140
|
+
if (!installRoot) return null;
|
|
141
|
+
const hookInput = parseHookPayload('hooks skill-start', payload);
|
|
142
|
+
if (!hookInput) return null;
|
|
143
|
+
try {
|
|
144
|
+
const result = await beginSkillInvocation({ installRoot, hookInput });
|
|
145
|
+
if (result.started) console.log(result.record.invocation_id);
|
|
146
|
+
process.exitCode = 0;
|
|
147
|
+
return result;
|
|
148
|
+
} catch (err) {
|
|
149
|
+
console.error(`hooks skill-start: ${err.message}`);
|
|
150
|
+
process.exitCode = 1;
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function runHooksSkillComplete({ session, dir, payload, boundaryEvent, json = false } = {}) {
|
|
156
|
+
const installRoot = installOrFail(dir);
|
|
157
|
+
if (!installRoot) return null;
|
|
158
|
+
if (!session) {
|
|
159
|
+
console.error('hooks skill-complete: --session <id> is required.');
|
|
160
|
+
process.exitCode = 1;
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
const hookInput = parseHookPayload('hooks skill-complete', payload);
|
|
164
|
+
if (!hookInput) return null;
|
|
165
|
+
if (boundaryEvent && !hookInput.hook_event_name) hookInput.hook_event_name = boundaryEvent;
|
|
166
|
+
try {
|
|
167
|
+
const result = await completeActiveSkillInvocation({
|
|
168
|
+
installRoot,
|
|
169
|
+
session,
|
|
170
|
+
hookInput,
|
|
171
|
+
reason: boundaryEvent || 'stop',
|
|
172
|
+
});
|
|
173
|
+
// (Schedule advancement for a completed `kind: skill` target now happens in core's
|
|
174
|
+
// completeActiveSkillInvocation — the single authoritative skill.completed boundary, so it
|
|
175
|
+
// fires on EVERY completion path, incl. next-start-closes-prior; change 0042 / Codex round-3.)
|
|
176
|
+
// J1 (core-0027): surface the zero-capture warning to the OPERATOR. As a
|
|
177
|
+
// Stop-hook stdout JSON, a top-level `systemMessage` is shown to the user,
|
|
178
|
+
// non-blocking, exit 0 (Claude Code hook output contract); session-sweep.sh passes
|
|
179
|
+
// this stdout through. --json keeps the full machine result for programmatic callers.
|
|
180
|
+
// No warning ⇒ nothing on stdout (the swept-session Stop output stays empty).
|
|
181
|
+
if (json) {
|
|
182
|
+
console.log(JSON.stringify(result));
|
|
183
|
+
} else if (result?.warning) {
|
|
184
|
+
console.log(JSON.stringify({ systemMessage: meshZeroWarningMessage(result.warning) }));
|
|
185
|
+
}
|
|
186
|
+
process.exitCode = 0;
|
|
187
|
+
return result;
|
|
188
|
+
} catch (err) {
|
|
189
|
+
console.error(`hooks skill-complete: ${err.message}`);
|
|
190
|
+
process.exitCode = 1;
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export async function runHooksSkillAbandon({ dir, payload, json = false } = {}) {
|
|
196
|
+
const installRoot = installOrFail(dir);
|
|
197
|
+
if (!installRoot) return null;
|
|
198
|
+
const hookInput = parseHookPayload('hooks skill-abandon', payload);
|
|
199
|
+
if (!hookInput) return null;
|
|
200
|
+
try {
|
|
201
|
+
const result = await abandonActiveSkillInvocation({ installRoot, hookInput });
|
|
202
|
+
if (json) console.log(JSON.stringify(result));
|
|
203
|
+
process.exitCode = 0;
|
|
204
|
+
return result;
|
|
205
|
+
} catch (err) {
|
|
206
|
+
console.error(`hooks skill-abandon: ${err.message}`);
|
|
207
|
+
process.exitCode = 1;
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export async function runHooksSkillSweepStale({ session, dir, json = false } = {}) {
|
|
213
|
+
const installRoot = installOrFail(dir);
|
|
214
|
+
if (!installRoot) return null;
|
|
215
|
+
if (!session) {
|
|
216
|
+
console.error('hooks skill-sweep-stale: --session <id> is required.');
|
|
217
|
+
process.exitCode = 1;
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
const result = await sweepStaleSkillInvocation({ installRoot, session });
|
|
222
|
+
if (json) console.log(JSON.stringify(result));
|
|
223
|
+
process.exitCode = 0;
|
|
224
|
+
return result;
|
|
225
|
+
} catch (err) {
|
|
226
|
+
console.error(`hooks skill-sweep-stale: ${err.message}`);
|
|
227
|
+
process.exitCode = 1;
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
}
|