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
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Phronesis static managed launcher (change 0065 D1). This file never changes when the
|
|
3
|
+
# backend changes; launcher.env is the commit point. Parsing is data-only: never source/eval.
|
|
4
|
+
|
|
5
|
+
launcher_fail() {
|
|
6
|
+
printf '%s\n' "phronesis launcher: $1; reinstall command-line tools." >&2
|
|
7
|
+
exit 1
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
decode_value() {
|
|
11
|
+
__raw=$1
|
|
12
|
+
case "$__raw" in
|
|
13
|
+
\"*\")
|
|
14
|
+
__raw=${__raw#\"}
|
|
15
|
+
__raw=${__raw%\"}
|
|
16
|
+
__out=
|
|
17
|
+
__escaped=0
|
|
18
|
+
while [ -n "$__raw" ]; do
|
|
19
|
+
__rest=${__raw#?}
|
|
20
|
+
__char=${__raw%"$__rest"}
|
|
21
|
+
__raw=$__rest
|
|
22
|
+
if [ "$__escaped" = 1 ]; then
|
|
23
|
+
case "$__char" in
|
|
24
|
+
[\\]|'"'|'$'|'`') __out=$__out$__char ;;
|
|
25
|
+
*) launcher_fail "invalid launcher.env escape" ;;
|
|
26
|
+
esac
|
|
27
|
+
__escaped=0
|
|
28
|
+
else
|
|
29
|
+
case "$__char" in
|
|
30
|
+
[\\]) __escaped=1 ;;
|
|
31
|
+
'$'|'`') launcher_fail "unsafe launcher.env value" ;;
|
|
32
|
+
*) __out=$__out$__char ;;
|
|
33
|
+
esac
|
|
34
|
+
fi
|
|
35
|
+
done
|
|
36
|
+
[ "$__escaped" = 0 ] || launcher_fail "invalid launcher.env escape"
|
|
37
|
+
DECODED=$__out
|
|
38
|
+
;;
|
|
39
|
+
\'*\')
|
|
40
|
+
__raw=${__raw#\'}
|
|
41
|
+
__raw=${__raw%\'}
|
|
42
|
+
case "$__raw" in *\'*) launcher_fail "invalid launcher.env quoted value" ;; esac
|
|
43
|
+
DECODED=$__raw
|
|
44
|
+
;;
|
|
45
|
+
*[\ \ \;\&\|\<\>\$\`\"\']*) launcher_fail "launcher.env value must quote shell metacharacters" ;;
|
|
46
|
+
*) DECODED=$__raw ;;
|
|
47
|
+
esac
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
manifest="$HOME/.phronesis/bin/launcher.env"
|
|
51
|
+
[ ! -L "$manifest" ] || launcher_fail "launcher.env is a symlink"
|
|
52
|
+
[ -f "$manifest" ] || launcher_fail "launcher.env is missing"
|
|
53
|
+
|
|
54
|
+
PHR_BACKEND=
|
|
55
|
+
PHR_VERSION=
|
|
56
|
+
PHR_TARGET=
|
|
57
|
+
PHR_RUNTIME=
|
|
58
|
+
PHR_GENERATION=
|
|
59
|
+
seen_backend=0
|
|
60
|
+
seen_version=0
|
|
61
|
+
seen_target=0
|
|
62
|
+
seen_runtime=0
|
|
63
|
+
seen_generation=0
|
|
64
|
+
|
|
65
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
66
|
+
case "$line" in ''|'#'*) continue ;; esac
|
|
67
|
+
case "$line" in *=*) ;; *) launcher_fail "launcher.env contains an invalid line" ;; esac
|
|
68
|
+
key=${line%%=*}
|
|
69
|
+
raw=${line#*=}
|
|
70
|
+
decode_value "$raw"
|
|
71
|
+
case "$key" in
|
|
72
|
+
PHR_BACKEND) [ "$seen_backend" = 0 ] || launcher_fail "launcher.env repeats PHR_BACKEND"; PHR_BACKEND=$DECODED; seen_backend=1 ;;
|
|
73
|
+
PHR_VERSION) [ "$seen_version" = 0 ] || launcher_fail "launcher.env repeats PHR_VERSION"; PHR_VERSION=$DECODED; seen_version=1 ;;
|
|
74
|
+
PHR_TARGET) [ "$seen_target" = 0 ] || launcher_fail "launcher.env repeats PHR_TARGET"; PHR_TARGET=$DECODED; seen_target=1 ;;
|
|
75
|
+
PHR_RUNTIME) [ "$seen_runtime" = 0 ] || launcher_fail "launcher.env repeats PHR_RUNTIME"; PHR_RUNTIME=$DECODED; seen_runtime=1 ;;
|
|
76
|
+
PHR_GENERATION) [ "$seen_generation" = 0 ] || launcher_fail "launcher.env repeats PHR_GENERATION"; PHR_GENERATION=$DECODED; seen_generation=1 ;;
|
|
77
|
+
*) launcher_fail "launcher.env contains unknown field $key" ;;
|
|
78
|
+
esac
|
|
79
|
+
done < "$manifest"
|
|
80
|
+
|
|
81
|
+
case "$PHR_BACKEND" in app|self-copy) ;; *) launcher_fail "launcher.env has invalid PHR_BACKEND" ;; esac
|
|
82
|
+
case "$PHR_VERSION" in ''|*[!A-Za-z0-9._+-]*) launcher_fail "launcher.env has invalid PHR_VERSION" ;; esac
|
|
83
|
+
case "$PHR_GENERATION" in ''|*[!A-Za-z0-9._+-]*) launcher_fail "launcher.env has invalid PHR_GENERATION" ;; esac
|
|
84
|
+
case "$PHR_GENERATION" in *[!.]*) ;; *) launcher_fail "launcher.env has invalid PHR_GENERATION" ;; esac
|
|
85
|
+
case "$PHR_TARGET" in /*) ;; *) launcher_fail "launcher.env has invalid PHR_TARGET" ;; esac
|
|
86
|
+
case "$PHR_RUNTIME" in /*) ;; *) launcher_fail "launcher.env has invalid PHR_RUNTIME" ;; esac
|
|
87
|
+
[ ! -L "$PHR_TARGET" ] || launcher_fail "recorded target is a symlink"
|
|
88
|
+
[ -f "$PHR_TARGET" ] || launcher_fail "recorded target is missing"
|
|
89
|
+
[ -f "$PHR_RUNTIME" ] && [ -x "$PHR_RUNTIME" ] || launcher_fail "recorded runtime is missing or not executable"
|
|
90
|
+
|
|
91
|
+
if [ "$PHR_BACKEND" = app ]; then
|
|
92
|
+
ELECTRON_RUN_AS_NODE=1
|
|
93
|
+
export ELECTRON_RUN_AS_NODE
|
|
94
|
+
fi
|
|
95
|
+
exec "$PHR_RUNTIME" "$PHR_TARGET" "$@"
|
|
@@ -1,38 +1,12 @@
|
|
|
1
|
-
#!/
|
|
2
|
-
#
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
# compile without the runtime simply leaves the projection for the next runtime pass; the
|
|
14
|
-
# validate-time staleness check is the backstop that flags it stale meanwhile.
|
|
15
|
-
set -uo pipefail
|
|
16
|
-
|
|
17
|
-
command -v phronesis >/dev/null 2>&1 || exit 0
|
|
18
|
-
|
|
19
|
-
input="$(cat || true)"
|
|
20
|
-
# The committed payload.path is domains/{domain}/compiled/{type-dir}/{name}.md; the domain
|
|
21
|
-
# is its second segment. node is present wherever phronesis is (the CLI runs on it), so
|
|
22
|
-
# parse the JSON properly instead of regexing it.
|
|
23
|
-
domain="$(printf '%s' "$input" | node -e '
|
|
24
|
-
let d = "";
|
|
25
|
-
process.stdin.on("data", (c) => (d += c)).on("end", () => {
|
|
26
|
-
try {
|
|
27
|
-
const ev = JSON.parse(d);
|
|
28
|
-
const p = String((ev && ev.payload && ev.payload.path) || "");
|
|
29
|
-
const parts = p.split("/");
|
|
30
|
-
process.stdout.write(parts[0] === "domains" && parts[1] ? parts[1] : "");
|
|
31
|
-
} catch {}
|
|
32
|
-
});' 2>/dev/null)"
|
|
33
|
-
[ -n "$domain" ] || exit 0
|
|
34
|
-
|
|
35
|
-
# cwd is the install root (the executor sets it); --dir makes discovery explicit.
|
|
36
|
-
phronesis active-context refresh --domain "$domain" --dir "$PWD" >/dev/null || true
|
|
37
|
-
|
|
38
|
-
exit 0
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Managed-first binding (change 0065 D2): stdin is inherited by the one resolved CLI.
|
|
3
|
+
PHRONESIS_BIN="$HOME/.phronesis/bin/phronesis"
|
|
4
|
+
PHRONESIS_MANAGED_BIN="$PHRONESIS_BIN"
|
|
5
|
+
if [ -f "$PHRONESIS_BIN" ] && [ -x "$PHRONESIS_BIN" ] && [ ! -L "$PHRONESIS_BIN" ]; then
|
|
6
|
+
:
|
|
7
|
+
else
|
|
8
|
+
PHRONESIS_BIN="$(command -v phronesis 2>/dev/null || true)"
|
|
9
|
+
[ -n "$PHRONESIS_BIN" ] || exit 0
|
|
10
|
+
[ "$PHRONESIS_BIN" != "$PHRONESIS_MANAGED_BIN" ] || exit 0
|
|
11
|
+
fi
|
|
12
|
+
exec "$PHRONESIS_BIN" hook compile-active-context --surface "${PHRONESIS_SURFACE:-claude-code}"
|
|
@@ -1,103 +1,12 @@
|
|
|
1
|
-
#!/
|
|
2
|
-
#
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
set -uo pipefail
|
|
14
|
-
|
|
15
|
-
command -v phronesis >/dev/null 2>&1 || exit 0
|
|
16
|
-
|
|
17
|
-
# Install-root resolution without assuming a surface env var (change 0028 / core-0034 P5):
|
|
18
|
-
# an explicit PHRONESIS_DIR (set by the Codex launcher) or CLAUDE_PROJECT_DIR (Claude Code)
|
|
19
|
-
# when it actually holds the install; else walk up from cwd to the nearest .phronesis/
|
|
20
|
-
# (Codex has no $CLAUDE_PROJECT_DIR; a non-git install behaves identically — it keys on
|
|
21
|
-
# .phronesis/, not .git/); else the prior ${CLAUDE_PROJECT_DIR:-$PWD} default. For the
|
|
22
|
-
# shipped Claude posture (opened at the install root, CLAUDE_PROJECT_DIR = that root) the
|
|
23
|
-
# CLAUDE_PROJECT_DIR branch returns exactly the old ${CLAUDE_PROJECT_DIR:-$PWD} value; the
|
|
24
|
-
# behavior changes only when CLAUDE_PROJECT_DIR is set without a .phronesis/ (an install
|
|
25
|
-
# nested under a larger project), where the walk-up now finds the real install instead of
|
|
26
|
-
# silently passing a non-install --dir (a strict improvement; resolver tests pin it).
|
|
27
|
-
resolve_install_dir() {
|
|
28
|
-
if [ -n "${PHRONESIS_DIR:-}" ] && [ -d "${PHRONESIS_DIR}/.phronesis" ]; then printf '%s' "$PHRONESIS_DIR"; return; fi
|
|
29
|
-
if [ -n "${CLAUDE_PROJECT_DIR:-}" ] && [ -d "${CLAUDE_PROJECT_DIR}/.phronesis" ]; then printf '%s' "$CLAUDE_PROJECT_DIR"; return; fi
|
|
30
|
-
local d="$PWD"
|
|
31
|
-
while [ "$d" != "/" ] && [ ! -d "$d/.phronesis" ]; do d="$(dirname "$d")"; done
|
|
32
|
-
if [ -d "$d/.phronesis" ]; then printf '%s' "$d"; return; fi
|
|
33
|
-
printf '%s' "${CLAUDE_PROJECT_DIR:-$PWD}"
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
PHRONESIS_RESOLVED_DIR="$(resolve_install_dir)"
|
|
37
|
-
# Restrict the surface to a safe token (it rides --surface and, in session-start, a JSON
|
|
38
|
-
# payload); the launcher only ever sets the literal "codex".
|
|
39
|
-
surface="$(printf '%s' "${PHRONESIS_SURFACE:-claude-code}" | tr -cd 'a-z0-9-')"
|
|
40
|
-
[ -n "$surface" ] || surface="claude-code"
|
|
41
|
-
|
|
42
|
-
input="$(cat || true)"
|
|
43
|
-
printf '%s' "$input" | PHRONESIS_RESOLVED_DIR="$PHRONESIS_RESOLVED_DIR" PHRONESIS_SURFACE="$surface" node -e '
|
|
44
|
-
const { spawnSync } = require("node:child_process");
|
|
45
|
-
const fs = require("node:fs");
|
|
46
|
-
const path = require("node:path");
|
|
47
|
-
|
|
48
|
-
function domainFromCwd(root, cwd) {
|
|
49
|
-
if (!root || !cwd) return "";
|
|
50
|
-
// Normalize BOTH to physical paths before comparing. The install root arrives as a
|
|
51
|
-
// LOGICAL path (the Codex launcher resolves it with `cd … && pwd`; CLAUDE_PROJECT_DIR is
|
|
52
|
-
// likewise logical), while a surface may report a REALPATH cwd — and Node`s process.cwd()
|
|
53
|
-
// is always physical. A logical-vs-physical split (macOS /tmp→/private/tmp, a synced or
|
|
54
|
-
// aliased workspace) makes path.relative yield `../…`, silently dropping --domain — which
|
|
55
|
-
// flips the guard to all-domain scope on a multi-domain install (core-0034 review #1).
|
|
56
|
-
let r = root;
|
|
57
|
-
let c = cwd;
|
|
58
|
-
try { r = fs.realpathSync(root); } catch {}
|
|
59
|
-
try { c = fs.realpathSync(cwd); } catch {}
|
|
60
|
-
const rel = path.relative(r, c);
|
|
61
|
-
const parts = rel.split(path.sep);
|
|
62
|
-
return parts[0] === "domains" && parts[1] ? parts[1] : "";
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
try {
|
|
66
|
-
const input = fs.readFileSync(0, "utf8");
|
|
67
|
-
const payload = JSON.parse(input);
|
|
68
|
-
const sessionId = typeof payload.session_id === "string" ? payload.session_id : "";
|
|
69
|
-
const prompt = typeof payload.prompt === "string" ? payload.prompt : "";
|
|
70
|
-
if (!sessionId || !prompt) process.exit(0);
|
|
71
|
-
|
|
72
|
-
const dir = process.env.PHRONESIS_RESOLVED_DIR || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
73
|
-
const surface = process.env.PHRONESIS_SURFACE || "claude-code";
|
|
74
|
-
const actor = process.env.PHRONESIS_ACTOR || "agent";
|
|
75
|
-
const args = [
|
|
76
|
-
"guard",
|
|
77
|
-
"--dir", dir,
|
|
78
|
-
"--session", sessionId,
|
|
79
|
-
"--surface", surface,
|
|
80
|
-
"--actor", actor,
|
|
81
|
-
"--json",
|
|
82
|
-
];
|
|
83
|
-
const domain = domainFromCwd(dir, typeof payload.cwd === "string" ? payload.cwd : process.cwd());
|
|
84
|
-
if (domain) args.push("--domain", domain);
|
|
85
|
-
|
|
86
|
-
const result = spawnSync("phronesis", args, { input: prompt, encoding: "utf8" });
|
|
87
|
-
if (result.status !== 0) process.exit(0);
|
|
88
|
-
const parsed = JSON.parse(result.stdout);
|
|
89
|
-
if (!parsed || parsed.fired !== true || typeof parsed.packet !== "string" || parsed.packet.trim() === "") {
|
|
90
|
-
process.exit(0);
|
|
91
|
-
}
|
|
92
|
-
process.stdout.write(JSON.stringify({
|
|
93
|
-
hookSpecificOutput: {
|
|
94
|
-
hookEventName: "UserPromptSubmit",
|
|
95
|
-
additionalContext: parsed.packet,
|
|
96
|
-
},
|
|
97
|
-
}));
|
|
98
|
-
} catch {
|
|
99
|
-
process.exit(0);
|
|
100
|
-
}
|
|
101
|
-
' 2>/dev/null
|
|
102
|
-
|
|
103
|
-
exit 0
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Managed-first binding (change 0065 D2): stdin is inherited by the one resolved CLI.
|
|
3
|
+
PHRONESIS_BIN="$HOME/.phronesis/bin/phronesis"
|
|
4
|
+
PHRONESIS_MANAGED_BIN="$PHRONESIS_BIN"
|
|
5
|
+
if [ -f "$PHRONESIS_BIN" ] && [ -x "$PHRONESIS_BIN" ] && [ ! -L "$PHRONESIS_BIN" ]; then
|
|
6
|
+
:
|
|
7
|
+
else
|
|
8
|
+
PHRONESIS_BIN="$(command -v phronesis 2>/dev/null || true)"
|
|
9
|
+
[ -n "$PHRONESIS_BIN" ] || exit 0
|
|
10
|
+
[ "$PHRONESIS_BIN" != "$PHRONESIS_MANAGED_BIN" ] || exit 0
|
|
11
|
+
fi
|
|
12
|
+
exec "$PHRONESIS_BIN" hook recall-guard --surface "${PHRONESIS_SURFACE:-claude-code}"
|
|
@@ -1,90 +1,12 @@
|
|
|
1
|
-
#!/
|
|
2
|
-
#
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
# 1. clear any stale skill lifecycle active record for this session (crash/resume
|
|
8
|
-
# safety net; the next turn's act calls must not inherit an old invocation_id);
|
|
9
|
-
# 2. emit `session.start` into the typed event stream;
|
|
10
|
-
# 3. run the due-check — `phronesis hooks due-check` computes due review_dates,
|
|
11
|
-
# emits one `outcome.due` per due decision (the V1 authoritative emitter), and
|
|
12
|
-
# prints the due list. Claude Code injects this stdout into the session context raw;
|
|
13
|
-
# Codex injects it via hookSpecificOutput.additionalContext (the only output branch).
|
|
14
|
-
#
|
|
15
|
-
# Runtime-free degrade (change 0013): with no phronesis on PATH this exits 0 silently —
|
|
16
|
-
# an honest gap, never an error. That is the ONLY silent path: a present-but-broken
|
|
17
|
-
# runtime (invalid registry, corrupted config) keeps its stderr, because a due-check
|
|
18
|
-
# that fails invisibly at every session start is the exact silent no-fire this hook
|
|
19
|
-
# exists to prevent. `|| true` only keeps a hook error from breaking the session.
|
|
20
|
-
set -uo pipefail
|
|
21
|
-
|
|
22
|
-
command -v phronesis >/dev/null 2>&1 || exit 0
|
|
23
|
-
|
|
24
|
-
input="$(cat || true)"
|
|
25
|
-
# Claude Code / Codex hook input is JSON on stdin; node is present wherever phronesis is
|
|
26
|
-
# (the CLI runs on it), so parse properly instead of regexing JSON.
|
|
27
|
-
session_id="$(printf '%s' "$input" | node -e '
|
|
28
|
-
let d = "";
|
|
29
|
-
process.stdin.on("data", (c) => (d += c)).on("end", () => {
|
|
30
|
-
try { process.stdout.write(String(JSON.parse(d).session_id || "")); } catch {}
|
|
31
|
-
});' 2>/dev/null)"
|
|
32
|
-
# No parseable session id → nothing to attribute events to: exit quietly (same posture
|
|
33
|
-
# as the sweep script) rather than inventing a synthetic session that unrelated
|
|
34
|
-
# sessions would pile onto.
|
|
35
|
-
[ -n "$session_id" ] || exit 0
|
|
36
|
-
|
|
37
|
-
# Install-root resolution without assuming a surface env var (change 0028 / core-0034 P5):
|
|
38
|
-
# an explicit PHRONESIS_DIR (set by the Codex launcher) or CLAUDE_PROJECT_DIR (Claude Code)
|
|
39
|
-
# when it actually holds the install; else walk up from cwd to the nearest .phronesis/
|
|
40
|
-
# (Codex has no $CLAUDE_PROJECT_DIR; a non-git install behaves identically — it keys on
|
|
41
|
-
# .phronesis/, not .git/); else the prior ${CLAUDE_PROJECT_DIR:-$PWD} default. For the
|
|
42
|
-
# shipped Claude posture — opened at the install root, CLAUDE_PROJECT_DIR = that root — the
|
|
43
|
-
# CLAUDE_PROJECT_DIR branch returns exactly what the old ${CLAUDE_PROJECT_DIR:-$PWD} did. The
|
|
44
|
-
# behavior changes ONLY when CLAUDE_PROJECT_DIR is set but does NOT contain .phronesis (a
|
|
45
|
-
# Claude session opened inside a larger project that nests the install): the old code passed
|
|
46
|
-
# that non-install dir straight to --dir (a silent downstream no-op), the new code walks up
|
|
47
|
-
# from cwd to find the real install — a strict improvement, exercised by the resolver tests.
|
|
48
|
-
resolve_install_dir() {
|
|
49
|
-
if [ -n "${PHRONESIS_DIR:-}" ] && [ -d "${PHRONESIS_DIR}/.phronesis" ]; then printf '%s' "$PHRONESIS_DIR"; return; fi
|
|
50
|
-
if [ -n "${CLAUDE_PROJECT_DIR:-}" ] && [ -d "${CLAUDE_PROJECT_DIR}/.phronesis" ]; then printf '%s' "$CLAUDE_PROJECT_DIR"; return; fi
|
|
51
|
-
local d="$PWD"
|
|
52
|
-
while [ "$d" != "/" ] && [ ! -d "$d/.phronesis" ]; do d="$(dirname "$d")"; done
|
|
53
|
-
if [ -d "$d/.phronesis" ]; then printf '%s' "$d"; return; fi
|
|
54
|
-
printf '%s' "${CLAUDE_PROJECT_DIR:-$PWD}"
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
dir="$(resolve_install_dir)"
|
|
58
|
-
# The surface name is interpolated into a JSON payload and passed as --surface; restrict it
|
|
59
|
-
# to a safe token so a hostile PHRONESIS_SURFACE cannot break out of the JSON string or the
|
|
60
|
-
# flag (defense in depth — the launcher only ever sets the literal "codex").
|
|
61
|
-
surface="$(printf '%s' "${PHRONESIS_SURFACE:-claude-code}" | tr -cd 'a-z0-9-')"
|
|
62
|
-
[ -n "$surface" ] || surface="claude-code"
|
|
63
|
-
|
|
64
|
-
phronesis hooks skill-sweep-stale --session "$session_id" --dir "$dir" >/dev/null || true
|
|
65
|
-
|
|
66
|
-
phronesis event emit session.start --session "$session_id" --surface "$surface" \
|
|
67
|
-
--dir "$dir" --payload "{\"surface\":\"$surface\"}" >/dev/null || true
|
|
68
|
-
|
|
69
|
-
if [ "$surface" = "codex" ]; then
|
|
70
|
-
# Codex SessionStart injects via hookSpecificOutput.additionalContext (Claude Code takes
|
|
71
|
-
# the raw stdout). Same due-check brain (it still emits outcome.due as a side effect),
|
|
72
|
-
# surface-specific wrapper only. due-check prints the due list when there are reviews and
|
|
73
|
-
# "No decisions due for review." otherwise — so the wrap normally fires every session
|
|
74
|
-
# (matching the Claude path, which injects that same line raw); the `-n` guard only
|
|
75
|
-
# suppresses the envelope in the degenerate case where due-check produced nothing at all.
|
|
76
|
-
due_out="$(phronesis hooks due-check --session "$session_id" --surface "$surface" --dir "$dir" || true)"
|
|
77
|
-
if [ -n "$due_out" ]; then
|
|
78
|
-
printf '%s' "$due_out" | node -e '
|
|
79
|
-
let d = "";
|
|
80
|
-
process.stdin.on("data", (c) => (d += c)).on("end", () => {
|
|
81
|
-
process.stdout.write(JSON.stringify({
|
|
82
|
-
hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: d },
|
|
83
|
-
}));
|
|
84
|
-
});' 2>/dev/null
|
|
85
|
-
fi
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Managed-first binding (change 0065 D2): stdin is inherited by the one resolved CLI.
|
|
3
|
+
PHRONESIS_BIN="$HOME/.phronesis/bin/phronesis"
|
|
4
|
+
PHRONESIS_MANAGED_BIN="$PHRONESIS_BIN"
|
|
5
|
+
if [ -f "$PHRONESIS_BIN" ] && [ -x "$PHRONESIS_BIN" ] && [ ! -L "$PHRONESIS_BIN" ]; then
|
|
6
|
+
:
|
|
86
7
|
else
|
|
87
|
-
|
|
8
|
+
PHRONESIS_BIN="$(command -v phronesis 2>/dev/null || true)"
|
|
9
|
+
[ -n "$PHRONESIS_BIN" ] || exit 0
|
|
10
|
+
[ "$PHRONESIS_BIN" != "$PHRONESIS_MANAGED_BIN" ] || exit 0
|
|
88
11
|
fi
|
|
89
|
-
|
|
90
|
-
exit 0
|
|
12
|
+
exec "$PHRONESIS_BIN" hook session-start --surface "${PHRONESIS_SURFACE:-claude-code}"
|
|
@@ -1,140 +1,17 @@
|
|
|
1
|
-
#!/
|
|
2
|
-
#
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
# output ({"decision":"block","reason":...}) and the stop_hook_active loop guard are
|
|
12
|
-
# identical across Claude Code and Codex (spike 2026-06-21), so the Stop path needs no
|
|
13
|
-
# surface branch. The sweep marker is a `session.end` event with a `sweep` payload;
|
|
14
|
-
# `candidates: 0` is the honest negative (existing vocabulary — no invented event type).
|
|
15
|
-
#
|
|
16
|
-
# As a PreCompact hook (--advisory) it cannot block; it prints a best-effort reminder
|
|
17
|
-
# before context is lost — raw on Claude Code, wrapped as Codex {"systemMessage":...}. The
|
|
18
|
-
# Stop hook stays the deterministic backstop. As a SubagentStop hook (--lifecycle-only), it
|
|
19
|
-
# skips the capture sweep and only closes the active skill lifecycle record for that
|
|
20
|
-
# subagent boundary.
|
|
21
|
-
#
|
|
22
|
-
# Runtime-free degrade (change 0013): with no phronesis on PATH this exits 0 silently —
|
|
23
|
-
# an honest gap, never an error.
|
|
24
|
-
set -uo pipefail
|
|
25
|
-
|
|
26
|
-
command -v phronesis >/dev/null 2>&1 || exit 0
|
|
27
|
-
|
|
28
|
-
advisory=0
|
|
29
|
-
lifecycle_only=0
|
|
30
|
-
for arg in "$@"; do
|
|
31
|
-
case "$arg" in
|
|
32
|
-
--advisory) advisory=1 ;;
|
|
33
|
-
--lifecycle-only) lifecycle_only=1 ;;
|
|
34
|
-
esac
|
|
35
|
-
done
|
|
36
|
-
|
|
37
|
-
input="$(cat || true)"
|
|
38
|
-
# Emit stop_hook_active (a bare boolean) FIRST and the session_id as the REST of the line,
|
|
39
|
-
# so `read -r flag id` keeps a session_id that contains whitespace intact in $session_id —
|
|
40
|
-
# a space-split id would otherwise spill into stop_hook_active and silently defeat the
|
|
41
|
-
# loop guard (core-0034 review #5). Real ids have no spaces; this is fail-safe hardening.
|
|
42
|
-
read -r stop_hook_active session_id <<EOF2
|
|
43
|
-
$(printf '%s' "$input" | node -e '
|
|
44
|
-
let d = "";
|
|
45
|
-
process.stdin.on("data", (c) => (d += c)).on("end", () => {
|
|
46
|
-
try {
|
|
47
|
-
const j = JSON.parse(d);
|
|
48
|
-
process.stdout.write(`${j.stop_hook_active === true} ${j.session_id || "-"}`);
|
|
49
|
-
} catch { process.stdout.write("false -"); }
|
|
50
|
-
});' 2>/dev/null)
|
|
51
|
-
EOF2
|
|
52
|
-
[ -n "${session_id:-}" ] && [ "$session_id" != "-" ] || exit 0
|
|
53
|
-
|
|
54
|
-
# Install-root resolution without assuming a surface env var (change 0028 / core-0034 P5):
|
|
55
|
-
# an explicit PHRONESIS_DIR (set by the Codex launcher) or CLAUDE_PROJECT_DIR (Claude Code)
|
|
56
|
-
# when it actually holds the install; else walk up from cwd to the nearest .phronesis/
|
|
57
|
-
# (Codex has no $CLAUDE_PROJECT_DIR; a non-git install behaves identically — it keys on
|
|
58
|
-
# .phronesis/, not .git/); else the prior ${CLAUDE_PROJECT_DIR:-$PWD} default. For the
|
|
59
|
-
# shipped Claude posture (opened at the install root, CLAUDE_PROJECT_DIR = that root) the
|
|
60
|
-
# CLAUDE_PROJECT_DIR branch returns exactly the old ${CLAUDE_PROJECT_DIR:-$PWD} value — a
|
|
61
|
-
# cwd-discovered sweep-status outside the install would fail, and a failed status read as
|
|
62
|
-
# "pending" would BLOCK a session that already swept (Codex round, reproduced). The behavior
|
|
63
|
-
# changes only when CLAUDE_PROJECT_DIR lacks a .phronesis/ (install nested under a larger
|
|
64
|
-
# project): the walk-up now finds the real install instead of passing a non-install --dir.
|
|
65
|
-
resolve_install_dir() {
|
|
66
|
-
if [ -n "${PHRONESIS_DIR:-}" ] && [ -d "${PHRONESIS_DIR}/.phronesis" ]; then printf '%s' "$PHRONESIS_DIR"; return; fi
|
|
67
|
-
if [ -n "${CLAUDE_PROJECT_DIR:-}" ] && [ -d "${CLAUDE_PROJECT_DIR}/.phronesis" ]; then printf '%s' "$CLAUDE_PROJECT_DIR"; return; fi
|
|
68
|
-
local d="$PWD"
|
|
69
|
-
while [ "$d" != "/" ] && [ ! -d "$d/.phronesis" ]; do d="$(dirname "$d")"; done
|
|
70
|
-
if [ -d "$d/.phronesis" ]; then printf '%s' "$d"; return; fi
|
|
71
|
-
printf '%s' "${CLAUDE_PROJECT_DIR:-$PWD}"
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
dir="$(resolve_install_dir)"
|
|
75
|
-
# Restrict the surface to a safe token (it gates the advisory branch); the launcher only
|
|
76
|
-
# ever sets the literal "codex".
|
|
77
|
-
surface="$(printf '%s' "${PHRONESIS_SURFACE:-claude-code}" | tr -cd 'a-z0-9-')"
|
|
78
|
-
[ -n "$surface" ] || surface="claude-code"
|
|
79
|
-
|
|
80
|
-
complete_lifecycle() {
|
|
81
|
-
[ "$advisory" = "1" ] && return 0
|
|
82
|
-
boundary_event="Stop"
|
|
83
|
-
[ "$lifecycle_only" = "1" ] && boundary_event="SubagentStop"
|
|
84
|
-
# stdout is NOT discarded: skill-complete prints a {"systemMessage":...} advisory when a
|
|
85
|
-
# substantive (heavy) skill completed with zero mesh enrichments (core-0027 J1). As a
|
|
86
|
-
# Stop-hook stdout JSON it surfaces to the operator, non-blocking; it prints nothing
|
|
87
|
-
# otherwise, so a swept session's Stop output stays empty. On Codex no invocation_id is
|
|
88
|
-
# minted (skill-lifecycle is Level-2 partial, change 0028), so there is no active record
|
|
89
|
-
# to complete — a clean no-op, no fabricated skill.completed (emission honesty, 0021).
|
|
90
|
-
# Every complete_lifecycle caller exits 0 immediately after, so this is the only thing
|
|
91
|
-
# on stdout in those branches.
|
|
92
|
-
phronesis hooks skill-complete --session "$session_id" --dir "$dir" \
|
|
93
|
-
--boundary-event "$boundary_event" --payload "$input" || true
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if [ "$lifecycle_only" = "1" ]; then
|
|
97
|
-
complete_lifecycle
|
|
98
|
-
exit 0
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Managed-first binding (change 0065 D2): stdin is inherited by the one resolved CLI.
|
|
3
|
+
PHRONESIS_BIN="$HOME/.phronesis/bin/phronesis"
|
|
4
|
+
PHRONESIS_MANAGED_BIN="$PHRONESIS_BIN"
|
|
5
|
+
if [ -f "$PHRONESIS_BIN" ] && [ -x "$PHRONESIS_BIN" ] && [ ! -L "$PHRONESIS_BIN" ]; then
|
|
6
|
+
:
|
|
7
|
+
else
|
|
8
|
+
PHRONESIS_BIN="$(command -v phronesis 2>/dev/null || true)"
|
|
9
|
+
[ -n "$PHRONESIS_BIN" ] || exit 0
|
|
10
|
+
[ "$PHRONESIS_BIN" != "$PHRONESIS_MANAGED_BIN" ] || exit 0
|
|
99
11
|
fi
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if [ "$advisory" = "1" ]; then
|
|
107
|
-
reminder="Phronesis sweep reminder: context is about to compact. Review the conversation for unstored decisions, people, and commitments — emit *.candidate events / run the action types, then mark the sweep: phronesis event emit session.end --session $session_id --dir $dir --payload '{\"sweep\":{\"candidates\":N}}' (candidates 0 = honest negative)."
|
|
108
|
-
if [ "$surface" = "codex" ]; then
|
|
109
|
-
# PreCompact cannot block; Codex surfaces a non-blocking systemMessage (Claude takes raw).
|
|
110
|
-
printf '%s' "$reminder" | node -e '
|
|
111
|
-
let d = "";
|
|
112
|
-
process.stdin.on("data", (c) => (d += c)).on("end", () => {
|
|
113
|
-
process.stdout.write(JSON.stringify({ systemMessage: d }));
|
|
114
|
-
});' 2>/dev/null
|
|
115
|
-
else
|
|
116
|
-
echo "$reminder"
|
|
117
|
-
fi
|
|
118
|
-
exit 0
|
|
119
|
-
fi
|
|
120
|
-
|
|
121
|
-
# Never loop: if the stop hook is already re-driving the agent and the marker still
|
|
122
|
-
# has not landed, let the session end — a missing sweep is an honest gap, not a trap.
|
|
123
|
-
# Codex sets stop_hook_active on its Stop event exactly as Claude Code does (spike).
|
|
124
|
-
if [ "$stop_hook_active" = "true" ]; then
|
|
125
|
-
complete_lifecycle
|
|
126
|
-
exit 0
|
|
127
|
-
fi
|
|
128
|
-
|
|
129
|
-
node -e '
|
|
130
|
-
const [session, dir] = process.argv.slice(1);
|
|
131
|
-
console.log(
|
|
132
|
-
JSON.stringify({
|
|
133
|
-
decision: "block",
|
|
134
|
-
reason:
|
|
135
|
-
`Phronesis capture sweep (session ${session}): before ending, review this conversation for anything that should have been stored — decisions made, people/context learned, commitments given. ` +
|
|
136
|
-
`Emit a typed candidate for each (phronesis event emit decision.candidate|person.candidate|commitment.candidate --session ${session} --dir ${dir} --payload ...) and/or run the matching action types (phronesis act ... --dir ${dir}). ` +
|
|
137
|
-
`Then mark the sweep — honestly, 0 candidates is a valid answer: phronesis event emit session.end --session ${session} --dir ${dir} --payload '"'"'{"sweep":{"candidates":N}}'"'"'. After the marker is emitted, stop again.`,
|
|
138
|
-
}),
|
|
139
|
-
);' "$session_id" "$dir"
|
|
140
|
-
exit 0
|
|
12
|
+
event="session-sweep"
|
|
13
|
+
case "${1:-}" in
|
|
14
|
+
--advisory) event="session-sweep-precompact" ;;
|
|
15
|
+
--lifecycle-only) event="session-sweep-subagent" ;;
|
|
16
|
+
esac
|
|
17
|
+
exec "$PHRONESIS_BIN" hook "$event" --surface "${PHRONESIS_SURFACE:-claude-code}"
|
|
@@ -1,37 +1,12 @@
|
|
|
1
|
-
#!/
|
|
2
|
-
#
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
set -uo pipefail
|
|
14
|
-
|
|
15
|
-
command -v phronesis >/dev/null 2>&1 || exit 0
|
|
16
|
-
|
|
17
|
-
mode="${1:-}"
|
|
18
|
-
case "$mode" in
|
|
19
|
-
start|abandon) ;;
|
|
20
|
-
*) exit 0 ;;
|
|
21
|
-
esac
|
|
22
|
-
|
|
23
|
-
input="$(cat || true)"
|
|
24
|
-
dir="${CLAUDE_PROJECT_DIR:-$PWD}"
|
|
25
|
-
|
|
26
|
-
case "$mode" in
|
|
27
|
-
start)
|
|
28
|
-
phronesis hooks skill-start --dir "$dir" --payload "$input" >/dev/null || true
|
|
29
|
-
;;
|
|
30
|
-
abandon)
|
|
31
|
-
# StopFailure ignores hook output/exit status; keep failures non-blocking but
|
|
32
|
-
# let stderr surface when the runtime exists and is broken.
|
|
33
|
-
phronesis hooks skill-abandon --dir "$dir" --payload "$input" >/dev/null || true
|
|
34
|
-
;;
|
|
35
|
-
esac
|
|
36
|
-
|
|
37
|
-
exit 0
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Managed-first binding (change 0065 D2): stdin is inherited by the one resolved CLI.
|
|
3
|
+
PHRONESIS_BIN="$HOME/.phronesis/bin/phronesis"
|
|
4
|
+
PHRONESIS_MANAGED_BIN="$PHRONESIS_BIN"
|
|
5
|
+
if [ -f "$PHRONESIS_BIN" ] && [ -x "$PHRONESIS_BIN" ] && [ ! -L "$PHRONESIS_BIN" ]; then
|
|
6
|
+
:
|
|
7
|
+
else
|
|
8
|
+
PHRONESIS_BIN="$(command -v phronesis 2>/dev/null || true)"
|
|
9
|
+
[ -n "$PHRONESIS_BIN" ] || exit 0
|
|
10
|
+
[ "$PHRONESIS_BIN" != "$PHRONESIS_MANAGED_BIN" ] || exit 0
|
|
11
|
+
fi
|
|
12
|
+
exec "$PHRONESIS_BIN" hook skill-lifecycle --surface "${PHRONESIS_SURFACE:-claude-code}" --mode "${1:-}"
|
|
@@ -92,10 +92,15 @@ Mechanical surfaces and finding kinds:
|
|
|
92
92
|
5. `seed_adoption` surface -> `unadopted_seed`, `duplicate_seed_adoption`
|
|
93
93
|
6. `relationship_value_consistency` surface -> `relationship_value_drift`
|
|
94
94
|
|
|
95
|
+
The `orphan` surface flags an in-scope object only when it has no typed edge in *either*
|
|
96
|
+
direction to another in-scope object; links to raw sources and self-links never count as
|
|
97
|
+
connectivity (a person wired only to `raw/` is a true orphan, a decision that links out to
|
|
98
|
+
a project is not).
|
|
99
|
+
|
|
95
100
|
Important `--all-domains` limitation from the deterministic core: bare domain-object
|
|
96
101
|
targets resolve within the source domain, even during an all-domain pass. A full target
|
|
97
|
-
such as `domains/{other}/compiled/projects/{name}.md` counts as a cross-domain
|
|
98
|
-
|
|
102
|
+
such as `domains/{other}/compiled/projects/{name}.md` counts as a cross-domain edge (in
|
|
103
|
+
either direction); a bare `projects/{name}` link from another domain does not.
|
|
99
104
|
|
|
100
105
|
> **Checkpoint 1 - Deterministic floor captured.** Before reading prose for judgment,
|
|
101
106
|
> confirm the `phronesis lint --json` output is in hand, list each mechanical surface as
|