forgedock 1.8.0 → 1.8.2
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/bin/engine/admission.mjs +57 -1
- package/bin/engine/admission.test.mjs +60 -0
- package/commands/analytics.md +3 -1
- package/commands/audit-agents.md +3 -1
- package/commands/audit.md +10 -8
- package/commands/autopilot.md +16 -14
- package/commands/ci-audit.md +4 -2
- package/commands/cleanup.md +1 -1
- package/commands/deploy-pr.md +31 -11
- package/commands/geo-audit.md +3 -1
- package/commands/incident-response.md +3 -1
- package/commands/issue.md +4 -2
- package/commands/milestone.md +8 -6
- package/commands/orchestrate/config.md +2 -2
- package/commands/orchestrate/phase-1-resolve.md +56 -12
- package/commands/orchestrate/phase-3-dependency.md +15 -5
- package/commands/orchestrate/phase-4-execution.md +593 -78
- package/commands/orchestrate/phase-5-cleanup.md +4 -2
- package/commands/orchestrate/phase-6-report.md +7 -3
- package/commands/orchestrate.md +13 -9
- package/commands/pipeline-health/phase-4-proposals.md +6 -4
- package/commands/pipeline-health/phase-5-report.md +4 -2
- package/commands/pipeline-health.md +9 -7
- package/commands/pipeline-resume.md +4 -2
- package/commands/qa-sweep.md +3 -1
- package/commands/quality-gate.md +4 -3
- package/commands/recover-orphans.md +172 -13
- package/commands/review-pr-agents.md +6 -4
- package/commands/review-pr-staging.md +13 -5
- package/commands/review-pr.md +187 -19
- package/commands/rollback.md +3 -1
- package/commands/security-audit.md +3 -1
- package/commands/signal-planner.md +6 -4
- package/commands/sync-ecosystem.md +4 -2
- package/commands/test-gate.md +4 -2
- package/commands/upgrade-deps.md +4 -2
- package/commands/validate.md +3 -1
- package/commands/work-on/build/architect.md +81 -19
- package/commands/work-on/build/context.md +61 -18
- package/commands/work-on/build/implement.md +62 -18
- package/commands/work-on/build/validate.md +240 -60
- package/commands/work-on/build.md +344 -146
- package/commands/work-on/close.md +617 -246
- package/commands/work-on/decompose.md +239 -41
- package/commands/work-on/investigate.md +377 -61
- package/commands/work-on/remediate.md +42 -9
- package/commands/work-on/review.md +240 -29
- package/commands/work-on-monolithic.md +48 -7
- package/commands/work-on.md +180 -2014
- package/docs/CODEX.md +2 -0
- package/docs/CONFIG.md +11 -0
- package/package.json +1 -1
- package/scripts/build-spec-graph.mjs +11 -4
- package/scripts/check-spec-markers.sh +11 -1
- package/scripts/checkbox-sections.test.sh +8 -23
- package/scripts/extract-affected-files.sh +5 -2
- package/scripts/extract-affected-files.test.sh +27 -0
- package/scripts/forge-root.test.sh +176 -0
- package/scripts/is-docs-only.sh +31 -0
- package/scripts/is-docs-only.test.sh +64 -0
- package/scripts/lint-dispatch-prompt.sh +217 -0
- package/scripts/lint-dispatch-prompt.test.sh +307 -0
- package/scripts/validate-spec-graph.sh +3 -2
- package/scripts/verify-phase-trail.sh +282 -0
- package/scripts/verify-phase-trail.test.sh +298 -0
- package/scripts/wait-ci-green.sh +98 -0
- package/scripts/wait-ci-green.test.sh +99 -0
package/docs/CODEX.md
CHANGED
|
@@ -48,6 +48,8 @@ Examples:
|
|
|
48
48
|
| `commands/work-on/investigate.md` | `forge-work-on-investigate` |
|
|
49
49
|
| `commands/work-on/build/context.md` | `forge-work-on-build-context` |
|
|
50
50
|
|
|
51
|
+
Nested `Skill("{FORGE_SKILL_PREFIX}work-on:build")` references in the specs resolve under Codex to `forge-work-on-build` (prefix `forge-`, `-` nesting); see `Skill Name Resolution` in `commands/work-on.md`.
|
|
52
|
+
|
|
51
53
|
`commands/review-pr-agents.md` is excluded because it is a read-on-demand catalog, not a standalone entrypoint.
|
|
52
54
|
|
|
53
55
|
## How the Adapter Works
|
package/docs/CONFIG.md
CHANGED
|
@@ -379,6 +379,14 @@ orchestration:
|
|
|
379
379
|
# Number of consecutive merged-unit observations at ratio >= 1.0 before
|
|
380
380
|
# printing a convergence warning. Default: 3.
|
|
381
381
|
convergence_window: 3
|
|
382
|
+
|
|
383
|
+
# On-by-default amplification circuit breaker (forge#3060). Counts ALL
|
|
384
|
+
# cascade findings (new surface as well as same-lineage refinements). When
|
|
385
|
+
# the ratio stays >= 1.0 for convergence_window merged units, P3-and-below
|
|
386
|
+
# admission pauses, the operator gets a report (ratio + queue size), and the
|
|
387
|
+
# paused P3s wait for bounded P3 batches. P1/P2 are never affected, and
|
|
388
|
+
# `policy: all` / unlimited overrides do not disable it. "on" (default) | "off".
|
|
389
|
+
amplification_breaker: on
|
|
382
390
|
```
|
|
383
391
|
|
|
384
392
|
| Field | Type | Required | Description |
|
|
@@ -394,6 +402,9 @@ orchestration:
|
|
|
394
402
|
| `cascade.p3_same_file_defer` | boolean | No | Defer P3 findings sharing a file with the active batch. Default: `true` |
|
|
395
403
|
| `cascade.max_amplification` | positive number or `"off"` | No | Opt-in ceiling for same-lineage refinement dispatch. Default: `"off"` |
|
|
396
404
|
| `cascade.convergence_window` | positive integer | No | Consecutive ratio observations at or above 1.0 before warning. Default: 3 |
|
|
405
|
+
| `cascade.amplification_breaker` | `on` \| `off` | No | Pause P3-and-below cascade admission while the findings-spawned / merged-units ratio stays at or above 1.0 for `convergence_window` units. Counts all cascade findings (not only same-lineage); P1/P2 unaffected; independent of `cascade.policy`, so `policy: all` does not disable it. Paused P3s route to bounded P3 batches. Default: `on`. Opt out with `off` (boolean `false` is accepted as `off`, `true` as `on`). |
|
|
406
|
+
|
|
407
|
+
**Upgrade note — `cascade.amplification_breaker` is on by default**: existing configs that do not set this key now get the breaker automatically. During a batch, once the findings-spawned / merged-units ratio stays at or above 1.0 for `convergence_window` units (it can trip at exactly 1.0 over 3 units), P3-and-below cascade admission pauses; paused P3s are routed to bounded P3 batches rather than dispatched individually. P1/P2 are unaffected. To keep the previous behavior, set `orchestration.cascade.amplification_breaker: off` (or `false`). Release notes for the version that introduced this default should call out the change.
|
|
397
408
|
|
|
398
409
|
**Hard invariant — not configurable**: safety exclusions (findings whose `## Problem` section indicates security/billing/anti-bot/auth concerns) are never batched and never auto-admitted by any `cascade.policy`, including `all`. That exclusion is enforced upstream of this section (the P3 batching eligibility check) and has no corresponding key here by design.
|
|
399
410
|
|
package/package.json
CHANGED
|
@@ -224,13 +224,14 @@ function commandNodeFromPath(rel) {
|
|
|
224
224
|
};
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
-
/** Map a Skill(skill="X") target to a command node id
|
|
227
|
+
/** Map a Skill(skill="X") target to a command node id (an optional leading
|
|
228
|
+
* `{FORGE_SKILL_PREFIX}` namespace placeholder is stripped). The skill name uses the
|
|
228
229
|
* same `:`-delimited convention as our sub-phase names (e.g. "work-on:build").
|
|
229
230
|
* Specs also write the `/`-delimited form (e.g. "work-on/review"); normalize it
|
|
230
231
|
* to the colon form so both resolve to the same node (kept in sync with the
|
|
231
232
|
* dangling-ref check in validate-spec-graph.sh). */
|
|
232
233
|
function skillTargetToId(skill) {
|
|
233
|
-
return `cmd:${skill.replace(/\//g, ":")}`;
|
|
234
|
+
return `cmd:${skill.replace(/^\{FORGE_SKILL_PREFIX\}/, "").replace(/\//g, ":")}`;
|
|
234
235
|
}
|
|
235
236
|
|
|
236
237
|
// ---------------------------------------------------------------------------
|
|
@@ -325,6 +326,12 @@ function build() {
|
|
|
325
326
|
}
|
|
326
327
|
}
|
|
327
328
|
|
|
329
|
+
// WRITES via the protocol codec (forge#1727): `cli.js emit X` / `forge-annotation.sh write X`
|
|
330
|
+
// produce the `<!-- FORGE:X -->` opening tag that is then posted, so they count as writes.
|
|
331
|
+
for (const m of line.matchAll(/(?:cli\.js|forge-annotation\.sh)\s+(?:emit|write)\s+([A-Z][A-Z0-9_]*)\b/g)) {
|
|
332
|
+
addEdge(fromId, "WRITES", `ann:FORGE:${m[1]}`, { file: rel, line: li + 1 });
|
|
333
|
+
}
|
|
334
|
+
|
|
328
335
|
// READS: command consumes a FORGE annotation.
|
|
329
336
|
// jq forms: contains("FORGE:X"), contains("FORGE:X:"),
|
|
330
337
|
// test("<!-- FORGE:X: ..."), capture("<!-- FORGE:X: ...")
|
|
@@ -351,7 +358,7 @@ function build() {
|
|
|
351
358
|
// Skill(skill="X"), and colon Skill(skill: "X") — with `/`-delimited
|
|
352
359
|
// sub-phase targets and uppercase/leading-digit names. Kept in sync with
|
|
353
360
|
// the dangling-ref check in validate-spec-graph.sh.
|
|
354
|
-
for (const m of line.matchAll(/Skill\(\s*(?:skill\s*[:=]\s*)?["']([A-Za-z0-9][A-Za-z0-9:_/-]*)["']/g)) {
|
|
361
|
+
for (const m of line.matchAll(/Skill\(\s*(?:skill\s*[:=]\s*)?["']((?:\{FORGE_SKILL_PREFIX\})?[A-Za-z0-9][A-Za-z0-9:_/-]*)["']/g)) {
|
|
355
362
|
const targetId = skillTargetToId(m[1]);
|
|
356
363
|
// Only link if the target resolves to a known command/sub-phase node.
|
|
357
364
|
if (nodes.has(targetId)) {
|
|
@@ -457,7 +464,7 @@ function selfCheck(graph, quiet) {
|
|
|
457
464
|
|
|
458
465
|
const hash = graph.graph.builtFromHash;
|
|
459
466
|
const checks = [
|
|
460
|
-
["work-on WRITES FORGE:TRAJECTORY", has("cmd:work-on", "WRITES", "ann:FORGE:TRAJECTORY")],
|
|
467
|
+
["work-on:close WRITES FORGE:TRAJECTORY", has("cmd:work-on:close", "WRITES", "ann:FORGE:TRAJECTORY")],
|
|
461
468
|
["review-pr READS FORGE:CONTRACT", has("cmd:review-pr", "READS", "ann:FORGE:CONTRACT")],
|
|
462
469
|
["builtFromHash is a sha256 hex digest", typeof hash === "string" && /^[0-9a-f]{64}$/.test(hash)],
|
|
463
470
|
];
|
|
@@ -89,18 +89,21 @@ MARKER_REGISTRY="
|
|
|
89
89
|
BLOCKED_ON_HUMAN_MERGE
|
|
90
90
|
BODY
|
|
91
91
|
BUILDER
|
|
92
|
+
BUILD_BLOCKED
|
|
92
93
|
CALIBRATION_CHECK
|
|
93
94
|
CARD
|
|
94
|
-
CLASS
|
|
95
95
|
CHECKPOINT
|
|
96
|
+
CI_REMEDIATION
|
|
96
97
|
CLAIM
|
|
97
98
|
CLAIM_RELEASED
|
|
99
|
+
CLASS
|
|
98
100
|
CONTEXT
|
|
99
101
|
CONTRACT
|
|
100
102
|
COORD_ISSUE
|
|
101
103
|
CRITIQUE
|
|
102
104
|
DECISION_RECORD
|
|
103
105
|
DECOMPOSED
|
|
106
|
+
DECOMPOSE_BLOCKED
|
|
104
107
|
DESIGN
|
|
105
108
|
DISPATCH
|
|
106
109
|
DISPATCHER
|
|
@@ -133,6 +136,9 @@ MARKER_REGISTRY="
|
|
|
133
136
|
PATTERN
|
|
134
137
|
PHASE
|
|
135
138
|
PHASE_COMPLETE
|
|
139
|
+
PHASE_TRAIL_ERROR
|
|
140
|
+
PHASE_TRAIL_FAILED
|
|
141
|
+
PHASE_TRAIL_RELEASED
|
|
136
142
|
PLAN_DAG
|
|
137
143
|
PRIOR_DECISIONS
|
|
138
144
|
PRIOR_GIST
|
|
@@ -140,6 +146,9 @@ MARKER_REGISTRY="
|
|
|
140
146
|
PUSH_BLOCKED
|
|
141
147
|
PUSH_BLOCKED_EMPTY_BRANCH
|
|
142
148
|
PUSH_FAILED
|
|
149
|
+
QUALITY_GATE
|
|
150
|
+
RECOVERY_CLAIM
|
|
151
|
+
RECOVERY_CLAIM_RELEASED
|
|
143
152
|
REMEDIATION
|
|
144
153
|
REREVIEW_SKIPPED
|
|
145
154
|
REVIEW
|
|
@@ -152,6 +161,7 @@ MARKER_REGISTRY="
|
|
|
152
161
|
SPAWN_POLICY
|
|
153
162
|
SPEC_DOCTOR_COMPLETE
|
|
154
163
|
SPEC_LOADED
|
|
164
|
+
STALE_REREVIEW
|
|
155
165
|
STALL_DETECTED
|
|
156
166
|
STATE
|
|
157
167
|
SYNTHESIS_BRIEF
|
|
@@ -20,33 +20,18 @@ extract_classifier() {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
CLOSE_BLOCK=$(extract_block "$ROOT/commands/work-on/close.md")
|
|
23
|
-
|
|
24
|
-
[[ "$CLOSE_BLOCK" == "$INLINE_BLOCK" ]] || {
|
|
25
|
-
printf 'FAIL: mirrored classifier blocks differ\n' >&2
|
|
26
|
-
exit 1
|
|
27
|
-
}
|
|
28
|
-
|
|
23
|
+
[ -n "$CLOSE_BLOCK" ] || { printf 'FAIL: close.md structural computation block not found\n' >&2; exit 1; }
|
|
29
24
|
CLOSE_CLASSIFIER=$(extract_classifier "$ROOT/commands/work-on/close.md")
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
25
|
+
[ -n "$CLOSE_CLASSIFIER" ] || { printf 'FAIL: close.md classifier not found\n' >&2; exit 1; }
|
|
26
|
+
# close.md is the single owner of the classifier; the work-on.md router must not carry a copy.
|
|
27
|
+
if grep -q '^FENCE_COUNT=' "$ROOT/commands/work-on.md"; then
|
|
28
|
+
printf 'FAIL: work-on.md (router) must not contain a copy of the close-path classifier\n' >&2
|
|
33
29
|
exit 1
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if grep -q 'echo "\$BODY\|echo "\$BODY_STRIPPED' \
|
|
37
|
-
"$ROOT/commands/work-on/close.md" "$ROOT/commands/work-on.md"; then
|
|
30
|
+
fi
|
|
31
|
+
if grep -q 'echo "\$BODY\|echo "\$BODY_STRIPPED' "$ROOT/commands/work-on/close.md"; then
|
|
38
32
|
printf 'FAIL: arbitrary issue bodies must be piped with printf, not echo\n' >&2
|
|
39
33
|
exit 1
|
|
40
34
|
fi
|
|
41
|
-
|
|
42
|
-
grep -Fq 'Keep the structural computation above and its consuming multi-phase guard' "$ROOT/commands/work-on.md" || {
|
|
43
|
-
printf 'FAIL: Phase 6A sync invariant must cover the consuming guard\n' >&2
|
|
44
|
-
exit 1
|
|
45
|
-
}
|
|
46
|
-
grep -Fq 'The guard is `CHECKBOX_SECTIONS >= 2 OR SUBISSUE_ITEMS > 0`' "$ROOT/commands/work-on.md" || {
|
|
47
|
-
printf 'FAIL: Phase 6A must document the Phase C1 guard\n' >&2
|
|
48
|
-
exit 1
|
|
49
|
-
}
|
|
50
35
|
grep -Fq 'if [ "${CHECKBOX_SECTIONS:-0}" -ge 2 ] || [ "${SUBISSUE_ITEMS:-0}" -gt 0 ]; then' "$ROOT/commands/work-on/close.md" || {
|
|
51
36
|
printf 'FAIL: Phase C1 multi-phase guard changed unexpectedly\n' >&2
|
|
52
37
|
exit 1
|
|
@@ -64,4 +49,4 @@ SINGLE_BODY=$'## Acceptance Criteria\n- [ ] complete this work'
|
|
|
64
49
|
exit 1
|
|
65
50
|
}
|
|
66
51
|
|
|
67
|
-
printf 'PASS:
|
|
52
|
+
printf 'PASS: close-path classifier supports setext section boundaries\n'
|
|
@@ -83,7 +83,10 @@
|
|
|
83
83
|
|
|
84
84
|
set -euo pipefail
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
# An optional line-reference suffix is accepted and stripped: review findings cite
|
|
87
|
+
# `file.md:211`, `file.sh:104-114,162` or `file.md:~1685`, which the strict form missed,
|
|
88
|
+
# leaving every finding-batch DAG on the low-confidence body fallback (field test).
|
|
89
|
+
EXT_REGEX='`[^`]*\.(py|tsx?|jsx?|sql|json|ya?ml|mjs|js|sh|md)(:~?[0-9][0-9,~-]*)?`'
|
|
87
90
|
|
|
88
91
|
# --------------------------------------------------------------------------- #
|
|
89
92
|
# Argument parsing — mirrors scripts/issue-dedup.sh's -R / -R\ * case arms
|
|
@@ -190,7 +193,7 @@ extract_paths() {
|
|
|
190
193
|
# locales` otherwise) — a portability trap the original inline pseudocode
|
|
191
194
|
# in phase-3-dependency.md carried (forge#2436) and that this script fixes
|
|
192
195
|
# by not needing PCRE at all.
|
|
193
|
-
grep -oE "$EXT_REGEX" <<< "$1" 2>/dev/null | tr -d '`' | sort -u || true
|
|
196
|
+
grep -oE "$EXT_REGEX" <<< "$1" 2>/dev/null | tr -d '`' | sed -E 's/:~?[0-9][0-9,~-]*$//' | sort -u || true
|
|
194
197
|
}
|
|
195
198
|
|
|
196
199
|
PROVENANCE="none"
|
|
@@ -472,6 +472,33 @@ MOCK_GH_CONTRACT_FAIL=1 MOCK_GH_COMMENTS="$INV_DIFFERENT" assert_output \
|
|
|
472
472
|
|
|
473
473
|
unset MOCK_GH_CONTRACT_FAIL
|
|
474
474
|
|
|
475
|
+
# --------------------------------------------------------------------------- #
|
|
476
|
+
# Line-reference suffixes (field test, orchestrate #3149 / stress batch): review
|
|
477
|
+
# findings cite paths as `file:line`, `file:a-b,c` or `file:~n`. Those must be
|
|
478
|
+
# extracted with the suffix stripped, and the same file cited twice deduplicated.
|
|
479
|
+
# --------------------------------------------------------------------------- #
|
|
480
|
+
BODY_LINESUFFIX="$TMP_FIXTURES/body-linesuffix.md"
|
|
481
|
+
cat > "$BODY_LINESUFFIX" <<'EOF'
|
|
482
|
+
## Problem
|
|
483
|
+
|
|
484
|
+
Gate trusts agent-chosen band.
|
|
485
|
+
|
|
486
|
+
## Affected Files
|
|
487
|
+
|
|
488
|
+
1. `scripts/verify-phase-trail.sh:211` — band not cross-checked
|
|
489
|
+
2. `scripts/verify-phase-trail.sh:104-114,162` — pagination
|
|
490
|
+
3. `commands/orchestrate/phase-4-execution.md:~1685` — completion check
|
|
491
|
+
4. `commands/work-on/build/validate.md:433` — QG PASS not SHA-bound
|
|
492
|
+
|
|
493
|
+
## Source Branch Context
|
|
494
|
+
|
|
495
|
+
**Code branch**: `staging`
|
|
496
|
+
EOF
|
|
497
|
+
unset MOCK_GH_COMMENTS
|
|
498
|
+
MOCK_GH_BODY="$BODY_LINESUFFIX" assert_output \
|
|
499
|
+
"line-reference suffixes are stripped and deduplicated" \
|
|
500
|
+
"body-fallback" "commands/orchestrate/phase-4-execution.md,commands/work-on/build/validate.md,scripts/verify-phase-trail.sh" 2390 -R test/repo
|
|
501
|
+
|
|
475
502
|
# --------------------------------------------------------------------------- #
|
|
476
503
|
# Summary
|
|
477
504
|
# --------------------------------------------------------------------------- #
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# forge-root.test.sh — guards the canonical FORGE_ROOT bootstrap (forge#3098).
|
|
3
|
+
# Plugin installs set neither FORGEDOCK_HOME nor FORGE_HOME; the specs must resolve ForgeDock's OWN
|
|
4
|
+
# install root (never the consumer repo) and fail closed when it cannot be found.
|
|
5
|
+
# Usage: bash scripts/forge-root.test.sh
|
|
6
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
7
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
8
|
+
set -uo pipefail
|
|
9
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"; ROOT="$HERE/.."
|
|
10
|
+
T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
|
|
11
|
+
PASS=0; FAILN=0
|
|
12
|
+
ok() { PASS=$((PASS+1)); }
|
|
13
|
+
bad() { FAILN=$((FAILN+1)); echo "FAIL: $1"; }
|
|
14
|
+
expect() { [ "$2" = "$3" ] && ok || bad "$1 (got '$3' want '$2')"; }
|
|
15
|
+
|
|
16
|
+
SITES="commands/work-on.md commands/work-on/review.md commands/work-on/investigate.md commands/work-on/decompose.md commands/work-on/build.md commands/work-on/close.md commands/review-pr.md commands/orchestrate/phase-1-resolve.md commands/orchestrate/phase-4-execution.md"
|
|
17
|
+
|
|
18
|
+
# extract the bootstrap block (comment line .. closing top-level fi), indentation stripped
|
|
19
|
+
# Block ends at the first line (after the marker) that starts with `fi` at the marker's own indentation.
|
|
20
|
+
# extract <file> <n> prints the n-th (1-based) bootstrap copy in the file.
|
|
21
|
+
extract() { awk -v want="$2" '/# FORGE_ROOT bootstrap/{c++; if(c==want){f=1; match($0,/^[ ]*/); ind=RLENGTH}} f{l=$0; sub(/^[ ]+/,"",l); print l} f&&match($0,/^[ ]*fi$/)&&RLENGTH-2==ind{exit}' "$ROOT/$1"; }
|
|
22
|
+
count_copies() { grep -c '# FORGE_ROOT bootstrap' "$ROOT/$1"; }
|
|
23
|
+
extract commands/work-on.md 1 > "$T/canon"
|
|
24
|
+
[ -s "$T/canon" ] && ok || bad "canonical bootstrap not found in work-on.md"
|
|
25
|
+
# Expected copies per file are DERIVED from the number of FORGE_ROOT resolution loops
|
|
26
|
+
# (`FORGE_ROOT="$_c"` lines) so adding a site never needs a hardcoded count here; count_copies
|
|
27
|
+
# (marker comments) must still agree with it, which catches a loop pasted without its marker.
|
|
28
|
+
want_copies() { grep -c 'FORGE_ROOT="\$_c"' "$ROOT/$1"; }
|
|
29
|
+
for f in $SITES; do
|
|
30
|
+
n=$(count_copies "$f"); expect "exact bootstrap copy count in $f" "$(want_copies "$f")" "$n"
|
|
31
|
+
i=1
|
|
32
|
+
while [ "$i" -le "$n" ]; do # compare EVERY copy
|
|
33
|
+
extract "$f" "$i" > "$T/s"
|
|
34
|
+
cmp -s "$T/canon" "$T/s" && ok || bad "bootstrap copy $i in $f differs from canonical"
|
|
35
|
+
i=$((i+1))
|
|
36
|
+
done
|
|
37
|
+
done
|
|
38
|
+
# the canonical block must not contain the stale lexical glob / unfiltered marketplaces glob
|
|
39
|
+
grep -v '^[[:space:]]*#' "$T/canon" > "$T/canon.code" # code only: the comments name the banned constructs
|
|
40
|
+
grep -qE 'mapfile|readarray|declare -A|sort[^|]* -[a-zA-Z]*V' "$T/canon.code" && bad "canonical bootstrap uses a bash-4+/non-portable construct" || ok
|
|
41
|
+
grep -q 'sort -k1,1nr -k2,2nr -k3,3nr' "$T/canon.code" && ok || bad "canonical bootstrap lacks numeric component version sort"
|
|
42
|
+
grep -qF 'marketplaces/*; do' "$T/canon" && bad "canonical bootstrap globs all marketplaces" || ok
|
|
43
|
+
# review.md must hard-exit on an unreadable trail
|
|
44
|
+
grep -qE 'TRAIL_RC.* -ge 2 .*exit 1' "$ROOT/commands/work-on/review.md" && ok || bad "work-on/review.md lacks hard exit guard for TRAIL_RC>=2"
|
|
45
|
+
# behavioral: execute the guard line itself. rc>=2 => BLOCKED on STDOUT + exit 1; rc 0/1 => falls through.
|
|
46
|
+
grep -E 'TRAIL_RC.* -ge 2 .*exit 1' "$ROOT/commands/work-on/review.md" | head -1 > "$T/guard"
|
|
47
|
+
for rc in 2 127; do
|
|
48
|
+
out=$(TRAIL_RC=$rc bash -c "$(cat "$T/guard")" 2>/dev/null); grc=$?
|
|
49
|
+
expect "review guard exits 1 for TRAIL_RC=$rc" 1 "$grc"
|
|
50
|
+
# The guard prints the full REVIEW_RESULT block (multi-line) to STDOUT.
|
|
51
|
+
expect "review guard prints REVIEW_RESULT block for TRAIL_RC=$rc" "REVIEW_RESULT:| status: BLOCKED| blocker: phase trail unreadable (rc=$rc)" "$(printf '%s\n' "$out" | grep -E '^(REVIEW_RESULT:| status:| blocker:)' | paste -sd'|' -)"
|
|
52
|
+
done
|
|
53
|
+
for rc in 0 1; do
|
|
54
|
+
out=$(TRAIL_RC=$rc bash -c "$(cat "$T/guard"); echo passed" 2>&1); expect "review guard falls through for TRAIL_RC=$rc" passed "$out"
|
|
55
|
+
done
|
|
56
|
+
|
|
57
|
+
# no consumer-repo fallback / fail-open remains in the gates
|
|
58
|
+
for f in $SITES; do
|
|
59
|
+
grep -nE 'FORGE_HOME:-\$?\{?\{?REPO_PATH\}*/scripts' "$ROOT/$f" >/dev/null && bad "repo-path fallback in $f" || ok
|
|
60
|
+
done
|
|
61
|
+
for f in commands/review-pr.md commands/work-on/review.md; do
|
|
62
|
+
grep -nE '^[[:space:]]*TRAIL_RC=0[[:space:]]*$' "$ROOT/$f" >/dev/null && bad "fail-open TRAIL_RC=0 in $f" || ok
|
|
63
|
+
done
|
|
64
|
+
|
|
65
|
+
run() { # run <home> [env assignments...] -> prints FORGE_ROOT
|
|
66
|
+
# CLAUDE_PLUGIN_ROOT=<v> is NOT an env var in a real session: Claude Code substitutes the literal
|
|
67
|
+
# text ${CLAUDE_PLUGIN_ROOT} when it loads a plugin spec. Simulate exactly that; every other
|
|
68
|
+
# assignment is passed through as environment.
|
|
69
|
+
local h="$1"; shift; local code; code="$(cat "$T/canon")"; local a; local envs=()
|
|
70
|
+
for a in "$@"; do
|
|
71
|
+
case "$a" in
|
|
72
|
+
CLAUDE_PLUGIN_ROOT=*) code="$(printf '%s' "$code" | awk -v v="${a#CLAUDE_PLUGIN_ROOT=}" '{gsub(/\$\{CLAUDE_PLUGIN_ROOT\}/, v); print}')" ;;
|
|
73
|
+
*) envs+=("$a") ;;
|
|
74
|
+
esac
|
|
75
|
+
done
|
|
76
|
+
( cd "$T/consumer" && env -i PATH="$PATH" HOME="$h" ${envs[@]+"${envs[@]}"} "$SH" -c "$code; printf %s \"\$FORGE_ROOT\"" )
|
|
77
|
+
}
|
|
78
|
+
# Run the behavioral cases under every available shell: bash always, zsh when installed (macOS ships it;
|
|
79
|
+
# zsh aborts on an unmatched glob, which the bootstrap must not trigger). FORGE_ROOT_TEST_SHELLS overrides.
|
|
80
|
+
SHELLS="${FORGE_ROOT_TEST_SHELLS:-bash}"
|
|
81
|
+
if [ -z "${FORGE_ROOT_TEST_SHELLS:-}" ] && command -v zsh >/dev/null 2>&1; then SHELLS="bash zsh"; fi
|
|
82
|
+
for SH in $SHELLS; do
|
|
83
|
+
echo "== bootstrap behavior under: $SH"
|
|
84
|
+
mkdir -p "$T/consumer/scripts"; : > "$T/consumer/scripts/verify-phase-trail.sh" # hostile/lookalike consumer repo
|
|
85
|
+
mkscripts() {
|
|
86
|
+
mkdir -p "$1/scripts" "$1/bin/engine"
|
|
87
|
+
: > "$1/scripts/verify-phase-trail.sh"; : > "$1/scripts/lint-dispatch-prompt.sh"; : > "$1/scripts/is-docs-only.sh"
|
|
88
|
+
: > "$1/bin/engine/resolve.mjs"; : > "$1/bin/engine/orchestrate-canary.mjs"; : > "$1/bin/engine/admission.mjs"
|
|
89
|
+
}
|
|
90
|
+
# plugin install: no env vars at all
|
|
91
|
+
mkscripts "$T/h1/.claude/plugins/cache/mk/forgedock/1.0.0"
|
|
92
|
+
expect "plugin cache resolves with no env" "$T/h1/.claude/plugins/cache/mk/forgedock/1.0.0" "$(run "$T/h1")"
|
|
93
|
+
expect "CLAUDE_PLUGIN_ROOT resolves" "$T/h1/.claude/plugins/cache/mk/forgedock/1.0.0" "$(run "$T/empty" CLAUDE_PLUGIN_ROOT="$T/h1/.claude/plugins/cache/mk/forgedock/1.0.0")"
|
|
94
|
+
# nothing installed: empty (never the consumer repo, never a relative path)
|
|
95
|
+
expect "unresolvable stays empty" "" "$(run "$T/empty")"
|
|
96
|
+
expect "relative FORGE_HOME ignored" "" "$(run "$T/empty" FORGE_HOME=.)"
|
|
97
|
+
# explicit FORGEDOCK_HOME is authoritative, even if bogus (caller then fails closed)
|
|
98
|
+
expect "FORGEDOCK_HOME authoritative" "$T/nowhere" "$(run "$T/h1" FORGEDOCK_HOME="$T/nowhere")"
|
|
99
|
+
# symlink install (install.sh): ~/.claude/commands/work-on.md -> <clone>/commands/work-on.md
|
|
100
|
+
mkdir -p "$T/clone/commands" "$T/h2/.claude/commands"; mkscripts "$T/clone"; : > "$T/clone/commands/work-on.md"
|
|
101
|
+
ln -sf "$T/clone/commands/work-on.md" "$T/h2/.claude/commands/work-on.md"
|
|
102
|
+
expect "install.sh symlink resolves" "$(cd "$T/clone" && pwd -P)" "$(cd "$(run "$T/h2")" && pwd -P)"
|
|
103
|
+
expect "FORGE_HOME without scripts falls through" "$(cd "$T/clone" && pwd -P)" "$(cd "$(run "$T/h2" FORGE_HOME="$T/h2/.claude")" && pwd -P)"
|
|
104
|
+
|
|
105
|
+
# newest cached version wins (1.10.0 must beat 1.9.0)
|
|
106
|
+
for v in 1.9.0 1.10.0 1.2.0; do mkscripts "$T/h3/.claude/plugins/cache/mk/forgedock/$v"; done
|
|
107
|
+
expect "newest cached version wins" "$T/h3/.claude/plugins/cache/mk/forgedock/1.10.0" "$(run "$T/h3")"
|
|
108
|
+
# HOME containing a space still resolves
|
|
109
|
+
mkscripts "$T/sp ace/.claude/plugins/cache/mk/forgedock/2.0.0"
|
|
110
|
+
expect "space in HOME resolves" "$T/sp ace/.claude/plugins/cache/mk/forgedock/2.0.0" "$(run "$T/sp ace")"
|
|
111
|
+
# relative FORGEDOCK_HOME / CLAUDE_PLUGIN_ROOT rejected (fail closed => empty)
|
|
112
|
+
expect "relative FORGEDOCK_HOME rejected" "" "$(run "$T/h1" FORGEDOCK_HOME=scripts/..)"
|
|
113
|
+
expect "relative CLAUDE_PLUGIN_ROOT ignored" "" "$(run "$T/empty" CLAUDE_PLUGIN_ROOT=../consumer)"
|
|
114
|
+
# a candidate with only one of the two needed scripts is rejected
|
|
115
|
+
mkdir -p "$T/h4/.claude/plugins/cache/mk/forgedock/1.0.0/scripts"; : > "$T/h4/.claude/plugins/cache/mk/forgedock/1.0.0/scripts/verify-phase-trail.sh"
|
|
116
|
+
expect "partial install (missing lint script) rejected" "" "$(run "$T/h4")"
|
|
117
|
+
# stale install: has the two original gate scripts but lacks is-docs-only.sh / engine modules => rejected
|
|
118
|
+
for missing in scripts/is-docs-only.sh bin/engine/resolve.mjs bin/engine/orchestrate-canary.mjs bin/engine/admission.mjs; do
|
|
119
|
+
rm -rf "$T/h12"; mkscripts "$T/h12/.claude/plugins/cache/mk/forgedock/1.0.0"; rm -f "$T/h12/.claude/plugins/cache/mk/forgedock/1.0.0/$missing"
|
|
120
|
+
expect "stale install missing $missing rejected" "" "$(run "$T/h12")"
|
|
121
|
+
done
|
|
122
|
+
# a stale newest version is skipped in favor of a complete older one
|
|
123
|
+
rm -rf "$T/h13"; mkscripts "$T/h13/.claude/plugins/cache/mk/forgedock/1.0.0"; mkscripts "$T/h13/.claude/plugins/cache/mk/forgedock/2.0.0"
|
|
124
|
+
rm -f "$T/h13/.claude/plugins/cache/mk/forgedock/2.0.0/scripts/is-docs-only.sh"
|
|
125
|
+
expect "stale newest skipped for complete older" "$T/h13/.claude/plugins/cache/mk/forgedock/1.0.0" "$(run "$T/h13")"
|
|
126
|
+
# non-ForgeDock marketplace dirs are ignored; ForgeDock-named ones resolve
|
|
127
|
+
mkscripts "$T/h5/.claude/plugins/marketplaces/other-tool"
|
|
128
|
+
expect "non-ForgeDock marketplace ignored" "" "$(run "$T/h5")"
|
|
129
|
+
mkscripts "$T/h6/.claude/plugins/marketplaces/forgedock"
|
|
130
|
+
expect "forgedock marketplace resolves" "$T/h6/.claude/plugins/marketplaces/forgedock" "$(run "$T/h6")"
|
|
131
|
+
|
|
132
|
+
# release outranks its own pre-release; a newer pre-release core still beats an older release
|
|
133
|
+
rm -rf "$T/h7" # fixture is mutated below; reset per shell
|
|
134
|
+
mkscripts "$T/h7/.claude/plugins/cache/mk/forgedock/1.9.0-rc1"; mkscripts "$T/h7/.claude/plugins/cache/mk/forgedock/1.9.0"
|
|
135
|
+
expect "release beats its pre-release" "$T/h7/.claude/plugins/cache/mk/forgedock/1.9.0" "$(run "$T/h7")"
|
|
136
|
+
mkscripts "$T/h7/.claude/plugins/cache/mk/forgedock/2.0.0-rc1"
|
|
137
|
+
expect "newer-core pre-release beats older release" "$T/h7/.claude/plugins/cache/mk/forgedock/2.0.0-rc1" "$(run "$T/h7")"
|
|
138
|
+
# version component decides, not the marketplace name (zz sorts after aa lexically but holds the older version)
|
|
139
|
+
mkscripts "$T/h8/.claude/plugins/cache/zz-market/forgedock/1.0.0"; mkscripts "$T/h8/.claude/plugins/cache/aa-market/forgedock/1.1.0"
|
|
140
|
+
expect "version beats marketplace name" "$T/h8/.claude/plugins/cache/aa-market/forgedock/1.1.0" "$(run "$T/h8")"
|
|
141
|
+
# a non-forgedock plugin in the cache is ignored even with a higher version
|
|
142
|
+
mkscripts "$T/h9/.claude/plugins/cache/mk/otherplugin/9.9.9"; mkscripts "$T/h9/.claude/plugins/cache/mk/forgedock/1.0.0"
|
|
143
|
+
expect "non-forgedock cache plugin ignored" "$T/h9/.claude/plugins/cache/mk/forgedock/1.0.0" "$(run "$T/h9")"
|
|
144
|
+
# unmatched globs: empty cache dir, absent marketplaces dir, absent ~/.claude entirely (must not abort under zsh)
|
|
145
|
+
mkdir -p "$T/h10/.claude/plugins/cache"
|
|
146
|
+
expect "empty cache, no match, no abort" "" "$(run "$T/h10")"
|
|
147
|
+
expect "no ~/.claude at all, no abort" "" "$(run "$T/empty")"
|
|
148
|
+
expect "unmatched glob does not break later candidates" "$T/h1/.claude/plugins/cache/mk/forgedock/1.0.0" "$(run "$T/h10" CLAUDE_PLUGIN_ROOT="$T/h1/.claude/plugins/cache/mk/forgedock/1.0.0")"
|
|
149
|
+
# plugin root (textually substituted) outranks an exported FORGE_HOME that also has the scripts:
|
|
150
|
+
# the running plugin's own files win over a stale clone named by a global env var (forge#3147 field test)
|
|
151
|
+
mkscripts "$T/h11/stale-clone"; mkscripts "$T/h11/running-plugin"
|
|
152
|
+
expect "substituted plugin root beats FORGE_HOME" "$T/h11/running-plugin" "$(run "$T/h11" FORGE_HOME="$T/h11/stale-clone" CLAUDE_PLUGIN_ROOT="$T/h11/running-plugin")"
|
|
153
|
+
# unsubstituted placeholder (non-Claude runtime) is a literal, rejected by the /* check, never expanded
|
|
154
|
+
expect "unsubstituted placeholder ignored, FORGE_HOME used" "$T/h11/stale-clone" "$(run "$T/h11" FORGE_HOME="$T/h11/stale-clone")"
|
|
155
|
+
expect "unsubstituted placeholder safe under set -u" "" "$( cd "$T/consumer" && env -i PATH="$PATH" HOME="$T/empty" "$SH" -c "set -u; $(cat "$T/canon"); printf %s \"\$FORGE_ROOT\"" 2>&1 )"
|
|
156
|
+
# the env var alone (no substitution) must NOT be relied on: Claude Code does not export it to Bash
|
|
157
|
+
expect "CLAUDE_PLUGIN_ROOT env var alone is not a candidate" "" "$( cd "$T/consumer" && env -i PATH="$PATH" HOME="$T/empty" CLAUDE_PLUGIN_ROOT="$T/h11/running-plugin" "$SH" -c "$(cat "$T/canon"); printf %s \"\$FORGE_ROOT\"" )"
|
|
158
|
+
done
|
|
159
|
+
# the canonical block must use the exact substitutable spelling, single-quoted (no :- form, which Claude Code leaves verbatim)
|
|
160
|
+
grep -qF "'\${CLAUDE_PLUGIN_ROOT}'" "$T/canon.code" && ok || bad "canonical bootstrap lacks the substitutable '\${CLAUDE_PLUGIN_ROOT}' candidate"
|
|
161
|
+
grep -qF 'CLAUDE_PLUGIN_ROOT:-' "$T/canon.code" && bad "canonical bootstrap uses \${CLAUDE_PLUGIN_ROOT:-}, which Claude Code never substitutes" || ok
|
|
162
|
+
|
|
163
|
+
# no hand-built file:// URL from FORGE_ROOT remains; imports go through pathToFileURL (a '#', '?' or space in the path must survive)
|
|
164
|
+
for f in $SITES commands/work-on/close.md; do
|
|
165
|
+
grep -nE 'file://\$\{?(FORGE_ROOT|\(pwd\))' "$ROOT/$f" >/dev/null && bad "concatenated file:// URL in $f" || ok
|
|
166
|
+
done
|
|
167
|
+
if command -v node >/dev/null 2>&1; then
|
|
168
|
+
for d in "we#ird" "sp ace" "q?x"; do
|
|
169
|
+
mkdir -p "$T/url/$d/bin/engine"; echo 'export const v = 42;' > "$T/url/$d/bin/engine/resolve.mjs"
|
|
170
|
+
out=$(node -e 'import(require("node:url").pathToFileURL(process.argv[1]).href).then(m => process.stdout.write(String(m.v)))' "$T/url/$d/bin/engine/resolve.mjs" 2>&1)
|
|
171
|
+
expect "pathToFileURL import survives path '$d'" 42 "$out"
|
|
172
|
+
done
|
|
173
|
+
fi
|
|
174
|
+
|
|
175
|
+
echo "forge-root tests: pass=$PASS fail=$FAILN"
|
|
176
|
+
[ "$FAILN" -eq 0 ]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# is-docs-only.sh — the single docs-only predicate shared by review-pr.md Phase 8,
|
|
3
|
+
# work-on/review.md R1.5 and orchestrate/phase-4-execution.md (forge#3134, forge#3145).
|
|
4
|
+
# Reads a newline-separated changed-file list on stdin. Callers MUST feed BOTH sides of every rename
|
|
5
|
+
# (git diff --no-renames, or filename + previous_filename from the PR files API): a rename-collapsed
|
|
6
|
+
# list hides the source path of a file moved into docs/.
|
|
7
|
+
# Exit 0 iff the list is non-empty and EVERY path is a Markdown file that is positively allowlisted AND
|
|
8
|
+
# is not executable pipeline/agent instruction content:
|
|
9
|
+
# - only *.md counts (docs/** non-markdown assets do NOT); matching is case-insensitive, leading ./ ignored
|
|
10
|
+
# - allowlist (unknown paths fail closed): docs/**/*.md, or a root-level README*/CHANGELOG*/CONTRIBUTING/SECURITY/GOVERNANCE *.md
|
|
11
|
+
# - any ".." path segment is rejected
|
|
12
|
+
# - excluded at ANY depth (even under docs/): commands/, devdocs/, templates/, skills/, agents/, hooks/,
|
|
13
|
+
# .claude/, .claude-plugin/, .agents/, .codex/, .cursor/, .github/, .opencode/, .gemini/, .kiro/ directories
|
|
14
|
+
# - excluded at ANY depth by exact basename: agents.md, claude.md, claude.local.md, skill.md, gemini.md
|
|
15
|
+
# (and dotted variants such as AGENTS.override.md); docs like skillset.md or agents-guide.md are fine
|
|
16
|
+
# Exit 1 otherwise (including empty input: fail closed).
|
|
17
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
18
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
19
|
+
awk '
|
|
20
|
+
NF==0 {next}
|
|
21
|
+
{ n++; p=tolower($0); sub(/^\.\//, "", p) }
|
|
22
|
+
!(p ~ /\.md$/) {bad=1; next}
|
|
23
|
+
p ~ /(^|\/)\.\.(\/|$)/ {bad=1; next}
|
|
24
|
+
p ~ /(^|\/)(commands|devdocs|templates|skills|agents|hooks|\.claude|\.claude-plugin|\.agents|\.codex|\.cursor|\.github|\.opencode|\.gemini|\.kiro)\// {bad=1; next}
|
|
25
|
+
p ~ /(^|\/)(agents|claude|skill|gemini)(\.[^\/]*)?\.md$/ {bad=1; next}
|
|
26
|
+
p ~ /^docs\/.+\.md$/ {next}
|
|
27
|
+
p ~ /^(readme|changelog)[^\/]*\.md$/ {next}
|
|
28
|
+
p ~ /^(contributing|security|governance)\.md$/ {next}
|
|
29
|
+
{bad=1}
|
|
30
|
+
END{exit (bad || n==0)}
|
|
31
|
+
'
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# is-docs-only.test.sh — cases for scripts/is-docs-only.sh (forge#3134).
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
set -uo pipefail
|
|
6
|
+
S="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/is-docs-only.sh"
|
|
7
|
+
PASS=0; FAILN=0
|
|
8
|
+
t() { # name want files...
|
|
9
|
+
local name="$1" want="$2"; shift 2
|
|
10
|
+
printf '%s\n' "$@" | bash "$S"; local rc=$?
|
|
11
|
+
if [ "$rc" = "$want" ]; then PASS=$((PASS+1)); else FAILN=$((FAILN+1)); echo "FAIL: $name (rc=$rc want $want)"; fi
|
|
12
|
+
}
|
|
13
|
+
t "docs md" 0 docs/a.md README.md docs/sub/b.md
|
|
14
|
+
t "root AGENTS.md" 1 AGENTS.md
|
|
15
|
+
t "root CLAUDE.md" 1 docs/a.md CLAUDE.md
|
|
16
|
+
t "nested AGENTS.md" 1 sub/AGENTS.md
|
|
17
|
+
t "nested CLAUDE.md" 1 pkg/x/CLAUDE.md
|
|
18
|
+
t ".codex md" 1 .codex/x.md
|
|
19
|
+
t "SKILL.md" 1 skills/a/SKILL.md
|
|
20
|
+
t ".github md" 1 .github/x.md
|
|
21
|
+
t ".agents md" 1 .agents/skills/x.md
|
|
22
|
+
t ".claude md" 1 .claude/x.md
|
|
23
|
+
t "commands md" 1 commands/work-on.md
|
|
24
|
+
t "nested commands md" 1 pkg/commands/x.md
|
|
25
|
+
t "docs non-md" 1 docs/diagram.png
|
|
26
|
+
t "docs yml" 1 docs/a.md docs/site/mkdocs.yml
|
|
27
|
+
t "code file" 1 docs/a.md bin/x.mjs
|
|
28
|
+
t "empty" 1
|
|
29
|
+
t "blank only" 1 "" ""
|
|
30
|
+
t "case agents.md" 1 agents.md
|
|
31
|
+
t "case Skill.md" 1 Skill.md
|
|
32
|
+
t "case Commands dir" 1 Commands/x.md
|
|
33
|
+
t "case .GitHub" 1 .GitHub/x.md
|
|
34
|
+
t "CLAUDE.local.md" 1 CLAUDE.local.md
|
|
35
|
+
t "GEMINI.md" 1 GEMINI.md
|
|
36
|
+
t ".cursor rules" 1 .cursor/rules/x.md
|
|
37
|
+
t "hooks md" 1 hooks/README.md
|
|
38
|
+
t ".claude-plugin" 1 .claude-plugin/x.md
|
|
39
|
+
t "upper .MD (case-insensitive markdown)" 0 docs/a.MD
|
|
40
|
+
t "leading ./ docs" 0 ./docs/a.md
|
|
41
|
+
t "leading ./ excluded" 1 ./commands/x.md
|
|
42
|
+
t "docs traversal" 1 docs/../commands/x.md
|
|
43
|
+
t "valid + blank lines" 0 docs/a.md "" README.md
|
|
44
|
+
t "rename pair (source in commands/)" 1 commands/x.md docs/x.md
|
|
45
|
+
t "rename collapsed dest only (caller bug, predicate alone cannot see)" 0 docs/x.md
|
|
46
|
+
t "devdocs md" 1 devdocs/agent/custom-instructions.md
|
|
47
|
+
t "templates devdocs agent md" 1 templates/devdocs/agent/x.md
|
|
48
|
+
t ".opencode md" 1 .opencode/x.md
|
|
49
|
+
t ".gemini md" 1 .gemini/x.md
|
|
50
|
+
t ".kiro md" 1 .kiro/steering/x.md
|
|
51
|
+
t "skills dir md" 1 skills/a/readme.md
|
|
52
|
+
t "templates md" 1 templates/x.md
|
|
53
|
+
t "unknown dir md fails closed" 1 notes/x.md
|
|
54
|
+
t "nested unknown README fails closed" 1 pkg/README.md
|
|
55
|
+
t "docs/skillset.md accepted" 0 docs/skillset.md
|
|
56
|
+
t "docs/Agents-guide.md accepted" 0 docs/Agents-guide.md
|
|
57
|
+
t "docs/AGENTS.md rejected" 1 docs/AGENTS.md
|
|
58
|
+
t "AGENTS.override.md rejected" 1 AGENTS.override.md
|
|
59
|
+
t "docs/skill.md rejected" 1 docs/skill.md
|
|
60
|
+
t "root CHANGELOG" 0 CHANGELOG.md
|
|
61
|
+
t "root CONTRIBUTING" 0 CONTRIBUTING.md
|
|
62
|
+
t "docs under devdocs-named dir" 1 docs/devdocs/x.md
|
|
63
|
+
echo "is-docs-only: $PASS passed, $FAILN failed"
|
|
64
|
+
[ "$FAILN" -eq 0 ]
|