@givolt/i3-composer-agent 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +23 -0
- package/.claude/commands/build.md +44 -0
- package/.claude/commands/code-simplify.md +22 -0
- package/.claude/commands/constraints.md +32 -0
- package/.claude/commands/plan.md +18 -0
- package/.claude/commands/review.md +16 -0
- package/.claude/commands/ship.md +72 -0
- package/.claude/commands/spec.md +17 -0
- package/.claude/commands/test.md +19 -0
- package/.claude/commands/webperf.md +32 -0
- package/.claude-plugin/marketplace.json +27 -0
- package/.claude-plugin/plugin.json +19 -0
- package/.codex-plugin/plugin.json +28 -0
- package/.gemini/commands/build.toml +43 -0
- package/.gemini/commands/code-simplify.toml +22 -0
- package/.gemini/commands/constraints.toml +32 -0
- package/.gemini/commands/planning.toml +18 -0
- package/.gemini/commands/review.toml +16 -0
- package/.gemini/commands/ship.toml +72 -0
- package/.gemini/commands/spec.toml +17 -0
- package/.gemini/commands/test.toml +19 -0
- package/.gemini/commands/webperf.toml +32 -0
- package/CHANGELOG.md +23 -0
- package/LICENSE +21 -0
- package/NOTICE.md +9 -0
- package/README.md +117 -2
- package/agents/code-reviewer.md +102 -0
- package/agents/security-auditor.md +112 -0
- package/agents/test-engineer.md +95 -0
- package/agents/web-performance-auditor.md +184 -0
- package/bin/i3-composer-agent.cjs +122 -0
- package/commands/build.toml +43 -0
- package/commands/code-simplify.toml +22 -0
- package/commands/constraints.toml +32 -0
- package/commands/planning.toml +18 -0
- package/commands/review.toml +16 -0
- package/commands/ship.toml +72 -0
- package/commands/spec.toml +17 -0
- package/commands/test.toml +19 -0
- package/commands/webperf.toml +32 -0
- package/docs/adoption-guide.md +129 -0
- package/docs/advanced-per-agent-configuration.md +70 -0
- package/docs/agents.md +123 -0
- package/docs/antigravity-setup.md +128 -0
- package/docs/codebuddy-setup.md +41 -0
- package/docs/codex-setup.md +34 -0
- package/docs/commandcode-setup.md +69 -0
- package/docs/comparison.md +131 -0
- package/docs/copilot-cli-setup.md +64 -0
- package/docs/copilot-setup.md +162 -0
- package/docs/cursor-setup.md +225 -0
- package/docs/developer-onboarding.md +116 -0
- package/docs/gemini-cli-setup.md +133 -0
- package/docs/getting-started.md +208 -0
- package/docs/opencode-setup.md +259 -0
- package/docs/other-hosts.md +7 -0
- package/docs/skill-anatomy.md +203 -0
- package/docs/windsurf-setup.md +48 -0
- package/evals/README.md +101 -0
- package/evals/cases/api-and-interface-design.json +45 -0
- package/evals/cases/browser-testing-with-devtools.json +44 -0
- package/evals/cases/ci-cd-and-automation.json +45 -0
- package/evals/cases/code-review-and-quality.json +49 -0
- package/evals/cases/code-simplification.json +45 -0
- package/evals/cases/constraint-driven-development.json +79 -0
- package/evals/cases/context-engineering.json +44 -0
- package/evals/cases/debugging-and-error-recovery.json +58 -0
- package/evals/cases/deprecation-and-migration.json +52 -0
- package/evals/cases/documentation-and-adrs.json +44 -0
- package/evals/cases/doubt-driven-development.json +43 -0
- package/evals/cases/frontend-ui-engineering.json +52 -0
- package/evals/cases/git-workflow-and-versioning.json +47 -0
- package/evals/cases/idea-refine.json +43 -0
- package/evals/cases/incremental-implementation.json +57 -0
- package/evals/cases/interview-me.json +42 -0
- package/evals/cases/observability-and-instrumentation.json +44 -0
- package/evals/cases/performance-optimization.json +53 -0
- package/evals/cases/planning-and-task-breakdown.json +45 -0
- package/evals/cases/security-and-hardening.json +53 -0
- package/evals/cases/shipping-and-launch.json +56 -0
- package/evals/cases/source-driven-development.json +44 -0
- package/evals/cases/spec-driven-development.json +72 -0
- package/evals/cases/test-driven-development.json +73 -0
- package/evals/cases/using-agent-skills.json +44 -0
- package/evals/fixtures/api-and-interface-design/service-brief.md +19 -0
- package/evals/fixtures/browser-testing-with-devtools/README.md +5 -0
- package/evals/fixtures/browser-testing-with-devtools/index.html +24 -0
- package/evals/fixtures/browser-testing-with-devtools/server.js +15 -0
- package/evals/fixtures/ci-cd-and-automation/package.json +8 -0
- package/evals/fixtures/ci-cd-and-automation/src/slug.js +3 -0
- package/evals/fixtures/ci-cd-and-automation/test/slug.test.js +9 -0
- package/evals/fixtures/code-review-and-quality/user-search.diff +16 -0
- package/evals/fixtures/code-simplification/config-parser.js +46 -0
- package/evals/fixtures/code-simplification/config-parser.test.js +15 -0
- package/evals/fixtures/context-engineering/context-audit.md +15 -0
- package/evals/fixtures/debugging-and-error-recovery/pagination.js +8 -0
- package/evals/fixtures/debugging-and-error-recovery/pagination.test.js +9 -0
- package/evals/fixtures/debugging-and-error-recovery/time-pressure.md +6 -0
- package/evals/fixtures/deprecation-and-migration/api-inventory.md +9 -0
- package/evals/fixtures/documentation-and-adrs/decision-context.md +16 -0
- package/evals/fixtures/doubt-driven-development/migration-plan.md +19 -0
- package/evals/fixtures/frontend-ui-engineering/Button.tsx +7 -0
- package/evals/fixtures/frontend-ui-engineering/design-system.md +11 -0
- package/evals/fixtures/git-workflow-and-versioning/.eval/working-tree.patch +21 -0
- package/evals/fixtures/git-workflow-and-versioning/app.js +7 -0
- package/evals/fixtures/git-workflow-and-versioning/app.test.js +9 -0
- package/evals/fixtures/incremental-implementation/reports.js +7 -0
- package/evals/fixtures/incremental-implementation/reports.test.js +12 -0
- package/evals/fixtures/incremental-implementation/tasks/plan.md +8 -0
- package/evals/fixtures/incremental-implementation-pressure/draft-export.js +17 -0
- package/evals/fixtures/incremental-implementation-pressure/scenario.md +9 -0
- package/evals/fixtures/observability-and-instrumentation/operations.md +11 -0
- package/evals/fixtures/observability-and-instrumentation/payment-retry.js +14 -0
- package/evals/fixtures/performance-optimization/benchmark.js +15 -0
- package/evals/fixtures/performance-optimization/products.js +14 -0
- package/evals/fixtures/planning-and-task-breakdown/notifications-spec.md +18 -0
- package/evals/fixtures/security-and-hardening/webhook.js +11 -0
- package/evals/fixtures/security-and-hardening/webhook.test.js +13 -0
- package/evals/fixtures/shipping-and-launch/authority-pressure.md +6 -0
- package/evals/fixtures/shipping-and-launch/launch-status.md +11 -0
- package/evals/fixtures/source-driven-development/framework-task.md +10 -0
- package/evals/fixtures/spec-driven-development/billing-brief.md +16 -0
- package/evals/fixtures/spec-driven-development-decomposition/portal-brief.md +22 -0
- package/evals/fixtures/test-driven-development/BUG.md +10 -0
- package/evals/fixtures/test-driven-development/README.md +28 -0
- package/evals/fixtures/test-driven-development/package.json +9 -0
- package/evals/fixtures/test-driven-development/src/split.js +8 -0
- package/evals/fixtures/test-driven-development/test/split.test.js +13 -0
- package/evals/fixtures/test-driven-development-ecosystem/README.md +9 -0
- package/evals/fixtures/test-driven-development-ecosystem/ledger.py +15 -0
- package/evals/fixtures/test-driven-development-ecosystem/test_ledger.py +19 -0
- package/evals/fixtures/using-agent-skills/incident.md +6 -0
- package/evals/plugin/code-review-fires/graders/off-by-one.md +7 -0
- package/evals/plugin/code-review-fires/graders/severity-labels.md +4 -0
- package/evals/plugin/code-review-fires/graders/skill-fired.md +5 -0
- package/evals/plugin/code-review-fires/prompt.md +31 -0
- package/evals/plugin/code-review-stays-quiet/graders/not-fired.md +8 -0
- package/evals/plugin/code-review-stays-quiet/graders/tdd-fired.md +6 -0
- package/evals/plugin/code-review-stays-quiet/prompt.md +8 -0
- package/evals/plugin/code-review-stays-quiet-on-commit-message/graders/not-fired.md +8 -0
- package/evals/plugin/code-review-stays-quiet-on-commit-message/prompt.md +25 -0
- package/evals/skill-impact.md +6 -0
- package/hooks/SDD-CACHE.md +167 -0
- package/hooks/SIMPLIFY-IGNORE.md +91 -0
- package/hooks/sdd-cache-post.sh +135 -0
- package/hooks/sdd-cache-pre.sh +106 -0
- package/hooks/sdd-cache-test.sh +246 -0
- package/hooks/session-start-test.sh +47 -0
- package/hooks/session-start.sh +33 -0
- package/hooks/simplify-ignore-test.sh +476 -0
- package/hooks/simplify-ignore.sh +352 -0
- package/package.json +89 -4
- package/plugin.json +7 -0
- package/rebrand/MAINTENANCE.md +112 -0
- package/rebrand/NOTICE.md +9 -0
- package/rebrand/PR-WORKFLOW.md +34 -0
- package/rebrand/UNIFICATION-SPEC.md +33 -0
- package/rebrand/VALIDATION.md +28 -0
- package/rebrand/VERSIONING.md +32 -0
- package/rebrand/apply.cjs +194 -0
- package/rebrand/brand.json +13 -0
- package/rebrand/catalog.cjs +56 -0
- package/rebrand/fork-patches.json +12 -0
- package/rebrand/overlay/.github/workflows/test-plugin-install.yml +177 -0
- package/rebrand/overlay/CHANGELOG.md +23 -0
- package/rebrand/overlay/bin/i3-composer-agent.cjs +122 -0
- package/rebrand/overlay/scripts/validate-brand.cjs +44 -0
- package/rebrand/overlay/tests/bundle.test.cjs +85 -0
- package/rebrand/package.template.json +82 -0
- package/rebrand/rebrand.sh +4 -0
- package/rebrand/record-upstream.cjs +37 -0
- package/rebrand/templates/README.md +86 -0
- package/rebrand/templates/codebuddy-setup.md +41 -0
- package/rebrand/templates/codex-setup.md +34 -0
- package/rebrand/tests/pipeline.test.cjs +185 -0
- package/rebrand/upstream.json +171 -0
- package/references/accessibility-checklist.md +160 -0
- package/references/definition-of-done.md +67 -0
- package/references/observability-checklist.md +92 -0
- package/references/orchestration-patterns.md +370 -0
- package/references/performance-checklist.md +236 -0
- package/references/security-checklist.md +248 -0
- package/references/testing-patterns.md +235 -0
- package/scripts/floor-guard-reference-test.js +291 -0
- package/scripts/lib/skill-lint-test.js +650 -0
- package/scripts/lib/skill-lint.js +584 -0
- package/scripts/run-evals-test.js +623 -0
- package/scripts/run-evals.js +638 -0
- package/scripts/validate-artifact-paths-test.js +179 -0
- package/scripts/validate-artifact-paths.js +128 -0
- package/scripts/validate-brand.cjs +44 -0
- package/scripts/validate-commands-test.js +216 -0
- package/scripts/validate-commands.js +222 -0
- package/scripts/validate-reference-links-test.js +495 -0
- package/scripts/validate-reference-links.js +187 -0
- package/scripts/validate-skills.js +69 -0
- package/scripts/validate-versions-test.js +32 -0
- package/scripts/validate-versions.js +35 -0
- package/skills/api-and-interface-design/SKILL.md +367 -0
- package/skills/browser-testing-with-devtools/SKILL.md +317 -0
- package/skills/ci-cd-and-automation/SKILL.md +390 -0
- package/skills/code-review-and-quality/SKILL.md +398 -0
- package/skills/code-simplification/SKILL.md +331 -0
- package/skills/constraint-driven-development/SKILL.md +311 -0
- package/skills/constraint-driven-development/references/floor-guard.md +182 -0
- package/skills/context-engineering/SKILL.md +353 -0
- package/skills/debugging-and-error-recovery/SKILL.md +300 -0
- package/skills/deprecation-and-migration/SKILL.md +247 -0
- package/skills/documentation-and-adrs/SKILL.md +288 -0
- package/skills/doubt-driven-development/SKILL.md +243 -0
- package/skills/frontend-ui-engineering/SKILL.md +340 -0
- package/skills/git-workflow-and-versioning/SKILL.md +355 -0
- package/skills/idea-refine/SKILL.md +178 -0
- package/skills/idea-refine/examples.md +238 -0
- package/skills/idea-refine/frameworks.md +99 -0
- package/skills/idea-refine/refinement-criteria.md +113 -0
- package/skills/idea-refine/scripts/idea-refine.sh +15 -0
- package/skills/incremental-implementation/SKILL.md +249 -0
- package/skills/interview-me/SKILL.md +235 -0
- package/skills/observability-and-instrumentation/SKILL.md +238 -0
- package/skills/performance-optimization/SKILL.md +267 -0
- package/skills/performance-optimization/references/optimization-patterns.md +250 -0
- package/skills/planning-and-task-breakdown/SKILL.md +257 -0
- package/skills/security-and-hardening/SKILL.md +216 -0
- package/skills/security-and-hardening/references/hardening-patterns.md +324 -0
- package/skills/shipping-and-launch/SKILL.md +330 -0
- package/skills/source-driven-development/SKILL.md +216 -0
- package/skills/spec-driven-development/SKILL.md +259 -0
- package/skills/test-driven-development/SKILL.md +398 -0
- package/skills/using-agent-skills/SKILL.md +192 -0
- package/tests/bundle.test.cjs +85 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# sdd-cache-pre.sh — PreToolUse hook for WebFetch.
|
|
3
|
+
#
|
|
4
|
+
# HTTP resource cache keyed by URL. Freshness is delegated to the origin via
|
|
5
|
+
# HTTP validators; 304 Not Modified is the only signal to serve from cache.
|
|
6
|
+
# On hit, exits 2 and writes the cached body to stderr so Claude Code can
|
|
7
|
+
# deliver it to the agent in place of the WebFetch result. Otherwise exits 0.
|
|
8
|
+
#
|
|
9
|
+
# No TTL: if validators don't catch a change, nothing will. Entries without
|
|
10
|
+
# ETag or Last-Modified are never cached (can't revalidate).
|
|
11
|
+
#
|
|
12
|
+
# Cached bodies are prompt-shaped (WebFetch post-processes through a model),
|
|
13
|
+
# so the key is URL-only and the original prompt is surfaced in the hit
|
|
14
|
+
# message so the next agent can tell if the earlier reading still applies.
|
|
15
|
+
#
|
|
16
|
+
# Dependencies: jq, curl, shasum (or sha256sum).
|
|
17
|
+
|
|
18
|
+
set -euo pipefail
|
|
19
|
+
|
|
20
|
+
# Graceful degradation: if any dependency is missing, let the fetch through.
|
|
21
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
22
|
+
command -v curl >/dev/null 2>&1 || exit 0
|
|
23
|
+
command -v shasum >/dev/null 2>&1 || command -v sha256sum >/dev/null 2>&1 || exit 0
|
|
24
|
+
|
|
25
|
+
if [ -t 0 ]; then INPUT="{}"; else INPUT=$(cat); fi
|
|
26
|
+
|
|
27
|
+
# Debug logging: active when SDD_CACHE_DEBUG=1 is set, or when a sentinel
|
|
28
|
+
# file exists at .claude/sdd-cache/.debug. Toggle with `touch` / `rm`.
|
|
29
|
+
dbg() {
|
|
30
|
+
local dir="${CLAUDE_PROJECT_DIR:-$PWD}/.claude/sdd-cache"
|
|
31
|
+
[ "${SDD_CACHE_DEBUG:-0}" = "1" ] || [ -f "$dir/.debug" ] || return 0
|
|
32
|
+
mkdir -p "$dir"
|
|
33
|
+
printf '%s [pre] %s\n' "$(date -u +%FT%TZ)" "$*" >> "$dir/.debug.log"
|
|
34
|
+
}
|
|
35
|
+
dbg "fired"
|
|
36
|
+
|
|
37
|
+
URL=$(printf '%s' "$INPUT" | jq -r '.tool_input.url // empty' 2>/dev/null || true)
|
|
38
|
+
if [ -z "$URL" ]; then dbg "no url in tool_input, exit"; exit 0; fi
|
|
39
|
+
dbg "url=$URL"
|
|
40
|
+
|
|
41
|
+
# Cache key is sha256(URL), truncated to 128 bits.
|
|
42
|
+
hash_key() {
|
|
43
|
+
if command -v shasum >/dev/null 2>&1; then
|
|
44
|
+
printf '%s' "$1" | shasum -a 256 | cut -c1-32
|
|
45
|
+
else
|
|
46
|
+
printf '%s' "$1" | sha256sum | cut -c1-32
|
|
47
|
+
fi
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
CACHE_DIR="${CLAUDE_PROJECT_DIR:-$PWD}/.claude/sdd-cache"
|
|
51
|
+
CACHE_FILE="$CACHE_DIR/$(hash_key "$URL").json"
|
|
52
|
+
|
|
53
|
+
if [ ! -f "$CACHE_FILE" ]; then dbg "no cache file at $CACHE_FILE, exit"; exit 0; fi
|
|
54
|
+
dbg "cache file exists: $CACHE_FILE"
|
|
55
|
+
|
|
56
|
+
FETCHED_AT=$(jq -r '.fetched_at // 0' "$CACHE_FILE" 2>/dev/null || echo 0)
|
|
57
|
+
ORIGINAL_PROMPT=$(jq -r '.prompt // empty' "$CACHE_FILE" 2>/dev/null || true)
|
|
58
|
+
ETAG=$(jq -r '.etag // empty' "$CACHE_FILE" 2>/dev/null || true)
|
|
59
|
+
LAST_MOD=$(jq -r '.last_modified // empty' "$CACHE_FILE" 2>/dev/null || true)
|
|
60
|
+
|
|
61
|
+
# No validator means we cannot verify freshness — never serve from cache.
|
|
62
|
+
if [ -z "$ETAG" ] && [ -z "$LAST_MOD" ]; then
|
|
63
|
+
dbg "cached entry has no etag/last-modified, cannot revalidate, bypass"
|
|
64
|
+
exit 0
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
HEADERS=()
|
|
68
|
+
[ -n "$ETAG" ] && HEADERS+=(-H "If-None-Match: $ETAG")
|
|
69
|
+
[ -n "$LAST_MOD" ] && HEADERS+=(-H "If-Modified-Since: $LAST_MOD")
|
|
70
|
+
|
|
71
|
+
STATUS=$(curl -sI -o /dev/null -w "%{http_code}" \
|
|
72
|
+
--max-time 5 -L \
|
|
73
|
+
"${HEADERS[@]}" \
|
|
74
|
+
"$URL" 2>/dev/null || echo "000")
|
|
75
|
+
dbg "revalidation HEAD status=$STATUS"
|
|
76
|
+
|
|
77
|
+
if [ "$STATUS" != "304" ]; then
|
|
78
|
+
dbg "not 304, letting WebFetch proceed"
|
|
79
|
+
exit 0
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
# Server confirmed content unchanged. Serve cached copy to the agent.
|
|
83
|
+
CONTENT=$(jq -r '.content // empty' "$CACHE_FILE" 2>/dev/null || true)
|
|
84
|
+
if [ -z "$CONTENT" ]; then dbg "cache file has empty content field, bypass"; exit 0; fi
|
|
85
|
+
dbg "cache HIT, blocking WebFetch with ${#CONTENT} bytes of cached content"
|
|
86
|
+
|
|
87
|
+
VERIFIED_AT_ISO=$(date -u -r "$FETCHED_AT" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null \
|
|
88
|
+
|| date -u -d "@$FETCHED_AT" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null \
|
|
89
|
+
|| echo "unknown")
|
|
90
|
+
|
|
91
|
+
# Emit the payload with printf so $CONTENT is never interpreted by the shell
|
|
92
|
+
# (docs contain backticks, $vars, and backslashes in code examples; an
|
|
93
|
+
# unquoted heredoc would treat them as command substitution).
|
|
94
|
+
{
|
|
95
|
+
printf '[sdd-cache] Cache hit for %s\n\n' "$URL"
|
|
96
|
+
printf 'Revalidated via HTTP 304; unchanged since %s. Use the cached\n' "$VERIFIED_AT_ISO"
|
|
97
|
+
printf 'content below as if WebFetch had just returned it.\n\n'
|
|
98
|
+
if [ -n "$ORIGINAL_PROMPT" ]; then
|
|
99
|
+
printf 'Original WebFetch prompt: "%s". If your angle differs, judge\n' "$ORIGINAL_PROMPT"
|
|
100
|
+
printf 'whether this reading still covers it.\n\n'
|
|
101
|
+
fi
|
|
102
|
+
printf -- '----- BEGIN CACHED CONTENT -----\n'
|
|
103
|
+
printf '%s\n' "$CONTENT"
|
|
104
|
+
printf -- '----- END CACHED CONTENT -----\n'
|
|
105
|
+
} >&2
|
|
106
|
+
exit 2
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# sdd-cache-test.sh — Tests for the sdd-cache pre/post WebFetch hooks
|
|
3
|
+
#
|
|
4
|
+
# No real network traffic: a deterministic curl stub is placed first on PATH.
|
|
5
|
+
# CURL_STUB_STATUS — printed when the hook probes a status code (pre hook,
|
|
6
|
+
# invoked with -w); defaults to 000
|
|
7
|
+
# CURL_STUB_HEADERS — printed for header captures (post hook, no -w)
|
|
8
|
+
# Every test URL is a public https:// host and every seeded cache entry uses
|
|
9
|
+
# a fresh fetched_at, so the suite passes both with and without the SSRF /
|
|
10
|
+
# TTL hardening of the hooks (issue #295).
|
|
11
|
+
#
|
|
12
|
+
# Run: bash hooks/sdd-cache-test.sh
|
|
13
|
+
|
|
14
|
+
set -euo pipefail
|
|
15
|
+
|
|
16
|
+
PASS=0 FAIL=0
|
|
17
|
+
TMPDIR=$(mktemp -d)
|
|
18
|
+
trap 'rm -rf "$TMPDIR"' EXIT
|
|
19
|
+
|
|
20
|
+
# The hooks require jq to do anything; without it they no-op by design.
|
|
21
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
22
|
+
printf 'SKIP: jq not available; sdd-cache hooks no-op without it\n'
|
|
23
|
+
exit 0
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
# ── curl stub ─────────────────────────────────────────────────────────────
|
|
27
|
+
STUB_BIN="$TMPDIR/bin"
|
|
28
|
+
mkdir -p "$STUB_BIN"
|
|
29
|
+
cat > "$STUB_BIN/curl" <<'EOF'
|
|
30
|
+
#!/bin/bash
|
|
31
|
+
# Deterministic curl replacement. -w present => status probe (pre hook),
|
|
32
|
+
# otherwise header capture (post hook).
|
|
33
|
+
has_w=0
|
|
34
|
+
for a in "$@"; do [ "$a" = "-w" ] && has_w=1; done
|
|
35
|
+
if [ $has_w -eq 1 ]; then
|
|
36
|
+
printf '%s' "${CURL_STUB_STATUS:-000}"
|
|
37
|
+
else
|
|
38
|
+
printf '%s\n' "${CURL_STUB_HEADERS:-}"
|
|
39
|
+
fi
|
|
40
|
+
exit 0
|
|
41
|
+
EOF
|
|
42
|
+
chmod +x "$STUB_BIN/curl"
|
|
43
|
+
|
|
44
|
+
hash_key() {
|
|
45
|
+
if command -v shasum >/dev/null 2>&1; then
|
|
46
|
+
printf '%s' "$1" | shasum -a 256 | cut -c1-32
|
|
47
|
+
else
|
|
48
|
+
printf '%s' "$1" | sha256sum | cut -c1-32
|
|
49
|
+
fi
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
assert_eq() {
|
|
53
|
+
local label="$1" expected="$2" actual="$3"
|
|
54
|
+
if [ "$expected" = "$actual" ]; then
|
|
55
|
+
PASS=$((PASS + 1))
|
|
56
|
+
printf ' PASS: %s\n' "$label"
|
|
57
|
+
else
|
|
58
|
+
FAIL=$((FAIL + 1))
|
|
59
|
+
printf ' FAIL: %s\n' "$label" >&2
|
|
60
|
+
printf ' expected: %s\n' "$(printf '%s' "$expected" | cat -v)" >&2
|
|
61
|
+
printf ' actual: %s\n' "$(printf '%s' "$actual" | cat -v)" >&2
|
|
62
|
+
fi
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# run_pre / run_post: invoke a hook with isolated project dir + stubbed curl.
|
|
66
|
+
# Usage: run_pre <project_dir> <input_json> ; rc in $RC, stderr in $ERR.
|
|
67
|
+
RC=0 ERR=""
|
|
68
|
+
run_hook() {
|
|
69
|
+
local script="$1" proj="$2" input="$3"
|
|
70
|
+
RC=0
|
|
71
|
+
ERR=$(printf '%s' "$input" | \
|
|
72
|
+
CLAUDE_PROJECT_DIR="$proj" PATH="$STUB_BIN:$PATH" \
|
|
73
|
+
CURL_STUB_STATUS="${CURL_STUB_STATUS:-000}" \
|
|
74
|
+
CURL_STUB_HEADERS="${CURL_STUB_HEADERS:-}" \
|
|
75
|
+
bash "hooks/$script" 2>&1 >/dev/null) || RC=$?
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# seed_entry <proj> <url> <content> [etag] — write a valid, fresh cache entry.
|
|
79
|
+
seed_entry() {
|
|
80
|
+
# ${4-...} (not ${4:-...}): an explicitly empty etag must stay empty.
|
|
81
|
+
local proj="$1" url="$2" content="$3" etag="${4-\"seed-etag\"}"
|
|
82
|
+
local dir="$proj/.claude/sdd-cache"
|
|
83
|
+
mkdir -p "$dir"
|
|
84
|
+
jq -n \
|
|
85
|
+
--arg url "$url" \
|
|
86
|
+
--arg prompt "original prompt" \
|
|
87
|
+
--arg etag "$etag" \
|
|
88
|
+
--arg last_modified "" \
|
|
89
|
+
--arg content "$content" \
|
|
90
|
+
--argjson fetched_at "$(date +%s)" \
|
|
91
|
+
'{url:$url, prompt:$prompt, etag:$etag, last_modified:$last_modified,
|
|
92
|
+
content:$content, fetched_at:$fetched_at}' \
|
|
93
|
+
> "$dir/$(hash_key "$url").json"
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
pre_input() { jq -n --arg url "$1" '{tool_input:{url:$url}}'; }
|
|
97
|
+
post_input() { jq -n --arg url "$1" --arg result "$2" \
|
|
98
|
+
'{tool_input:{url:$url, prompt:"test prompt"}, tool_response:{result:$result, code:200}}'; }
|
|
99
|
+
|
|
100
|
+
URL="https://docs.example.com/guide"
|
|
101
|
+
|
|
102
|
+
# ── Test 1: pre — no url in input lets WebFetch proceed ───────────────────
|
|
103
|
+
printf 'Test 1: pre — no url in tool_input\n'
|
|
104
|
+
P="$TMPDIR/t1"; mkdir -p "$P"
|
|
105
|
+
run_hook sdd-cache-pre.sh "$P" '{"tool_input":{}}'
|
|
106
|
+
assert_eq "exit 0 without url" "0" "$RC"
|
|
107
|
+
|
|
108
|
+
# ── Test 2: pre — malformed JSON input is not fatal ───────────────────────
|
|
109
|
+
printf '\nTest 2: pre — malformed JSON input\n'
|
|
110
|
+
P="$TMPDIR/t2"; mkdir -p "$P"
|
|
111
|
+
run_hook sdd-cache-pre.sh "$P" 'NOT_JSON{{{'
|
|
112
|
+
assert_eq "exit 0 on malformed JSON" "0" "$RC"
|
|
113
|
+
|
|
114
|
+
# ── Test 3: pre — no cache entry lets WebFetch proceed ────────────────────
|
|
115
|
+
printf '\nTest 3: pre — cache miss\n'
|
|
116
|
+
P="$TMPDIR/t3"; mkdir -p "$P"
|
|
117
|
+
run_hook sdd-cache-pre.sh "$P" "$(pre_input "$URL")"
|
|
118
|
+
assert_eq "exit 0 on cache miss" "0" "$RC"
|
|
119
|
+
|
|
120
|
+
# ── Test 4: pre — entry without validators is never served ────────────────
|
|
121
|
+
printf '\nTest 4: pre — entry lacking ETag/Last-Modified bypasses cache\n'
|
|
122
|
+
P="$TMPDIR/t4"; mkdir -p "$P"
|
|
123
|
+
seed_entry "$P" "$URL" "cached body" ""
|
|
124
|
+
CURL_STUB_STATUS=304 run_hook sdd-cache-pre.sh "$P" "$(pre_input "$URL")"
|
|
125
|
+
assert_eq "exit 0 despite 304 when no validator stored" "0" "$RC"
|
|
126
|
+
|
|
127
|
+
# ── Test 5: pre — non-304 status lets WebFetch proceed ────────────────────
|
|
128
|
+
printf '\nTest 5: pre — origin answers 200\n'
|
|
129
|
+
P="$TMPDIR/t5"; mkdir -p "$P"
|
|
130
|
+
seed_entry "$P" "$URL" "cached body"
|
|
131
|
+
CURL_STUB_STATUS=200 run_hook sdd-cache-pre.sh "$P" "$(pre_input "$URL")"
|
|
132
|
+
assert_eq "exit 0 on 200 (content changed)" "0" "$RC"
|
|
133
|
+
|
|
134
|
+
# ── Test 6: pre — network failure lets WebFetch proceed ───────────────────
|
|
135
|
+
printf '\nTest 6: pre — revalidation request fails\n'
|
|
136
|
+
P="$TMPDIR/t6"; mkdir -p "$P"
|
|
137
|
+
seed_entry "$P" "$URL" "cached body"
|
|
138
|
+
CURL_STUB_STATUS=000 run_hook sdd-cache-pre.sh "$P" "$(pre_input "$URL")"
|
|
139
|
+
assert_eq "exit 0 on network failure" "0" "$RC"
|
|
140
|
+
|
|
141
|
+
# ── Test 7: pre — 304 serves cached content byte-exactly ──────────────────
|
|
142
|
+
printf '\nTest 7: pre — cache hit on 304\n'
|
|
143
|
+
P="$TMPDIR/t7"; mkdir -p "$P"
|
|
144
|
+
# Content with the shell-hostile shapes the hook promises to pass through:
|
|
145
|
+
# backticks, $vars, backslashes, glob chars, multiple lines.
|
|
146
|
+
TRICKY='Line with `backticks` and $HOME and \backslash
|
|
147
|
+
second line: *glob* [brackets] and a "quote"
|
|
148
|
+
$(this must not execute)'
|
|
149
|
+
seed_entry "$P" "$URL" "$TRICKY"
|
|
150
|
+
CURL_STUB_STATUS=304 run_hook sdd-cache-pre.sh "$P" "$(pre_input "$URL")"
|
|
151
|
+
assert_eq "exit 2 on cache hit" "2" "$RC"
|
|
152
|
+
assert_eq "hit message names the URL" "1" "$(printf '%s' "$ERR" | grep -cF "$URL")"
|
|
153
|
+
assert_eq "hit message says revalidated" "1" "$(printf '%s' "$ERR" | grep -c 'Revalidated')"
|
|
154
|
+
assert_eq "original prompt surfaced" "1" "$(printf '%s' "$ERR" | grep -cF 'original prompt')"
|
|
155
|
+
served=$(printf '%s\n' "$ERR" | sed -n '/BEGIN CACHED CONTENT/,/END CACHED CONTENT/p' | sed '1d;$d')
|
|
156
|
+
assert_eq "cached content served byte-exactly" "$TRICKY" "$served"
|
|
157
|
+
|
|
158
|
+
# ── Test 8: pre — empty content field bypasses even on 304 ────────────────
|
|
159
|
+
printf '\nTest 8: pre — empty cached content\n'
|
|
160
|
+
P="$TMPDIR/t8"; mkdir -p "$P"
|
|
161
|
+
seed_entry "$P" "$URL" ""
|
|
162
|
+
CURL_STUB_STATUS=304 run_hook sdd-cache-pre.sh "$P" "$(pre_input "$URL")"
|
|
163
|
+
assert_eq "exit 0 when cached content empty" "0" "$RC"
|
|
164
|
+
|
|
165
|
+
# ── Test 9: post — no url in input writes nothing ─────────────────────────
|
|
166
|
+
printf '\nTest 9: post — no url in tool_input\n'
|
|
167
|
+
P="$TMPDIR/t9"; mkdir -p "$P"
|
|
168
|
+
run_hook sdd-cache-post.sh "$P" '{"tool_input":{},"tool_response":{"result":"body"}}'
|
|
169
|
+
assert_eq "exit 0 without url" "0" "$RC"
|
|
170
|
+
assert_eq "no cache dir created" "0" "$([ -d "$P/.claude/sdd-cache" ] && echo 1 || echo 0)"
|
|
171
|
+
|
|
172
|
+
# ── Test 10: post — unknown tool_response shape writes nothing ────────────
|
|
173
|
+
printf '\nTest 10: post — unextractable tool_response\n'
|
|
174
|
+
P="$TMPDIR/t10"; mkdir -p "$P"
|
|
175
|
+
run_hook sdd-cache-post.sh "$P" \
|
|
176
|
+
"$(jq -n --arg url "$URL" '{tool_input:{url:$url}, tool_response:42}')"
|
|
177
|
+
assert_eq "exit 0 on unknown shape" "0" "$RC"
|
|
178
|
+
assert_eq "no cache dir created" "0" "$([ -d "$P/.claude/sdd-cache" ] && echo 1 || echo 0)"
|
|
179
|
+
|
|
180
|
+
# ── Test 11: post — caches when the origin provides an ETag ───────────────
|
|
181
|
+
printf '\nTest 11: post — entry written with origin validators\n'
|
|
182
|
+
P="$TMPDIR/t11"; mkdir -p "$P"
|
|
183
|
+
CURL_STUB_HEADERS='HTTP/2 200
|
|
184
|
+
etag: "abc123"
|
|
185
|
+
last-modified: Wed, 01 Jan 2026 00:00:00 GMT' \
|
|
186
|
+
run_hook sdd-cache-post.sh "$P" "$(post_input "$URL" "fetched body")"
|
|
187
|
+
assert_eq "exit 0" "0" "$RC"
|
|
188
|
+
CF="$P/.claude/sdd-cache/$(hash_key "$URL").json"
|
|
189
|
+
assert_eq "cache file exists" "1" "$([ -f "$CF" ] && echo 1 || echo 0)"
|
|
190
|
+
assert_eq "url stored" "$URL" "$(jq -r '.url' "$CF")"
|
|
191
|
+
assert_eq "etag stored" '"abc123"' "$(jq -r '.etag' "$CF")"
|
|
192
|
+
assert_eq "content stored" "fetched body" "$(jq -r '.content' "$CF")"
|
|
193
|
+
assert_eq "prompt stored" "test prompt" "$(jq -r '.prompt' "$CF")"
|
|
194
|
+
assert_eq "fetched_at is numeric" "number" "$(jq -r '.fetched_at | type' "$CF")"
|
|
195
|
+
|
|
196
|
+
# ── Test 12: post — string-shaped tool_response is accepted ───────────────
|
|
197
|
+
printf '\nTest 12: post — string tool_response\n'
|
|
198
|
+
P="$TMPDIR/t12"; mkdir -p "$P"
|
|
199
|
+
CURL_STUB_HEADERS='HTTP/2 200
|
|
200
|
+
etag: "str-etag"' \
|
|
201
|
+
run_hook sdd-cache-post.sh "$P" \
|
|
202
|
+
"$(jq -n --arg url "$URL" '{tool_input:{url:$url}, tool_response:"plain string body"}')"
|
|
203
|
+
CF="$P/.claude/sdd-cache/$(hash_key "$URL").json"
|
|
204
|
+
assert_eq "content from string response" "plain string body" "$(jq -r '.content' "$CF" 2>/dev/null)"
|
|
205
|
+
|
|
206
|
+
# ── Test 13: post — origin without validators removes stale entry ─────────
|
|
207
|
+
printf '\nTest 13: post — no validators from origin\n'
|
|
208
|
+
P="$TMPDIR/t13"; mkdir -p "$P"
|
|
209
|
+
seed_entry "$P" "$URL" "stale body"
|
|
210
|
+
CF="$P/.claude/sdd-cache/$(hash_key "$URL").json"
|
|
211
|
+
CURL_STUB_HEADERS='HTTP/2 200
|
|
212
|
+
content-type: text/html' \
|
|
213
|
+
run_hook sdd-cache-post.sh "$P" "$(post_input "$URL" "new body")"
|
|
214
|
+
assert_eq "exit 0" "0" "$RC"
|
|
215
|
+
assert_eq "stale entry removed, nothing cached" "0" "$([ -f "$CF" ] && echo 1 || echo 0)"
|
|
216
|
+
|
|
217
|
+
# ── Test 14: post — validators taken from final response block ────────────
|
|
218
|
+
printf '\nTest 14: post — multi-block HEAD output uses last block\n'
|
|
219
|
+
P="$TMPDIR/t14"; mkdir -p "$P"
|
|
220
|
+
CURL_STUB_HEADERS='HTTP/1.1 301 Moved Permanently
|
|
221
|
+
location: https://docs.example.com/new
|
|
222
|
+
etag: "redirect-etag"
|
|
223
|
+
|
|
224
|
+
HTTP/1.1 200 OK
|
|
225
|
+
etag: "final-etag"' \
|
|
226
|
+
run_hook sdd-cache-post.sh "$P" "$(post_input "$URL" "body")"
|
|
227
|
+
CF="$P/.claude/sdd-cache/$(hash_key "$URL").json"
|
|
228
|
+
assert_eq "etag from final block" '"final-etag"' "$(jq -r '.etag' "$CF" 2>/dev/null)"
|
|
229
|
+
|
|
230
|
+
# ── Test 15: round-trip — post writes, pre serves the same bytes ──────────
|
|
231
|
+
printf '\nTest 15: round-trip post → pre\n'
|
|
232
|
+
P="$TMPDIR/t15"; mkdir -p "$P"
|
|
233
|
+
BODY='Round-trip body with `ticks`, $vars and
|
|
234
|
+
a second line \ with a backslash'
|
|
235
|
+
CURL_STUB_HEADERS='HTTP/2 200
|
|
236
|
+
etag: "rt-etag"' \
|
|
237
|
+
run_hook sdd-cache-post.sh "$P" "$(post_input "$URL" "$BODY")"
|
|
238
|
+
CURL_STUB_STATUS=304 run_hook sdd-cache-pre.sh "$P" "$(pre_input "$URL")"
|
|
239
|
+
assert_eq "pre serves the entry post wrote (exit 2)" "2" "$RC"
|
|
240
|
+
served=$(printf '%s\n' "$ERR" | sed -n '/BEGIN CACHED CONTENT/,/END CACHED CONTENT/p' | sed '1d;$d')
|
|
241
|
+
assert_eq "round-trip content byte-exact" "$BODY" "$served"
|
|
242
|
+
|
|
243
|
+
# ── Summary ──────────────────────────────────────────────────────────────
|
|
244
|
+
printf '\n══════════════════════════════════════════\n'
|
|
245
|
+
printf 'Results: %d passed, %d failed\n' "$PASS" "$FAIL"
|
|
246
|
+
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# session-start-test.sh - Tests for the SessionStart hook JSON payload
|
|
3
|
+
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
tmp_payload="$(mktemp)"
|
|
7
|
+
trap 'rm -f "$tmp_payload"' EXIT
|
|
8
|
+
|
|
9
|
+
has_jq=0
|
|
10
|
+
if command -v jq >/dev/null 2>&1; then
|
|
11
|
+
has_jq=1
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
payload="$(bash hooks/session-start.sh)"
|
|
15
|
+
printf '%s' "$payload" > "$tmp_payload"
|
|
16
|
+
|
|
17
|
+
HAS_JQ="$has_jq" PAYLOAD_PATH="$tmp_payload" node <<'NODE'
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
|
|
20
|
+
const payload = JSON.parse(fs.readFileSync(process.env.PAYLOAD_PATH, 'utf8'));
|
|
21
|
+
const hasJq = process.env.HAS_JQ === '1';
|
|
22
|
+
const out = payload.hookSpecificOutput;
|
|
23
|
+
|
|
24
|
+
if (!out || typeof out !== 'object') {
|
|
25
|
+
throw new Error('payload is missing hookSpecificOutput (hosts reject other shapes)');
|
|
26
|
+
}
|
|
27
|
+
if (out.hookEventName !== 'SessionStart') {
|
|
28
|
+
throw new Error(`expected hookEventName SessionStart, got ${out.hookEventName}`);
|
|
29
|
+
}
|
|
30
|
+
if (typeof out.additionalContext !== 'string' || !out.additionalContext.trim()) {
|
|
31
|
+
throw new Error('additionalContext must be a non-empty string');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const ctx = out.additionalContext;
|
|
35
|
+
if (hasJq) {
|
|
36
|
+
if (!ctx.includes('i3-composer-agent loaded.')) {
|
|
37
|
+
throw new Error('additionalContext is missing startup preface');
|
|
38
|
+
}
|
|
39
|
+
if (!ctx.includes('# Using i3 Composer Agent')) {
|
|
40
|
+
throw new Error('additionalContext is missing using-agent-skills content');
|
|
41
|
+
}
|
|
42
|
+
} else if (!ctx.includes('jq is required')) {
|
|
43
|
+
throw new Error('additionalContext is missing jq fallback guidance');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.log('session-start JSON payload OK');
|
|
47
|
+
NODE
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# i3-composer-agent session start hook
|
|
3
|
+
# Injects the using-agent-skills meta-skill into a new session.
|
|
4
|
+
#
|
|
5
|
+
# Not wired by the plugin: hosts that already route skills from their
|
|
6
|
+
# descriptions (Claude Code, Codex CLI) would run a second router on top of
|
|
7
|
+
# the native one — see docs/getting-started.md. Wire this script into a
|
|
8
|
+
# SessionStart hook only on hosts without native skill routing.
|
|
9
|
+
#
|
|
10
|
+
# Every output path must emit the standard SessionStart envelope
|
|
11
|
+
# {"hookSpecificOutput": {"hookEventName": "SessionStart", "additionalContext": "..."}}
|
|
12
|
+
# Hosts that validate hook output (Codex CLI, Claude Code) reject other shapes.
|
|
13
|
+
|
|
14
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
15
|
+
SKILLS_DIR="$(dirname "$SCRIPT_DIR")/skills"
|
|
16
|
+
META_SKILL="$SKILLS_DIR/using-agent-skills/SKILL.md"
|
|
17
|
+
|
|
18
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
19
|
+
echo '{"hookSpecificOutput": {"hookEventName": "SessionStart", "additionalContext": "i3-composer-agent: jq is required for the session-start hook but was not found on PATH. Install jq (e.g. `brew install jq` or `apt-get install jq`) to enable meta-skill injection. Skills remain available individually."}}'
|
|
20
|
+
exit 0
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
if [ -f "$META_SKILL" ]; then
|
|
24
|
+
CONTENT=$(cat "$META_SKILL")
|
|
25
|
+
# Use jq to properly escape and construct valid JSON
|
|
26
|
+
jq -cn \
|
|
27
|
+
--arg context "i3-composer-agent loaded. Use the skill discovery flowchart to find the right skill for your task.
|
|
28
|
+
|
|
29
|
+
$CONTENT" \
|
|
30
|
+
'{hookSpecificOutput: {hookEventName: "SessionStart", additionalContext: $context}}'
|
|
31
|
+
else
|
|
32
|
+
echo '{"hookSpecificOutput": {"hookEventName": "SessionStart", "additionalContext": "i3-composer-agent: using-agent-skills meta-skill not found. Skills may still be available individually."}}'
|
|
33
|
+
fi
|