mirror of
https://github.com/boshu2/agentops.git
synced 2026-09-14 15:08:13 +08:00
729fbf8ab6
* feat(rpi): executionPacket criterion contract foundation (UW0 of soc-bcrn) Extend executionPacket with EpicCriteria []rpi.Criterion and BeadCriteria map[string][]rpi.Criterion, backed by a new exported rpi.Criterion type. Add JSON schema (draft 2020-12) covering envelope + criterion shape; if/then enforces agent_judge when check_type == custom_rubric (pre-mortem advisory fix #1). DoneCriteria preserved verbatim for v1 back-compat. New round-trip tests cover criteria-populated, v1-no-criteria, custom-rubric, and omitempty cases. Lands soc-bcrn.1.1 (E1.1). go test ./... → 11911 passed in 50 packages. * feat(skills): acceptance_criteria contract across discovery/plan/crank/validation (UW1 of soc-bcrn) Wire the per-criterion contract through the lifecycle skills (text-only; no code changes). Skill text now references the JSON schema landed in UW0 (schemas/execution-packet.schema.json) as canonical. soc-bcrn.1.2 — discovery/plan emit acceptance_criteria fences: - skills/discovery/SKILL.md STEP 4 + STEP 6 amendments - skills/discovery/references/output-templates.md links the schema - skills/plan/SKILL.md issue-decomposition step requires the block soc-bcrn.1.3 — crank pre-spawn read + per-criterion checkpoint: - Step 4 instructs orchestrator to inject metadata.validation.acceptance_criteria - Step 5.7 checkpoint format adds criterion_verdicts[] soc-bcrn.1.4 (E1.4+E1.5 merged on shared file) — validation: - Per-criterion verdict table replaces vibe-only - Back-compat WARN until 2026-06-30, then FAIL (pre-mortem fix #2) - New skills/validation/references/per-criterion-rubric.md go test ./... → 11833 passed in 50 packages. * fix(skills): correct check_type terminology in crank acceptance-criteria injection Worker B's UW1 amendment hallucinated `kind (test|structure|content|command|lint)` as the criterion field/enum. Actual schema uses `check_type` with enum `test_pass | command_exit_zero | file_exists | grep_match | manual | council_judge | custom_rubric`, plus the conditional `agent_judge` field for `custom_rubric`. Replace with the verbatim shape from schemas/execution-packet.schema.json $defs/Criterion. Caught by self-review of 81d729fc. * feat(rpi,skills): isolation contract + lint + addSignalWithEvidence (UW2 of soc-bcrn) soc-bcrn.2.1 (E2.1+E2.3 merged): - skills/rpi/references/isolation-contract.md (NEW): codifies PRODUCT.md operational principle #5 with declaration-vs-enforcement framing, four-lever table (text / hook / daemon / combination — recommends D), compression patterns + false-positive guard. - scripts/check-skill-isolation.sh (NEW, executable): bash lint that greps phase-skill SKILL.md for compression patterns. Runs clean on current tree; injection-regression test passes (rc=1 on 'I will research' violation). soc-bcrn.3.3 (E3.S2): - session_outcome.go: addSignalWithEvidence(regex, evidence, weight) override path; tests_pass now credited via either regex or evidence. - internal/rpi/artifacts.go: LaneResult + ParseValidationLanesResults (forward-compat — accepts {passed:bool}, {status:"PASS"}, {result:"PASSED"}; empty slice when packet field absent). - rpi_evaluator.go: collectPhaseSessionOutcome reads packet validation_lanes_results and threads any-PASS reduce to evidence path. - session_outcome_test.go: 3 new tests cover override, both-false, regex-alone. UW2 verify-only beads (closed without code): - soc-bcrn.3.1 (E3.S1): captureCreatedEpicID already merged at rpi_phased_discovery.go:43, primary callsite rpi_phased_gates.go:83. - soc-bcrn.3.2 (E3.S3): preserveWorktreeCommits already merged at rpi_cleanup.go:399, called BEFORE git worktree remove --force. go test ./... → 11833 passed in 50 packages. * feat(skills,scripts): isolation-contract cross-links + RPI surface smoke (UW3 of soc-bcrn) soc-bcrn.2.2 (E2.2): Cross-link skills/{rpi,discovery,crank,validation}/SKILL.md to the isolation contract reference doc landed in UW2. Each link uses the lint-allowed `See [...](path)` form. lint runs clean. soc-bcrn.3.4 (E3.S4): scripts/test-rpi-surface-smoke.sh (NEW, executable). Static-surface smoke covering 6 checks: bin presence, schema validity, skill-isolation lint, dangling commits (informational), bd workspace, schema-criterion-fields. Returns PASS/FAIL summary. Path-collision recovery: bead body said scripts/test-evolve-cycle-smoke.sh, but that path was already taken by PR #241 (end-to-end cycle smoke wired into pre-push-gate's --smoke-evolve flag). Restored the original; renamed this new static-surface smoke to scripts/test-rpi-surface-smoke.sh. The two scripts have distinct purposes (cycle E2E vs static surface) and should coexist. go test ./... → 11832 passed in 50 packages. * docs(rpi): rollback discipline note in phase-data-contracts.md (UW4 of soc-bcrn) soc-bcrn.3.5 (E3.W3): document phase-3-first / phase-2-second rollback discipline. Re-routed from the plan's `program-templates/phase-{2,3}.md` path (which doesn't exist in the repo) to the existing phase-data-contracts.md ref doc. Pre-mortem F3 of soc-bcrn flagged this as the primary rollback risk for E3 (consolidated daemon epic). E3.S3 (already merged) is the worked example: preserveWorktreeCommits (phase-3 logic) shipped BEFORE the phase-2 cleanup-removal. soc-bcrn.2.3 (E2.4 verify): closed without code change — isolation-contract.md already has 3 forward-references to rpi_run.go from UW2. go test ./... → 11834 passed in 50 packages. * feat(daemon): RPIRunExecutor in-process runner replaces CLI-fallback shell-out (UW5/5a of soc-bcrn) soc-bcrn.3.6 (E3.W4 sub-wave 5a, additive): - NEW cli/internal/daemon/rpi_run.go: RPIRunExecutor with function-pointer pattern (RPIRunFunc). Mirrors DreamExecutor structure. Validates full-cycle specs (start_phase=1, max_phase=3) and emits artifacts tagged executor_policy=in-process / backend=in-process. - NEW cli/internal/daemon/rpi_run_test.go: 5 tests (runs-via-func, rejects-wrong-jobtype, propagates-runner-error, validate-full-run, requires-fields). - agentopsd.go: buildAgentOpsDaemonCLIFallbackRPIExecutor now wires RPIRunExecutor with defaultCLIFallbackRPIRunFunc that calls runPhasedEngine in-process. New CLIFallbackRPIRunFunc option lets tests inject fakes. buildPhasedEngineOptionsFromSpec maps obvious fields (RunID, TestFirst, ExecutionPacketPath→DiscoveryArtifact, PhaseTimeout); other phasedEngineOptions fields use defaults. - agentopsd_test.go: TestAgentOpsDaemonCLIFallbackExecutorPolicyCompletesRPIRunJob rewritten to inject a fake RPIRunFunc instead of writing fake shell scripts. Asserts executor_policy=in-process, backend=in-process, rpi_run_status=completed. ADDITIVE only — RPICLIExecutor and scripts/ao-rpi-autonomous-cycle.sh preserved for nightly-evolution.sh until sub-waves 5b/5c migrate them. RPICLIExecutor has zero production callers after this change (only its own test file references the type). go test ./... → 11832 passed in 50 packages. * fix(daemon): RPIRunExecutor wraps supervisor (gates+landing+cycles) (UW5/5a-fix of soc-bcrn) soc-bcrn.3.8 — fix the regression sub-wave 5a introduced. Sub-wave 5a's defaultCLIFallbackRPIRunFunc called runPhasedEngine alone, silently losing the gate/landing/cycle semantics of the old shell wrapper that ran ao rpi loop --supervisor. Daemon-submitted rpi.run jobs would have skipped landing-policy enforcement. Changes: - RPIRunJobSpec gains 7 supervisor policy fields (all omitempty for back-compat): MaxCycles, GatePolicy, LandingPolicy, LandingBranch, BDSyncPolicy, FailurePolicy, KillSwitchPath. - agentopsd.go:defaultCLIFallbackRPIRunFunc now calls executeLoopCyclesCtx (thin context-aware wrapper around executeLoopCycles) — full supervisor loop with gates + landing + cycle management. - New buildSupervisorConfigFromSpec(spec, root) helper translates spec policy fields into rpiLoopSupervisorConfig, then validates via the supervisor's own validateLoopConfigPolicies. - Lease disabled in daemon path (cfg.LeaseEnabled=false) — daemon already owns queue-level claim; lease would deadlock. - Deleted dead helper buildPhasedEngineOptionsFromSpec. - 5 new tests: spec round-trip with policy fields, defaults-empty back-compat, spec→cfg mapping (3 cases incl. invalid-enum rejection). Forward-compat caveats noted in worker discoveries (NOT fully wired): - MaxCycles in spec is read but rpiLoopSupervisorConfig has no MaxCycles field; cycle count comes from the cobra-bound rpiMaxCycles global. Daemon path runs one cycle (matches nightly script default). - RunID, ExecutionPacketPath, TestFirst, PhaseTimeout in spec are not yet projected onto the cfg. go test ./... → 11835 passed in 50 packages. * feat(scripts,docs): nightly-evolution + 3 docs migrate to ao daemon submit (UW5/5b of soc-bcrn) soc-bcrn.3.7 (E3.W4 sub-wave 5b): scripts/nightly-evolution.sh — replace shell wrapper invocation (`scripts/ao-rpi-autonomous-cycle.sh`) with `ao daemon jobs submit --type=rpi.run` + `ao daemon jobs wait`. Liveness-probe via `ao daemon jobs list --json` before submit. Payload now carries supervisor policy fields (max_cycles, gate_policy, landing_policy, optional landing_branch) via the spec extension landed in 5a-fix; daemon executor runs the full supervisor loop (cycles + gates + landing). docs/contracts/agentops-daemon.md, docs/runbooks/daemon-factory-admission.md, docs/contracts/factory-admission.md — replace stale references to `scripts/ao-rpi-autonomous-cycle.sh` with the new in-process path (RPIRunExecutor, cli/internal/daemon/rpi_run.go). scripts/ao-rpi-autonomous-cycle.sh and cli/internal/daemon/rpi_cli_executor.go remain in-tree pending sub-wave 5c (deletion). go test ./... → 11835 passed in 50 packages. bash -n scripts/nightly-evolution.sh → clean. * chore(daemon): delete RPICLIExecutor + ao-rpi-autonomous-cycle.sh shell wrapper (UW5/5c of soc-bcrn) soc-bcrn.3 final E3.W4 cleanup. After sub-waves 5a (RPIRunExecutor), 5a-fix (supervisor wrapping + policy fields), and 5b (nightly migration to ao daemon submit), all production callers of these surfaces have been retired. Deleting: - cli/internal/daemon/rpi_cli_executor.go (5.6KB) — old shell-shelling executor - cli/internal/daemon/rpi_cli_executor_test.go (6.6KB) - scripts/ao-rpi-autonomous-cycle.sh (4.7KB) — legacy shell wrapper Final consumer audit: zero non-comment references to RPICLIExecutor or NewRPICLIExecutor remain. The new in-process path (cli/internal/daemon/rpi_run.go → executeLoopCyclesCtx) carries the same gate + landing semantics via supervisor wrapping. go test ./... → 11833 passed in 50 packages (down from 11835 — the 2 diff is from the deleted RPICLIExecutor's own tests). go build ./... + go vet ./... → clean. UW5 (E3.W4 / M3.5 daemon executor swap) complete. * docs(skills): best-practices citation table + 4 SKILL.md cross-links (UW6 of soc-bcrn) soc-bcrn.4 (E4 — Best-practices encoding): E4.1: NEW skills/rpi/references/best-practices.md. Table A (14 lifecycle principles) + Table B (6 anti-patterns) extracted verbatim from .agents/research/2026-05-07-rpi-lifecycle-sharpening.md §Objective 4. Each row carries a source citation and which skills should encode it. The doc explicitly says lifecycle skills MUST cite by # — never duplicate the body. E4.2: skills/{rpi,discovery,crank,validation}/SKILL.md — one-line cross-link added next to the existing isolation-contract.md citation. Each link uses the lint-allowed `See [...](path)` form. lint runs clean. Mechanical-enforcement section in best-practices.md cross-references prior soc-bcrn waves (UW0 schema $defs/Criterion, UW1 per-criterion-rubric, UW2 check-skill-isolation lint). go test ./... → 11832 passed in 50 packages. * feat(daemon,schema): agent-update protocol surface (UW7-1 of soc-y0ct) soc-y0ct.1 — protocol surface (schema + event types + payload structs). Defers ac-aup.3 (rpi_run.go emission) to a follow-up bead since it requires careful integration with the supervisor's existing event flow. - schemas/agent-update.schema.json (NEW): draft 2020-12 envelope {agent_update_version, event_type, payload}; 4 $defs (phase_start, phase_complete, criterion_verdict, phase_handoff) with allOf+if/then for type-tagged payload dispatch (mirrors UW0 Criterion's custom_rubric pattern). - cli/internal/daemon/types.go: 4 new EventType constants registered in the const block AND eventTypeSet (required for ValidateEventType). - cli/internal/daemon/agent_update.go (NEW): AgentUpdateVersion=1, 4 payload structs, 4 NewAgentUpdate*Event constructors that stamp the version, default empty timestamps, emit only-non-zero optional fields. Constructors deliberately leave EventID/RequestID/JobID/Actor unset — emission caller populates those. - cli/internal/daemon/agent_update_test.go (NEW): 7 test functions covering const + ValidateEventType registration, 4 payload round-trips, version stamping, and timestamp default. Status enums per spec: phase_complete uses lowercase (success|failure| timeout) matching JobResultStatus; criterion_verdict uses uppercase (PASS|FAIL|SKIP) matching the verdict vocabulary in schemas/verdict.v1.schema.json. go test ./... → 11842 passed in 50 packages (up from 11833 — the +9 is the new tests under ./internal/daemon/). * feat(tests,docs): wave 0 operator canary + software-factory drift stopgap (soc-kizn.11) Adds tests/e2e/factory-operator-canary.sh: narrow L1/L2 smoke that prints the five operator-surface anchors (briefing, next delivery, validation, evidence, closeout) and reports each as resolved-path or explicit-unavailable. Does not claim factory yield, autonomous merge, or L3 proof. Reframes the Codex section of docs/software-factory.md so the canonical hookless route is `agentopsd` (`ao daemon submit ...`), and `ao codex *` is documented only as deprecated legacy fallback. Strips routine `ao codex *` recommendations from the Surface Map. Validation now passes: `rg 'ao codex (start|stop)|ao codex ensure-(start|stop)' docs/software-factory.md | rg -v 'hookless|compat|legacy|fallback|deprecated'` returns empty. Full Codex skill/runtime parity cleanup remains tracked in soc-kizn.8. * docs(skills): move forge Examples to references/ to fit 248-line CI limit (soc-pul8) skills/forge/SKILL.md was 260 lines after the soc-yjzp.3 cross-vendor analog edit, over the 248-line skill-lint ceiling. Moves the two Examples walkthroughs (SessionEnd hook invocation + manual transcript mining) to skills/forge/references/examples.md, replacing the section body with a link. Now 237 lines. heal.sh --strict clean (new references file is linked). hooks/skill-lint-gate.sh exits 0. Codex parity audit passes. * fix(skills): copy rpi references into crank/discovery/validation, fix dead-ref gate UW3 (e74b2ff0) and UW6 (2272510e) cross-linked from skills/{crank,discovery, validation}/SKILL.md to ../rpi/references/{isolation-contract,best-practices}.md. heal.sh's DEAD_REF check only resolves local references/ and ../shared/references/ paths, so it flagged the cross-skill links as dead — blocking pre-push. Per CLAUDE.md "no symlinks; copy instead", copies both files into each affected skill's own references/ dir and rewrites the SKILL.md links to local paths. skills/validation/SKILL.md additionally compresses Setup+Gate sections, drops the Per-criterion-verdicts example table from Phase Summary code fence, and inlines the back-compat fallback to fit the 248-line skill-lint ceiling (was 274, now 247). Verified: heal.sh --strict clean across all skills; skill-lint-gate exit 0; codex parity audit passes for crank, discovery, validation. * chore(codex,registry): refresh codex artifacts + registry.json after skill ref edits Auto-regenerated by `scripts/refresh-codex-artifacts.sh --scope upstream` after the dead-ref fix touched skills/{crank,discovery,validation}/SKILL.md and skills/forge/SKILL.md (cycle 5 close). Updates .agentops-generated.json hashes for 7 skills + manifest + rolls registry.json reference_count for forge (1 -> 2 after examples.md split) and knowledge_stores count refresh. No source-code changes; this is the metadata catch-up the pre-push codex-artifact-metadata gate flagged. * fix(tests): nightly-evolution bats — mock daemon wait/list, assert payload not log soc-bcrn UW5/5b migrated nightly-evolution.sh from shelling ao-rpi-autonomous-cycle.sh to `ao daemon jobs submit` + `wait`. Two bats tests still asserted the old shell-wrapper behavior: - Mock `ao` only handled `daemon jobs submit`. Add handlers for `daemon jobs wait` (returns completed) and `daemon jobs list` (liveness probe — returns []). Also strip `--output FOO` global flags so subcommand match still works. - Test 462 grep'd $AO_RPI_LOG for `--landing-branch` (the legacy CLI arg). Replace with jq assertion on the new payload file evolve-rpi-run-payload.json. Now 31/31 nightly-evolution bats pass (was 29/31). * fix(skills): copy phase-data-contracts.md to fix transitive broken-link gate isolation-contract.md (just copied to crank/discovery/validation in053111a8) references phase-data-contracts.md as a sibling. The doc-release link validator flagged it as broken in the new locations because the file lived only in skills/rpi/references/. Per CLAUDE.md "no symlinks; copy", copies phase-data-contracts.md into each affected skill's references/ and adds the link to each SKILL.md's Reference Documents section so heal.sh's UNLINKED_REF check stays green. Refreshes codex .agentops-generated.json hashes for the 4 touched skills. Verified: link validator 1990/1990 ok, heal.sh --strict clean, codex artifact metadata + parity audits both pass. * chore(registry): refresh registry.json after phase-data-contracts.md copies Auto-regenerated by scripts/generate-registry.sh after commit3ae3ce85copied phase-data-contracts.md into crank/discovery/validation references. Reference counts: crank 24→27, discovery 5→8, validation +3. CI registry-check (--check mode) flagged the drift on PR #255. * chore(registry): regen without polluting from gitignored .agents/ scripts/generate-registry.sh's build_knowledge_stores walks .agents/ which is gitignored. CI clones clean (no .agents/ tree), so its regen output has no knowledge_stores entries; mine had them populated from session state, causing registry-check stale-detection to fail. Regenerate with .agents/ temporarily moved aside so the committed registry.json matches the CI-clone's expected output. Net effect: drop the knowledge_stores section that was a build-environment artifact. Resolves PR #255 registry-check failure.
298 lines
9.1 KiB
Bash
Executable File
298 lines
9.1 KiB
Bash
Executable File
#!/bin/bash
|
|
#
|
|
# check-skill-isolation.sh
|
|
#
|
|
# Lint phase-skill SKILL.md files for compression patterns that violate the
|
|
# phase-isolation contract declared in PRODUCT.md operational principle #5
|
|
# and documented at skills/rpi/references/isolation-contract.md.
|
|
#
|
|
# Compression patterns flagged:
|
|
# 1. Cross-phase first-person verbs:
|
|
# "I will research|plan|crank|validate"
|
|
# 2. Inline research vocabulary near phase context:
|
|
# "let me grep|read|search" / "I'll grep|read|search"
|
|
# 3. A phase-skill SKILL.md calling another phase skill it should not orchestrate.
|
|
# Per-file allowlist:
|
|
# rpi/SKILL.md may call: discovery, crank, validation
|
|
# discovery/SKILL.md may call: research, plan
|
|
# crank/SKILL.md may NOT call: research, plan, crank, validation
|
|
# validation/SKILL.md may NOT call: research, plan, crank, validation
|
|
#
|
|
# False-positive guard:
|
|
# - Lines beginning with `See [` (markdown reference links)
|
|
# - Lines beginning with `Read <path>` (reference doc reads)
|
|
# - Lines inside fenced code blocks (``` ... ```)
|
|
#
|
|
# Usage:
|
|
# check-skill-isolation.sh # lint default tree
|
|
# check-skill-isolation.sh <path> # lint a different skills/ tree
|
|
# check-skill-isolation.sh -q # quiet mode, exit code only
|
|
# check-skill-isolation.sh --self-test # internal regression check
|
|
#
|
|
# Exit codes:
|
|
# 0 = clean (no compression patterns matched)
|
|
# 1 = at least one compression pattern matched
|
|
# 2 = script error (bad invocation, missing files)
|
|
|
|
set -uo pipefail
|
|
|
|
QUIET=0
|
|
SELF_TEST=0
|
|
TARGET_PATH=""
|
|
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
-q|--quiet)
|
|
QUIET=1
|
|
;;
|
|
--self-test)
|
|
SELF_TEST=1
|
|
;;
|
|
-h|--help)
|
|
sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//'
|
|
exit 0
|
|
;;
|
|
-*)
|
|
echo "check-skill-isolation: unknown flag: $arg" >&2
|
|
exit 2
|
|
;;
|
|
*)
|
|
if [[ -z "$TARGET_PATH" ]]; then
|
|
TARGET_PATH="$arg"
|
|
else
|
|
echo "check-skill-isolation: unexpected extra argument: $arg" >&2
|
|
exit 2
|
|
fi
|
|
;;
|
|
esac
|
|
done
|
|
|
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
DEFAULT_ROOT="${REPO_ROOT}/skills"
|
|
|
|
emit() {
|
|
if [[ $QUIET -eq 0 ]]; then
|
|
echo "$@" >&2
|
|
fi
|
|
}
|
|
|
|
# Resolve the four phase-skill SKILL.md files under a given root.
|
|
# Echoes one path per line for files that exist.
|
|
resolve_phase_files() {
|
|
local root="$1"
|
|
local name
|
|
for name in rpi discovery crank validation; do
|
|
local f="${root}/${name}/SKILL.md"
|
|
if [[ -f "$f" ]]; then
|
|
echo "$f"
|
|
fi
|
|
done
|
|
}
|
|
|
|
# Per-file Skill() callsite check.
|
|
# Returns 0 if the line is an allowed callsite for this file, 1 if it's a violation.
|
|
# $1 = basename-of-parent-dir (rpi|discovery|crank|validation)
|
|
# $2 = sub-skill name captured from the line (research|plan|crank|validation)
|
|
is_skill_call_allowed() {
|
|
local owner="$1"
|
|
local target="$2"
|
|
case "$owner" in
|
|
rpi)
|
|
# rpi orchestrates discovery, crank, validation.
|
|
# research/plan are discovery's sub-skills, not rpi's — flag those.
|
|
case "$target" in
|
|
crank|validation) return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
;;
|
|
discovery)
|
|
# discovery orchestrates research and plan.
|
|
case "$target" in
|
|
research|plan) return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
;;
|
|
crank|validation)
|
|
# phase 2 and phase 3 are sealed — they should not call any of the watched phase skills.
|
|
return 1
|
|
;;
|
|
*)
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Lint a single SKILL.md file. Echoes diagnostics to stderr (when not quiet).
|
|
# Sets the global `violations` count via stdout (one line per violation, "file:lineno:pattern").
|
|
lint_file() {
|
|
local file="$1"
|
|
local owner
|
|
owner="$(basename "$(dirname "$file")")"
|
|
|
|
awk -v file="$file" -v owner="$owner" '
|
|
BEGIN {
|
|
in_fence = 0
|
|
}
|
|
# Toggle fenced-code state. A line whose first non-space chars are ``` flips state.
|
|
{
|
|
line = $0
|
|
stripped = line
|
|
sub(/^[ \t]*/, "", stripped)
|
|
if (substr(stripped, 1, 3) == "```") {
|
|
in_fence = 1 - in_fence
|
|
next
|
|
}
|
|
}
|
|
# Skip lines inside fenced code blocks.
|
|
in_fence == 1 { next }
|
|
# False-positive guard: markdown reference link lines.
|
|
/^See \[/ { next }
|
|
# False-positive guard: reference-doc read instructions.
|
|
/^Read [^[:space:]]+/ { next }
|
|
# Pattern 1: cross-phase first-person verbs.
|
|
{
|
|
if (match(tolower(line), /i will (research|plan|crank|validate)/)) {
|
|
printf("%s:%d:cross-phase first-person verb: %s\n", file, NR, line) > "/dev/stderr"
|
|
printf("%s\t%d\tcross-phase-verb\n", file, NR)
|
|
}
|
|
}
|
|
# Pattern 2: inline research vocabulary.
|
|
{
|
|
lc = tolower(line)
|
|
if (match(lc, /let me (grep|read|search)/) ||
|
|
match(lc, /i.ll (grep|read|search)/)) {
|
|
printf("%s:%d:inline research vocabulary: %s\n", file, NR, line) > "/dev/stderr"
|
|
printf("%s\t%d\tinline-research\n", file, NR)
|
|
}
|
|
}
|
|
# Pattern 3: phase-skill calling another phase skill.
|
|
# Capture the target sub-skill name and let the caller validate the allowlist.
|
|
{
|
|
if (match(line, /Skill\(skill="(research|plan|crank|validation)"/, m)) {
|
|
printf("%s\t%d\tskill-call\t%s\n", file, NR, m[1])
|
|
}
|
|
}
|
|
' "$file"
|
|
}
|
|
|
|
run_lint() {
|
|
local root="$1"
|
|
local violations=0
|
|
|
|
if [[ ! -d "$root" ]]; then
|
|
echo "check-skill-isolation: target path is not a directory: $root" >&2
|
|
return 2
|
|
fi
|
|
|
|
local files
|
|
mapfile -t files < <(resolve_phase_files "$root")
|
|
|
|
if [[ ${#files[@]} -eq 0 ]]; then
|
|
# No phase-skill SKILL.md files under this root. Nothing to lint.
|
|
# This is not an error — callers may pass a tree intended to test
|
|
# specific files only. Emit a debug note and return clean.
|
|
emit "check-skill-isolation: no phase-skill SKILL.md files found under $root"
|
|
return 0
|
|
fi
|
|
|
|
local file
|
|
for file in "${files[@]}"; do
|
|
local raw
|
|
raw="$(lint_file "$file")"
|
|
if [[ -z "$raw" ]]; then
|
|
continue
|
|
fi
|
|
|
|
# Each output line is tab-separated:
|
|
# file<TAB>lineno<TAB>kind[<TAB>extra]
|
|
# kind in {cross-phase-verb, inline-research, skill-call}
|
|
local line
|
|
while IFS= read -r line; do
|
|
[[ -z "$line" ]] && continue
|
|
|
|
local kind
|
|
kind="$(echo "$line" | cut -f3)"
|
|
local extra
|
|
extra="$(echo "$line" | cut -f4)"
|
|
local hit_file
|
|
hit_file="$(echo "$line" | cut -f1)"
|
|
local hit_lineno
|
|
hit_lineno="$(echo "$line" | cut -f2)"
|
|
local owner
|
|
owner="$(basename "$(dirname "$hit_file")")"
|
|
|
|
case "$kind" in
|
|
cross-phase-verb|inline-research)
|
|
violations=$((violations + 1))
|
|
;;
|
|
skill-call)
|
|
if is_skill_call_allowed "$owner" "$extra"; then
|
|
# Legitimate orchestration callsite — no violation.
|
|
:
|
|
else
|
|
emit "$hit_file:$hit_lineno:phase-skill calling another phase skill (target=$extra)"
|
|
violations=$((violations + 1))
|
|
fi
|
|
;;
|
|
esac
|
|
done <<< "$raw"
|
|
done
|
|
|
|
if [[ $violations -gt 0 ]]; then
|
|
emit ""
|
|
emit "check-skill-isolation: FAIL ($violations compression pattern(s) found)"
|
|
emit ""
|
|
emit "See skills/rpi/references/isolation-contract.md for the rules."
|
|
return 1
|
|
fi
|
|
|
|
if [[ $QUIET -eq 0 ]]; then
|
|
echo "check-skill-isolation: PASS (no compression patterns in phase-skill SKILL.md files under $root)"
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
SELF_TEST_TMP=""
|
|
self_test_cleanup() {
|
|
if [[ -n "${SELF_TEST_TMP:-}" && -d "${SELF_TEST_TMP:-}" ]]; then
|
|
rm -rf "$SELF_TEST_TMP"
|
|
fi
|
|
}
|
|
|
|
self_test() {
|
|
# Build a tmpdir mimicking skills/<phase>/SKILL.md, inject a known violation,
|
|
# run the lint, and assert non-zero exit.
|
|
SELF_TEST_TMP="$(mktemp -d)"
|
|
trap self_test_cleanup EXIT
|
|
|
|
mkdir -p "$SELF_TEST_TMP/discovery"
|
|
cat > "$SELF_TEST_TMP/discovery/SKILL.md" <<'EOF'
|
|
---
|
|
name: discovery
|
|
---
|
|
# /discovery
|
|
|
|
I will research the codebase before doing anything else.
|
|
EOF
|
|
|
|
if "$0" --quiet "$SELF_TEST_TMP"; then
|
|
echo "check-skill-isolation: SELF-TEST FAILED — lint did not catch injected violation" >&2
|
|
return 1
|
|
fi
|
|
|
|
echo "check-skill-isolation: self-test PASS"
|
|
return 0
|
|
}
|
|
|
|
if [[ $SELF_TEST -eq 1 ]]; then
|
|
self_test
|
|
exit $?
|
|
fi
|
|
|
|
if [[ -z "$TARGET_PATH" ]]; then
|
|
TARGET_PATH="$DEFAULT_ROOT"
|
|
fi
|
|
|
|
run_lint "$TARGET_PATH"
|
|
exit $?
|