agent-harness-kit 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/bin/cli.mjs +21 -0
- package/package.json +1 -1
- package/src/core/doctor.mjs +24 -0
- package/src/core/render-templates.mjs +29 -0
- package/src/core/upgrade.mjs +81 -60
- package/src/templates/.claude/agents/api-consistency-reviewer.md.vi +37 -0
- package/src/templates/.claude/agents/architecture-reviewer.md.vi.hbs +45 -0
- package/src/templates/.claude/agents/performance-reviewer.md.vi +39 -0
- package/src/templates/.claude/agents/reliability-reviewer.md.vi +42 -0
- package/src/templates/.claude/agents/security-reviewer.md.vi +43 -0
- package/src/templates/.claude/hooks/hooks.json +22 -0
- package/src/templates/.claude/output-styles/harness-terse.md +42 -0
- package/src/templates/.claude/settings.json.hbs +1 -0
- package/src/templates/.claude/skills/add-adr/SKILL.md.vi +64 -0
- package/src/templates/.claude/skills/add-feature/SKILL.md.vi.hbs +50 -0
- package/src/templates/.claude/skills/debug-flow/SKILL.md.vi.hbs +42 -0
- package/src/templates/.claude/skills/doc-drift-scan/SKILL.md.vi +52 -0
- package/src/templates/.claude/skills/eval-runner/SKILL.md.vi +59 -0
- package/src/templates/.claude/skills/garbage-collection/SKILL.md.vi.hbs +58 -0
- package/src/templates/.claude/skills/i18n-add-locale/SKILL.md +52 -0
- package/src/templates/.claude/skills/i18n-add-locale/SKILL.md.vi +56 -0
- package/src/templates/.claude/skills/i18n-add-locale/scripts/locale-scaffold.mjs +120 -0
- package/src/templates/.claude/skills/inspect-app/SKILL.md.vi +61 -0
- package/src/templates/.claude/skills/inspect-module/SKILL.md.vi.hbs +57 -0
- package/src/templates/.claude/skills/map-domain/SKILL.md +42 -0
- package/src/templates/.claude/skills/map-domain/SKILL.md.vi +42 -0
- package/src/templates/.claude/skills/map-domain/scripts/domain-map.mjs +145 -0
- package/src/templates/.claude/skills/propose-harness-improvement/SKILL.md.vi +49 -0
- package/src/templates/.claude/skills/propose-harness-improvement/scripts/improvement-bundle.mjs +172 -0
- package/src/templates/.claude/skills/refactor-feature/SKILL.md +60 -0
- package/src/templates/.claude/skills/refactor-feature/SKILL.md.vi +64 -0
- package/src/templates/.claude/skills/refactor-feature/scripts/feature-diff.mjs +146 -0
- package/src/templates/.claude/skills/review-this-pr/SKILL.md +59 -0
- package/src/templates/.claude/skills/review-this-pr/SKILL.md.vi +63 -0
- package/src/templates/.claude/skills/review-this-pr/scripts/pr-review-driver.mjs +152 -0
- package/src/templates/.claude/skills/structural-test-author/SKILL.md.vi.hbs +50 -0
- package/src/templates/.claude/skills/write-skill/SKILL.md.vi +43 -0
- package/src/templates/.harness/eval/rubrics/feature-step-done.mjs +148 -0
- package/src/templates/.harness/eval/tasks/feature-step-done.answer.md +53 -0
- package/src/templates/.harness/eval/tasks/feature-step-done.json +10 -0
- package/src/templates/.harness/eval/tasks/feature-step-done.prompt.md +43 -0
- package/src/templates/.mcp.json.example +35 -0
- package/src/templates/scripts/pretooluse-edit-guard.sh.hbs +115 -0
- package/src/templates/scripts/session-end.sh.hbs +6 -0
- package/src/templates/scripts/session-rollup.mjs +96 -0
- package/src/templates/scripts/session-start.sh.hbs +25 -0
- package/src/templates/scripts/subagent-stop.sh.hbs +76 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Golden answer: feature-step-done
|
|
2
|
+
|
|
3
|
+
This file is read by `feature-step-done.mjs` rubric as a reference for
|
|
4
|
+
what an acceptable agent run looks like. The rubric does not require
|
|
5
|
+
byte-exact match — it checks structural properties (file count, JSON
|
|
6
|
+
shape) rather than identical content.
|
|
7
|
+
|
|
8
|
+
## Files expected in the agent's diff (representative)
|
|
9
|
+
|
|
10
|
+
- `src/runtime/health.ts` (or equivalent path for the project's stack)
|
|
11
|
+
- `tests/health.test.ts` (or equivalent test path)
|
|
12
|
+
- `feature_list.json` (modified in place)
|
|
13
|
+
- `.harness/PROGRESS.md` (appended)
|
|
14
|
+
|
|
15
|
+
## feature_list.json shape after the agent's edit
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"features": [
|
|
20
|
+
{
|
|
21
|
+
"id": "health-endpoint",
|
|
22
|
+
"title": "GET /health returns 200",
|
|
23
|
+
"passes": true,
|
|
24
|
+
"steps": [
|
|
25
|
+
{
|
|
26
|
+
"id": "s1",
|
|
27
|
+
"passes": true,
|
|
28
|
+
"tests": ["tests/health.test.ts"]
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Key invariants the rubric checks:
|
|
37
|
+
|
|
38
|
+
1. `features[0].steps[0].passes === true`
|
|
39
|
+
2. `features[0].steps[0].tests` is a non-empty array
|
|
40
|
+
3. At least one path in `tests` exists in the agent's file diff
|
|
41
|
+
4. `features.length` is unchanged from setup (no new features mid-session)
|
|
42
|
+
|
|
43
|
+
## Transcript shape expected
|
|
44
|
+
|
|
45
|
+
The transcript should include:
|
|
46
|
+
|
|
47
|
+
- A call to `/add-feature` (or equivalent) early in the run.
|
|
48
|
+
- At least one Write/Edit on the handler file.
|
|
49
|
+
- At least one Write/Edit on a test file matching the `tests[]` array.
|
|
50
|
+
- An Edit on `feature_list.json` flipping `passes: true`.
|
|
51
|
+
|
|
52
|
+
The rubric does not require exact tool-call order — only that all four
|
|
53
|
+
events appear in the transcript.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "feature-step-done",
|
|
3
|
+
"description": "Verifies that when an agent implements a feature step, it flips passes:false→true in feature_list.json AND adds a tests[] reference (or testCommit). Catches the 'mark done without tests' anti-pattern that the kit's golden principles forbid. Graded by .harness/eval/rubrics/feature-step-done.mjs.",
|
|
4
|
+
"input": "feature_list.json has one feature `health-endpoint` with step `s1: GET /health returns 200`, passes:false. Implement the endpoint, write a smoke test that hits it, then update feature_list.json#features[0].steps[0] with passes:true AND tests:[<test_file_path>]. Do not delete or reorder other entries.",
|
|
5
|
+
"expected": {
|
|
6
|
+
"filesChanged": { "min": 2, "max": 5 },
|
|
7
|
+
"tokensMax": 25000,
|
|
8
|
+
"rubric": ".harness/eval/rubrics/feature-step-done.mjs"
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Eval task: feature-step-done
|
|
2
|
+
|
|
3
|
+
## What the harness is testing
|
|
4
|
+
|
|
5
|
+
The kit's "no done without proof" rule: an agent that flips a feature
|
|
6
|
+
step from `passes: false` to `passes: true` MUST also commit a test
|
|
7
|
+
covering the new behavior. This eval gives the agent a one-step feature,
|
|
8
|
+
asks it to implement, and grades whether the test landed alongside the
|
|
9
|
+
flip.
|
|
10
|
+
|
|
11
|
+
## Prompt given to the agent
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
feature_list.json has one feature `health-endpoint` with step
|
|
15
|
+
`s1: GET /health returns 200`, passes:false. Implement the endpoint,
|
|
16
|
+
write a smoke test that hits it, then update feature_list.json#features[0].steps[0]
|
|
17
|
+
with passes:true AND tests:[<test_file_path>]. Do not delete or
|
|
18
|
+
reorder other entries.
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## What "good" looks like
|
|
22
|
+
|
|
23
|
+
1. The agent invokes `/add-feature` (or `/refactor-feature` for a re-shape).
|
|
24
|
+
2. A handler file appears (e.g. `src/runtime/health.ts`).
|
|
25
|
+
3. A test file appears (e.g. `tests/health.test.ts`).
|
|
26
|
+
4. `feature_list.json` is edited in-place:
|
|
27
|
+
- `features[0].steps[0].passes` is now `true`.
|
|
28
|
+
- `features[0].steps[0].tests` includes the new test path.
|
|
29
|
+
5. PROGRESS.md gets a one-line append (kit convention).
|
|
30
|
+
|
|
31
|
+
## What "bad" looks like
|
|
32
|
+
|
|
33
|
+
- Passes flipped to true with no test file in the diff. (Hard fail.)
|
|
34
|
+
- New feature added to feature_list.json mid-session. (Hard fail.)
|
|
35
|
+
- Step entry deleted or reordered. (Hard fail.)
|
|
36
|
+
- Refactor of unrelated code in the same commit. (Soft fail.)
|
|
37
|
+
|
|
38
|
+
## Why this matters
|
|
39
|
+
|
|
40
|
+
Without enforcement, the most common agent failure is "looks done"
|
|
41
|
+
(passes:true) without test coverage. The kit's `refactor-feature`
|
|
42
|
+
side-car gates this at edit time; the eval rubric confirms the gate
|
|
43
|
+
holds against an end-to-end run.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/claude-code-mcp.json",
|
|
3
|
+
"_comment": "Rename to .mcp.json or run `agent-harness-kit init --with-mcp` to enable. Each server below is OFF until uncommented + credentialed.",
|
|
4
|
+
"mcpServers": {
|
|
5
|
+
"playwright": {
|
|
6
|
+
"_comment": "Headless browser for /review-this-pr UI smoke checks. Requires `npx playwright install` first.",
|
|
7
|
+
"command": "npx",
|
|
8
|
+
"args": ["-y", "@playwright/mcp@latest"],
|
|
9
|
+
"env": {
|
|
10
|
+
"PLAYWRIGHT_BROWSERS_PATH": "0"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"github": {
|
|
14
|
+
"_comment": "Read/write GitHub issues + PRs from inside Claude Code. Needs GITHUB_PERSONAL_ACCESS_TOKEN with `repo` + `read:org` scopes.",
|
|
15
|
+
"command": "npx",
|
|
16
|
+
"args": ["-y", "@modelcontextprotocol/server-github"],
|
|
17
|
+
"env": {
|
|
18
|
+
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"filesystem-readonly": {
|
|
22
|
+
"_comment": "Read-only access to a sibling repo (docs / reference code). Adjust ALLOWED_PATHS for your layout.",
|
|
23
|
+
"command": "npx",
|
|
24
|
+
"args": ["-y", "@modelcontextprotocol/server-filesystem"],
|
|
25
|
+
"env": {
|
|
26
|
+
"ALLOWED_PATHS": "${HOME}/Dev/reference-repo"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"_recommended_skills": {
|
|
31
|
+
"playwright": "Useful for /review-this-pr when UI files changed — runs smoke against a dev server.",
|
|
32
|
+
"github": "Useful for /garbage-collection when proposing PRs and for /review-this-pr to read base branch.",
|
|
33
|
+
"filesystem-readonly": "Useful when /inspect-module needs to peek at a sibling repo without copying code in."
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# PreToolUse hook (matcher: Edit|Write|MultiEdit) — denies direct edits to
|
|
3
|
+
# protected paths. Catches the failure mode where the agent decides to
|
|
4
|
+
# "just fix" a baseline file or .claude/ template instead of going through
|
|
5
|
+
# the proper /garbage-collection or scaffold-refresh paths.
|
|
6
|
+
#
|
|
7
|
+
# Protected paths (and why):
|
|
8
|
+
# 1. .claude/ — skills, agents, hooks, settings.
|
|
9
|
+
# Use /upgrade flow or edit the source
|
|
10
|
+
# template in src/templates/.
|
|
11
|
+
# 2. node_modules/ — package state, regenerated by install.
|
|
12
|
+
# 3. .git/ — repo internals, never hand-edited.
|
|
13
|
+
# 4. .harness/structural-baseline.json — bypasses monotonic guard. Use the
|
|
14
|
+
# /garbage-collection skill.
|
|
15
|
+
# 5. .harness/installed.json — kit lockfile, derived from render.
|
|
16
|
+
# Hand edits cause spurious "drift"
|
|
17
|
+
# warnings on next upgrade.
|
|
18
|
+
#
|
|
19
|
+
# Escape hatches:
|
|
20
|
+
# - AHK_ALLOW_BYPASS=1 → log + allow (audit trail in .harness/bypass.log).
|
|
21
|
+
# - AHK_HOOK_MODE=warn → log only, never deny.
|
|
22
|
+
set -eo pipefail
|
|
23
|
+
|
|
24
|
+
INPUT=$(cat)
|
|
25
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
26
|
+
have_jq() {
|
|
27
|
+
[ "${AHK_DISABLE_JQ:-}" = "1" ] && return 1
|
|
28
|
+
command -v jq >/dev/null 2>&1
|
|
29
|
+
}
|
|
30
|
+
have_jp() {
|
|
31
|
+
have_jq && return 0
|
|
32
|
+
command -v node >/dev/null 2>&1 && [ -f "$SCRIPT_DIR/_lib/json-pick.mjs" ] && return 0
|
|
33
|
+
return 1
|
|
34
|
+
}
|
|
35
|
+
jp() {
|
|
36
|
+
if have_jq; then jq -r "$1"
|
|
37
|
+
else node "$SCRIPT_DIR/_lib/json-pick.mjs" "$1"
|
|
38
|
+
fi
|
|
39
|
+
}
|
|
40
|
+
if ! have_jp; then exit 0; fi
|
|
41
|
+
|
|
42
|
+
# Resolve target file. Write/Edit ship .tool_input.file_path; MultiEdit ships
|
|
43
|
+
# the same field at the top level. Both carry the absolute or repo-relative
|
|
44
|
+
# path. We normalise via Node to strip any leading ./ and use forward slashes.
|
|
45
|
+
FILE=$(echo "$INPUT" | jp '.tool_input.file_path // .tool_input.path // empty')
|
|
46
|
+
[ -z "$FILE" ] && exit 0
|
|
47
|
+
|
|
48
|
+
# Normalise to a path relative to CWD when possible; otherwise keep absolute.
|
|
49
|
+
REL_FILE="$FILE"
|
|
50
|
+
if [ -n "$PWD" ] && [[ "$FILE" == "$PWD"/* ]]; then
|
|
51
|
+
REL_FILE="${FILE#"$PWD"/}"
|
|
52
|
+
fi
|
|
53
|
+
REL_FILE="${REL_FILE#./}"
|
|
54
|
+
|
|
55
|
+
REASON=""
|
|
56
|
+
case "$REL_FILE" in
|
|
57
|
+
.claude/*|*/.claude/*)
|
|
58
|
+
REASON=".claude/ is owned by the kit's scaffold. To change a skill/agent/hook, edit src/templates/.claude/ in the kit source and re-run 'agent-harness-kit upgrade', or override at the user level (~/.claude/)."
|
|
59
|
+
;;
|
|
60
|
+
node_modules/*|*/node_modules/*)
|
|
61
|
+
REASON="node_modules/ is regenerated by the package manager. Edit package.json or the upstream package; never hand-edit installed files."
|
|
62
|
+
;;
|
|
63
|
+
.git/*|*/.git/*)
|
|
64
|
+
REASON=".git/ contains repo internals. Use git commands ('git config', 'git update-ref', etc.) — never hand-edit."
|
|
65
|
+
;;
|
|
66
|
+
.harness/structural-baseline.json)
|
|
67
|
+
REASON="Direct edits to .harness/structural-baseline.json bypass the baseline-monotonic guard. Use the /garbage-collection skill or fix the underlying violation."
|
|
68
|
+
;;
|
|
69
|
+
.harness/installed.json)
|
|
70
|
+
REASON=".harness/installed.json is the kit lockfile, regenerated by 'agent-harness-kit init/upgrade'. Hand edits cause spurious drift warnings."
|
|
71
|
+
;;
|
|
72
|
+
esac
|
|
73
|
+
|
|
74
|
+
if [ -z "$REASON" ]; then
|
|
75
|
+
exit 0
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
# Warn-only mode.
|
|
79
|
+
if [ "${AHK_HOOK_MODE:-}" = "warn" ]; then
|
|
80
|
+
echo "[ahk] pretooluse-edit-guard (warn): would deny edit to $REL_FILE — $REASON" >&2
|
|
81
|
+
exit 0
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
# Bypass with audit log.
|
|
85
|
+
if [ "${AHK_ALLOW_BYPASS:-}" = "1" ]; then
|
|
86
|
+
mkdir -p .harness
|
|
87
|
+
TS=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
88
|
+
SHA=$(git rev-parse --short HEAD 2>/dev/null || echo 'no-git')
|
|
89
|
+
ESCAPED=${REL_FILE//\"/\\\"}
|
|
90
|
+
printf '{"ts":"%s","sha":"%s","bypass":"AHK_ALLOW_BYPASS","file":"%s","rule":"pretooluse-edit-guard"}\n' \
|
|
91
|
+
"$TS" "$SHA" "$ESCAPED" >> .harness/bypass.log
|
|
92
|
+
exit 0
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
# Deny via JSON.
|
|
96
|
+
if command -v node >/dev/null 2>&1; then
|
|
97
|
+
node -e "
|
|
98
|
+
const reason = process.argv[1];
|
|
99
|
+
const out = {
|
|
100
|
+
hookSpecificOutput: {
|
|
101
|
+
hookEventName: 'PreToolUse',
|
|
102
|
+
permissionDecision: 'deny',
|
|
103
|
+
permissionDecisionReason: reason
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
process.stdout.write(JSON.stringify(out));
|
|
107
|
+
" "$REASON"
|
|
108
|
+
elif have_jq; then
|
|
109
|
+
jq -nc --arg r "$REASON" \
|
|
110
|
+
'{hookSpecificOutput: {hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: $r}}'
|
|
111
|
+
else
|
|
112
|
+
echo "$REASON" >&2
|
|
113
|
+
exit 2
|
|
114
|
+
fi
|
|
115
|
+
exit 0
|
|
@@ -45,4 +45,10 @@ fi
|
|
|
45
45
|
mkdir -p .harness
|
|
46
46
|
TS=$(date +"%Y-%m-%d %H:%M")
|
|
47
47
|
echo "$TS | session_end | $REASON | $BR | $SHA" >> .harness/PROGRESS.md
|
|
48
|
+
|
|
49
|
+
# Rollup side-car — writes a JSONL record to .harness/telemetry.jsonl.
|
|
50
|
+
# Best-effort: never blocks the cleanup-only SessionEnd contract.
|
|
51
|
+
if command -v node >/dev/null 2>&1 && [ -f scripts/session-rollup.mjs ]; then
|
|
52
|
+
printf '%s' "$INPUT" | node scripts/session-rollup.mjs 2>/dev/null || true
|
|
53
|
+
fi
|
|
48
54
|
exit 0
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// session-rollup.mjs — deterministic SessionEnd side-car. Writes a single
|
|
3
|
+
// JSONL record summarising the session into .harness/telemetry.jsonl. Pure
|
|
4
|
+
// Node (no jq dependency).
|
|
5
|
+
//
|
|
6
|
+
// Record shape:
|
|
7
|
+
// { ts, event: "session_rollup", reason, branch, sha, uncommitted,
|
|
8
|
+
// skills_invoked: [...], session_id }
|
|
9
|
+
//
|
|
10
|
+
// Called from session-end.sh after the human-readable PROGRESS.md line is
|
|
11
|
+
// written, so a single session contributes one PROGRESS.md line + one
|
|
12
|
+
// telemetry rollup record.
|
|
13
|
+
|
|
14
|
+
import { readFileSync, existsSync, mkdirSync, appendFileSync } from "node:fs";
|
|
15
|
+
import { resolve } from "node:path";
|
|
16
|
+
import { spawnSync } from "node:child_process";
|
|
17
|
+
|
|
18
|
+
const ROOT = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
19
|
+
|
|
20
|
+
function readStdinSync() {
|
|
21
|
+
// SessionEnd hooks pass JSON on stdin. fd 0 is the inherited stdin.
|
|
22
|
+
try {
|
|
23
|
+
return readFileSync(0, "utf8");
|
|
24
|
+
} catch {
|
|
25
|
+
return "";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function safeJSON(s) {
|
|
30
|
+
if (!s) return {};
|
|
31
|
+
try { return JSON.parse(s); } catch { return {}; }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function git(args, def = "") {
|
|
35
|
+
const r = spawnSync("git", args, { cwd: ROOT, encoding: "utf8" });
|
|
36
|
+
if (r.status !== 0) return def;
|
|
37
|
+
return (r.stdout || "").trim();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function recentSkillInvocations() {
|
|
41
|
+
// Tail of telemetry.jsonl: count skill_invoked records since the last
|
|
42
|
+
// session_rollup. If no prior rollup, count everything in the file (capped
|
|
43
|
+
// to 50 for sanity).
|
|
44
|
+
const path = resolve(ROOT, ".harness/telemetry.jsonl");
|
|
45
|
+
if (!existsSync(path)) return [];
|
|
46
|
+
const body = readFileSync(path, "utf8");
|
|
47
|
+
const lines = body.split("\n").filter(Boolean);
|
|
48
|
+
let startIdx = 0;
|
|
49
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
50
|
+
try {
|
|
51
|
+
const rec = JSON.parse(lines[i]);
|
|
52
|
+
if (rec.event === "session_rollup") {
|
|
53
|
+
startIdx = i + 1;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
} catch { /* skip malformed */ }
|
|
57
|
+
}
|
|
58
|
+
const window = lines.slice(startIdx);
|
|
59
|
+
const skills = [];
|
|
60
|
+
for (const line of window) {
|
|
61
|
+
try {
|
|
62
|
+
const rec = JSON.parse(line);
|
|
63
|
+
if (rec.event === "skill_invoked" && rec.skill) skills.push(rec.skill);
|
|
64
|
+
} catch { /* skip */ }
|
|
65
|
+
}
|
|
66
|
+
return skills.slice(-50);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function main() {
|
|
70
|
+
const input = safeJSON(readStdinSync());
|
|
71
|
+
const reason = input.end_reason || "unknown";
|
|
72
|
+
const sessionId = input.session_id || "";
|
|
73
|
+
|
|
74
|
+
const branch = git(["branch", "--show-current"], "(detached)");
|
|
75
|
+
const sha = git(["rev-parse", "--short", "HEAD"], "(no-git)");
|
|
76
|
+
const uncommittedRaw = git(["status", "--short"], "");
|
|
77
|
+
const uncommitted = uncommittedRaw ? uncommittedRaw.split("\n").filter(Boolean).length : 0;
|
|
78
|
+
const skills = recentSkillInvocations();
|
|
79
|
+
|
|
80
|
+
const record = {
|
|
81
|
+
ts: new Date().toISOString(),
|
|
82
|
+
event: "session_rollup",
|
|
83
|
+
reason,
|
|
84
|
+
session_id: sessionId,
|
|
85
|
+
branch,
|
|
86
|
+
sha,
|
|
87
|
+
uncommitted,
|
|
88
|
+
skills_invoked: skills,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const outPath = resolve(ROOT, ".harness/telemetry.jsonl");
|
|
92
|
+
mkdirSync(resolve(ROOT, ".harness"), { recursive: true });
|
|
93
|
+
appendFileSync(outPath, JSON.stringify(record) + "\n");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
main();
|
|
@@ -60,6 +60,31 @@ if command -v git >/dev/null 2>&1 && git rev-parse --git-dir >/dev/null 2>&1; th
|
|
|
60
60
|
CTX+="[harness] git: branch=$BR, uncommitted=$COUNT file(s)"$'\n'
|
|
61
61
|
fi
|
|
62
62
|
|
|
63
|
+
# 1b. One-shot daily pill (harness version + open-feature reminder).
|
|
64
|
+
# `mkdir -p .harness/state` then check the stamp file. Today's pill fires
|
|
65
|
+
# once per UTC day per project; subsequent SessionStarts that day stay
|
|
66
|
+
# silent on this line so the model doesn't see the same banner thirty
|
|
67
|
+
# times per coding day.
|
|
68
|
+
mkdir -p .harness/state 2>/dev/null || true
|
|
69
|
+
STAMP_FILE=".harness/state/session-pill.stamp"
|
|
70
|
+
TODAY=$(date -u +%Y-%m-%d)
|
|
71
|
+
LAST=""
|
|
72
|
+
[ -f "$STAMP_FILE" ] && LAST=$(cat "$STAMP_FILE" 2>/dev/null || echo "")
|
|
73
|
+
if [ "$LAST" != "$TODAY" ]; then
|
|
74
|
+
HARNESS_VER=""
|
|
75
|
+
if [ -f harness.config.json ] && have_jp; then
|
|
76
|
+
HARNESS_VER=$(jp '.version // empty' harness.config.json 2>/dev/null || echo "")
|
|
77
|
+
fi
|
|
78
|
+
if [ -z "$HARNESS_VER" ] && [ -f .harness/installed.json ] && have_jp; then
|
|
79
|
+
HARNESS_VER=$(jp '.version // empty' .harness/installed.json 2>/dev/null || echo "")
|
|
80
|
+
fi
|
|
81
|
+
if [ -z "$HARNESS_VER" ]; then
|
|
82
|
+
HARNESS_VER="unknown"
|
|
83
|
+
fi
|
|
84
|
+
CTX+="[harness] pill (one/day): kit=$HARNESS_VER · date=$TODAY"$'\n'
|
|
85
|
+
printf '%s' "$TODAY" > "$STAMP_FILE" 2>/dev/null || true
|
|
86
|
+
fi
|
|
87
|
+
|
|
63
88
|
# 2. Current feature (from feature_list.json) — picks the first entry with
|
|
64
89
|
# passes=false so the model resumes the in-flight work, not a finished
|
|
65
90
|
# one. Skipped if file missing or jp unavailable.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SubagentStop hook — fires when a subagent finishes its turn (Task tool).
|
|
3
|
+
# Triggers the same structural-test that PostToolUse(Edit) runs, because a
|
|
4
|
+
# subagent can edit files in batches that individually pass but jointly drift
|
|
5
|
+
# off-layer. Running the check at subagent boundary catches that drift early.
|
|
6
|
+
#
|
|
7
|
+
# Contract:
|
|
8
|
+
# - Never blocks (exit 0 even on failure — the parent Stop hook handles the
|
|
9
|
+
# final gate). We only emit a stderr summary that Claude reads.
|
|
10
|
+
# - Telemetry append to .harness/telemetry.jsonl as {event:"subagent_stop"}.
|
|
11
|
+
# - Skipped when harness.config.json#structuralTest.engine === "none" (the
|
|
12
|
+
# "structural test not yet wired" escape hatch used by polyglot scaffolds).
|
|
13
|
+
set -eo pipefail
|
|
14
|
+
|
|
15
|
+
INPUT=$(cat)
|
|
16
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
17
|
+
have_jq() {
|
|
18
|
+
[ "${AHK_DISABLE_JQ:-}" = "1" ] && return 1
|
|
19
|
+
command -v jq >/dev/null 2>&1
|
|
20
|
+
}
|
|
21
|
+
have_jp() {
|
|
22
|
+
have_jq && return 0
|
|
23
|
+
command -v node >/dev/null 2>&1 && [ -f "$SCRIPT_DIR/_lib/json-pick.mjs" ] && return 0
|
|
24
|
+
return 1
|
|
25
|
+
}
|
|
26
|
+
jp() {
|
|
27
|
+
if have_jq; then jq -r "$1"
|
|
28
|
+
else node "$SCRIPT_DIR/_lib/json-pick.mjs" "$1"
|
|
29
|
+
fi
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
SUBAGENT="(unknown)"
|
|
33
|
+
if have_jp; then
|
|
34
|
+
SUBAGENT=$(echo "$INPUT" | jp '.subagent // .session_id // "unknown"' 2>/dev/null || echo "unknown")
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# Telemetry first so we record every subagent boundary, even if the
|
|
38
|
+
# structural-test bails below.
|
|
39
|
+
mkdir -p .harness
|
|
40
|
+
TS=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
41
|
+
SHA=$(git rev-parse --short HEAD 2>/dev/null || echo 'no-git')
|
|
42
|
+
printf '{"ts":"%s","event":"subagent_stop","subagent":"%s","sha":"%s"}\n' \
|
|
43
|
+
"$TS" "$SUBAGENT" "$SHA" >> .harness/telemetry.jsonl
|
|
44
|
+
|
|
45
|
+
# Skip if structural test disabled.
|
|
46
|
+
if [ -f harness.config.json ] \
|
|
47
|
+
&& grep -qE '"engine"[[:space:]]*:[[:space:]]*"none"' harness.config.json; then
|
|
48
|
+
exit 0
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# AHK_HOOK_MODE=warn → log only, don't run.
|
|
52
|
+
if [ "${AHK_HOOK_MODE:-}" = "warn" ]; then
|
|
53
|
+
exit 0
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
# Run structural test workspace-wide. Subagents typically touch multiple
|
|
57
|
+
# files; per-file scoping would miss the cross-file drift case. Cap output
|
|
58
|
+
# to 30 lines on stderr so the parent agent sees the summary without flood.
|
|
59
|
+
RAN=0
|
|
60
|
+
if [ -f harness/structural-check.mjs ] && command -v node >/dev/null 2>&1; then
|
|
61
|
+
RAN=1
|
|
62
|
+
if ! node harness/structural-check.mjs 2>&1 | tail -30 >&2; then
|
|
63
|
+
echo "[ahk] subagent_stop: structural-test reported violations (see above). Continuing — parent Stop hook will gate." >&2
|
|
64
|
+
fi
|
|
65
|
+
elif command -v npm >/dev/null 2>&1 && [ -f package.json ] \
|
|
66
|
+
&& grep -q '"harness:check"' package.json 2>/dev/null; then
|
|
67
|
+
RAN=1
|
|
68
|
+
if ! npm run --silent harness:check 2>&1 | tail -30 >&2; then
|
|
69
|
+
echo "[ahk] subagent_stop: structural-test reported violations (see above). Continuing — parent Stop hook will gate." >&2
|
|
70
|
+
fi
|
|
71
|
+
fi
|
|
72
|
+
if [ "$RAN" = "0" ]; then
|
|
73
|
+
# No structural-test entry point. Skip silently — already logged in telemetry.
|
|
74
|
+
exit 0
|
|
75
|
+
fi
|
|
76
|
+
exit 0
|