bigpowers 2.77.0 → 2.77.1

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.
Files changed (72) hide show
  1. package/.pi/package.json +1 -1
  2. package/.pi/prompts/audit-code.md +1 -1
  3. package/.pi/prompts/craft-skill.md +2 -2
  4. package/.pi/prompts/elaborate-spec.md +1 -1
  5. package/.pi/prompts/evolve-skill.md +1 -1
  6. package/.pi/prompts/grill-me.md +1 -1
  7. package/.pi/prompts/guard-git.md +1 -1
  8. package/.pi/prompts/investigate-bug.md +2 -2
  9. package/.pi/prompts/model-domain.md +3 -3
  10. package/.pi/prompts/plan-refactor.md +2 -2
  11. package/.pi/prompts/plan-release.md +2 -2
  12. package/.pi/prompts/using-bigpowers.md +1 -1
  13. package/.pi/prompts/write-document.md +2 -2
  14. package/.pi/skills/audit-code/SKILL.md +1 -1
  15. package/.pi/skills/change-request/SKILL.md +1 -1
  16. package/.pi/skills/commit-message/SKILL.md +3 -3
  17. package/.pi/skills/compose-workflow/SKILL.md +1 -1
  18. package/.pi/skills/craft-skill/SKILL.md +4 -4
  19. package/.pi/skills/deepen-architecture/SKILL.md +13 -13
  20. package/.pi/skills/deploy/SKILL.md +4 -4
  21. package/.pi/skills/develop-tdd/SKILL.md +5 -5
  22. package/.pi/skills/elaborate-spec/SKILL.md +1 -1
  23. package/.pi/skills/evolve-skill/SKILL.md +2 -2
  24. package/.pi/skills/extract-design/SKILL.md +1 -1
  25. package/.pi/skills/generate-allure-report/SKILL.md +1 -1
  26. package/.pi/skills/grill-me/SKILL.md +2 -2
  27. package/.pi/skills/grill-with-docs/SKILL.md +1 -1
  28. package/.pi/skills/guard-git/SKILL.md +5 -5
  29. package/.pi/skills/harden-vps/SKILL.md +1 -1
  30. package/.pi/skills/investigate-bug/SKILL.md +2 -2
  31. package/.pi/skills/migrate-spec/SKILL.md +13 -13
  32. package/.pi/skills/model-domain/SKILL.md +5 -5
  33. package/.pi/skills/orchestrate-project/SKILL.md +2 -2
  34. package/.pi/skills/organize-workspace/SKILL.md +1 -1
  35. package/.pi/skills/plan-refactor/SKILL.md +2 -2
  36. package/.pi/skills/plan-release/SKILL.md +2 -2
  37. package/.pi/skills/plan-work/SKILL.md +2 -2
  38. package/.pi/skills/publish-package/SKILL.md +8 -8
  39. package/.pi/skills/release-branch/SKILL.md +2 -2
  40. package/.pi/skills/research-first/SKILL.md +1 -1
  41. package/.pi/skills/run-evals/SKILL.md +1 -1
  42. package/.pi/skills/security-review/SKILL.md +3 -3
  43. package/.pi/skills/seed-conventions/SKILL.md +3 -3
  44. package/.pi/skills/smoke-test/SKILL.md +5 -5
  45. package/.pi/skills/stocktake-skills/SKILL.md +1 -1
  46. package/.pi/skills/using-bigpowers/SKILL.md +1 -1
  47. package/.pi/skills/validate-contracts/SKILL.md +6 -6
  48. package/.pi/skills/verify-work/SKILL.md +1 -1
  49. package/.pi/skills/wire-ci/SKILL.md +8 -8
  50. package/.pi/skills/write-document/SKILL.md +3 -3
  51. package/CHANGELOG.md +7 -0
  52. package/package.json +4 -3
  53. package/scripts/adapters/pi.sh +4 -1
  54. package/scripts/check-skill-links.py +78 -0
  55. package/scripts/generate-reference-tables.sh +2 -1
  56. package/scripts/lib/__pycache__/link_utils.cpython-312.pyc +0 -0
  57. package/scripts/lib/link_utils.py +49 -0
  58. package/scripts/lib/srp-engine.py +86 -1
  59. package/skills/audit-code/SKILL.md +1 -1
  60. package/skills/craft-skill/SKILL.md +2 -2
  61. package/skills/elaborate-spec/SKILL.md +1 -1
  62. package/skills/evolve-skill/REFERENCE.md +1 -1
  63. package/skills/grill-me/REFERENCE.md +1 -1
  64. package/skills/guard-git/SKILL.md +1 -1
  65. package/skills/investigate-bug/SKILL.md +2 -2
  66. package/skills/model-domain/CONTEXT-FORMAT.md +3 -3
  67. package/skills/plan-refactor/SKILL.md +2 -2
  68. package/skills/plan-release/SKILL.md +2 -2
  69. package/skills/using-bigpowers/SKILL.md +1 -1
  70. package/skills/write-document/REFERENCE.md +2 -2
  71. package/skills-lock.json +8 -8
  72. package/tests/test_srp_engine.py +148 -0
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env python3
2
+ # check-skill-links.py — fail on machine-absolute paths or dangling relative
3
+ # links in skill docs. Scans skills/**/SKILL.md and .pi/skills/**/SKILL.md.
4
+ # Run: python3 scripts/check-skill-links.py (exit 1 with a report on failure)
5
+ #
6
+ # NOTE: .cursor/rules/ and .gemini/extensions/ are intentionally excluded.
7
+ # Those trees are auto-generated from SKILL.md sources via sync-skills.sh and
8
+ # are not published to npm, so dangling links there are a rendering gap, not a
9
+ # source defect. Enable once the sync pipeline gains link-rewriting support.
10
+ # See: https://github.com/danielvm-git/bigpowers/issues/78
11
+ import glob
12
+ import os
13
+ import sys
14
+
15
+ # Import shared patterns — single source of truth so the rewriter (srp-engine)
16
+ # and the gate always agree on what constitutes a "link".
17
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
18
+ from lib.link_utils import LINK_RE, EXTERNAL_RE, MACHINE_PATH_RE, strip_code_spans # noqa: E402
19
+
20
+
21
+ def resolve_repo_root() -> str:
22
+ here = os.path.dirname(os.path.abspath(__file__))
23
+ candidate = os.path.dirname(here) # scripts/ -> repo root
24
+ if os.path.isdir(os.path.join(candidate, "skills")):
25
+ return candidate
26
+ return os.getcwd()
27
+
28
+
29
+ def check_file_links(smd: str, root: str) -> list[str]:
30
+ """Return a list of problem strings for a single SKILL.md file."""
31
+ rel = os.path.relpath(smd, root)
32
+ with open(smd, encoding="utf-8") as f:
33
+ text = f.read()
34
+
35
+ problems = []
36
+
37
+ # Machine-absolute path check runs on raw text (line-by-line for location).
38
+ for i, line in enumerate(text.splitlines(), 1):
39
+ if MACHINE_PATH_RE.search(line):
40
+ problems.append(f"{rel}:{i}: machine-absolute path: {line.strip()[:100]}")
41
+
42
+ # Link check runs on shadow (code spans blanked) to avoid false positives
43
+ # on example paths shown inside fenced blocks or inline code.
44
+ shadow = strip_code_spans(text)
45
+ for m in LINK_RE.finditer(shadow):
46
+ target = m.group(2).split("#", 1)[0] # strip fragment
47
+ if not target or EXTERNAL_RE.match(target) or target.startswith(("#", "/")):
48
+ continue
49
+ resolved = os.path.normpath(os.path.join(os.path.dirname(smd), target))
50
+ if not os.path.exists(resolved):
51
+ problems.append(f"{rel}: dangling link -> {target}")
52
+
53
+ return problems
54
+
55
+
56
+ def check(root: str) -> list[str]:
57
+ problems = []
58
+ for tree in ("skills", os.path.join(".pi", "skills")):
59
+ pattern = os.path.join(root, tree, "*", "SKILL.md")
60
+ for smd in sorted(glob.glob(pattern)):
61
+ problems.extend(check_file_links(smd, root))
62
+ return problems
63
+
64
+
65
+ def main() -> int:
66
+ root = resolve_repo_root()
67
+ problems = check(root)
68
+ if problems:
69
+ print(f"check-skill-links: {len(problems)} problem(s):")
70
+ for p in problems:
71
+ print(f" {p}")
72
+ return 1
73
+ print("check-skill-links: OK — no machine paths or dangling links")
74
+ return 0
75
+
76
+
77
+ if __name__ == "__main__":
78
+ sys.exit(main())
@@ -32,10 +32,11 @@ BLOCK="<!-- AUTO-GENERATED-CATALOG: begin — do not edit manually; run scripts/
32
32
  for row in "${ROWS[@]}"; do
33
33
  skill="${row%% *}"
34
34
  model="${row##* }"
35
+ skill_rel="${skill#$REPO_ROOT/}"
35
36
  # capitalise first letter for display
36
37
  model_cap="$(echo "${model:0:1}" | tr '[:lower:]' '[:upper:]')${model:1}"
37
38
  BLOCK+="
38
- | \`$skill\` | **$model_cap** |"
39
+ | \`$skill_rel\` | **$model_cap** |"
39
40
  done
40
41
  BLOCK+="
41
42
 
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env python3
2
+ """link_utils.py — shared Markdown link patterns used by srp-engine and check-skill-links.
3
+
4
+ Single source of truth for what constitutes a "link" so the rewriter and the
5
+ regression gate always agree on the definition.
6
+ """
7
+ import re
8
+
9
+ # Matches standard Markdown links — with or without an optional title attribute.
10
+ # [text](target)
11
+ # [text](target "title")
12
+ # Group 1: link text, Group 2: raw target (may include URL fragment)
13
+ LINK_RE = re.compile(
14
+ r'\[([^\]]*)\]\(' # [text](
15
+ r'([^)\s"]+)' # target (no spaces, no quotes)
16
+ r'(?:\s+"[^"]*")?' # optional title attribute: "title text"
17
+ r'\s*\)' # closing )
18
+ )
19
+
20
+ # Matches scheme-prefixed (non-relative) URLs that should never be rewritten
21
+ # or resolved as local file paths.
22
+ EXTERNAL_RE = re.compile(r'(?i)^(https?|ftp|file|mailto):')
23
+
24
+ # Detects machine-absolute paths that must not appear in skill docs.
25
+ # Covers macOS (/Users/), Linux (/home/), and Windows (C:\Users, D:\Users …).
26
+ MACHINE_PATH_RE = re.compile(r'file:///|/Users/|/home/|[A-Z]:\\Users')
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # Markdown code-block stripping
30
+ # ---------------------------------------------------------------------------
31
+
32
+ # Fenced code block: ``` or ~~~ with optional language tag, any content, close fence.
33
+ _FENCED_BLOCK_RE = re.compile(r'(?m)^(`{3,}|~{3,})[^\n]*\n.*?\n\1\s*$', re.DOTALL)
34
+ # Inline code: `…` (single backtick, non-empty)
35
+ _INLINE_CODE_RE = re.compile(r'`[^`\n]+`')
36
+
37
+
38
+ def strip_code_spans(text: str) -> str:
39
+ """Return *text* with fenced blocks and inline code replaced by blank space.
40
+
41
+ Preserves original character positions so that any offset-based logic
42
+ remains valid; only the link-matching content is neutralised.
43
+ """
44
+ def _blank(m: re.Match) -> str:
45
+ return ' ' * len(m.group(0))
46
+
47
+ text = _FENCED_BLOCK_RE.sub(_blank, text)
48
+ text = _INLINE_CODE_RE.sub(_blank, text)
49
+ return text
@@ -7,6 +7,83 @@ import glob
7
7
  import subprocess
8
8
  import yaml
9
9
 
10
+ # Ensure scripts/lib is on the path so link_utils resolves regardless of cwd.
11
+ _LIB_DIR = os.path.dirname(os.path.abspath(__file__))
12
+ if _LIB_DIR not in sys.path:
13
+ sys.path.insert(0, _LIB_DIR)
14
+
15
+ from link_utils import LINK_RE, EXTERNAL_RE, strip_code_spans # noqa: E402
16
+
17
+ def rewrite_links_for_pi(body, name):
18
+ """Repoint relative links so they resolve from .pi/skills/<name>/.
19
+
20
+ Sibling *.md content is inlined into the rendered SKILL.md body, but the
21
+ links to those files (and to repo-root docs) still point at paths relative
22
+ to the source dir, so they dangle in the rendered tree. Repoint them at
23
+ the shipped source tree (skills/, profiles/, repo-root files) so links
24
+ resolve in repo checkouts and npm installs alike. External, anchor, and
25
+ absolute links pass through untouched.
26
+
27
+ Code fences and inline code are skipped — links inside them are examples,
28
+ not navigation targets.
29
+ """
30
+ out_dir = os.path.join(".pi", "skills", name)
31
+ # Build a "shadow" copy with code spans blanked out so LINK_RE never
32
+ # matches inside ``` blocks or inline `code`.
33
+ shadow = strip_code_spans(body)
34
+
35
+ def repl(m):
36
+ # If this match falls inside a code span the shadow char is a space;
37
+ # the real body char at that position is not a '[' — so LINK_RE won't
38
+ # match in the shadow at all. The sub() runs on the shadow but we
39
+ # return the replacement for the real body using the captured groups.
40
+ text, raw_target = m.group(1), m.group(2).strip()
41
+ if EXTERNAL_RE.match(raw_target) or raw_target.startswith(('#', '/')):
42
+ return m.group(0)
43
+ # Split fragment before normpath to avoid mangling '#section/sub'.
44
+ if '#' in raw_target:
45
+ path_part, fragment = raw_target.split('#', 1)
46
+ frag_suffix = '#' + fragment
47
+ else:
48
+ path_part, frag_suffix = raw_target, ''
49
+ resolved = os.path.normpath(os.path.join("skills", name, path_part))
50
+ if resolved.startswith(".."):
51
+ return m.group(0) # escapes repo root; leave untouched
52
+ new_target = os.path.relpath(resolved, out_dir).replace(os.sep, '/') + frag_suffix
53
+ return f"[{text}]({new_target})"
54
+
55
+ return LINK_RE.sub(repl, shadow).replace(shadow, body) if shadow == body else _apply_repl_on_real(body, shadow, name, out_dir)
56
+
57
+
58
+ def _apply_repl_on_real(body, shadow, name, out_dir):
59
+ """Apply link rewrites to *body* using *shadow* to skip code spans.
60
+
61
+ Iterates LINK_RE matches on *shadow* (which has code spans blanked) and
62
+ rebuilds body with only the matched positions replaced.
63
+ """
64
+ result = []
65
+ last = 0
66
+ for m in LINK_RE.finditer(shadow):
67
+ result.append(body[last:m.start()])
68
+ text, raw_target = m.group(1), m.group(2).strip()
69
+ if EXTERNAL_RE.match(raw_target) or raw_target.startswith(('#', '/')):
70
+ result.append(body[m.start():m.end()])
71
+ else:
72
+ if '#' in raw_target:
73
+ path_part, fragment = raw_target.split('#', 1)
74
+ frag_suffix = '#' + fragment
75
+ else:
76
+ path_part, frag_suffix = raw_target, ''
77
+ resolved = os.path.normpath(os.path.join("skills", name, path_part))
78
+ if resolved.startswith(".."):
79
+ result.append(body[m.start():m.end()])
80
+ else:
81
+ new_target = os.path.relpath(resolved, out_dir).replace(os.sep, '/') + frag_suffix
82
+ result.append(f"[{text}]({new_target})")
83
+ last = m.end()
84
+ result.append(body[last:])
85
+ return ''.join(result)
86
+
10
87
  def resolve_repo_root():
11
88
  lib_dir = os.path.dirname(os.path.abspath(__file__))
12
89
  # scripts/lib/srp-engine.py -> two levels up is REPO_ROOT
@@ -210,7 +287,11 @@ def main():
210
287
  continue
211
288
 
212
289
  for adapter in adapters:
213
- dispatch_to_adapter(skill_data, adapter, repo_root)
290
+ data = skill_data
291
+ if adapter == "pi":
292
+ data = dict(skill_data)
293
+ data["body_pi_skill"] = rewrite_links_for_pi(skill_data['body'], skill_data['name'])
294
+ dispatch_to_adapter(data, adapter, repo_root)
214
295
 
215
296
  if okf_mode:
216
297
  render_okf_concept(skill_data, okf_wiki_skills)
@@ -235,6 +316,10 @@ def main():
235
316
 
236
317
  skill_data = parse_skill(skill_md_path)
237
318
 
319
+ if target == "pi":
320
+ skill_data = dict(skill_data)
321
+ skill_data["body_pi_skill"] = rewrite_links_for_pi(skill_data['body'], skill_data['name'])
322
+
238
323
  if dry_run or not target:
239
324
  print(json.dumps(skill_data, indent=2))
240
325
  return
@@ -31,7 +31,7 @@ Apply the full checklist to churn-ranked files in descending order. Files with z
31
31
  - Default: full checklist
32
32
  - --quick: Run only Supply Chain and Test Coverage. Use for changes under 50 LOC.
33
33
  - --gate: Non-interactive mode for automated CI gating (used by build-epic step 6). Exit with non-zero status code (`exit 1`) on ANY checklist failure; `exit 0` only if ALL items pass. Produces a compact pass/fail summary to stderr. On failure, list every ✗ item with reason.
34
- - --parallel: Run checklist sections in **isolated git worktrees** (e45s18) so concurrent checks cannot corrupt each other's working tree. See [REFERENCE.md](REFERENCE.md) or:
34
+ - --parallel: Run checklist sections in **isolated git worktrees** (e45s18) so concurrent checks cannot corrupt each other's working tree:
35
35
 
36
36
  ```bash
37
37
  bash scripts/lib/parallel-review-worktrees.sh audit-code
@@ -33,7 +33,7 @@ Move process detail into the SKILL.md body or REFERENCE.md — never into `descr
33
33
  - Any reference materials to include?
34
34
  - What specs/ output does it produce (if any)?
35
35
 
36
- 2. **Verify Principles** — Ensure the skill aligns with [PRINCIPLES.md](../docs/PRINCIPLES.md):
36
+ 2. **Verify Principles** — Ensure the skill aligns with [PRINCIPLES.md](https://github.com/danielvm-git/bigpowers/blob/main/docs/PRINCIPLES.md):
37
37
  - Is it atomic (verb-noun)?
38
38
  - Is it "deep" (simple interface, complex internal logic)?
39
39
  - Does it include Hard Gates?
@@ -46,7 +46,7 @@ Move process detail into the SKILL.md body or REFERENCE.md — never into `descr
46
46
 
47
47
  **Auto-skill from library README:** When user provides a library README or API docs URL, extract: triggers, HARD GATEs, verify commands, specs/ output — draft SKILL.md without inventing APIs not in the source.
48
48
 
49
- 4. Add `model:` frontmatter (`haiku` | `sonnet` | `opus`) per [model-profiles.md](../docs/references/model-profiles.md).
49
+ 4. Add `model:` frontmatter (`haiku` | `sonnet` | `opus`) per [model-profiles.md](https://github.com/danielvm-git/bigpowers/blob/main/docs/references/model-profiles.md).
50
50
 
51
51
  > **STREAM CONTINUITY** — When writing file content, output in continuous chunks of ~200 lines. Do not pause. Continue immediately until complete. If you need time, emit a placeholder comment rather than going silent.
52
52
 
@@ -65,7 +65,7 @@ Once the user has answered the main questions, probe for assumptions:
65
65
 
66
66
  ### 4. Synthesize and confirm
67
67
 
68
- Summarize your understanding in 3–5 bullet points aligned with [countable-story-format.md](file:///Users/danielvm/Developer/bigpowers/countable-story-format.md):
68
+ Summarize your understanding in 3–5 bullet points aligned with [countable-story-format.md](https://github.com/danielvm-git/bigpowers/blob/main/docs/countable-story-format.md):
69
69
  - The problem (feeds into §1 Business narrative)
70
70
  - The solution and main flow (feeds into §5)
71
71
  - The key constraints and alternative flows (feeds into §6)
@@ -9,4 +9,4 @@
9
9
  **Evidence:** path/to/benchmark-report.md
10
10
  ```
11
11
 
12
- Benchmark repo: `/Users/danielvm/Developer/bigpowers-benchmark/`
12
+ Benchmark repo: `bigpowers-benchmark` — resolve the local clone on this machine (e.g. `gh repo clone danielvm-git/bigpowers-benchmark` if absent); never assume a hard-coded absolute path.
@@ -43,7 +43,7 @@ Ask one question at a time. For each challenge, cite the specific URL and sectio
43
43
 
44
44
  When an assumption doesn't match the docs:
45
45
 
46
- > "Your plan uses `library.doThing(a, b)` but the [docs](URL) show the signature is `doThing(config: {a, b})` with a config object. This will fail at runtime."
46
+ > "Your plan uses `library.doThing(a, b)` but the [docs](https://example.com/api-reference) show the signature is `doThing(config: {a, b})` with a config object. This will fail at runtime."
47
47
 
48
48
  Document each discrepancy clearly.
49
49
 
@@ -22,7 +22,7 @@ Installs a shared hook that blocks destructive git operations and enforces workf
22
22
  ## Quick start
23
23
 
24
24
  1. **Scope**: ask project-only vs global (paths differ per product).
25
- 2. **Copy the hook bundle** from the root [hooks/](hooks/) directory to the client's hooks directory.
25
+ 2. **Write the hook bundle** from [REFERENCE.md](REFERENCE.md) into the client's hooks directory.
26
26
  3. **Run `chmod +x`** on `pre-tool-use.sh`.
27
27
  4. **Merge** the hook snippet from [REFERENCE.md](REFERENCE.md) into the right settings file — do not wipe unrelated keys.
28
28
  5. **Verify** with the tests in [REFERENCE.md](REFERENCE.md).
@@ -124,6 +124,6 @@ After writing the bug file, print a one-line summary of the root cause and sugge
124
124
 
125
125
  <!-- story: e35s10 -->
126
126
 
127
- - [Feathers' Seams & Characterization Tests](../../docs/references/feathers.md) — seam types, characterization tests, and the legacy code change algorithm
128
- - [Fowler's Code Smells](../../docs/references/fowler.md) — identifying structural problems by smell before investigating
127
+ - [Feathers' Seams & Characterization Tests](https://github.com/danielvm-git/bigpowers/blob/main/docs/references/feathers.md) — seam types, characterization tests, and the legacy code change algorithm
128
+ - [Fowler's Code Smells](https://github.com/danielvm-git/bigpowers/blob/main/docs/references/fowler.md) — identifying structural problems by smell before investigating
129
129
 
@@ -57,9 +57,9 @@ _Avoid_: Client, buyer, account
57
57
 
58
58
  ## Contexts
59
59
 
60
- - [Ordering](./src/ordering/CONTEXT.md) — receives and tracks customer orders
61
- - [Billing](./src/billing/CONTEXT.md) — generates invoices and processes payments
62
- - [Fulfillment](./src/fulfillment/CONTEXT.md) — manages warehouse picking and shipping
60
+ - Ordering — `src/ordering/CONTEXT.md` — receives and tracks customer orders
61
+ - Billing — `src/billing/CONTEXT.md` — generates invoices and processes payments
62
+ - Fulfillment — `src/fulfillment/CONTEXT.md` — manages warehouse picking and shipping
63
63
 
64
64
  ## Relationships
65
65
 
@@ -82,8 +82,8 @@ After writing `specs/REFACTOR_LATEST.md`, suggest running `kickoff-branch` next
82
82
 
83
83
  <!-- story: e35s10 -->
84
84
 
85
- - [Fowler's Refactoring Catalog](../../docs/references/fowler.md) — canonical refactoring vocabulary and code-smell taxonomy
86
- - [Beck's Tidy First?](../../docs/references/kent-beck.md) — structural change before behavioral change
85
+ - [Fowler's Refactoring Catalog](https://github.com/danielvm-git/bigpowers/blob/main/docs/references/fowler.md) — canonical refactoring vocabulary and code-smell taxonomy
86
+ - [Beck's Tidy First?](https://github.com/danielvm-git/bigpowers/blob/main/docs/references/kent-beck.md) — structural change before behavioral change
87
87
 
88
88
  ## Verify
89
89
 
@@ -17,7 +17,7 @@ Synthesize the conversation context into `specs/release-plan.yaml` (index) and s
17
17
  |------|---------|
18
18
  | `specs/release-plan.yaml` | `release.version`, semver bump hint, WSJF-ordered epic list with `id`, `capsule_dir`, `wsjf`, `bcps` — **no story status** |
19
19
  | `specs/epics/eNN-<slug>/epic.yaml` | Epic manifest: `id`, `title`, `wsjf`, `total_bcps`, `status`, `stories[]` list |
20
- | `specs/epics/eNN-<slug>/eNNsYY-<slug>.md` | Story spec in [countable-story-format.md](file:///Users/danielvm/Developer/bigpowers/countable-story-format.md) with 20 sections and Gherkin acceptance criteria |
20
+ | `specs/epics/eNN-<slug>/eNNsYY-<slug>.md` | Story spec in [countable-story-format.md](https://github.com/danielvm-git/bigpowers/blob/main/docs/countable-story-format.md) with 20 sections and Gherkin acceptance criteria |
21
21
  | `specs/epics/eNN-<slug>/eNNsYY-tasks.yaml` | Decoupled task checklist with `verify:` commands per task |
22
22
  | `specs/execution-status.yaml` | Flat key-value store for story status (`eNNsYY: todo`) |
23
23
 
@@ -102,7 +102,7 @@ stories:
102
102
  ```
103
103
  ### 6. Save story specs (countable-story-format .md)
104
104
 
105
- Each story becomes a standalone `.md` file following [countable-story-format.md](file:///Users/danielvm/Developer/bigpowers/countable-story-format.md). Minimum: maturity 3 (Countable) with all 20 sections present. Acceptance criteria in §17 use Gherkin scenarios.
105
+ Each story becomes a standalone `.md` file following [countable-story-format.md](https://github.com/danielvm-git/bigpowers/blob/main/docs/countable-story-format.md). Minimum: maturity 3 (Countable) with all 20 sections present. Acceptance criteria in §17 use Gherkin scenarios.
106
106
  ### 7. Save decoupled task files (`-tasks.yaml`)
107
107
 
108
108
  Each story has a decoupled `-tasks.yaml` with implementation steps:
@@ -78,7 +78,7 @@ UTILITY terse-mode / craft-skill / edit-document (any phase)
78
78
 
79
79
  If you work alone and do not want PR ceremony every task:
80
80
 
81
- 1. Read [profiles/solo-git.md](../profiles/solo-git.md).
81
+ 1. Read [profiles/solo-git.md](../../profiles/solo-git.md).
82
82
  2. Register with `compose-workflow` → `specs/WORKFLOW-solo-git.md`.
83
83
  3. Ship with `release-branch` in **solo-local** mode (`land-branch.sh`), not `gh pr create`.
84
84
 
@@ -141,7 +141,7 @@ Auto-fill from CLAUDE.md `Test` and `Lint` commands.
141
141
  ```markdown
142
142
  ## Changelog
143
143
 
144
- See [CHANGELOG.md](CHANGELOG.md) or [Releases](https://github.com/user/repo/releases).
144
+ See [CHANGELOG.md](../../CHANGELOG.md) or [Releases](https://github.com/user/repo/releases).
145
145
  ```
146
146
 
147
147
  ### 13. Links
@@ -158,7 +158,7 @@ See [CHANGELOG.md](CHANGELOG.md) or [Releases](https://github.com/user/repo/rele
158
158
  ```markdown
159
159
  ## License
160
160
 
161
- MIT — see [LICENSE](LICENSE) for details.
161
+ MIT — see [LICENSE](../../LICENSE) for details.
162
162
  ```
163
163
 
164
164
  Detect from CLAUDE.md or project LICENSE file.
package/skills-lock.json CHANGED
@@ -13,7 +13,7 @@
13
13
  },
14
14
  "audit-code": {
15
15
  "description": "Self-review checklist for the coding agent to run before dispatching a reviewer. Checks CONVENTIONS.md compliance, Boy Scout Rule, test coverage, types, and SOLID. Produces a pass/fail checklist. Use before request-review, before committing, or when user asks for a code quality check.",
16
- "sha256": "bbb128254464f8f4",
16
+ "sha256": "0636f1d9d6c591a3",
17
17
  "path": "skills/audit-codeSKILL.md"
18
18
  },
19
19
  "audit-plan": {
@@ -48,7 +48,7 @@
48
48
  },
49
49
  "craft-skill": {
50
50
  "description": "Create new bigpowers skills with proper structure, progressive disclosure, and bundled resources. Use when user wants to create, write, or build a new skill for the bigpowers lifecycle.",
51
- "sha256": "cbf8ab939cfbab87",
51
+ "sha256": "2caa9d8ae9298b4b",
52
52
  "path": "skills/craft-skillSKILL.md"
53
53
  },
54
54
  "deepen-architecture": {
@@ -108,7 +108,7 @@
108
108
  },
109
109
  "elaborate-spec": {
110
110
  "description": "Refine a rough idea into a clear, detailed specification through dialogue. Does not produce code. Use when user has a vague idea, wants to think through a feature before planning, or needs to turn \"I want X\" into a concrete spec.",
111
- "sha256": "62ebfca87e6d0162",
111
+ "sha256": "9307ab61c789a0a0",
112
112
  "path": "skills/elaborate-specSKILL.md"
113
113
  },
114
114
  "enforce-first": {
@@ -158,7 +158,7 @@
158
158
  },
159
159
  "guard-git": {
160
160
  "description": "Block dangerous git commands (push, force push, reset --hard, clean, branch -D, checkout/restore .) and enforce Conventional Commits & Branch Protection before an AI agent runs them. Installs hook scripts for Claude Code, Cursor, Cursor CLI, and Gemini CLI; documents Google Antigravity Terminal deny lists. Use when the user wants git safety hooks, to block git push or destructive git in agents, or to mirror the same policy across AI coding tools.",
161
- "sha256": "9ee881df89273b00",
161
+ "sha256": "e7083cba6d95a15f",
162
162
  "path": "skills/guard-gitSKILL.md"
163
163
  },
164
164
  "harden-vps": {
@@ -178,7 +178,7 @@
178
178
  },
179
179
  "investigate-bug": {
180
180
  "description": "Investigate a bug or issue by exploring the codebase to find root cause, then write a TDD-based fix plan to specs/bugs/BUG-*.md. Use when user reports a bug, wants to investigate a problem, mentions \"triage\", or wants to plan a fix.",
181
- "sha256": "b842fd329d1f7741",
181
+ "sha256": "54f8c5417be34ef0",
182
182
  "path": "skills/investigate-bugSKILL.md"
183
183
  },
184
184
  "kickoff-branch": {
@@ -218,12 +218,12 @@
218
218
  },
219
219
  "plan-refactor": {
220
220
  "description": "Create a detailed refactor plan with tiny commits via user interview, then save it as specs/REFACTOR_LATEST.md. Use when user wants to plan a refactor, create a refactoring RFC, or break a refactor into safe incremental steps.",
221
- "sha256": "c418c850806e2f16",
221
+ "sha256": "2d18809da7cc9de8",
222
222
  "path": "skills/plan-refactorSKILL.md"
223
223
  },
224
224
  "plan-release": {
225
225
  "description": "\"RELEASE-INDEX BUILDER — Sequence elaborated epics into specs/release-plan.yaml with WSJF ordering and BCP baselines. NOT a planning-spine substitute: it does not scope work (scope-work) or write story tasks (plan-work). Use after elaborate-spec when the user wants a versioned release index of epics.\"",
226
- "sha256": "b56b800817cd383e",
226
+ "sha256": "9334ff48e5b1fab3",
227
227
  "path": "skills/plan-releaseSKILL.md"
228
228
  },
229
229
  "plan-tests": {
@@ -358,7 +358,7 @@
358
358
  },
359
359
  "using-bigpowers": {
360
360
  "description": "One-time bootstrap that introduces the bigpowers skills system, the PMBOK lifecycle arc, and tells you which skill to call first for your situation. Use when starting with bigpowers for the first time, when user asks \"where do I start?\", or when the skills system needs to be explained.",
361
- "sha256": "332d6468868b759b",
361
+ "sha256": "bad9ebe9bf303b36",
362
362
  "path": "skills/using-bigpowersSKILL.md"
363
363
  },
364
364
  "validate-contracts": {
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env python3
2
+ """tests/test_srp_engine.py — unit tests for rewrite_links_for_pi and link_utils.
3
+
4
+ Run: python3 -m pytest tests/test_srp_engine.py -v
5
+ or: python3 tests/test_srp_engine.py
6
+ """
7
+ import os
8
+ import sys
9
+ import unittest
10
+
11
+ # Make scripts/lib importable without installation.
12
+ _REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
13
+ _LIB = os.path.join(_REPO_ROOT, "scripts", "lib")
14
+ sys.path.insert(0, _LIB)
15
+ sys.path.insert(0, os.path.join(_REPO_ROOT, "scripts"))
16
+
17
+ # srp-engine.py has a hyphen so we load it via importlib.
18
+ import importlib.util as _ilu
19
+ _spec = _ilu.spec_from_file_location("srp_engine", os.path.join(_LIB, "srp-engine.py"))
20
+ _srp = _ilu.module_from_spec(_spec)
21
+ sys.modules["srp_engine"] = _srp
22
+ _spec.loader.exec_module(_srp)
23
+
24
+ from srp_engine import rewrite_links_for_pi # type: ignore[import]
25
+ from link_utils import strip_code_spans, LINK_RE, MACHINE_PATH_RE
26
+
27
+
28
+ class TestStripCodeSpans(unittest.TestCase):
29
+ def test_fenced_block_blanked(self):
30
+ md = "Before.\n```\n[fake](fake.md)\n```\nAfter."
31
+ shadow = strip_code_spans(md)
32
+ self.assertNotIn("[fake](fake.md)", shadow)
33
+ self.assertEqual(len(shadow), len(md)) # positions preserved
34
+
35
+ def test_inline_code_blanked(self):
36
+ md = "Use `[x](y.md)` as an example."
37
+ shadow = strip_code_spans(md)
38
+ self.assertNotIn("[x](y.md)", shadow)
39
+ self.assertEqual(len(shadow), len(md))
40
+
41
+ def test_real_link_survives(self):
42
+ md = "See [docs](guide.md) for details."
43
+ shadow = strip_code_spans(md)
44
+ self.assertIn("[docs](guide.md)", shadow)
45
+
46
+
47
+ class TestLinkRE(unittest.TestCase):
48
+ def test_plain_link(self):
49
+ m = LINK_RE.search("[text](target.md)")
50
+ self.assertIsNotNone(m)
51
+ self.assertEqual(m.group(2), "target.md")
52
+
53
+ def test_link_with_title(self):
54
+ m = LINK_RE.search('[text](target.md "My Title")')
55
+ self.assertIsNotNone(m)
56
+ self.assertEqual(m.group(2), "target.md")
57
+
58
+ def test_link_with_fragment(self):
59
+ m = LINK_RE.search("[text](doc.md#section)")
60
+ self.assertIsNotNone(m)
61
+ self.assertEqual(m.group(2), "doc.md#section")
62
+
63
+ def test_no_match_on_image(self):
64
+ # Image links (![...](url)) contain the same syntax — LINK_RE will match
65
+ # the inner part; they should be treated as pass-through by callers.
66
+ m = LINK_RE.search("![alt](image.png)")
67
+ # The regex matches the inner ](url) portion — callers must handle images.
68
+ self.assertIsNotNone(m)
69
+
70
+
71
+ class TestMachinePathRE(unittest.TestCase):
72
+ def test_file_uri(self):
73
+ self.assertTrue(MACHINE_PATH_RE.search("file:///Users/alice/project"))
74
+
75
+ def test_macos_path(self):
76
+ self.assertTrue(MACHINE_PATH_RE.search("/Users/alice/project"))
77
+
78
+ def test_linux_path(self):
79
+ self.assertTrue(MACHINE_PATH_RE.search("/home/alice/project"))
80
+
81
+ def test_windows_path_single_backslash(self):
82
+ self.assertTrue(MACHINE_PATH_RE.search("C:\\Users\\alice"))
83
+
84
+ def test_github_url_not_flagged(self):
85
+ self.assertFalse(
86
+ MACHINE_PATH_RE.search("https://github.com/danielvm-git/bigpowers")
87
+ )
88
+
89
+
90
+ class TestRewriteLinksForPi(unittest.TestCase):
91
+ """Tests run without a real repo tree — path existence is irrelevant here;
92
+ we only verify that the URL construction logic is correct."""
93
+
94
+ NAME = "my-skill"
95
+
96
+ def _rw(self, body):
97
+ return rewrite_links_for_pi(body, self.NAME)
98
+
99
+ def test_external_link_untouched(self):
100
+ body = "[docs](https://example.com)"
101
+ self.assertEqual(self._rw(body), body)
102
+
103
+ def test_anchor_only_untouched(self):
104
+ body = "[section](#heading)"
105
+ self.assertEqual(self._rw(body), body)
106
+
107
+ def test_absolute_path_untouched(self):
108
+ body = "[root](/absolute/path.md)"
109
+ self.assertEqual(self._rw(body), body)
110
+
111
+ def test_relative_link_repointed(self):
112
+ body = "[ref](REFERENCE.md)"
113
+ result = self._rw(body)
114
+ # Must not contain the original bare target
115
+ self.assertNotIn("](REFERENCE.md)", result)
116
+ # Must be a relative path targeting the skills source tree
117
+ self.assertIn("REFERENCE.md", result)
118
+
119
+ def test_fragment_preserved(self):
120
+ body = "[section](REFERENCE.md#anchor)"
121
+ result = self._rw(body)
122
+ self.assertIn("#anchor", result)
123
+ # Fragment must not be mangled into a path component
124
+ self.assertNotIn("anchor" + os.sep, result)
125
+
126
+ def test_link_with_title_attribute(self):
127
+ body = '[ref](REFERENCE.md "My Title")'
128
+ result = self._rw(body)
129
+ # Must not be left unrewritten
130
+ self.assertNotIn("](REFERENCE.md", result)
131
+
132
+ def test_code_block_links_not_rewritten(self):
133
+ body = "Real: [ref](REFERENCE.md)\n\n```\n[fake](nowhere.md)\n```\n"
134
+ result = self._rw(body)
135
+ # The real link should be rewritten
136
+ self.assertNotIn("](REFERENCE.md)", result)
137
+ # The code-block link must be untouched
138
+ self.assertIn("[fake](nowhere.md)", result)
139
+
140
+ def test_inline_code_link_not_rewritten(self):
141
+ body = "Example: `[x](bad.md)` — real [ref](REFERENCE.md)."
142
+ result = self._rw(body)
143
+ self.assertIn("[x](bad.md)", result) # inline code preserved
144
+ self.assertNotIn("](REFERENCE.md)", result) # real link rewritten
145
+
146
+
147
+ if __name__ == "__main__":
148
+ unittest.main()