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.
Files changed (41) hide show
  1. package/package.json +1 -1
  2. package/src/act.js +62 -1
  3. package/src/adapter.js +366 -61
  4. package/src/cli.js +527 -32
  5. package/src/compile.js +141 -10
  6. package/src/doctor.js +63 -2
  7. package/src/hook.js +312 -0
  8. package/src/hooks-refresh.js +1363 -0
  9. package/src/hooks.js +48 -33
  10. package/src/init.js +37 -4
  11. package/src/launcher.js +2105 -0
  12. package/src/layout.js +51 -7
  13. package/src/profile.js +169 -0
  14. package/src/prompts.js +88 -0
  15. package/src/skills-bump.js +88 -20
  16. package/src/skills.js +103 -10
  17. package/templates/codex/INDEX.md +4 -2
  18. package/templates/codex/seed/ai-practice/the-workspace-is-part-of-the-thinking.md +96 -0
  19. package/templates/codex-surface/README.md +15 -16
  20. package/templates/codex-surface/hooks/_resolve.sh +25 -24
  21. package/templates/codex-surface/hooks/recall-guard.sh +5 -30
  22. package/templates/codex-surface/hooks/session-start.sh +5 -18
  23. package/templates/codex-surface/hooks/session-sweep-precompact.sh +5 -20
  24. package/templates/codex-surface/hooks/session-sweep-subagent.sh +5 -23
  25. package/templates/codex-surface/hooks/session-sweep.sh +5 -19
  26. package/templates/launcher/phronesis +95 -0
  27. package/templates/phronesis-hooks/compile-active-context.sh +12 -38
  28. package/templates/phronesis-hooks/recall-guard.sh +12 -103
  29. package/templates/phronesis-hooks/session-start.sh +10 -88
  30. package/templates/phronesis-hooks/session-sweep.sh +16 -139
  31. package/templates/phronesis-hooks/skill-lifecycle.sh +12 -37
  32. package/templates/skills/lint/SKILL.md +7 -2
  33. package/templates/skills/onboard/SKILL.md +47 -9
  34. package/templates/skills/onboard/evals/rubric.md +5 -3
  35. package/templates/skills/prd-draft/SKILL.md +34 -26
  36. package/templates/skills/prd-draft/evals/rubric.md +1 -1
  37. package/vendor/core/src/action-registry.js +6 -3
  38. package/vendor/core/src/actions.js +120 -16
  39. package/vendor/core/src/index.js +3 -0
  40. package/vendor/core/src/lint.js +22 -5
  41. 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
- #!/usr/bin/env bash
2
- # Phronesis post-compile hook (core-0017 H5): refresh a domain's active-context projection
3
- # (domains/{domain}/ACTIVE_CONTEXT.md) after a compile commits canonical state. Subscribes
4
- # to action_type.committed filtered to payload.action_type == compile; the V1 in-pipeline
5
- # executor fires it synchronously at the commit point (compile accept fires it too, since
6
- # acceptance emits the same committed event). The committed event arrives as JSON on stdin.
7
- #
8
- # It is a MUTATION hook (it writes the tracked projection) — a failure is audited but never
9
- # blocks the commit. Generated state is never a free-form action write (core-0016 review
10
- # finding #2), so the refresh runs through the deterministic generator, not a hand-edit.
11
- #
12
- # Runtime-free degrade (change 0013): with no phronesis on PATH this exits 0 silently — a
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
- #!/usr/bin/env bash
2
- # Phronesis prompt-submit recall guard (Tier 1 core-0019 / T5.2a). Surface-aware
3
- # (change 0028 / core-0034): runs under Claude Code's UserPromptSubmit and Codex's (the
4
- # .codex/ launcher sets PHRONESIS_SURFACE=codex + PHRONESIS_DIR). Both surfaces share the
5
- # additionalContext output shape (spike 2026-06-21), so only the install-root resolution
6
- # and the --surface attribution differ — the emitted JSON is identical.
7
- #
8
- # Reads the surface's UserPromptSubmit JSON on stdin, extracts session_id + prompt (+ cwd
9
- # when present), pipes ONLY the prompt body to `phronesis guard` over stdin, then
10
- # translates the surface-agnostic {fired, packet} result to the add-only additionalContext
11
- # shape.
12
- # Runtime-free degrade: no `phronesis` on PATH means exit 0 with no output.
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
- #!/usr/bin/env bash
2
- # Phronesis session-start hook (Tier 1 specs/hooks.md; change 0010 D4a). Surface-aware
3
- # (change 0028 / core-0034): runs under Claude Code's SessionStart and Codex's (the
4
- # .codex/ launcher sets PHRONESIS_SURFACE=codex + PHRONESIS_DIR).
5
- #
6
- # Three jobs, all deterministic:
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
- phronesis hooks due-check --session "$session_id" --surface "$surface" --dir "$dir" || true
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
- #!/usr/bin/env bash
2
- # Phronesis session-boundary sweep (Tier 1 specs/hooks.md §"Emission discipline";
3
- # change 0010 D4b). Surface-aware (change 0028 / core-0034): runs under Claude Code's
4
- # Stop/SubagentStop/PreCompact and Codex's (the .codex/ launchers set
5
- # PHRONESIS_SURFACE=codex + PHRONESIS_DIR and bake the variant flag in). The misfire class
6
- # this closes: "did you store all that?" → "oh, sorry, I was in the flow."
7
- #
8
- # A shell script cannot judge "what should have been stored" — the model can. So the
9
- # deterministic half lives here: as a Stop hook, this blocks the session's end ONCE
10
- # until the capture sweep has run, then verifies the marker and lets go. The block-continue
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
- if phronesis hooks sweep-status --session "$session_id" --dir "$dir" 2>/dev/null | grep -qx 'swept'; then
102
- complete_lifecycle
103
- exit 0
104
- fi
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
- #!/usr/bin/env bash
2
- # Phronesis skill lifecycle hook (Tier 1, Claude Code specs/event-log.md
3
- # per-invocation attribution; change 0021; core-0032).
4
- #
5
- # start: consumes PreToolUse(Skill) or UserPromptExpansion hook JSON, mints the
6
- # adapter-owned invocation_id, writes .phronesis/invocations/, emits
7
- # skill.invoked, and completes any prior sequential invocation first.
8
- # abandon: consumes StopFailure hook JSON and clears the active record without
9
- # fabricating a normal skill.completed event.
10
- #
11
- # Runtime-free degrade (change 0013): with no phronesis on PATH this exits 0
12
- # silently an honest gap, never an error.
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 inbound
98
- edge; a bare `projects/{name}` link from another domain does not.
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