Compare commits
5 Commits
2bf0365aa5
...
89fc44bccc
| Author | SHA1 | Date | |
|---|---|---|---|
| 89fc44bccc | |||
| dd1959b12b | |||
| 32cd2e3128 | |||
| 93e3de4d02 | |||
| e50c98f722 |
@@ -8,6 +8,11 @@
|
||||
"name": "kyberforge",
|
||||
"description": "Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.",
|
||||
"source": "./plugins/kyberforge"
|
||||
},
|
||||
{
|
||||
"name": "bin",
|
||||
"description": "A place for things to be binned",
|
||||
"source": "./plugins/bin"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
5
.github/plugin/marketplace.json
vendored
5
.github/plugin/marketplace.json
vendored
@@ -8,6 +8,11 @@
|
||||
"name": "kyberforge",
|
||||
"description": "Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.",
|
||||
"source": "./plugins/kyberforge"
|
||||
},
|
||||
{
|
||||
"name": "bin",
|
||||
"description": "A place for things to be binned",
|
||||
"source": "./plugins/bin"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -4,9 +4,6 @@ This repo is the global AI development configuration repository — the authorit
|
||||
|
||||
## Structure
|
||||
|
||||
- `core/` — provider-agnostic source of truth (plain language, no tool-specific references)
|
||||
- `.agents/skills/` — directly-deployed skills (Agent Skills standard); deployed to `~/.agents/skills/` via `install.sh`; marketplace and factory skills live in `plugins/kyberforge/` instead
|
||||
- `.agents/evals/` — eval.yaml files for skills not bundled into a plugin
|
||||
- `.claude-plugin/` — marketplace manifest (`marketplace.json`); read by both Claude Code and Copilot CLI
|
||||
- `plugins/` — installable plugin units; each is self-contained (skills, agents, hooks, MCP servers, bundled assets); install separately via `claude plugin install <name>@holocron`
|
||||
- `providers/claude-code/` — Claude Code adapter (deployed to `~/.claude/` via `install.sh`)
|
||||
@@ -33,7 +30,6 @@ Read these on demand:
|
||||
|
||||
## Key rules
|
||||
|
||||
- `core/` content must use plain imperative language — no tool names, provider APIs, or format assumptions
|
||||
- Never edit files deployed by `sync.sh` directly in a project; put customizations in override files
|
||||
- `providers/claude-code/CLAUDE.md` is the deployed global config — edit it there, not here
|
||||
- Governance constraints from `core/instructions/governance.md` apply when building content in this repo — hard prohibitions on secrets and data, HITL requirements before irreversible actions, sycophancy resistance, and deterministic execution preference are always in effect
|
||||
|
||||
35
CONTEXT.md
35
CONTEXT.md
@@ -7,9 +7,6 @@ description: Domain language and decisions for the global AI development config
|
||||
|
||||
## Principles
|
||||
|
||||
### Provider-agnostic core
|
||||
`core/` content uses plain imperative language — no tool names, provider APIs, or format assumptions. Anything referencing a specific tool belongs in `providers/`, not `core/`. Providers translate core content into the tool's expected format and language.
|
||||
|
||||
### CLAUDE.md index model
|
||||
`AGENTS.md` is the source of always-on universal rules (provider-agnostic). `providers/claude-code/CLAUDE.md` is a thin adapter: it imports `~/.agents/AGENTS.md` via `@~/.agents/AGENTS.md` and appends Claude Code-specific additions (`@import` for governance.md, content index). Deployed to `~/.claude/CLAUDE.md` via `install.sh`. Context size is kept minimal — only what is needed every session is loaded upfront; detailed content is pulled on demand. See ADR-0012.
|
||||
|
||||
@@ -21,8 +18,6 @@ Workflow artifacts are committed to `docs/` in subdirectories by type. All are t
|
||||
|
||||
**Naming:**
|
||||
- `docs/prd/<slug>.md` — Product Requirements Documents
|
||||
- `docs/ard/<slug>.md` — Architecture Requirements Documents
|
||||
- `docs/bug/<slug>.md` — Bug Briefs
|
||||
- `docs/notes/<slug>.md` — Exploration Notes
|
||||
- `docs/adr/NNNN-<slug>.md` — Architecture Decision Records
|
||||
- `docs/issues/NNNN-<slug>.md` — Issues
|
||||
@@ -35,7 +30,7 @@ Workflow artifacts are committed to `docs/` in subdirectories by type. All are t
|
||||
|
||||
**Slug** — kebab-case, lowercase, max 4–5 words, derived from the document title. No dates (git history carries dates). Examples: `chunk-2-instructions`, `user-auth-flow`, `database-migration`.
|
||||
|
||||
**When each is written:** PRDs, ARDs, Bug Briefs, and Notes are pre-work — produced by a grill session before issues are created. ADRs are post-decision — written during or after implementation of an ARD when a hard-to-reverse choice is made. An improvement kick-off produces either a PRD (user-facing scope) or ARD (architectural scope).
|
||||
**When each is written:** PRDs, ARDs — produced by a grill session before issues are created. ADRs are post-decision — written during or after implementation of an ARD when a hard-to-reverse choice is made. An improvement kick-off produces either a PRD (user-facing scope) or ARD (architectural scope).
|
||||
|
||||
### Content chunk QA
|
||||
Instruction files and other content chunks cannot be unit tested. Verification is human-executed after implementation: open a new Claude session, exercise the relevant behaviour, and confirm the rules take effect. Each issue includes a short acceptance criteria checklist for the human to run post-commit. Automated QA applies to tooling (scripts, hooks); manual QA applies to agent behaviour and content correctness.
|
||||
@@ -45,14 +40,8 @@ Instruction files and other content chunks cannot be unit tested. Verification i
|
||||
### Conventional commits
|
||||
All commits in this repo follow the Conventional Commits specification (`feat:`, `fix:`, `docs:`, `chore:`, `refactor:`, `test:`). Convention is defined in `core/instructions/git.md`. Changelog tooling is a follow-on issue — convention is established first.
|
||||
|
||||
### Project override model
|
||||
Projects override on-demand content (workflows, agent roles, prompts) by placing their own versions in `.claude/`. Universal rules are additive — projects extend them, not replace them. A rule that needs per-project suppression is not truly universal.
|
||||
|
||||
### Sync model
|
||||
Projects must never edit synced files directly — customizations live in separate override files. A sync conflict is a signal that a synced file was edited directly.
|
||||
|
||||
### Repo as source of truth
|
||||
All project state, decisions, context, and working conventions live in this repo. External memory systems should not be used for this project — they create a split-brain risk where cached state diverges from the repo. At the start of every session, read `CLAUDE.md`, `CONTEXT.md`, `docs/VISION.md`, and `docs/spec/overview.md`. Everything needed to orient is here.
|
||||
### Repo/gitea as source of truth
|
||||
All project state, decisions, context, and working conventions live in this repo ro Gitea. External memory systems should not be used for this project — they create a split-brain risk where cached state diverges from the repo. At the start of every session, read `CLAUDE.md`, `CONTEXT.md`, `docs/VISION.md`, and `docs/spec/overview.md`. Everything needed to orient is here.
|
||||
|
||||
Before answering any design or architecture question, check for existing decisions: `docs/adr/` (hard architectural decisions) and the resolved rows (marked ✅) in the `docs/ROADMAP.md` open questions table. Never propose an approach without verifying no decision already covers it.
|
||||
|
||||
@@ -100,17 +89,14 @@ Contains always-on rules in plain markdown with no provider-specific syntax (no
|
||||
### Skill composition
|
||||
A skill calling another skill by name to delegate a sub-task. The calling skill focuses on the orchestration decision ("when to do X"); the called skill owns the mechanics ("how to do X"). Established compositions: `grill-me` calls `write-adr` when a decision crystallises; `implement-feature` calls `tdd` as its implementation methodology. Composition chains are formalised as workflows in Chunk 4.
|
||||
|
||||
### Source field
|
||||
Field (`source:`) in a skill's `META.md` tracking upstream provenance. An array — supports multiple upstream sources per skill. Each entry: `repo` (GitHub slug, e.g. `mattpocock/skills` — no URL, slug is stable and searchable), `commit` (exact SHA reviewed at adoption), `files` (list of files adopted with inline comments on what was taken), `updated` (date of last upstream review for this entry). Absence of `source:` means self-authored original. Upstream review cadence: per-skill during Chunk 3 (run during source review step); quarterly after roadmap completion (post Chunk 7). Companion field: `references:` (array of URLs or citations) for general external citations — distinct from `source:` which tracks adoptions with commit-level traceability. Both fields live in `META.md`, not in SKILL.md frontmatter.
|
||||
<!-- ### Source field
|
||||
Field (`source:`) in a skill's `META.md` tracking upstream provenance. An array — supports multiple upstream sources per skill. Each entry: `repo` (GitHub slug, e.g. `mattpocock/skills` — no URL, slug is stable and searchable), `commit` (exact SHA reviewed at adoption), `files` (list of files adopted with inline comments on what was taken), `updated` (date of last upstream review for this entry). Absence of `source:` means self-authored original. Upstream review cadence: per-skill during Chunk 3 (run during source review step); quarterly after roadmap completion (post Chunk 7). Companion field: `references:` (array of URLs or citations) for general external citations — distinct from `source:` which tracks adoptions with commit-level traceability. Both fields live in `META.md`, not in SKILL.md frontmatter. -->
|
||||
|
||||
### META.md
|
||||
A per-skill markdown file containing a single YAML code block with provenance and audit fields: `version`, `updated`, `when`, `source`, and `references`. Lives alongside the SKILL.md in the skill directory (either `.agents/skills/<name>/META.md` or `plugins/<plugin>/skills/<name>/META.md`). Not loaded at agent startup — progressive disclosure principle: name and description route the skill; provenance is only needed for upgrade reviews and audits. Prevents these fields from being scanned on every session start alongside every skill's name and description. The authoritative schema is `META-TEMPLATE.md` in `plugins/kyberforge/skills/write-skill/`. See also: [[Source field]].
|
||||
<!-- ### META.md
|
||||
A per-skill markdown file containing a single YAML code block with provenance and audit fields: `version`, `updated`, `when`, `source`, and `references`. Lives alongside the SKILL.md in the skill directory (either `.agents/skills/<name>/META.md` or `plugins/<plugin>/skills/<name>/META.md`). Not loaded at agent startup — progressive disclosure principle: name and description route the skill; provenance is only needed for upgrade reviews and audits. Prevents these fields from being scanned on every session start alongside every skill's name and description. The authoritative schema is `META-TEMPLATE.md` in `plugins/kyberforge/skills/write-skill/`. See also: [[Source field]]. -->
|
||||
|
||||
### Provider-agnostic issue tracker
|
||||
Skills and workflows reference "linked issue" generically rather than a specific provider. In the file-based phase, an issue is a `docs/issues/NNNN-<slug>.md` file. When Gitea MCP is configured, the same skills use it instead. The active backend is determined at runtime by MCP availability. "Issue" is the canonical cross-provider term (GitHub, GitLab, Gitea all use it). Gitea-specific skills are a provider adapter (`providers/gitea/`), not part of the core library. See ADR-0011.
|
||||
|
||||
### Design phase sequence
|
||||
The canonical pre-implementation sequence within any workstream: `grill-lean` (optional lightweight interrogation, no docs) → `grill-me` (primary: deep interrogation + domain alignment + ADR writing) → `write-prd` (why + what only, never how) → `architecture-review` (optional: technical approach evaluation, ≥2 options) → `break-into-issues` (independently shippable slices; proposes Gitea milestone groupings for PRDs producing >5 issues).
|
||||
Skills and workflows reference "linked issue" generically rather than a specific provider. In the file-based phase, an issue is a `docs/issues/NNNN-<slug>.md` file. When Gitea MCP is configured, the same skills use it instead. The active backend is determined at runtime by MCP availability. "Issue" is the canonical cross-provider term (GitHub, GitLab, Gitea all use it).
|
||||
|
||||
### PRD scope
|
||||
A PRD contains: problem statement, goals, explicit non-goals, functional requirements at feature level, success criteria. Never contains: technical approach, implementation steps, or EARS-level detail. HOW is handled downstream: workstream-level technical approach belongs in `architecture-review` (≥2 options, tradeoffs, optional step after `write-prd`); issue-level HOW belongs in issue design notes. Prerequisite: a completed grill session. Validated by inline self-checks in the `write-prd` skill.
|
||||
@@ -125,13 +111,10 @@ Files that reference other files should declare those references explicitly. The
|
||||
A focused work session oriented around a single goal — a feature, bug, improvement, or exploration. Starts with a grill to produce an artifact (PRD, Bug Brief, ADR, etc.), runs through issue implementation, and closes with docs + commit. Ongoing skills (/diagnose, /prototype, /zoom-out) are invoked ad hoc within a workstream as needed.
|
||||
|
||||
### Workflow artifacts
|
||||
Output documents produced by a grill session that scope the work before implementation. All are committed to the repo under `docs/` following the docs convention. Each artifact generates one or more issues in `docs/issues/` but is not itself an issue.
|
||||
Output documents produced by a grill session that scope the work before implementation. All are committed to the repo under `docs/` following the docs convention. Each artifact generates one or more issues but is not itself an issue.
|
||||
|
||||
Pre-work (grill output):
|
||||
- **PRD** (Product Requirements Document) — for features and improvements with user-facing scope
|
||||
- **ARD** (Architecture Requirements Document) — for architectural changes; defines what needs to change and why, analogous to a PRD but for architecture. Produced before implementation; not the same as an ADR.
|
||||
- **Bug Brief** — for bugs; feeds into /diagnose
|
||||
- **Exploration Note** — for ideation; may or may not produce issues
|
||||
|
||||
Post-decision:
|
||||
- **ADR** (Architecture Decision Record) — records the decision made, alternatives considered, and rationale. Written during or after implementation of an ARD, not before. Hard-to-reverse decisions only.
|
||||
|
||||
9
plugins/bin/.claude-plugin/plugin.json
Normal file
9
plugins/bin/.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "bin",
|
||||
"displayName": "bin",
|
||||
"version": "1.0.0",
|
||||
"description": "A place for things to be binned",
|
||||
"author": { "name": "Defame1297", "url": "https://git.dev.rkdr.net/Defame1297/" },
|
||||
"license": "MIT",
|
||||
"keywords": []
|
||||
}
|
||||
40
plugins/bin/README.md
Normal file
40
plugins/bin/README.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# bin
|
||||
|
||||
A place for things to be binned
|
||||
|
||||
## Install
|
||||
|
||||
**Claude Code:**
|
||||
|
||||
```bash
|
||||
claude plugin marketplace add <owner>/<repo>
|
||||
claude plugin install bin@<marketplace-name>
|
||||
```
|
||||
|
||||
**GitHub Copilot CLI:**
|
||||
|
||||
```bash
|
||||
copilot plugin marketplace add <owner>/<repo>
|
||||
copilot plugin install bin
|
||||
```
|
||||
|
||||
**Local (development):**
|
||||
|
||||
```bash
|
||||
# Claude Code
|
||||
claude --plugin-dir ./plugins/bin
|
||||
|
||||
# GitHub Copilot CLI
|
||||
copilot plugin install ./plugins/bin
|
||||
```
|
||||
|
||||
## Contents
|
||||
|
||||
| Component | Path | Description |
|
||||
| -------------| ------------------------------------------------------| ---------------------------------------------------------------|
|
||||
| Skills | `skills/` | Slash commands available after install |
|
||||
| Agents | `agents/` | Role-based agents (`.md` for Claude, `.agent.md` for Copilot) |
|
||||
|
||||
## Author
|
||||
|
||||
Defame1297
|
||||
9
plugins/bin/plugin.json
Normal file
9
plugins/bin/plugin.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "bin",
|
||||
"description": "A place for things to be binned",
|
||||
"author": { "name": "Defame1297", "email": "defame1297@rkdr.net" },
|
||||
"license": "MIT",
|
||||
"keywords": [],
|
||||
"agents": "agents/",
|
||||
"skills": ["skills/"]
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Claude Code provider manifest — sourced by install.sh after deploy-manifest.sh.
|
||||
# Appends to SKILL_ADAPTERS, which must be initialised before sourcing this file.
|
||||
|
||||
# Claude Code reads skills from ~/.claude/skills/, not ~/.agents/skills/ natively.
|
||||
SKILL_ADAPTERS+=(".claude/skills")
|
||||
@@ -8,6 +8,7 @@ source "$REPO_ROOT/scripts/deploy-manifest.sh"
|
||||
# Collect provider skill adapters from all provider-manifest.sh files
|
||||
SKILL_ADAPTERS=()
|
||||
for provider_manifest in "$REPO_ROOT"/providers/*/provider-manifest.sh; do
|
||||
[[ -f "$provider_manifest" ]] || continue
|
||||
# shellcheck source=/dev/null
|
||||
source "$provider_manifest"
|
||||
done
|
||||
@@ -35,13 +36,19 @@ for entry in "${DEPLOY_DIRS[@]}"; do
|
||||
done
|
||||
|
||||
# Deploy skills to canonical location — merge per-skill, never wipe the parent
|
||||
# Skipped when source does not exist (marketplace architecture: skills installed via plugin)
|
||||
skills_dest="$HOME/$DEPLOY_SKILLS_SRC"
|
||||
mkdir -p "$skills_dest"
|
||||
for skill_dir in "$REPO_ROOT/$DEPLOY_SKILLS_SRC"/*/; do
|
||||
skills_deployed=()
|
||||
if [[ -d "$REPO_ROOT/$DEPLOY_SKILLS_SRC" ]]; then
|
||||
mkdir -p "$skills_dest"
|
||||
for skill_dir in "$REPO_ROOT/$DEPLOY_SKILLS_SRC"/*/; do
|
||||
[[ -d "$skill_dir" ]] || continue
|
||||
skill_name="$(basename "$skill_dir")"
|
||||
rm -rf "${skills_dest:?}/${skill_name:?}"
|
||||
cp -r "$skill_dir" "$skills_dest/$skill_name"
|
||||
done
|
||||
skills_deployed+=("$skill_name")
|
||||
done
|
||||
fi
|
||||
|
||||
# Create provider skill adapters as symlinks to the canonical skills location
|
||||
for adapter in "${SKILL_ADAPTERS[@]}"; do
|
||||
@@ -63,7 +70,9 @@ done
|
||||
for entry in "${DEPLOY_DIRS[@]}"; do
|
||||
echo " ~/${entry##*:}/"
|
||||
done
|
||||
echo " ~/$DEPLOY_SKILLS_SRC/ (skills)"
|
||||
if [[ ${#skills_deployed[@]} -gt 0 ]]; then
|
||||
echo " ~/$DEPLOY_SKILLS_SRC/ (skills: ${skills_deployed[*]})"
|
||||
fi
|
||||
for adapter in "${SKILL_ADAPTERS[@]}"; do
|
||||
echo " ~/$adapter -> ~/$DEPLOY_SKILLS_SRC (symlink)"
|
||||
done
|
||||
|
||||
@@ -197,11 +197,7 @@ pre_push_block() {
|
||||
HOOKS_SCRIPT_DIR="$script_dir"
|
||||
REPO_ROOT="\$(git rev-parse --show-toplevel)"
|
||||
|
||||
echo "Running test suite..."
|
||||
bash "\$REPO_ROOT/tests/test-install.sh"
|
||||
bash "\$REPO_ROOT/tests/test-governance-layer.sh"
|
||||
bash "\$REPO_ROOT/tests/test-check-manifests.sh"
|
||||
bash "\$REPO_ROOT/tests/test-setup-hooks.sh"
|
||||
bash "\$REPO_ROOT/tests/run-tests.sh"
|
||||
|
||||
echo "Checking manifests..."
|
||||
bash "\$HOOKS_SCRIPT_DIR/check-manifests.sh" "\$REPO_ROOT"
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"skills": {}
|
||||
}
|
||||
57
tests/run-tests.sh
Executable file
57
tests/run-tests.sh
Executable file
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run all test-*.sh files in the repo (including plugins) and the bats suite.
|
||||
# Usage: bash tests/run-tests.sh [--bats-only]
|
||||
#
|
||||
# TEST_DIR — override root to search for test-*.sh (default: REPO_ROOT); used by tests.
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
BATS="$REPO_ROOT/tests/run-bats.sh"
|
||||
BATS_ONLY=false
|
||||
[[ "${1:-}" == "--bats-only" ]] && BATS_ONLY=true
|
||||
|
||||
SEARCH_ROOT="${TEST_DIR:-$REPO_ROOT}"
|
||||
|
||||
FAILED=()
|
||||
PASSED=0
|
||||
|
||||
run_bats() {
|
||||
if [[ -x "$BATS" ]]; then
|
||||
echo "=== bats ==="
|
||||
bash "$BATS"
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
if $BATS_ONLY; then
|
||||
run_bats
|
||||
exit 0
|
||||
fi
|
||||
|
||||
run_bats
|
||||
|
||||
mapfile -t SCRIPTS < <(
|
||||
find "$SEARCH_ROOT" -name "test-*.sh" \
|
||||
-not -path "*/.git/*" \
|
||||
| sort
|
||||
)
|
||||
|
||||
for script in "${SCRIPTS[@]}"; do
|
||||
rel="${script#"$SEARCH_ROOT/"}"
|
||||
echo "=== $rel ==="
|
||||
if bash "$script"; then
|
||||
PASSED=$((PASSED + 1))
|
||||
else
|
||||
FAILED+=("$rel")
|
||||
fi
|
||||
echo ""
|
||||
done
|
||||
|
||||
echo "=== Summary: $PASSED passed, ${#FAILED[@]} failed ==="
|
||||
if [[ ${#FAILED[@]} -gt 0 ]]; then
|
||||
echo "Failed scripts:"
|
||||
for s in "${FAILED[@]}"; do
|
||||
echo " $s"
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
@@ -18,7 +18,6 @@ echo ""
|
||||
echo "--- providers/claude-code/ → ~/.claude/ ---"
|
||||
while IFS= read -r -d '' src; do
|
||||
rel="${src#"$REPO_ROOT/providers/claude-code/"}"
|
||||
# provider-manifest.sh is sourced by install.sh, not deployed to ~/.claude/
|
||||
[[ "$rel" == "provider-manifest.sh" ]] && continue
|
||||
dest="$TEMP_HOME/.claude/$rel"
|
||||
if diff -q "$src" "$dest" > /dev/null 2>&1; then
|
||||
@@ -57,12 +56,13 @@ echo "--- core/AGENTS.md → ~/.agents/AGENTS.md (0015) ---"
|
||||
if diff -q "$REPO_ROOT/core/AGENTS.md" "$TEMP_HOME/.agents/AGENTS.md" > /dev/null 2>&1; then
|
||||
pass "core/AGENTS.md deployed and matches source"
|
||||
else
|
||||
fail "~/.agents/AGENTS.md — missing or differs from source"
|
||||
fail "\$HOME/.agents/AGENTS.md — missing or differs from source"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "--- skills deployed to ~/.agents/skills/ ---"
|
||||
while IFS= read -r -d '' src_skill; do
|
||||
if [[ -d "$REPO_ROOT/.agents/skills" ]]; then
|
||||
echo ""
|
||||
echo "--- skills deployed to ~/.agents/skills/ ---"
|
||||
while IFS= read -r -d '' src_skill; do
|
||||
skill_name="$(basename "$src_skill")"
|
||||
dest_skill="$TEMP_HOME/.agents/skills/$skill_name"
|
||||
if [[ -d "$dest_skill" ]]; then
|
||||
@@ -70,11 +70,11 @@ while IFS= read -r -d '' src_skill; do
|
||||
else
|
||||
fail "$skill_name missing from ~/.agents/skills/"
|
||||
fi
|
||||
done < <(find "$REPO_ROOT/.agents/skills" -mindepth 1 -maxdepth 1 -type d -print0)
|
||||
done < <(find "$REPO_ROOT/.agents/skills" -mindepth 1 -maxdepth 1 -type d -print0)
|
||||
|
||||
echo ""
|
||||
echo "--- skill files match source ---"
|
||||
while IFS= read -r -d '' src; do
|
||||
echo ""
|
||||
echo "--- skill files match source ---"
|
||||
while IFS= read -r -d '' src; do
|
||||
rel="${src#"$REPO_ROOT/.agents/skills/"}"
|
||||
dest="$TEMP_HOME/.agents/skills/$rel"
|
||||
if diff -q "$src" "$dest" > /dev/null 2>&1; then
|
||||
@@ -82,31 +82,20 @@ while IFS= read -r -d '' src; do
|
||||
else
|
||||
fail "skills/$rel — missing or differs from source"
|
||||
fi
|
||||
done < <(find "$REPO_ROOT/.agents/skills" -type f -print0)
|
||||
done < <(find "$REPO_ROOT/.agents/skills" -type f -print0)
|
||||
|
||||
echo ""
|
||||
echo "--- provider adapter: ~/.claude/skills/ is a symlink to ~/.agents/skills/ ---"
|
||||
adapter="$TEMP_HOME/.claude/skills"
|
||||
skills_canonical="$TEMP_HOME/.agents/skills"
|
||||
if [[ -L "$adapter" ]]; then
|
||||
resolved="$(readlink "$adapter")"
|
||||
if [[ "$resolved" == "$skills_canonical" ]]; then
|
||||
pass "~/.claude/skills → ~/.agents/skills (correct target)"
|
||||
else
|
||||
fail "~/.claude/skills symlink points to wrong target: $resolved"
|
||||
fi
|
||||
else
|
||||
fail "~/.claude/skills is not a symlink"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "--- skills correctly replaced on second install (no double-nesting) ---"
|
||||
skill_name="$(basename "$(find "$REPO_ROOT/.agents/skills" -mindepth 1 -maxdepth 1 -type d | head -1)")"
|
||||
nested="$TEMP_HOME/.agents/skills/$skill_name/$skill_name"
|
||||
if [[ -d "$nested" ]]; then
|
||||
echo ""
|
||||
echo "--- skills correctly replaced on second install (no double-nesting) ---"
|
||||
first_skill="$(find "$REPO_ROOT/.agents/skills" -mindepth 1 -maxdepth 1 -type d | head -1)"
|
||||
if [[ -n "$first_skill" ]]; then
|
||||
skill_name="$(basename "$first_skill")"
|
||||
nested="$TEMP_HOME/.agents/skills/$skill_name/$skill_name"
|
||||
if [[ -d "$nested" ]]; then
|
||||
fail "$skill_name/$skill_name exists — skill was nested instead of replaced"
|
||||
else
|
||||
else
|
||||
pass "$skill_name not double-nested after second install"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
@@ -123,31 +112,17 @@ else
|
||||
fail "pre-existing user skill was wiped by install"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "--- warning emitted when adapter target is a real directory ---"
|
||||
TEMP_HOME3="$(mktemp -d)"
|
||||
trap 'rm -rf "$TEMP_HOME3"' EXIT
|
||||
mkdir -p "$TEMP_HOME3/.claude/skills/some-user-skill"
|
||||
output="$(HOME="$TEMP_HOME3" bash "$REPO_ROOT/scripts/install.sh" 2>&1)"
|
||||
if echo "$output" | grep -q "Warning"; then
|
||||
pass "warning emitted when ~/.claude/skills exists as real directory"
|
||||
else
|
||||
fail "no warning when ~/.claude/skills is a real directory"
|
||||
fi
|
||||
if [[ -d "$TEMP_HOME3/.claude/skills/some-user-skill" ]]; then
|
||||
pass "real directory left intact when warning emitted"
|
||||
else
|
||||
fail "real directory was destroyed despite warning"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "--- idempotency: second run state is correct ---"
|
||||
HOME="$TEMP_HOME" bash "$REPO_ROOT/scripts/install.sh" > /dev/null 2>&1 \
|
||||
&& pass "second run exits zero" \
|
||||
|| fail "second run failed"
|
||||
if HOME="$TEMP_HOME" bash "$REPO_ROOT/scripts/install.sh" > /dev/null 2>&1; then
|
||||
pass "second run exits zero"
|
||||
else
|
||||
fail "second run failed"
|
||||
fi
|
||||
|
||||
# skill files still match source after second run
|
||||
while IFS= read -r -d '' src; do
|
||||
if [[ -d "$REPO_ROOT/.agents/skills" ]]; then
|
||||
# skill files still match source after second run
|
||||
while IFS= read -r -d '' src; do
|
||||
rel="${src#"$REPO_ROOT/.agents/skills/"}"
|
||||
dest="$TEMP_HOME/.agents/skills/$rel"
|
||||
if diff -q "$src" "$dest" > /dev/null 2>&1; then
|
||||
@@ -155,10 +130,10 @@ while IFS= read -r -d '' src; do
|
||||
else
|
||||
fail "idempotent: skills/$rel corrupted after second install"
|
||||
fi
|
||||
done < <(find "$REPO_ROOT/.agents/skills" -type f -print0)
|
||||
done < <(find "$REPO_ROOT/.agents/skills" -type f -print0)
|
||||
|
||||
# no double-nesting after second run
|
||||
while IFS= read -r -d '' skill_dir; do
|
||||
# no double-nesting after second run
|
||||
while IFS= read -r -d '' skill_dir; do
|
||||
skill_name="$(basename "$skill_dir")"
|
||||
nested="$TEMP_HOME/.agents/skills/$skill_name/$skill_name"
|
||||
if [[ -d "$nested" ]]; then
|
||||
@@ -166,14 +141,7 @@ while IFS= read -r -d '' skill_dir; do
|
||||
else
|
||||
pass "idempotent: $skill_name not double-nested after second install"
|
||||
fi
|
||||
done < <(find "$REPO_ROOT/.agents/skills" -mindepth 1 -maxdepth 1 -type d -print0)
|
||||
|
||||
# symlink still correct after second run
|
||||
adapter="$TEMP_HOME/.claude/skills"
|
||||
if [[ -L "$adapter" ]] && [[ "$(readlink "$adapter")" == "$TEMP_HOME/.agents/skills" ]]; then
|
||||
pass "idempotent: ~/.claude/skills symlink intact after second install"
|
||||
else
|
||||
fail "idempotent: ~/.claude/skills symlink broken after second install"
|
||||
done < <(find "$REPO_ROOT/.agents/skills" -mindepth 1 -maxdepth 1 -type d -print0)
|
||||
fi
|
||||
|
||||
# AGENTS.md still correct after second run
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# shellcheck disable=SC2015 # pass()/fail() always exit 0; A && pass || fail is safe here
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
@@ -35,18 +36,19 @@ CLAUDE="$REPO_ROOT/providers/claude-code/CLAUDE.md"
|
||||
&& pass "behavior: rules not duplicated in CLAUDE.md (moved to core/AGENTS.md)" \
|
||||
|| fail "behavior: rules still inline in CLAUDE.md — 0015 refactor incomplete"
|
||||
|
||||
# Content index — must reference each on-demand file
|
||||
contains "coding" "$CLAUDE" \
|
||||
&& pass "content index: coding conventions trigger present" \
|
||||
|| fail "content index: coding conventions trigger missing"
|
||||
# Content index lives in core/AGENTS.md (deployed as ~/.agents/AGENTS.md), not in CLAUDE.md
|
||||
CORE_AGENTS_FOR_0004="$REPO_ROOT/core/AGENTS.md"
|
||||
contains "coding" "$CORE_AGENTS_FOR_0004" \
|
||||
&& pass "content index: coding conventions trigger present (core/AGENTS.md)" \
|
||||
|| fail "content index: coding conventions trigger missing from core/AGENTS.md"
|
||||
|
||||
contains "git" "$CLAUDE" \
|
||||
&& pass "content index: git conventions trigger present" \
|
||||
|| fail "content index: git conventions trigger missing"
|
||||
contains "git" "$CORE_AGENTS_FOR_0004" \
|
||||
&& pass "content index: git conventions trigger present (core/AGENTS.md)" \
|
||||
|| fail "content index: git conventions trigger missing from core/AGENTS.md"
|
||||
|
||||
contains "testing" "$CLAUDE" \
|
||||
&& pass "content index: testing conventions trigger present" \
|
||||
|| fail "content index: testing conventions trigger missing"
|
||||
contains "testing" "$CORE_AGENTS_FOR_0004" \
|
||||
&& pass "content index: testing conventions trigger present (core/AGENTS.md)" \
|
||||
|| fail "content index: testing conventions trigger missing from core/AGENTS.md"
|
||||
|
||||
# global.md must be retired — no longer referenced in content index
|
||||
! contains "global\.md" "$CLAUDE" \
|
||||
@@ -156,7 +158,7 @@ echo ""
|
||||
|
||||
echo "--- 0008: docs/ restructure ---"
|
||||
|
||||
for dir in prd ard bug notes adr; do
|
||||
for dir in prd notes adr; do
|
||||
[[ -d "$REPO_ROOT/docs/$dir" ]] \
|
||||
&& pass "docs/$dir/ exists" \
|
||||
|| fail "docs/$dir/ missing"
|
||||
@@ -285,7 +287,8 @@ contains "core/AGENTS\.md" "$ARCH" \
|
||||
&& pass "architecture.md: core/AGENTS.md entry present" \
|
||||
|| fail "architecture.md: core/AGENTS.md entry missing"
|
||||
|
||||
contains "~/\.agents/AGENTS\.md" "$ARCH" \
|
||||
# shellcheck disable=SC2088 # tilde is a literal search string, not a path
|
||||
grep -qF '~/.agents/AGENTS.md' "$ARCH" \
|
||||
&& pass "architecture.md: ~/.agents/AGENTS.md deployment path present" \
|
||||
|| fail "architecture.md: ~/.agents/AGENTS.md deployment path missing"
|
||||
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
INSTALL_SCRIPT="$REPO_ROOT/scripts/setup-neuledge-context.sh"
|
||||
SECURE_SCRIPT="$REPO_ROOT/scripts/secure-context-config.sh"
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
pass() { echo " PASS: $1"; PASS=$((PASS + 1)); }
|
||||
fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); }
|
||||
|
||||
# Fake bin directory — npm and context stubs go here
|
||||
FAKE_BIN="$(mktemp -d)"
|
||||
trap 'rm -rf "$FAKE_BIN"' EXIT
|
||||
|
||||
# --- setup-neuledge-context.sh ---
|
||||
|
||||
echo ""
|
||||
echo "--- install: already at target version, skips npm ---"
|
||||
|
||||
# context returns target version; npm must NOT be called
|
||||
printf '#!/bin/sh\necho "1.2.0"\n' > "$FAKE_BIN/context"; chmod +x "$FAKE_BIN/context"
|
||||
printf '#!/bin/sh\necho "npm called unexpectedly" >&2; exit 1\n' > "$FAKE_BIN/npm"; chmod +x "$FAKE_BIN/npm"
|
||||
|
||||
OUT="$(PATH="$FAKE_BIN:$PATH" bash "$INSTALL_SCRIPT" "1.2.0" 2>&1)"
|
||||
if echo "$OUT" | grep -q "nothing to do"; then
|
||||
pass "skips install when already at target version"
|
||||
else
|
||||
fail "expected 'nothing to do' — got: $OUT"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "--- install: missing version, runs npm ---"
|
||||
|
||||
# context not found; npm installs and context then returns the version
|
||||
rm -f "$FAKE_BIN/context"
|
||||
printf '#!/bin/sh\necho ""\n' > "$FAKE_BIN/context"; chmod +x "$FAKE_BIN/context"
|
||||
# npm installs and makes context return the right version on next call
|
||||
FAKE_BIN2="$(mktemp -d)"
|
||||
trap 'rm -rf "$FAKE_BIN2"' EXIT
|
||||
cat > "$FAKE_BIN2/npm" <<'SH'
|
||||
#!/bin/sh
|
||||
# After npm runs, replace the context stub to return the installed version
|
||||
printf '#!/bin/sh\necho "1.2.0"\n' > "${FAKE_BIN_DIR}/context"
|
||||
chmod +x "${FAKE_BIN_DIR}/context"
|
||||
exit 0
|
||||
SH
|
||||
chmod +x "$FAKE_BIN2/npm"
|
||||
export FAKE_BIN_DIR="$FAKE_BIN"
|
||||
|
||||
OUT="$(PATH="$FAKE_BIN2:$FAKE_BIN:$PATH" bash "$INSTALL_SCRIPT" "1.2.0" 2>&1)"
|
||||
if echo "$OUT" | grep -q "Done:"; then
|
||||
pass "installs when context absent"
|
||||
else
|
||||
fail "expected 'Done:' — got: $OUT"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "--- install: version mismatch after npm — exits non-zero ---"
|
||||
|
||||
# npm runs but context still reports wrong version
|
||||
printf '#!/bin/sh\necho "1.1.0"\n' > "$FAKE_BIN/context"; chmod +x "$FAKE_BIN/context"
|
||||
printf '#!/bin/sh\necho "npm ok"; exit 0\n' > "$FAKE_BIN/npm"; chmod +x "$FAKE_BIN/npm"
|
||||
if PATH="$FAKE_BIN:$PATH" bash "$INSTALL_SCRIPT" "1.2.0" >/dev/null 2>&1; then
|
||||
fail "expected non-zero exit when installed version mismatches target"
|
||||
else
|
||||
pass "exits non-zero when installed version does not match target"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "--- install: upgrade path message ---"
|
||||
|
||||
printf '#!/bin/sh\necho "1.1.0"\n' > "$FAKE_BIN/context"; chmod +x "$FAKE_BIN/context"
|
||||
cat > "$FAKE_BIN/npm" <<'SH'
|
||||
#!/bin/sh
|
||||
printf '#!/bin/sh\necho "1.2.0"\n' > "${FAKE_BIN_DIR}/context"
|
||||
chmod +x "${FAKE_BIN_DIR}/context"
|
||||
exit 0
|
||||
SH
|
||||
chmod +x "$FAKE_BIN/npm"
|
||||
|
||||
OUT="$(FAKE_BIN_DIR="$FAKE_BIN" PATH="$FAKE_BIN:$PATH" bash "$INSTALL_SCRIPT" "1.2.0" 2>&1)"
|
||||
if echo "$OUT" | grep -q "Upgrading"; then
|
||||
pass "prints 'Upgrading' when bumping from an older version"
|
||||
else
|
||||
fail "expected 'Upgrading' — got: $OUT"
|
||||
fi
|
||||
|
||||
# --- secure-context-config.sh ---
|
||||
|
||||
echo ""
|
||||
echo "--- secure: skips when config file does not exist ---"
|
||||
|
||||
FAKE_HOME="$(mktemp -d)"
|
||||
trap 'rm -rf "$FAKE_HOME"' EXIT
|
||||
OUT="$(HOME="$FAKE_HOME" bash "$SECURE_SCRIPT" 2>&1)"
|
||||
if echo "$OUT" | grep -q "Skipped"; then
|
||||
pass "skips when config file absent"
|
||||
else
|
||||
fail "expected 'Skipped' — got: $OUT"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "--- secure: sets 600 on existing config ---"
|
||||
|
||||
mkdir -p "$FAKE_HOME/.context"
|
||||
echo '{"servers":[]}' > "$FAKE_HOME/.context/config.json"
|
||||
chmod 644 "$FAKE_HOME/.context/config.json"
|
||||
|
||||
HOME="$FAKE_HOME" bash "$SECURE_SCRIPT" > /dev/null
|
||||
PERMS="$(stat -c '%a' "$FAKE_HOME/.context/config.json")"
|
||||
if [ "$PERMS" = "600" ]; then
|
||||
pass "config.json set to 600"
|
||||
else
|
||||
fail "expected 600, got $PERMS"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "--- secure: idempotent on already-600 file ---"
|
||||
|
||||
HOME="$FAKE_HOME" bash "$SECURE_SCRIPT" > /dev/null
|
||||
PERMS="$(stat -c '%a' "$FAKE_HOME/.context/config.json")"
|
||||
if [ "$PERMS" = "600" ]; then
|
||||
pass "idempotent: 600 remains 600 on re-run"
|
||||
else
|
||||
fail "expected 600 after second run, got $PERMS"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "--- install: npm exits non-zero — script propagates failure ---"
|
||||
|
||||
printf '#!/bin/sh\necho ""\n' > "$FAKE_BIN/context"; chmod +x "$FAKE_BIN/context"
|
||||
printf '#!/bin/sh\necho "npm error" >&2; exit 1\n' > "$FAKE_BIN/npm"; chmod +x "$FAKE_BIN/npm"
|
||||
if PATH="$FAKE_BIN:$PATH" bash "$INSTALL_SCRIPT" "1.2.0" >/dev/null 2>&1; then
|
||||
fail "expected non-zero exit when npm fails"
|
||||
else
|
||||
pass "exits non-zero when npm exits with error"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "--- install: no arg uses default version 1.2.0 ---"
|
||||
|
||||
printf '#!/bin/sh\necho "1.2.0"\n' > "$FAKE_BIN/context"; chmod +x "$FAKE_BIN/context"
|
||||
printf '#!/bin/sh\necho "npm called unexpectedly" >&2; exit 1\n' > "$FAKE_BIN/npm"; chmod +x "$FAKE_BIN/npm"
|
||||
OUT="$(PATH="$FAKE_BIN:$PATH" bash "$INSTALL_SCRIPT" 2>&1)"
|
||||
if echo "$OUT" | grep -q "nothing to do"; then
|
||||
pass "default version 1.2.0 used when no arg given"
|
||||
else
|
||||
fail "expected default version skip — got: $OUT"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "--- install: fresh install prints 'Installing' not 'Upgrading' ---"
|
||||
|
||||
printf '#!/bin/sh\necho ""\n' > "$FAKE_BIN/context"; chmod +x "$FAKE_BIN/context"
|
||||
FAKE_BIN3="$(mktemp -d)"
|
||||
trap 'rm -rf "$FAKE_BIN3"' EXIT
|
||||
cat > "$FAKE_BIN3/npm" <<'SH'
|
||||
#!/bin/sh
|
||||
printf '#!/bin/sh\necho "1.2.0"\n' > "${FAKE_BIN_DIR}/context"
|
||||
chmod +x "${FAKE_BIN_DIR}/context"
|
||||
exit 0
|
||||
SH
|
||||
chmod +x "$FAKE_BIN3/npm"
|
||||
OUT="$(FAKE_BIN_DIR="$FAKE_BIN" PATH="$FAKE_BIN3:$FAKE_BIN:$PATH" bash "$INSTALL_SCRIPT" "1.2.0" 2>&1)"
|
||||
if echo "$OUT" | grep -q "Installing"; then
|
||||
pass "fresh install prints 'Installing'"
|
||||
else
|
||||
fail "expected 'Installing' on fresh install — got: $OUT"
|
||||
fi
|
||||
if echo "$OUT" | grep -q "Upgrading"; then
|
||||
fail "fresh install should not print 'Upgrading'"
|
||||
else
|
||||
pass "fresh install does not print 'Upgrading'"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "--- install: context --version with v-prefix is parsed correctly ---"
|
||||
|
||||
printf '#!/bin/sh\necho "v1.2.0"\n' > "$FAKE_BIN/context"; chmod +x "$FAKE_BIN/context"
|
||||
printf '#!/bin/sh\necho "npm called unexpectedly" >&2; exit 1\n' > "$FAKE_BIN/npm"; chmod +x "$FAKE_BIN/npm"
|
||||
OUT="$(PATH="$FAKE_BIN:$PATH" bash "$INSTALL_SCRIPT" "1.2.0" 2>&1)"
|
||||
if echo "$OUT" | grep -q "nothing to do"; then
|
||||
pass "v-prefixed version output parsed correctly as match"
|
||||
else
|
||||
fail "expected 'nothing to do' for v-prefixed version — got: $OUT"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "--- secure: downgrades world-writable (777) permissions to 600 ---"
|
||||
|
||||
FAKE_HOME2="$(mktemp -d)"
|
||||
trap 'rm -rf "$FAKE_HOME2"' EXIT
|
||||
mkdir -p "$FAKE_HOME2/.context"
|
||||
echo '{"servers":[]}' > "$FAKE_HOME2/.context/config.json"
|
||||
chmod 777 "$FAKE_HOME2/.context/config.json"
|
||||
HOME="$FAKE_HOME2" bash "$SECURE_SCRIPT" > /dev/null
|
||||
PERMS="$(stat -c '%a' "$FAKE_HOME2/.context/config.json")"
|
||||
if [ "$PERMS" = "600" ]; then
|
||||
pass "777 permissions correctly downgraded to 600"
|
||||
else
|
||||
fail "expected 600 from 777, got $PERMS"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Results: $PASS passed, $FAIL failed"
|
||||
[[ $FAIL -eq 0 ]]
|
||||
@@ -140,6 +140,11 @@ if grep -q "check-manifests" "$PUSH_HOOK"; then
|
||||
else
|
||||
fail "pre-push hook missing check-manifests.sh call"
|
||||
fi
|
||||
if grep -q "run-tests.sh" "$PUSH_HOOK"; then
|
||||
pass "pre-push hook calls run-tests.sh"
|
||||
else
|
||||
fail "pre-push hook missing run-tests.sh call"
|
||||
fi
|
||||
|
||||
# --- 6. Idempotent: second run replaces each block exactly once ---
|
||||
echo ""
|
||||
|
||||
Reference in New Issue
Block a user