@spexcode/spec-cli 0.6.5
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 +95 -0
- package/bin/spex.mjs +76 -0
- package/dist/attach.d.ts +2 -0
- package/dist/attach.js +48 -0
- package/dist/claude-headless.d.ts +33 -0
- package/dist/claude-headless.js +307 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1859 -0
- package/dist/client.d.ts +111 -0
- package/dist/client.js +476 -0
- package/dist/cockpit.d.ts +7 -0
- package/dist/cockpit.js +26 -0
- package/dist/codex-headless.d.ts +1 -0
- package/dist/codex-headless.js +7 -0
- package/dist/codex-runtime-generations.d.ts +53 -0
- package/dist/codex-runtime-generations.js +686 -0
- package/dist/commit-surgery.d.ts +1 -0
- package/dist/commit-surgery.js +90 -0
- package/dist/contract-filter.d.ts +14 -0
- package/dist/contract-filter.js +219 -0
- package/dist/dashboard-assets.d.ts +6 -0
- package/dist/dashboard-assets.js +50 -0
- package/dist/delivery-queue.d.ts +23 -0
- package/dist/delivery-queue.js +179 -0
- package/dist/doctor.d.ts +9 -0
- package/dist/doctor.js +547 -0
- package/dist/eval-host.d.ts +1 -0
- package/dist/eval-host.js +19 -0
- package/dist/execution-trace.d.ts +26 -0
- package/dist/execution-trace.js +475 -0
- package/dist/file-write.d.ts +2 -0
- package/dist/file-write.js +26 -0
- package/dist/flat.d.ts +89 -0
- package/dist/flat.js +1076 -0
- package/dist/gateway-auth.d.ts +56 -0
- package/dist/gateway-auth.js +168 -0
- package/dist/gateway-hub.d.ts +28 -0
- package/dist/gateway-hub.js +361 -0
- package/dist/gateway.d.ts +37 -0
- package/dist/gateway.js +503 -0
- package/dist/graphCache.d.ts +27 -0
- package/dist/graphCache.js +716 -0
- package/dist/graphSnapshot.d.ts +14 -0
- package/dist/graphSnapshot.js +35 -0
- package/dist/graphStream.d.ts +85 -0
- package/dist/graphStream.js +1195 -0
- package/dist/guidance-catalog.d.ts +64 -0
- package/dist/guidance-catalog.js +166 -0
- package/dist/guide.d.ts +8 -0
- package/dist/guide.js +684 -0
- package/dist/harness-select.d.ts +18 -0
- package/dist/harness-select.js +62 -0
- package/dist/harness.d.ts +325 -0
- package/dist/harness.js +3026 -0
- package/dist/headless-controller.d.ts +9 -0
- package/dist/headless-controller.js +47 -0
- package/dist/help.d.ts +10 -0
- package/dist/help.js +574 -0
- package/dist/hook-prompts.d.ts +18 -0
- package/dist/hook-prompts.js +122 -0
- package/dist/hooks.d.ts +1 -0
- package/dist/hooks.js +27 -0
- package/dist/host-resources.d.ts +97 -0
- package/dist/host-resources.js +795 -0
- package/dist/host.d.ts +88 -0
- package/dist/host.js +663 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +899 -0
- package/dist/init.d.ts +1 -0
- package/dist/init.js +284 -0
- package/dist/issues-cli.d.ts +6 -0
- package/dist/issues-cli.js +334 -0
- package/dist/issues.d.ts +69 -0
- package/dist/issues.js +189 -0
- package/dist/lint.d.ts +34 -0
- package/dist/lint.js +476 -0
- package/dist/listen.d.ts +7 -0
- package/dist/listen.js +24 -0
- package/dist/localIssues.d.ts +65 -0
- package/dist/localIssues.js +582 -0
- package/dist/login-page.d.ts +5 -0
- package/dist/login-page.js +86 -0
- package/dist/loop-in.d.ts +23 -0
- package/dist/loop-in.js +68 -0
- package/dist/machine-peer.d.ts +69 -0
- package/dist/machine-peer.js +603 -0
- package/dist/materialize.d.ts +18 -0
- package/dist/materialize.js +468 -0
- package/dist/mentions.d.ts +50 -0
- package/dist/mentions.js +117 -0
- package/dist/opencode-headless.d.ts +4 -0
- package/dist/opencode-headless.js +208 -0
- package/dist/opencode.d.ts +3 -0
- package/dist/opencode.js +142 -0
- package/dist/pi-harness.d.ts +3 -0
- package/dist/pi-harness.js +129 -0
- package/dist/pi-headless.d.ts +25 -0
- package/dist/pi-headless.js +196 -0
- package/dist/plugin-harness.d.ts +21 -0
- package/dist/plugin-harness.js +146 -0
- package/dist/pty-bridge.d.ts +10 -0
- package/dist/pty-bridge.js +404 -0
- package/dist/pty-helper.d.mts +1 -0
- package/dist/pty-helper.mjs +101 -0
- package/dist/pty-native-helper.d.mts +2 -0
- package/dist/pty-native-helper.mjs +23 -0
- package/dist/public-graph.d.ts +46 -0
- package/dist/public-graph.js +56 -0
- package/dist/ranker.d.ts +15 -0
- package/dist/ranker.js +188 -0
- package/dist/reaper.d.ts +6 -0
- package/dist/reaper.js +81 -0
- package/dist/reviews.d.ts +189 -0
- package/dist/reviews.js +350 -0
- package/dist/runtime-guard.d.ts +6 -0
- package/dist/runtime-guard.js +37 -0
- package/dist/runtime-ownership.d.ts +21 -0
- package/dist/runtime-ownership.js +84 -0
- package/dist/search.bench.d.mts +1 -0
- package/dist/search.bench.mjs +75 -0
- package/dist/search.d.ts +20 -0
- package/dist/search.js +61 -0
- package/dist/session-cursors.d.ts +14 -0
- package/dist/session-cursors.js +82 -0
- package/dist/session-declarations.d.ts +13 -0
- package/dist/session-declarations.js +126 -0
- package/dist/session-execution.d.ts +10 -0
- package/dist/session-execution.js +70 -0
- package/dist/session-files.d.ts +28 -0
- package/dist/session-files.js +130 -0
- package/dist/session-follow.d.ts +31 -0
- package/dist/session-follow.js +179 -0
- package/dist/session-reparent.d.ts +10 -0
- package/dist/session-reparent.js +33 -0
- package/dist/session-timeline.d.ts +51 -0
- package/dist/session-timeline.js +233 -0
- package/dist/session-web.d.ts +24 -0
- package/dist/session-web.js +141 -0
- package/dist/sessions.d.ts +412 -0
- package/dist/sessions.js +4239 -0
- package/dist/sh.d.ts +1 -0
- package/dist/sh.js +3 -0
- package/dist/shim-runtime.d.ts +1 -0
- package/dist/shim-runtime.js +161 -0
- package/dist/slash-commands.d.ts +9 -0
- package/dist/slash-commands.js +307 -0
- package/dist/source-files.d.ts +9 -0
- package/dist/source-files.js +113 -0
- package/dist/supervise.d.ts +1 -0
- package/dist/supervise.js +308 -0
- package/dist/tree.d.ts +25 -0
- package/dist/tree.js +98 -0
- package/dist/tsx-bin.d.ts +3 -0
- package/dist/tsx-bin.js +41 -0
- package/dist/uninstall.d.ts +3 -0
- package/dist/uninstall.js +177 -0
- package/dist/uploads.d.ts +36 -0
- package/dist/uploads.js +243 -0
- package/dist/worktree-sources.d.ts +1 -0
- package/dist/worktree-sources.js +47 -0
- package/hooks/compat/mark-active-0.5.2-eef1.fixture +53 -0
- package/hooks/compat/mark-active-sed-v0.fixture +46 -0
- package/hooks/dispatch.sh +105 -0
- package/hooks/harness.sh +272 -0
- package/package.json +47 -0
- package/templates/hooks/commit-msg +14 -0
- package/templates/hooks/post-checkout +25 -0
- package/templates/hooks/post-merge +34 -0
- package/templates/hooks/pre-commit +120 -0
- package/templates/hooks/prepare-commit-msg +35 -0
- package/templates/hooks/reference-transaction +105 -0
- package/templates/spec/project/.plugins/commands/extract/spec.md +66 -0
- package/templates/spec/project/.plugins/commands/regroup/spec.md +25 -0
- package/templates/spec/project/.plugins/commands/rename/spec.md +11 -0
- package/templates/spec/project/.plugins/commands/spec.md +30 -0
- package/templates/spec/project/.plugins/commands/supervisor/spec.md +8 -0
- package/templates/spec/project/.plugins/commands/tidy/spec.md +43 -0
- package/templates/spec/project/.plugins/core/comment-altitude/spec.md +9 -0
- package/templates/spec/project/.plugins/core/idle/idle.sh +15 -0
- package/templates/spec/project/.plugins/core/idle/spec.md +13 -0
- package/templates/spec/project/.plugins/core/mark-active/mark-active.sh +59 -0
- package/templates/spec/project/.plugins/core/mark-active/spec.md +22 -0
- package/templates/spec/project/.plugins/core/session-fail/fail.sh +12 -0
- package/templates/spec/project/.plugins/core/session-fail/spec.md +15 -0
- package/templates/spec/project/.plugins/core/spec-first/spec-first.sh +43 -0
- package/templates/spec/project/.plugins/core/spec-first/spec.md +17 -0
- package/templates/spec/project/.plugins/core/spec-of-file/spec-of-file.sh +66 -0
- package/templates/spec/project/.plugins/core/spec-of-file/spec.md +15 -0
- package/templates/spec/project/.plugins/core/spec.md +22 -0
- package/templates/spec/project/.plugins/core/stop-gate/spec.md +23 -0
- package/templates/spec/project/.plugins/core/stop-gate/stop-gate.sh +149 -0
- package/templates/spec/project/.plugins/prompts/atomic-landing/spec.md +15 -0
- package/templates/spec/project/.plugins/prompts/forge-link/spec.md +10 -0
- package/templates/spec/project/.plugins/prompts/memory-hygiene/spec.md +13 -0
- package/templates/spec/project/.plugins/prompts/reproduce-before-fix/spec.md +12 -0
- package/templates/spec/project/.plugins/prompts/spec.md +24 -0
- package/templates/spec/project/.plugins/skills/distill/digest.mjs +136 -0
- package/templates/spec/project/.plugins/skills/distill/spec.md +67 -0
- package/templates/spec/project/.plugins/skills/spec.md +19 -0
- package/templates/spec/project/.plugins/spec.md +23 -0
- package/templates/spec/project/spec.md +20 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# spexcode-managed-hook-v1
|
|
3
|
+
# @@@ candidate lint ([[code-anchor]]) - one unmarked predicate at Git's prepared ref boundary:
|
|
4
|
+
# a refs/heads update that introduces a commit object which is not already reachable from refs/reflogs.
|
|
5
|
+
# Structural ref plumbing is inert; no commit-msg arm, marker, TTL, tree, parent, or message identity exists.
|
|
6
|
+
set -u
|
|
7
|
+
state="${1:-}"
|
|
8
|
+
payload=$(cat)
|
|
9
|
+
[ -z "${SPEXCODE_GATE_TRACE:-}" ] || printf 'spexcode-gate: reference state=%s action=%s payload=%s\n' "$state" "${GIT_REFLOG_ACTION:-}" "$(printf '%s' "$payload" | tr '\n' ';')" >&2
|
|
10
|
+
[ "$state" = prepared ] || exit 0
|
|
11
|
+
[ -z "${SPEXCODE_SKIP_LINT:-}" ] || exit 0
|
|
12
|
+
|
|
13
|
+
candidate=
|
|
14
|
+
candidate_old=
|
|
15
|
+
operation_git_dir=$(git rev-parse --git-dir 2>/dev/null || printf .git)
|
|
16
|
+
while read -r old new ref; do
|
|
17
|
+
[ -n "${new:-}" ] || continue
|
|
18
|
+
case "$ref" in HEAD|refs/heads/*) ;; *) continue ;; esac
|
|
19
|
+
case "$new" in *[!0-9a-f]*) continue ;; esac
|
|
20
|
+
[ "$(git cat-file -t "$new" 2>/dev/null || true)" = commit ] || continue
|
|
21
|
+
# Git exposes no operation name at this boundary. Do not infer one from old=0 or from ancestor process
|
|
22
|
+
# command lines: an unreachable ref creation is the same observable candidate as any other new ref object.
|
|
23
|
+
case "$old" in '' ) continue ;; esac
|
|
24
|
+
candidate="$new"
|
|
25
|
+
candidate_old="$old"
|
|
26
|
+
break
|
|
27
|
+
done <<< "$payload"
|
|
28
|
+
[ -n "$candidate" ] || exit 0
|
|
29
|
+
|
|
30
|
+
# Rebase/cherry-pick/revert/am author new content even when a detached rewrite left the object in a reflog.
|
|
31
|
+
# Ordinary reset/branch/checkout targets are already reachable and therefore remain inert.
|
|
32
|
+
force_operation=0
|
|
33
|
+
if [ -d "$operation_git_dir/rebase-merge" ] || [ -d "$operation_git_dir/rebase-apply" ] \
|
|
34
|
+
|| [ -d "$operation_git_dir/sequencer" ] || [ -f "$operation_git_dir/CHERRY_PICK_HEAD" ] \
|
|
35
|
+
|| [ -f "$operation_git_dir/REVERT_HEAD" ]; then
|
|
36
|
+
force_operation=1
|
|
37
|
+
fi
|
|
38
|
+
if [ "$force_operation" -eq 0 ]; then
|
|
39
|
+
if [ -n "$(git for-each-ref --contains "$candidate" --format='%(refname)' 2>/dev/null)" ] \
|
|
40
|
+
|| git reflog --all --format=%H 2>/dev/null | grep -Fqx "$candidate"; then
|
|
41
|
+
[ -z "${SPEXCODE_GATE_TRACE:-}" ] || echo "spexcode-gate: reachable candidate=$candidate skipped" >&2
|
|
42
|
+
exit 0
|
|
43
|
+
fi
|
|
44
|
+
fi
|
|
45
|
+
[ -z "${SPEXCODE_GATE_TRACE:-}" ] || echo "spexcode-gate: candidate=$candidate force=$force_operation" >&2
|
|
46
|
+
|
|
47
|
+
# Local issue/remark commits change only non-topology data. With no code path and no node spec.md in the
|
|
48
|
+
# candidate diff, an anchored unit cannot have moved and no governing claim can have changed. Prove that
|
|
49
|
+
# narrow shape directly at the ref boundary so dashboard writes do not launch a full CLI merely to rediscover
|
|
50
|
+
# the same fact. This is candidate classification, not a writer-set bypass: every other shape still lints.
|
|
51
|
+
issue_only=1
|
|
52
|
+
parent_count=$(git rev-list --parents -n1 "$candidate" 2>/dev/null | awk '{print NF-1}')
|
|
53
|
+
[ "${parent_count:-0}" -gt 1 ] && issue_only=0
|
|
54
|
+
changed_paths=$(git -c core.quotePath=false diff-tree --no-commit-id --name-only -r -m "$candidate_old" "$candidate" 2>/dev/null || true)
|
|
55
|
+
[ -n "$changed_paths" ] || issue_only=0
|
|
56
|
+
while IFS= read -r path; do
|
|
57
|
+
[ -n "$path" ] || continue
|
|
58
|
+
case "$path" in .spec/.issues/*) ;; *) issue_only=0; break ;; esac
|
|
59
|
+
done <<< "$changed_paths"
|
|
60
|
+
if [ "$issue_only" -eq 1 ]; then
|
|
61
|
+
[ -z "${SPEXCODE_GATE_TRACE:-}" ] || echo "spexcode-gate: issue-only candidate=$candidate skipped before lint" >&2
|
|
62
|
+
exit 0
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
main_root=$(dirname "$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null)")
|
|
66
|
+
repo_root=$(git rev-parse --show-toplevel 2>/dev/null)
|
|
67
|
+
spex_kind=
|
|
68
|
+
if command -v spex >/dev/null 2>&1; then
|
|
69
|
+
spex_kind=path
|
|
70
|
+
elif [ -x "$repo_root/node_modules/.bin/spex" ]; then
|
|
71
|
+
spex_kind=local
|
|
72
|
+
elif [ -x "$main_root/spec-cli/bin/spex.mjs" ]; then
|
|
73
|
+
spex_kind=pkg
|
|
74
|
+
fi
|
|
75
|
+
spex_cli() {
|
|
76
|
+
case "$spex_kind" in
|
|
77
|
+
path) spex "$@" ;;
|
|
78
|
+
local) "$repo_root/node_modules/.bin/spex" "$@" ;;
|
|
79
|
+
pkg) "$main_root/spec-cli/bin/spex.mjs" "$@" ;;
|
|
80
|
+
*) return 127 ;;
|
|
81
|
+
esac
|
|
82
|
+
}
|
|
83
|
+
if [ -z "$spex_kind" ]; then
|
|
84
|
+
echo "• SpexCode: candidate spec-lint skipped — no \`spex\` CLI found; CI still enforces." >&2
|
|
85
|
+
exit 0
|
|
86
|
+
fi
|
|
87
|
+
SPEXCODE_GATE_SCOPE_ONLY=1 spex_cli spec lint --pending "$candidate" >&2
|
|
88
|
+
lint_rc=$?
|
|
89
|
+
if [ "$lint_rc" -eq 76 ]; then
|
|
90
|
+
[ -z "${SPEXCODE_GATE_TRACE:-}" ] || echo "spexcode-gate: non-governed candidate=$candidate skipped before full lint" >&2
|
|
91
|
+
exit 0
|
|
92
|
+
fi
|
|
93
|
+
[ -z "${SPEXCODE_GATE_TRACE:-}" ] || echo "spexcode-gate: lint_rc=$lint_rc candidate=$candidate" >&2
|
|
94
|
+
if [ "$lint_rc" -eq 75 ]; then
|
|
95
|
+
echo "• SpexCode: candidate spec-lint skipped — spex is paused mid-merge (exit 75); CI still enforces." >&2
|
|
96
|
+
exit 0
|
|
97
|
+
fi
|
|
98
|
+
if [ "$lint_rc" -ne 0 ]; then
|
|
99
|
+
echo "✗ SpexCode: candidate spec-lint failed; the ref is unchanged and staged/sequencer state is intact." >&2
|
|
100
|
+
echo " Explicit bypass: SPEXCODE_SKIP_LINT=1 git commit …" >&2
|
|
101
|
+
echo " Continue after staging a repair: git merge --continue | git cherry-pick --continue | git rebase --continue" >&2
|
|
102
|
+
echo " Abort the active operation: git merge --abort | git cherry-pick --abort | git rebase --abort" >&2
|
|
103
|
+
exit 1
|
|
104
|
+
fi
|
|
105
|
+
exit 0
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: extract
|
|
3
|
+
surface: command
|
|
4
|
+
status: active
|
|
5
|
+
hue: 30
|
|
6
|
+
desc: Reverse-engineer — or re-run to reconcile — a faithful spec tree out of existing code: responsibility nodes at contract altitude, intent never fabricated, every frontend node carrying a loss signal.
|
|
7
|
+
kind: mutating
|
|
8
|
+
---
|
|
9
|
+
Reverse-engineer a spec tree for the target source area(s) below — or **re-run to reconcile** an
|
|
10
|
+
already-extracted area, the same pass that closes the gaps code growth opens (an unclaimed governed file, a
|
|
11
|
+
frontend node missing its loss signal, a thin scenario). Extraction is not a one-shot. Aim for a tree where
|
|
12
|
+
every governed file is claimed and `spex spec lint` is clean, bodies at contract altitude and in the codebase's
|
|
13
|
+
own primary language (a predominantly-Chinese repo → Chinese specs).
|
|
14
|
+
|
|
15
|
+
{{targets}}
|
|
16
|
+
|
|
17
|
+
**Find the spine the code already declares first** — a barrel of exports, a README or design doc, the
|
|
18
|
+
dependency direction between modules — and adopt it as the top-level shape, refined where the code reveals
|
|
19
|
+
finer responsibilities. Only when the code declares no architecture is the top-level cut a judgment call
|
|
20
|
+
worth raising with the human.
|
|
21
|
+
|
|
22
|
+
Then grow nodes under that spine:
|
|
23
|
+
|
|
24
|
+
- **Decompose by responsibility, not by file.** A node is one job the code does. A fat file split across
|
|
25
|
+
several jobs becomes several nodes that each claim it; one job spanning several files becomes one node
|
|
26
|
+
claiming them all. Every governed file is claimed by at least one node; nest into subtrees where warranted.
|
|
27
|
+
- **Group wide layers; don't mirror the file tree — at every level, the root included.** One-node-per-folder
|
|
28
|
+
is a smell. If a node would have more than ~7 direct children you're under-grouping: add intermediate
|
|
29
|
+
**sub-domain** nodes that cluster siblings serving one concern (model-config + selection + auth → a *model*
|
|
30
|
+
domain), and recurse until every level reads as a handful of siblings, not a flat wall. Fold cross-cutting
|
|
31
|
+
substrate (design system, i18n, platform glue) under one *foundation* node that claims the cluster's
|
|
32
|
+
barrel/wiring files (so it isn't pure-prose); split a fat folder holding several distinct jobs. Group by
|
|
33
|
+
responsibility, never to hit a number.
|
|
34
|
+
- **Stay at contract altitude.** State each node's intent, invariants, and outward behavior — what it
|
|
35
|
+
guarantees and why — not how the code does it.
|
|
36
|
+
- **Never fabricate intent.** Code shows *what it does*, rarely *why*. Read any README/design docs for real
|
|
37
|
+
intent; where you can still only see behavior, state the behavior and mark the intent as inferred rather
|
|
38
|
+
than inventing a rationale.
|
|
39
|
+
- **Reserve pure-prose nodes** (no `code:`) for a genuine cross-cutting contract no single file owns. Use
|
|
40
|
+
sparingly.
|
|
41
|
+
- **Mind the scope boundary.** A file that looks like a thin wrapper may be the foot of a feature defined
|
|
42
|
+
outside the target area — flag it instead of mis-homing it, and prefer extracting the whole repo so
|
|
43
|
+
cross-cutting features stay visible. If nothing reaches a file, say it's likely dead rather than
|
|
44
|
+
dignifying it with a confident spec.
|
|
45
|
+
|
|
46
|
+
**Give every frontend node a loss signal.** A node that governs UI or visual code (`.tsx`/`.jsx`/`.vue`/
|
|
47
|
+
`.svelte`/`.css`, or the dashboard) is a blind spot until it carries a `eval.md` — so write one as you
|
|
48
|
+
extract it: a **real user-path** scenario — a goal and the steps to reach it through the running app (never a
|
|
49
|
+
bare render-check), covering a failure/empty/edge state — with a **description** of those steps, the
|
|
50
|
+
**expected** zero-loss result, and **≥1 `tags`** classifying it (a surface like `frontend-e2e`/`backend-api`/
|
|
51
|
+
`cli`, a device like `desktop`/`mobile`) drawn from the configured library (`lint.scenarioTags`); a tag
|
|
52
|
+
outside the library fails `scan`, so use an existing one or extend the library. Frontend scenarios are measured by looking (YATU) — a screenshot filed with
|
|
53
|
+
`spex eval add <node> --image <png> --pass`. Backend nodes don't need one yet; run `spex eval lint` to
|
|
54
|
+
list the frontend nodes still uncovered. On a **re-run** this is also where you sharpen — give an uncovered
|
|
55
|
+
node its first scenario, tighten a thin one (a bare render-check, or a stale `expected`); `spex guide eval`
|
|
56
|
+
has the schema.
|
|
57
|
+
|
|
58
|
+
**Extract incrementally — don't plan the whole tree before writing.** For a large area (hundreds of files),
|
|
59
|
+
enumerating the whole partition up front or scripting a generator to emit it at once burns context before a
|
|
60
|
+
node lands and loses everything to one interruption. Fix the top-level cut and commit it, then take ONE
|
|
61
|
+
subtree at a time (write the leaf, list its files, lint, COMMIT) before the next — never more than one
|
|
62
|
+
subtree uncommitted, so progress survives context limits.
|
|
63
|
+
|
|
64
|
+
Confirm `spexcode.json`'s `governedRoots` points at the real source dirs first — lint reads silently empty
|
|
65
|
+
otherwise. Commit one node per commit (`spec: <id> — extract from <area>`) and run `spex spec lint` after each:
|
|
66
|
+
0 errors, 0 coverage/altitude warnings.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: regroup
|
|
3
|
+
surface: command
|
|
4
|
+
status: active
|
|
5
|
+
hue: 130
|
|
6
|
+
desc: Resolve a node's breadth — a flat fan-out of too many children — by lifting children onto their true owner or a new grouping layer, only along real seams.
|
|
7
|
+
kind: mutating
|
|
8
|
+
---
|
|
9
|
+
A node flagged by `spex doctor` for **breadth** has many direct children: a flat fan-out the eye may not hold. Breadth is an unvalidated health hypothesis, not a lint defect — a flat list of genuine peers can be right. Find the **natural seams** in the fan-out and lift the children onto them, *only where a real group exists*. Getting under the configured child budget is the floor, not the goal; never manufacture structure to hit a number.
|
|
10
|
+
|
|
11
|
+
{{targets}}
|
|
12
|
+
|
|
13
|
+
Read before you move: the over-broad node's spec, every child's spec, and the `[[links]]`/`related:` between them. A flat fan-out is rarely one undifferentiated list — some children are **misfiled** under the wrong parent, some **cohere into a surface no node owns yet**, and some are **genuine independent peers**. Sort each child into exactly one disposition:
|
|
14
|
+
|
|
15
|
+
- **Reparent under an existing sibling.** When a child's own spec says it is *part of* another child — its tab, its row, its input, a sub-surface of it — that sibling is its true owner. `git mv` it under that node. No new parent: the breadth was the symptom of a misparented child, and putting it under its real owner fixes the miscategorization for free. **Try this first** — it is the cheapest, most honest move and adds nothing to the tree. (Second-order case: if a reparent pushes the new host over budget, that host now needs its own seam.)
|
|
16
|
+
- **Group under a new intermediate parent.** When several children genuinely cohere — one surface, one concept, read together to understand one thing — but no existing node owns them. Create one parent along that seam. It must **earn its existence**: a body stating what the group *is* and why these children belong, a contract at altitude (it is a real node and must stay clear in the doctor's altitude diagnosis) — never a hollow container echoing a table of contents. The two-for-one test: a true seam also makes the siblings around it read more clearly.
|
|
17
|
+
- **Leave it flat.** When a child shares no boundary with the rest. A flat list of genuine peers is sometimes right; refusing to force a "misc"/"everything-else" bucket is the correct move, not a failure. A couple of real groups plus a handful of still-flat peers is a good outcome.
|
|
18
|
+
|
|
19
|
+
Honor these:
|
|
20
|
+
|
|
21
|
+
- **Reparent, never rewrite.** Move a node by `git mv`-ing its folder; its id (= folder basename), its `[[links]]`, its `code:` governance, and its `eval.md` all ride along untouched. If a child needs its body edited to belong in a group, it doesn't belong.
|
|
22
|
+
- **Fewest, deepest-justified parents.** Don't trade one flat layer for six two-child wrappers — that relocates the sprawl instead of resolving it. Between two passing groupings, take the one that adds fewer nodes.
|
|
23
|
+
- Parent ids name the concept (kebab-case); give the parent a `hue` near its children's family.
|
|
24
|
+
|
|
25
|
+
Work the order: (1) read everything; (2) write each candidate seam with the one-sentence intent that justifies it — kill the unjustifiable, and mark each survivor as reparent-under-existing or new-parent; (3) make the moves, one reviewable commit per group (`spec: <parent> — regroup <children>`, with a `Session:` trailer); (4) run `spex spec lint` and keep it at 0 errors, then run bare `spex doctor` and confirm the breadth finding is resolved. Uncommitted `git mv`s churn the drift count transiently — committing settles it; don't chase it.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: rename
|
|
3
|
+
surface: command
|
|
4
|
+
status: active
|
|
5
|
+
hue: 300
|
|
6
|
+
desc: Give this SpexCode session a short, specific name derived from the work it is currently doing.
|
|
7
|
+
kind: mutating
|
|
8
|
+
---
|
|
9
|
+
Review the work this session is currently doing and choose a short, specific name that will distinguish it
|
|
10
|
+
from the other sessions on the SpexCode board. Then run `spex session rename . "<name>"` to set that name.
|
|
11
|
+
Rename the SpexCode session, not the harness conversation, and do not edit project files for this task.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: commands
|
|
3
|
+
status: active
|
|
4
|
+
hue: 40
|
|
5
|
+
desc: Grouping shelf for the `surface: command` prompt presets offered wherever a human composes work for an agent. A shelf, not a surface — routing stays field-driven per surface.
|
|
6
|
+
---
|
|
7
|
+
# commands
|
|
8
|
+
|
|
9
|
+
The invocable **command** plugins live here: leaf plugins whose body is a prompt preset a human can pick
|
|
10
|
+
from the `/` dropdown while launching or driving a session, each carrying `surface: command`. Grouping them keeps `.plugins/` legible at
|
|
11
|
+
a glance — the command presets on this shelf, the skill plugins on [[skills]], the auxiliary system
|
|
12
|
+
contracts on [[prompts]], with [[core]] (the dev-flow contract subsystem) a flat child beside them.
|
|
13
|
+
|
|
14
|
+
Invocation belongs to the backend prompt boundary, not to whichever client happens to render the picker.
|
|
15
|
+
Every compose surface sends the raw `/<preset> [[node]]… <free text>` prompt; the shared resolver expands the
|
|
16
|
+
live `surface: command` body before either launch starts a worker or dispatch sends text to one. At
|
|
17
|
+
launch, the raw invocation remains the session's originating prompt and identity source, so links inside a
|
|
18
|
+
plugin body can never invent a node target. Dashboard and phone menus are therefore discovery/insertion
|
|
19
|
+
chrome, while dashboard, phone, CLI, API, and in-process fallback all invoke through the same backend
|
|
20
|
+
resolution. A preset with `{{targets}}` always receives the resolved target block; one without that placeholder
|
|
21
|
+
gets a target block only when the invocation actually names a target, so a targetless utility remains a small
|
|
22
|
+
prompt. An unknown leading `/name` stays ordinary prompt text and is never swallowed or guessed.
|
|
23
|
+
|
|
24
|
+
This node is a **shelf, not a surface** (the [[prompts]] shape): it declares no `surface` field and
|
|
25
|
+
gathers nothing itself. Discovery is recursive and field-driven (surface), so a resident plugs in
|
|
26
|
+
exactly as it would at the root — the gather set is path-independent, so shelving a command changes
|
|
27
|
+
nothing about what `/api/plugins` and the launcher offer. A plugin that serves BOTH surfaces (e.g.
|
|
28
|
+
[[distill]], skill and command) shelves once by its primary identity, never duplicated. The init
|
|
29
|
+
templates mirror this layout. The shelf stays pure presentation: moving a resident beneath it changes
|
|
30
|
+
neither that plugin's identity nor the surfaces gathered from its frontmatter.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: supervisor
|
|
3
|
+
surface: command
|
|
4
|
+
status: active
|
|
5
|
+
hue: 280
|
|
6
|
+
desc: Launch a supervisor agent that manages other agents from the main checkout to drive a goal to completion.
|
|
7
|
+
---
|
|
8
|
+
You are a SpexCode supervisor — a **manager**, not a feature worker. Your work base is the main checkout (the repository root), NOT your own worktree: do all git via `git -C <root>`, everything else via the `spex` CLI, and never write feature code. This preset IS your complete playbook (dispatch → monitor → review → merge → close, and how to parallelize) — the CLI's own `spex help session` is the reference for every verb's exact semantics. Drive the goal: decompose it into worker-sized tasks and dispatch one worker per independent task (`spex session new "<task>"` creates the parent-child relation and automatically installs your managed `parent` watch; inspect that existing relation/source with `spex session watch list`, so do NOT add a manual watch for this child — give it ONLY its task; a task about one specific node mentions it as `[[<id>]]`, which only sets the branch name and board attribution; the session's real node links come from what it edits), review proposals with `spex session review <id>`, dispatch the merge of good ones back to their own session (`spex session merge <id>`; the doer syncs the base into its own worktree first, so what reaches `<root>` is a trivial merge) and confirm it landed, then close. `<root>` is the fleet's ONE landing door — it takes one landing at a time, so a worker whose merge finds it mid-merge waits rather than racing, and you never fix up another lane's half-merged index yourself. Never let a worker self-merge; keep `spex spec lint` at 0 errors. To READ a worker's current state, use the one-shot snapshots (`spex session review <id>` or `spex session ls` — both return immediately); for a local next-lifecycle-edge read when no managed delivery exists, background `spex session wait <id>` — it returns only when it OBSERVES the worker transition from non-actionable into an actionable status (an already-actionable arrival state does not return it), printing the observed status path; `spex session watch stream` is human-only and STREAMS forever, so never block on it. **Stay parked while your fleet runs:** `spex session new`'s managed watch delivery is a real wake-up, so park while it exists. A manual `spex session watch <id>` is deliberate ongoing supervision of an existing session, not a one-off wait: it adds your `manual` source, and every future AUTHORED state transition is queued as a message to the watcher until `spex session watch cancel <id>` removes that source. That creates continuing traffic/noise, so use manual watch only when you want that consequence; `watch list` inspects the relation/source and `watch cancel` removes only the manual source. An `@<session>` explanation request is point-to-point: use `spex session send <id> "<question>"`; the reply hint returns over send, never watch/wait. Only go `asking` when you genuinely need the human. This matters because the dashboard **folds each child under you and shows YOUR own status for the whole group** (session-nesting, no child-status aggregation), so a supervisor that stays parked-while-they-run is what makes that folded group status honest. Two footguns that bite a fresh supervisor. First: before `spex session close <id>`, confirm the merge landed (`git -C <root> log -1` shows HEAD at the new merge commit) — closing an unmerged branch discards the work. Second: `<id>` always names a WORKER YOU DISPATCHED, spelled out — never `.` and never your own id. `.` means the session running the command, so `close .` deletes your own worktree, branch and record mid-turn and takes your fleet's manager down with it; your own ending is a declaration (`done --propose close`), never a close you run on yourself. **DRAIN THE ISSUES** (issues / local-issues) as part of your loop: `spex issue ls` lists every open concern in one place — the taste concerns finished sessions recorded locally, AND the forge's issues, store-tagged. Cluster the same concern yourself (use judgment — duplicates are a recurrence SIGNAL, not noise; fold them into one) and weigh by recurrence AND novelty — **recurrence is salience, not importance, so never just fix the highest count**: a sharp single-voice concern can outrank a popular gripe. For the ones worth acting on, `spex session new "<task>"` a worker to land it (mention the concern's node as `[[<id>]]` if it has one), then `spex issue ls resolve <id> --as accepted|landed` (or `rejected`, with a reply saying why) so the store reflects the decision. Report progress as you go and when the goal is complete. Your goal follows:
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: tidy
|
|
3
|
+
surface: command
|
|
4
|
+
status: active
|
|
5
|
+
hue: 140
|
|
6
|
+
desc: Diagnose or fix a node's body altitude — grade it against the contract-surface test (read-only), or rewrite it to contract altitude — by what the invocation asks for.
|
|
7
|
+
kind: mutating
|
|
8
|
+
---
|
|
9
|
+
Bring each target spec node's body to **contract** altitude — observable behavior, not implementation, and not vague
|
|
10
|
+
hand-waving either. One node, two modes chosen by the invoking prompt: **diagnose** (grade only, change nothing) or
|
|
11
|
+
**fix** (rewrite in place). Default to diagnose when the ask is "how healthy / what's wrong", to fix when it is "tidy /
|
|
12
|
+
clean up".
|
|
13
|
+
|
|
14
|
+
{{targets}}
|
|
15
|
+
|
|
16
|
+
The objective rule for every sentence is the contract-surface test: *"could a behavior-preserving refactor delete or
|
|
17
|
+
change this?"*
|
|
18
|
+
|
|
19
|
+
- **Yes ⇒ it is implementation** (operators, call names, data structures, "added a parameter", step-by-step how-to). In
|
|
20
|
+
fix mode it leaves the body and becomes an `@@@title - explanation` comment at the code that owns it.
|
|
21
|
+
- **No ⇒ it is contract surface.** Keep it — public names, signatures, return types, invariants, edges/errors, the
|
|
22
|
+
WHEN → outcome a caller observes.
|
|
23
|
+
|
|
24
|
+
**Diagnose** (read-only — no edits, no commits). Run bare `spex doctor` once and consume its **Spec health
|
|
25
|
+
diagnosis** for every target; never restate or locally reproduce the altitude proxy thresholds. For each target
|
|
26
|
+
report two layers:
|
|
27
|
+
|
|
28
|
+
- **Deterministic (git + `spex spec lint`):** *Lint* — errors/warnings naming this node (integrity, living, coverage, drift);
|
|
29
|
+
*Drift* — whether its governed `code:` files moved ahead of its latest version, by how much; *Link-gap* — does `code:`
|
|
30
|
+
name every implementing file? an unlinked file is invisible to lint and drift, so the spec silently stops governing it.
|
|
31
|
+
- **Quality grade (judge the body, not the code):** score 1–5 — *declarative*, *refactor-resistant*, *edges*,
|
|
32
|
+
*testable*, *concise*. Include the doctor's altitude finding (or explicit clear result) as the cheap mechanical
|
|
33
|
+
signal. Two failure directions remain a semantic judgment: **too low** (a mechanics dump — leaks, code identifiers,
|
|
34
|
+
how-to) and **too thin** (so vague a refactor couldn't violate it, e.g. "validates input appropriately"; only you catch
|
|
35
|
+
this). End with a one-line verdict (`healthy` | `needs-tidy` | `too-thin` | `drifting` | `link-gap`) and the single
|
|
36
|
+
highest-value next action. Change nothing.
|
|
37
|
+
|
|
38
|
+
**Fix** (mutating — one commit per node). Rewrite the body at the right altitude: **preserve the contract** (never drop a
|
|
39
|
+
requirement; rephrase, don't delete meaning), **raise don't hollow out** (keep every testable specific; cut only the
|
|
40
|
+
how — too-thin is as broken as a mechanics dump), **cut redundancy** (say each thing once), **stay a living document**
|
|
41
|
+
(rewrite in place, never a `## vN` history — git carries versions). Commit per node (`spec: <id> — tidy to contract
|
|
42
|
+
altitude`) with a `Session:` trailer; run `spex spec lint` after each — it must stay at 0 errors.
|
|
43
|
+
Then run bare `spex doctor` and confirm the target no longer carries an altitude finding.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: comment-altitude
|
|
3
|
+
surface: system
|
|
4
|
+
status: active
|
|
5
|
+
hue: 200
|
|
6
|
+
desc: A config plugin — code comments navigate non-obvious local reasoning; specs own product intent and contract.
|
|
7
|
+
---
|
|
8
|
+
Specs own intent, invariants, policy, and observable contracts. Comments only navigate non-obvious local decisions.
|
|
9
|
+
`spex guide spec` has the comment-altitude details.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# On an idle_prompt notification, mark the session idle (the active-only guard in `internal session-idle` keeps a
|
|
3
|
+
# deliberate awaiting/asking/parked/error declaration from being clobbered). GATED on `governed`: only a
|
|
4
|
+
# dashboard-launched session has board state to mark — a self-launched agent's idle is none of our business.
|
|
5
|
+
# State lives in the per-session GLOBAL record session.json (keyed by the harness session_id, grouped per-
|
|
6
|
+
# project — see hp_store_dir); the id is passed to the cli via `--session` so it writes the right record
|
|
7
|
+
# without depending on the worktree (which no longer holds any session file). NOTE the Notification event is
|
|
8
|
+
# Claude-only ([[harness-adapter]]: Codex fires no Notification), so this never runs under Codex.
|
|
9
|
+
. "${SPEXCODE_HARNESS_LIB:?harness.sh not exported by dispatch.sh}"
|
|
10
|
+
payload=$(cat 2>/dev/null)
|
|
11
|
+
sid=$(hp_session_id "$payload"); [ -n "$sid" ] || exit 0
|
|
12
|
+
sdir=$(hp_store_dir "$sid") || exit 0
|
|
13
|
+
rec="$sdir/session.json"
|
|
14
|
+
grep -q '"governed"[[:space:]]*:[[:space:]]*true' "$rec" 2>/dev/null || exit 0
|
|
15
|
+
[ "$(hp_notification_type "$payload")" = idle_prompt ] && exec ${SPEX:-spex} internal session-idle --session "$sid"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: idle
|
|
3
|
+
surface: hook
|
|
4
|
+
status: active
|
|
5
|
+
hue: 200
|
|
6
|
+
events:
|
|
7
|
+
- Notification
|
|
8
|
+
order: 10
|
|
9
|
+
block: false
|
|
10
|
+
---
|
|
11
|
+
Catches the undeclared stop the [[stop-gate]] misses. When the harness signals — via an idle-prompt notification — that the agent is simply sitting idle at its prompt rather than working, this hook marks the session `idle`, so a session that quietly ran out of things to do is not left reading as active on the board.
|
|
12
|
+
|
|
13
|
+
It acts only on the idle-prompt notification, ignoring every other notification kind. As a board-lifecycle hook it also acts only on a GOVERNED session — it resolves the record in the global store from the payload's `session_id` and no-ops unless `governed: true` — then marks idle via `spex internal session-idle --session <id>`. It is guarded so it never clobbers a deliberate declaration: marking idle applies only to a session still in the undeclared `active` state, leaving any considered `awaiting`, `asking`, `parked`, or `error` claim untouched. Together with [[stop-gate]] and [[session-fail]] it closes the last gap where a session could stop without its true state reaching the board.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# @@@ mark-active - the SINGLE turn-boundary hook, wired to BOTH UserPromptSubmit and PreToolUse. It has ONE
|
|
3
|
+
# job, keyed off the session's global record dir: keep the declared FRESHNESS state honest. It carries no
|
|
4
|
+
# conversation — a message reaches this agent as an ordinary prompt through the harness adapter
|
|
5
|
+
# ([[delivery-queue]]), which is the only way anything enters a turn. A hook that also injected mail handed
|
|
6
|
+
# every message over twice and made the agent's context depend on which of two paths won a race.
|
|
7
|
+
# Freshness branches on ONE structured signal read straight from the hook payload (stdin JSON), so the state is
|
|
8
|
+
# HARD — never text-sniffed from the TUI:
|
|
9
|
+
# the agent is pausing to ask the HUMAN (hp_is_ask) → status: asking, with the question text as the note
|
|
10
|
+
# (the deterministic capture of a question).
|
|
11
|
+
# any other tool, or a prompt submit → the agent is working → status: active (drop a now-stale proposal/note).
|
|
12
|
+
# WHAT counts as "asking" is the [[harness-adapter]]'s call (Claude: the AskUserQuestion tool; Codex: the
|
|
13
|
+
# request_user_input tool) — read via hp_is_ask, so this hook never names a harness tool.
|
|
14
|
+
# Fires BEFORE the tool runs, so a `spex session done` declaration (itself a tool) lands AFTER this and wins;
|
|
15
|
+
# the next real tool flips back to active, forcing a fresh Stop-gate declaration.
|
|
16
|
+
# @@@ read cheap, write through the ONE writer - this hook is on the hot path (every tool call), so it does
|
|
17
|
+
# its own READ in pure shell: three exact-line greps answer "already active, nothing stale to clear?", which
|
|
18
|
+
# is the overwhelmingly common case, and that path exits without spawning anything. When there IS a change to
|
|
19
|
+
# make it hands the write to `spex internal session-state`, the same structured writer the CLI declarations
|
|
20
|
+
# use. It never edits session.json itself: an asking note is arbitrary human/agent prose, and a writer that
|
|
21
|
+
# substitutes prose into existing JSON meets a quote, a backslash, or a newline and leaves a record nothing
|
|
22
|
+
# can parse — which is how a live session came to report "no session record" ([[sessions-core]]).
|
|
23
|
+
# @@@ global store - state lives NOT in the worktree but in the per-session GLOBAL record session.json, keyed
|
|
24
|
+
# by the harness session_id, grouped per-project (see hp_store_dir). GATED on `governed`: a user-self-launched
|
|
25
|
+
# (non-governed) session has no board to feed, so this no-ops on it. cwd = the session worktree.
|
|
26
|
+
. "${SPEXCODE_HARNESS_LIB:?harness.sh not exported by dispatch.sh}"
|
|
27
|
+
payload=$(cat 2>/dev/null)
|
|
28
|
+
# an IN-PROCESS SUBAGENT's tool call (Claude's Task tool) fires the parent's hooks with the PARENT's
|
|
29
|
+
# session_id — flipping here let a supervising parent's own subagents erase its declared park/ask within
|
|
30
|
+
# seconds and race the stop-gate into "undeclared stop" (issue #60). A subagent working is not the parent
|
|
31
|
+
# agent ACTING, so its calls never touch the record; the parent's own next tool call still flips. The
|
|
32
|
+
# discriminator is the payload's own top-level agent_id stamp (hp_is_subagent) — deterministic, never a
|
|
33
|
+
# timing window.
|
|
34
|
+
[ -n "$(hp_is_subagent "$payload")" ] && exit 0
|
|
35
|
+
sid=$(hp_session_id "$payload"); [ -n "$sid" ] || exit 0
|
|
36
|
+
sdir=$(hp_store_dir "$sid") || exit 0
|
|
37
|
+
rec="$sdir/session.json"
|
|
38
|
+
# board-lifecycle gate: only a GOVERNED (dashboard-launched) session has a board state to maintain.
|
|
39
|
+
grep -q '^[[:space:]]*"governed"[[:space:]]*:[[:space:]]*true,\?$' "$rec" 2>/dev/null || exit 0
|
|
40
|
+
|
|
41
|
+
# does FIELD's line hold exactly VALUE? The record is written one-field-per-line by the single writer
|
|
42
|
+
# (sessions.ts writeRecord), so a whole-line match is exact — and, unlike a value regex, it cannot be fooled
|
|
43
|
+
# by an escaped quote inside a neighbouring note.
|
|
44
|
+
jline_is() { grep -q "^[[:space:]]*\"$1\"[[:space:]]*:[[:space:]]*\"$2\",\?$" "$rec" 2>/dev/null; }
|
|
45
|
+
|
|
46
|
+
# The writer's own stdout is a human confirmation, not hook output — swallow it so a PreToolUse handler never
|
|
47
|
+
# emits a decision-shaped line; its stderr (a refusal — a corrupt or retired record) still surfaces. We always
|
|
48
|
+
# exit 0: this hook observes freshness, it is not a gate on the tool that triggered it.
|
|
49
|
+
if [ -n "$(hp_is_ask "$payload")" ]; then
|
|
50
|
+
# first question's text → the note (best-effort). It is passed as ONE argv word to the writer, so quotes,
|
|
51
|
+
# backslashes, newlines, and non-ASCII reach the record intact — no shell ever composes the JSON.
|
|
52
|
+
${SPEX:-spex} internal session-state asking --session "$sid" --note "$(hp_ask_note "$payload")" >/dev/null
|
|
53
|
+
exit 0
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
# cheap path: already active with nothing stale to clear → no-op (the common every-tool case), no spawn.
|
|
57
|
+
jline_is status active && jline_is proposal '' && jline_is note '' && exit 0
|
|
58
|
+
${SPEX:-spex} internal session-state active --session "$sid" >/dev/null
|
|
59
|
+
exit 0
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: mark-active
|
|
3
|
+
surface: hook
|
|
4
|
+
status: active
|
|
5
|
+
hue: 200
|
|
6
|
+
events:
|
|
7
|
+
- UserPromptSubmit
|
|
8
|
+
- PreToolUse
|
|
9
|
+
order: 10
|
|
10
|
+
block: false
|
|
11
|
+
---
|
|
12
|
+
The single freshness signal for a session. Any work a session does — a new prompt, or any tool about to run — flips its declared state to `active`, which also drops a now-stale proposal or note: once the agent is moving again, an old "ready to merge" claim no longer stands. The one exception is asking the human: when the tool is AskUserQuestion the state becomes `asking`, carrying the question itself as the note, so a pause for the human is captured deterministically without the agent having to also declare it.
|
|
13
|
+
|
|
14
|
+
The state is read from ONE structured field in the hook payload, never sniffed from the terminal UI, so the signal is hard rather than guessed. Because it fires before the tool runs, a deliberate [[stop-gate]] declaration (itself made via a tool) lands after this and wins; the next real tool flips back to `active`, forcing a fresh declaration at the following stop. It is pure shell so it stays cheap firing on every tool call.
|
|
15
|
+
|
|
16
|
+
The one activity that does NOT count as the session acting is an IN-PROCESS SUBAGENT's tool call (the harness's Task tool — a sub-conversation inside the same process). Such a call fires the parent's hooks carrying the parent's `session_id`, so without a discriminator a supervising parent could never hold a declared state: its own subagents erased every park/ask within seconds and raced the stop-gate into "undeclared stop". The harness stamps subagent-executed calls with a top-level `agent_id` field the parent's own calls never carry; `hp_is_subagent` reads that stamp deterministically (scanning only the pre-`tool_input` payload prefix, where a tool parameter or file content can never fake an unescaped key), and this hook skips the flip entirely. A subagent working is its parent supervising, not the parent moving on — the parent's own next tool call still flips as before.
|
|
17
|
+
|
|
18
|
+
It is a board-lifecycle hook, so it acts only on a GOVERNED (dashboard-launched) session — it resolves that session's record in the global per-session store from the payload's `session_id` and no-ops unless `governed: true`. The state it writes lives in that record's `session.json` (state), but it never edits that file itself: it READS it in pure shell (whole-line matches, the hot path stays jq-free) and hands every write to `spex internal session-state`, the one structured writer the CLI declarations use — an asking note is arbitrary prose, and a shell that substitutes prose into existing JSON eventually writes a record nothing can parse.
|
|
19
|
+
|
|
20
|
+
This hook carries no conversation. A message addressed to the session reaches its agent as an ordinary prompt through the harness adapter (delivery-queue), which is the only way anything enters a turn, so an inter-agent message is indistinguishable from a human one at the point of arrival. A hook that also injected mail delivered every message a second time and made the agent's context depend on which of two paths won a race; a freshness signal reports a fact about the session and hands nothing over.
|
|
21
|
+
|
|
22
|
+
This is the freshness half of the [[core]] discipline: it keeps the board honest about whether a session is working, waiting, or asking, so the gates and the dashboard read a true present state rather than a stale one.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Mark the session errored when a turn ends on an API failure (StopFailure). GATED on `governed`: only a
|
|
3
|
+
# dashboard-launched session has board state to mark. State lives in the per-session GLOBAL record (keyed by
|
|
4
|
+
# the harness session_id, grouped per-project — see hp_store_dir); the id is passed to the cli via `--session`
|
|
5
|
+
# so it writes the right record without depending on the worktree.
|
|
6
|
+
. "${SPEXCODE_HARNESS_LIB:?harness.sh not exported by dispatch.sh}"
|
|
7
|
+
payload=$(cat 2>/dev/null)
|
|
8
|
+
sid=$(hp_session_id "$payload"); [ -n "$sid" ] || exit 0
|
|
9
|
+
sdir=$(hp_store_dir "$sid") || exit 0
|
|
10
|
+
rec="$sdir/session.json"
|
|
11
|
+
grep -q '"governed"[[:space:]]*:[[:space:]]*true' "$rec" 2>/dev/null || exit 0
|
|
12
|
+
exec ${SPEX:-spex} internal session-fail --session "$sid"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: session-fail
|
|
3
|
+
surface: hook
|
|
4
|
+
status: active
|
|
5
|
+
hue: 200
|
|
6
|
+
events:
|
|
7
|
+
- StopFailure
|
|
8
|
+
order: 10
|
|
9
|
+
block: false
|
|
10
|
+
code:
|
|
11
|
+
- .spec/project/.plugins/core/session-fail/fail.sh
|
|
12
|
+
---
|
|
13
|
+
When a turn ends not because the agent declared but because the API itself failed, this hook structurally marks the session `error`. A failed turn is a real outcome the board must show, and without this signal the session would freeze under whatever state it last held — reading as "active" or "awaiting" long after it actually died.
|
|
14
|
+
|
|
15
|
+
It is non-blocking on the failure event: the failure already happened, so the only job is to report it truthfully. As a board-lifecycle hook it acts only on a GOVERNED session — resolved in the global store from the payload's `session_id` — and writes via `spex internal session-fail --session <id>`. That machine entry reaches the same live-active compare-and-set as Codex's native failed completion and a headless turn's non-zero exit (harness-adapter): only an undeclared, non-stopped `active` record becomes `error`. A declaration, explicit stop, or archive that landed first remains authoritative; a late native failure never rewrites it. This one writer keeps the [[stop-gate]] family's invariant intact for every harness while each adapter retains only its native failure signal.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# @@@ spec-first - a one-shot governed READ gate. It advances only when the adapter resolves a read path AND
|
|
3
|
+
# the spec graph resolves a real `code:` governor for that path. Irrelevant tools, unresolvable reads, and
|
|
4
|
+
# uncovered/related-only files leave the sentinel absent, so any number of ungoverned reads cannot mute the
|
|
5
|
+
# first later governed read. That read spends the gate and blocks once with its actual governor; retries pass.
|
|
6
|
+
# @@@ event vs matcher - materialized shims bind PreToolUse event-wide on every harness. hp_code_path ... read
|
|
7
|
+
# is the ONE adapter matcher that reduces Claude/Codex payload differences to a path. This handler owns the
|
|
8
|
+
# harness-agnostic state transition and governor lookup; it has no tool-name, harness, or filename branches.
|
|
9
|
+
# @@@ all sessions, global sentinel - file governance is independent of a record's `governed` bit, so the
|
|
10
|
+
# same gate serves dashboard and self-launched agents. The sentinel lives in the per-session global store dir
|
|
11
|
+
# (see hp_store_dir) and is created only by the first governed read. cwd = the worktree.
|
|
12
|
+
. "${SPEXCODE_HARNESS_LIB:?harness.sh not exported by dispatch.sh}"
|
|
13
|
+
S="${SPEX:-spex}"
|
|
14
|
+
payload=$(cat 2>/dev/null)
|
|
15
|
+
sid=$(hp_session_id "$payload"); [ -n "$sid" ] || exit 0
|
|
16
|
+
sdir=$(hp_store_dir "$sid") || exit 0
|
|
17
|
+
sent="$sdir/spec-checked"
|
|
18
|
+
[ -f "$sent" ] && exit 0
|
|
19
|
+
|
|
20
|
+
paths=$(hp_code_path "$payload" read)
|
|
21
|
+
[ -n "$paths" ] || exit 0
|
|
22
|
+
repo=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
|
|
23
|
+
|
|
24
|
+
# The internal projection uses the authoritative code: edge resolver and emits stable id<TAB>spec-path rows;
|
|
25
|
+
# empty output means uncovered or related-only, deliberately a non-transition.
|
|
26
|
+
path=""; owner=""
|
|
27
|
+
while IFS= read -r candidate; do
|
|
28
|
+
[ -n "$candidate" ] || continue
|
|
29
|
+
governors=$(cd "$repo" && $S internal spec-governors "$candidate" 2>/dev/null)
|
|
30
|
+
[ -n "$governors" ] || continue
|
|
31
|
+
path="$candidate"
|
|
32
|
+
owner=$(printf '%s\n' "$governors" | awk -F '\t' 'BEGIN{sep=""} {printf "%s%s [%s]",sep,$2,$1; sep=", "}')
|
|
33
|
+
break
|
|
34
|
+
done <<EOF
|
|
35
|
+
$paths
|
|
36
|
+
EOF
|
|
37
|
+
[ -n "$owner" ] || exit 0
|
|
38
|
+
|
|
39
|
+
mkdir -p "$sdir"; : > "$sent"
|
|
40
|
+
reason=$($S internal hook-prompt spec-first --path "$path" --owner "$owner") || exit 1
|
|
41
|
+
esc=$(printf '%s' "$reason" | sed 's/\\/\\\\/g; s/"/\\"/g' | awk 'BEGIN{ORS=""} NR>1{print "\\n"} {print}')
|
|
42
|
+
printf '{"decision":"block","reason":"%s"}\n' "$esc"
|
|
43
|
+
exit 0
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: spec-first
|
|
3
|
+
surface: hook
|
|
4
|
+
status: active
|
|
5
|
+
hue: 200
|
|
6
|
+
events:
|
|
7
|
+
- PreToolUse
|
|
8
|
+
order: 20
|
|
9
|
+
block: true
|
|
10
|
+
---
|
|
11
|
+
A one-shot, governed-aware READ gate. Its state advances only when the agent is about to read code that has a real governor (`code:` ownership). The first such read creates the session sentinel and blocks once, naming the resolved governing spec and directing the agent through the relevant parent, sibling, and child contracts before retrying. Once that contract-read path has been demanded, later code reads pass.
|
|
12
|
+
|
|
13
|
+
The state machine has no transition for an irrelevant tool, an unresolvable path, or an uncovered/related-only file. In particular, any number of ungoverned reads remain allowed without consuming or muting the gate; a later governed read must still block. This is file governance, distinct from a session record's `governed` field: spec-awareness still serves dashboard-launched and user-self-launched agents alike, with the sentinel created on demand in the session's global store directory.
|
|
14
|
+
|
|
15
|
+
Event delivery and semantic matching have separate responsibilities. The hook subscribes to the shared `PreToolUse` lifecycle event because Claude and Codex shims deliver that event broadly. The harness adapter's single `read` matcher decides whether the payload represents a file read and extracts its path; the hook then asks the spec graph whether that path has a governor. Harness payload differences stay inside the adapter, while the gate and its state transitions stay one mechanism.
|
|
16
|
+
|
|
17
|
+
This enforces the read-the-contract-first rule of [[core]] only where a contract actually exists, at the moment before understanding hardens around governed code.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# @@@ spec-of-file - a PostToolUse ANNOTATE hook: the FIRST time a session edits a given file, it tells the
|
|
3
|
+
# agent which spec node(s) GOVERN it — and, when a file is OVER-owned (> maxOwners), flags it as doing too
|
|
4
|
+
# much and points at the split — so the contract is in view AT THE MOMENT OF THE EDIT, not just later at
|
|
5
|
+
# commit (lint/drift). NON-BLOCKING (additionalContext only — never a verdict) and dedup'd PER FILE via a
|
|
6
|
+
# ledger, so a 50-edit refactor annotates each file ONCE. Uses MAIN's tsx+cli ($SPEX) for the file→spec
|
|
7
|
+
# resolve (`spex spec owner`); cwd = the session worktree.
|
|
8
|
+
# @@@ harness-agnostic - WHICH tool/path counts as a code MUTATION is the [[harness-adapter]]'s call, read via
|
|
9
|
+
# hp_code_path … mutate (Claude Edit/Write/NotebookEdit + file_path; Codex tool_name:Bash + an apply_patch /
|
|
10
|
+
# write-shape command). So this annotates edits on Claude AND Codex.
|
|
11
|
+
# @@@ all sessions, global ledger - like [[spec-first]], spec-awareness is UNIVERSAL so this is NOT gated on
|
|
12
|
+
# `governed`. The once-per-file ledger lives in the session's GLOBAL store dir (keyed by the harness
|
|
13
|
+
# session_id, grouped per-project — see hp_store_dir).
|
|
14
|
+
. "${SPEXCODE_HARNESS_LIB:?harness.sh not exported by dispatch.sh}"
|
|
15
|
+
S="${SPEX:-spex}"
|
|
16
|
+
payload=$(cat 2>/dev/null)
|
|
17
|
+
sid=$(hp_session_id "$payload"); [ -n "$sid" ] || exit 0
|
|
18
|
+
sdir=$(hp_store_dir "$sid") || exit 0
|
|
19
|
+
repo=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
|
|
20
|
+
repo=$(cd "$repo" 2>/dev/null && pwd -P) || exit 0
|
|
21
|
+
|
|
22
|
+
hp_actionable_repo_path() {
|
|
23
|
+
local raw="$1" target dir base abs rel
|
|
24
|
+
case "$raw" in
|
|
25
|
+
/*) target="$raw" ;;
|
|
26
|
+
*) target="$PWD/$raw" ;;
|
|
27
|
+
esac
|
|
28
|
+
dir=${target%/*}; base=${target##*/}
|
|
29
|
+
[ "$dir" = "$target" ] && dir=.
|
|
30
|
+
abs=$(cd "$dir" 2>/dev/null && printf '%s/%s' "$(pwd -P)" "$base") || return 1
|
|
31
|
+
case "$abs" in
|
|
32
|
+
"$repo"/*) rel=${abs#"$repo"/} ;;
|
|
33
|
+
*) return 1 ;;
|
|
34
|
+
esac
|
|
35
|
+
[ -n "$rel" ] || return 1
|
|
36
|
+
# editing the spec itself is not a governed-code edit -> nothing to annotate.
|
|
37
|
+
case "$rel" in .spec/*|*/.spec/*|*/spec.md|spec.md|.git/*) return 1 ;; esac
|
|
38
|
+
# Speak only for files Git would care about: tracked files, or untracked files that are not ignored.
|
|
39
|
+
git -C "$repo" ls-files --error-unmatch -- "$rel" >/dev/null 2>&1 && { printf '%s' "$rel"; return 0; }
|
|
40
|
+
git -C "$repo" check-ignore -q -- "$rel" && return 1
|
|
41
|
+
printf '%s' "$rel"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# the code file(s) just MUTATED (empty when this tool didn't mutate a file, e.g. a pure read). A codex
|
|
45
|
+
# multi-file apply_patch yields several paths (one per line) — annotate EACH governed code file, once.
|
|
46
|
+
paths=$(hp_code_path "$payload" mutate)
|
|
47
|
+
[ -n "$paths" ] || exit 0
|
|
48
|
+
led="$sdir/spec-of-file-seen" # dedupe: once per session per file. Lists already-annotated paths.
|
|
49
|
+
msg=""
|
|
50
|
+
while IFS= read -r path; do
|
|
51
|
+
[ -n "$path" ] || continue
|
|
52
|
+
path=$(hp_actionable_repo_path "$path") || continue
|
|
53
|
+
[ -f "$led" ] && grep -qxF -- "$path" "$led" && continue
|
|
54
|
+
mkdir -p "$sdir"; echo "$path" >> "$led"
|
|
55
|
+
m=$(cd "$repo" && $S spec owner "$path" --actionable 2>/dev/null) # --actionable: silent on a sanely-owned file; speaks only for an OVER-owned / uncovered file
|
|
56
|
+
[ -n "$m" ] || continue
|
|
57
|
+
msg="${msg:+$msg
|
|
58
|
+
}$m"
|
|
59
|
+
done <<EOF
|
|
60
|
+
$paths
|
|
61
|
+
EOF
|
|
62
|
+
[ -n "$msg" ] || exit 0
|
|
63
|
+
msg=$($S internal hook-prompt spec-of-file --details "$msg") || exit 1
|
|
64
|
+
esc=$(printf '%s' "$msg" | sed 's/\\/\\\\/g; s/"/\\"/g' | awk 'BEGIN{ORS=""} NR>1{print "\\n"} {print}')
|
|
65
|
+
printf '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"%s"}}\n' "$esc"
|
|
66
|
+
exit 0
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: spec-of-file
|
|
3
|
+
surface: hook
|
|
4
|
+
status: active
|
|
5
|
+
hue: 200
|
|
6
|
+
events:
|
|
7
|
+
- PostToolUse
|
|
8
|
+
order: 10
|
|
9
|
+
block: false
|
|
10
|
+
---
|
|
11
|
+
A non-blocking per-edit annotation. The first time a session edits a given file, it names the spec node(s) that GOVERN that file — and, when a file is over-owned, flags that it is doing too much and points at the split — so the contract is in view at the very moment of the edit, not only later at commit or drift time.
|
|
12
|
+
|
|
13
|
+
It never renders a verdict: it only adds context, so it can inform without interrupting. Like [[spec-first]], spec-awareness is universal — it is NOT gated on `governed` and runs for any agent. It is deduplicated once per normalized repo-relative file via a ledger that lives as a sibling file in the session's global store dir (keyed by the payload's `session_id`), so a fifty-edit refactor annotates each file once rather than on every write — the discipline that keeps a pervasive signal from decaying into the noise it is meant to cure. It speaks only when there is something to say: a sanely-owned file draws silence, an over-owned or uncovered one draws the pointer. It only speaks for Git-relevant files inside the current repo: tracked files, plus new untracked files that are not ignored. Paths outside the repo, `.spec`, `.git`, and ignored artifacts stay silent.
|
|
14
|
+
|
|
15
|
+
This is the at-the-keystroke companion to the read-first gate [[spec-first]] and the commit-time checks: together they keep the [[core]] rule — code must not silently diverge from its spec — visible across the whole edit loop.
|