bigpowers 2.88.16 → 2.88.18

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 (34) hide show
  1. package/.gemini/extensions/bigpowers/gemini-extension.json +1 -1
  2. package/.mcp.json +2 -2
  3. package/.pi/package.json +1 -1
  4. package/CHANGELOG.md +18 -0
  5. package/bigpowers-mcp/build/lib/git-context.js +52 -36
  6. package/bigpowers-mcp/build/lib/git-context.js.map +1 -1
  7. package/bigpowers-mcp/src/lib/git-context.ts +9 -5
  8. package/dashboard/src/web/client.html +15 -8
  9. package/dashboard/src/web/server.js +2 -1
  10. package/package.json +1 -1
  11. package/scripts/bp-read-agents.sh +7 -5
  12. package/scripts/lib/__pycache__/link_utils.cpython-310.pyc +0 -0
  13. package/scripts/lib/__pycache__/srp_engine_links.cpython-310.pyc +0 -0
  14. package/scripts/lib/__pycache__/srp_engine_okf.cpython-310.pyc +0 -0
  15. package/scripts/lib/golden-suite-gates.sh +6 -0
  16. package/scripts/lib/migrate-version-common.sh +15 -5
  17. package/scripts/lib/migrate-version-plan.sh +7 -7
  18. package/scripts/lib/migrate-version-run.sh +3 -0
  19. package/scripts/lib/srp-engine.py +1 -60
  20. package/scripts/lib/srp_engine_okf.py +65 -0
  21. package/scripts/lib/story-verify-memo.sh +56 -0
  22. package/scripts/lib/test-assertions.sh +6 -0
  23. package/scripts/mcp-smoke.ts +48 -43
  24. package/scripts/run-story-verify.sh +3 -50
  25. package/scripts/test-golden-gate-runner.sh +18 -18
  26. package/scripts/test-project-runtime.sh +10 -19
  27. package/scripts/validate-docs-base-path.sh +15 -22
  28. package/scripts/validate-mcp-package.sh +18 -20
  29. package/scripts/validate-omp-extension.sh +23 -30
  30. package/tests/security/agents-import.cjs +36 -0
  31. package/tests/security/dashboard-bind.cjs +31 -0
  32. package/tests/security/dashboard-render.cjs +60 -0
  33. package/tests/security/git-context.cjs +44 -0
  34. package/tests/security/migration.cjs +76 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bigpowers",
3
- "version": "2.88.16",
3
+ "version": "2.88.18",
4
4
  "description": "81 skills — 73 agent skills synthesizing 17 years of software engineering discipline into a prescriptive methodology for solo developers",
5
5
  "keywords": [
6
6
  "gemini-extension"
package/.mcp.json CHANGED
@@ -2,8 +2,8 @@
2
2
  "mcpServers": {
3
3
  "bigpowers-mcp": {
4
4
  "command": "node",
5
- "args": ["${OMP_PLUGIN_ROOT}/bigpowers-mcp/build/index.js"],
6
- "cwd": "${OMP_PLUGIN_ROOT}"
5
+ "args": ["${OMP_PLUGIN_ROOT:-.}/bigpowers-mcp/build/index.js"],
6
+ "cwd": "${OMP_PLUGIN_ROOT:-.}"
7
7
  }
8
8
  }
9
9
  }
package/.pi/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bigpowers",
3
- "version": "2.88.16",
3
+ "version": "2.88.18",
4
4
  "description": "81 skills — 73 agent skills synthesizing 17 years of software engineering discipline into a prescriptive methodology for solo developers",
5
5
  "keywords": [
6
6
  "pi-package"
package/CHANGELOG.md CHANGED
@@ -1,3 +1,21 @@
1
+ ## [2.88.18](https://github.com/danielvm-git/bigpowers/compare/v2.88.17...v2.88.18) (2026-11-02)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **dashboard:** bind the project dashboard to loopback ([#175](https://github.com/danielvm-git/bigpowers/issues/175)) ([b796b17](https://github.com/danielvm-git/bigpowers/commit/b796b17f3b077a36d725018d3ad6f2fb5cc0426f))
7
+ * **dashboard:** escape project metadata before rendering HTML ([#178](https://github.com/danielvm-git/bigpowers/issues/178)) ([f905d5b](https://github.com/danielvm-git/bigpowers/commit/f905d5b30753e715d8675401aeceefa98d850476))
8
+ * **mcp:** filter every diff before returning its contents ([#174](https://github.com/danielvm-git/bigpowers/issues/174)) ([d0b9f70](https://github.com/danielvm-git/bigpowers/commit/d0b9f70a7319d396858890e2abfa1253b68837ff))
9
+ * **migrate:** treat version metadata and preview paths as data ([#177](https://github.com/danielvm-git/bigpowers/issues/177)) ([2b0649c](https://github.com/danielvm-git/bigpowers/commit/2b0649c0a8723695e2c1f078112bca3993b8b383))
10
+ * **scripts:** escape AGENTS commands when emitting Bash exports ([#176](https://github.com/danielvm-git/bigpowers/issues/176)) ([1800dd5](https://github.com/danielvm-git/bigpowers/commit/1800dd5f682cb98afdbbdce06cbcc8cae2a6eb9f))
11
+
12
+ ## [2.88.17](https://github.com/danielvm-git/bigpowers/compare/v2.88.16...v2.88.17) (2026-10-30)
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * **compliance:** restore akita.feature to 0 FAIL — dedupe helpers, split oversized files ([#171](https://github.com/danielvm-git/bigpowers/issues/171)) ([4ed9a26](https://github.com/danielvm-git/bigpowers/commit/4ed9a2663c39689de04fd2ff796b09777a78cfcf)), closes [#118](https://github.com/danielvm-git/bigpowers/issues/118) [123/#125](https://github.com/danielvm-git/bigpowers/issues/125) [#161](https://github.com/danielvm-git/bigpowers/issues/161) [#155](https://github.com/danielvm-git/bigpowers/issues/155) [#161](https://github.com/danielvm-git/bigpowers/issues/161) [#168](https://github.com/danielvm-git/bigpowers/issues/168) [#169](https://github.com/danielvm-git/bigpowers/issues/169) [#168](https://github.com/danielvm-git/bigpowers/issues/168) [#168](https://github.com/danielvm-git/bigpowers/issues/168)
18
+
1
19
  ## [2.88.16](https://github.com/danielvm-git/bigpowers/compare/v2.88.15...v2.88.16) (2026-10-25)
2
20
 
3
21
 
@@ -6,17 +6,15 @@ import { GIT_SCOPE_DIRS } from "../config.js";
6
6
  import { isSecretPath } from "./paths.js";
7
7
  function inScope(filePath) {
8
8
  const norm = filePath.replace(/\\/g, "/");
9
- if (isSecretPath(norm))
10
- return false;
11
- return GIT_SCOPE_DIRS.some((d) => norm === d || norm.startsWith(`${d}/`));
9
+ if (isSecretPath(norm)) return false;
10
+ return GIT_SCOPE_DIRS.some((d)=>norm === d || norm.startsWith(`${d}/`));
12
11
  }
13
12
  function groupByDir(files) {
14
13
  const grouped = {};
15
- for (const f of files) {
14
+ for (const f of files){
16
15
  const parts = f.split("/");
17
16
  const dir = parts.length > 1 ? parts[0] : ".";
18
- if (!grouped[dir])
19
- grouped[dir] = [];
17
+ if (!grouped[dir]) grouped[dir] = [];
20
18
  grouped[dir].push(f);
21
19
  }
22
20
  return grouped;
@@ -25,18 +23,20 @@ function git(repoRoot, args) {
25
23
  return execFileSync("git", args, {
26
24
  cwd: repoRoot,
27
25
  encoding: "utf8",
28
- maxBuffer: 10 * 1024 * 1024,
26
+ maxBuffer: 10 * 1024 * 1024
29
27
  }).trim();
30
28
  }
31
29
  export function getGitContext(repoRoot, action) {
32
30
  if (action === "status") {
33
- const out = git(repoRoot, ["status", "--porcelain"]);
34
- const files = out
35
- .split("\n")
36
- .filter(Boolean)
37
- .map((line) => line.slice(3).trim())
38
- .filter(inScope);
39
- return { action, changed_files: groupByDir(files) };
31
+ const out = git(repoRoot, [
32
+ "status",
33
+ "--porcelain"
34
+ ]);
35
+ const files = out.split("\n").filter(Boolean).map((line)=>line.slice(3).trim()).filter(inScope);
36
+ return {
37
+ action,
38
+ changed_files: groupByDir(files)
39
+ };
40
40
  }
41
41
  if (action === "log") {
42
42
  const out = git(repoRoot, [
@@ -45,48 +45,64 @@ export function getGitContext(repoRoot, action) {
45
45
  "--pretty=format:%H|%s",
46
46
  "--name-only",
47
47
  "--",
48
- ...GIT_SCOPE_DIRS,
48
+ ...GIT_SCOPE_DIRS
49
49
  ]);
50
50
  const commits = [];
51
51
  let current = null;
52
- for (const line of out.split("\n")) {
52
+ for (const line of out.split("\n")){
53
53
  if (line.includes("|")) {
54
- if (current)
55
- commits.push(current);
54
+ if (current) commits.push(current);
56
55
  const [hash, subject] = line.split("|");
57
- current = { hash, subject, files: [] };
58
- }
59
- else if (line.trim() && current && inScope(line.trim())) {
56
+ current = {
57
+ hash,
58
+ subject,
59
+ files: []
60
+ };
61
+ } else if (line.trim() && current && inScope(line.trim())) {
60
62
  current.files.push(line.trim());
61
63
  }
62
64
  }
63
- if (current)
64
- commits.push(current);
65
- return { action, changed_files: {}, commits };
65
+ if (current) commits.push(current);
66
+ return {
67
+ action,
68
+ changed_files: {},
69
+ commits
70
+ };
66
71
  }
67
- const diffOut = git(repoRoot, ["diff", "--", ...GIT_SCOPE_DIRS]);
72
+ const diffOut = git(repoRoot, [
73
+ "diff",
74
+ "--",
75
+ ...GIT_SCOPE_DIRS
76
+ ]);
68
77
  const diffs = {};
69
78
  let currentFile = "";
70
79
  let buf = [];
71
- for (const line of diffOut.split("\n")) {
80
+ const flushDiff = ()=>{
81
+ if (currentFile && buf.length && inScope(currentFile)) {
82
+ diffs[currentFile] = buf.join("\n");
83
+ }
84
+ };
85
+ for (const line of diffOut.split("\n")){
72
86
  if (line.startsWith("diff --git")) {
73
- if (currentFile && buf.length)
74
- diffs[currentFile] = buf.join("\n");
87
+ flushDiff();
75
88
  const match = / b\/(.+)$/.exec(line);
76
89
  currentFile = match?.[1] ?? "";
77
- buf = [line];
78
- }
79
- else {
90
+ buf = [
91
+ line
92
+ ];
93
+ } else {
80
94
  buf.push(line);
81
95
  }
82
96
  }
83
- if (currentFile && buf.length && inScope(currentFile) && !isSecretPath(currentFile)) {
84
- diffs[currentFile] = buf.join("\n");
85
- }
97
+ flushDiff();
86
98
  const changed = Object.keys(diffs);
87
- return { action, changed_files: groupByDir(changed), diffs };
99
+ return {
100
+ action,
101
+ changed_files: groupByDir(changed),
102
+ diffs
103
+ };
88
104
  }
89
105
  export function isGitRepo(repoRoot) {
90
106
  return fs.existsSync(path.join(repoRoot, ".git"));
91
107
  }
92
- //# sourceMappingURL=git-context.js.map
108
+ //# sourceMappingURL=git-context.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"git-context.js","sourceRoot":"","sources":["../../src/lib/git-context.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/lib/git-context.ts"],"names":[],"mappings":"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","file":"git-context.js","sourceRoot":""}
@@ -2,7 +2,7 @@
2
2
  import { execFileSync } from "node:child_process";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
- import { GIT_SCOPE_DIRS, SECRET_DENYLIST } from "../config.js";
5
+ import { GIT_SCOPE_DIRS } from "../config.js";
6
6
  import { isSecretPath } from "./paths.js";
7
7
 
8
8
  export interface GitContextResult {
@@ -76,9 +76,14 @@ export function getGitContext(repoRoot: string, action: "status" | "log" | "diff
76
76
  const diffs: Record<string, string> = {};
77
77
  let currentFile = "";
78
78
  let buf: string[] = [];
79
+ const flushDiff = () => {
80
+ if (currentFile && buf.length && inScope(currentFile)) {
81
+ diffs[currentFile] = buf.join("\n");
82
+ }
83
+ };
79
84
  for (const line of diffOut.split("\n")) {
80
85
  if (line.startsWith("diff --git")) {
81
- if (currentFile && buf.length) diffs[currentFile] = buf.join("\n");
86
+ flushDiff();
82
87
  const match = / b\/(.+)$/.exec(line);
83
88
  currentFile = match?.[1] ?? "";
84
89
  buf = [line];
@@ -86,9 +91,7 @@ export function getGitContext(repoRoot: string, action: "status" | "log" | "diff
86
91
  buf.push(line);
87
92
  }
88
93
  }
89
- if (currentFile && buf.length && inScope(currentFile) && !isSecretPath(currentFile)) {
90
- diffs[currentFile] = buf.join("\n");
91
- }
94
+ flushDiff();
92
95
 
93
96
  const changed = Object.keys(diffs);
94
97
  return { action, changed_files: groupByDir(changed), diffs };
@@ -97,3 +100,4 @@ export function getGitContext(repoRoot: string, action: "status" | "log" | "diff
97
100
  export function isGitRepo(repoRoot: string): boolean {
98
101
  return fs.existsSync(path.join(repoRoot, ".git"));
99
102
  }
103
+
@@ -218,6 +218,12 @@
218
218
  let eventSource = null;
219
219
  let pollInterval = null;
220
220
 
221
+ // Project metadata belongs in HTML text content, never in markup.
222
+ function escapeHtml(value) {
223
+ const entities = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
224
+ return String(value).replace(/[&<>"']/g, char => entities[char]);
225
+ }
226
+
221
227
  function render(data) {
222
228
  if (!data) return;
223
229
  renderPipeline(data);
@@ -246,14 +252,14 @@
246
252
 
247
253
  let html = '';
248
254
  if (metrics) {
249
- html += `<div class="metric-row"><span class="metric-label">Total BCPs</span><span class="metric-value">${metrics.totalBcps}</span></div>`;
255
+ html += `<div class="metric-row"><span class="metric-label">Total BCPs</span><span class="metric-value">${escapeHtml(metrics.totalBcps)}</span></div>`;
250
256
  html += `<div class="metric-row"><span class="metric-label">Avg BCP/Hour</span><span class="metric-value">${metrics.avgBcpPerHour.toFixed(2)}</span></div>`;
251
257
  }
252
258
  if (velocity) {
253
259
  html += `<div class="metric-row"><span class="metric-label">Current Velocity</span><span class="metric-value">${velocity.avgBcpPerHour.toFixed(2)} BCP/h</span></div>`;
254
260
  }
255
261
  if (data.state?.release?.version) {
256
- html += `<div class="metric-row"><span class="metric-label">Version</span><span class="metric-value">${data.state.release.version}</span></div>`;
262
+ html += `<div class="metric-row"><span class="metric-label">Version</span><span class="metric-value">${escapeHtml(data.state.release.version)}</span></div>`;
257
263
  }
258
264
  if (timestamp) {
259
265
  html += `<div class="metric-row"><span class="metric-label">Updated</span><span class="metric-value" style="font-size: 10px;">${new Date(timestamp).toLocaleTimeString()}</span></div>`;
@@ -271,7 +277,7 @@
271
277
  { key: 'Git Branch', value: state.gitBranch }
272
278
  ];
273
279
  container.innerHTML = pairs.map(pair => {
274
- const val = pair.value || '(none)';
280
+ const val = escapeHtml(pair.value || '(none)');
275
281
  return `<div class="state-pair"><span class="state-key">${pair.key}</span><span class="state-value">${val}</span></div>`;
276
282
  }).join('');
277
283
  }
@@ -288,8 +294,8 @@
288
294
  const storyCount = stories.length;
289
295
  return `
290
296
  <div class="epic-item">
291
- <div class="epic-title">${epic.id || 'Unknown'}</div>
292
- <div style="color: var(--ink-soft); font-size: 11px; margin-bottom: 0;">${epic.title || 'Untitled'}</div>
297
+ <div class="epic-title">${escapeHtml(epic.id || 'Unknown')}</div>
298
+ <div style="color: var(--ink-soft); font-size: 11px; margin-bottom: 0;">${escapeHtml(epic.title || 'Untitled')}</div>
293
299
  <div class="epic-stories">${storyCount} stories</div>
294
300
  </div>
295
301
  `;
@@ -308,10 +314,10 @@
308
314
  const end = row.end ? new Date(row.end).toLocaleDateString() : '(active)';
309
315
  return `
310
316
  <div class="ledger-row">
311
- <div class="ledger-id">${row.id}</div>
317
+ <div class="ledger-id">${escapeHtml(row.id)}</div>
312
318
  <div style="text-align: right;">
313
- <div class="ledger-value">${row.bcps} BCP</div>
314
- <div class="ledger-meta">${row.cycleMin}min | ${row.bcpPerHour.toFixed(2)}/h</div>
319
+ <div class="ledger-value">${escapeHtml(row.bcps)} BCP</div>
320
+ <div class="ledger-meta">${escapeHtml(row.cycleMin)}min | ${row.bcpPerHour.toFixed(2)}/h</div>
315
321
  </div>
316
322
  </div>
317
323
  `;
@@ -417,3 +423,4 @@
417
423
  </script>
418
424
  </body>
419
425
  </html>
426
+
@@ -104,9 +104,10 @@ function createServer(projectRoot, port = 7742) {
104
104
 
105
105
  return {
106
106
  listen(portNum = port, cb) {
107
- return app.listen(portNum, cb);
107
+ return app.listen(portNum, '127.0.0.1', cb);
108
108
  }
109
109
  };
110
110
  }
111
111
 
112
112
  module.exports = { createServer };
113
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bigpowers",
3
- "version": "2.88.16",
3
+ "version": "2.88.18",
4
4
  "description": "73 agent skills synthesizing 17 years of software engineering discipline into a prescriptive methodology for solo developers",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -53,9 +53,11 @@ if $PRINT_MODE; then
53
53
  if [ -n "$BP_LINT" ]; then echo "Lint: $BP_LINT"; fi
54
54
  if [ -n "$BP_DEPLOY" ]; then echo "Deploy: $BP_DEPLOY"; fi
55
55
  else
56
- if [ -n "$BP_PREFLIGHT" ]; then echo "export BP_PREFLIGHT='$BP_PREFLIGHT'"; fi
57
- if [ -n "$BP_TEST" ]; then echo "export BP_TEST='$BP_TEST'"; fi
58
- if [ -n "$BP_BUILD" ]; then echo "export BP_BUILD='$BP_BUILD'"; fi
59
- if [ -n "$BP_LINT" ]; then echo "export BP_LINT='$BP_LINT'"; fi
60
- if [ -n "$BP_DEPLOY" ]; then echo "export BP_DEPLOY='$BP_DEPLOY'"; fi
56
+ # Bash %q preserves the command as data when the caller evaluates exports.
57
+ if [ -n "$BP_PREFLIGHT" ]; then printf 'export BP_PREFLIGHT=%q\n' "$BP_PREFLIGHT"; fi
58
+ if [ -n "$BP_TEST" ]; then printf 'export BP_TEST=%q\n' "$BP_TEST"; fi
59
+ if [ -n "$BP_BUILD" ]; then printf 'export BP_BUILD=%q\n' "$BP_BUILD"; fi
60
+ if [ -n "$BP_LINT" ]; then printf 'export BP_LINT=%q\n' "$BP_LINT"; fi
61
+ if [ -n "$BP_DEPLOY" ]; then printf 'export BP_DEPLOY=%q\n' "$BP_DEPLOY"; fi
61
62
  fi
63
+
@@ -9,6 +9,7 @@ GOLDEN_GATES=(
9
9
  "compliance:npm run compliance:false"
10
10
  "g04-selftest:bash scripts/golden-g04-selftest.sh:true"
11
11
  "g06-migrate-version:bash tests/run-golden-migrate-version.sh:true"
12
+ "security-migration:node tests/security/migration.cjs:false"
12
13
  "g07-negative-path:bash scripts/golden-g07-negative-path.sh:false"
13
14
  "g08-anti-vacuity:bash scripts/golden-g08-anti-vacuity.sh:false"
14
15
  "g09-yaml-roundtrip:bash scripts/golden-g09-yaml-roundtrip.sh:false"
@@ -66,10 +67,14 @@ GOLDEN_GATES=(
66
67
  "golden-g11-worktree-venv:bash scripts/test-golden-g11-worktree-venv.sh:false"
67
68
  "install-helpers:bash scripts/test-install-helpers.sh:false"
68
69
  "project-runtime:bash scripts/test-project-runtime.sh:false"
70
+ "security-agents-import:node tests/security/agents-import.cjs:false"
69
71
  "omp-extension:bash scripts/validate-omp-extension.sh:false"
70
72
  "mcp-package:bash scripts/validate-mcp-package.sh:false"
73
+ "security-git-context:node tests/security/git-context.cjs:false"
71
74
  "docs-base-path:bash scripts/validate-docs-base-path.sh:false"
75
+ "security-dashboard-bind:node tests/security/dashboard-bind.cjs:false"
72
76
  "import-boundaries:bash scripts/check-import-boundaries.sh:false"
77
+ "security-dashboard-render:node tests/security/dashboard-render.cjs:false"
73
78
  "skill-catalog:bash scripts/validate-skill-catalog.sh:false"
74
79
  "specs-parse:bash scripts/validate-specs-yaml.sh:false"
75
80
  # The runner itself: failing output is shown, skips are named and reasoned.
@@ -204,3 +209,4 @@ golden_run_deterministic_gates() {
204
209
  fi
205
210
  done
206
211
  }
212
+
@@ -21,23 +21,32 @@ EOF
21
21
  }
22
22
 
23
23
  migrate_installed_version() {
24
- node -e "console.log(require('$BIGPOWERS_ROOT/package.json').version)" 2>/dev/null || echo "unknown"
24
+ node -e 'console.log(require(process.argv[1]).version)' "$BIGPOWERS_ROOT/package.json" 2>/dev/null || echo "unknown"
25
+ }
26
+
27
+ migrate_validate_version() {
28
+ local version="$1" label="$2"
29
+ local pattern='^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$'
30
+ if [[ ! "$version" =~ $pattern ]]; then
31
+ echo "migrate-version: invalid $label version; expected major.minor.patch with optional prerelease/build metadata" >&2
32
+ return 4
33
+ fi
25
34
  }
26
35
 
27
36
  migrate_yaml_get() {
28
37
  local file="$1" key="$2"
29
- $PYTHON -c "
38
+ "$PYTHON" -c "
30
39
  import yaml, sys
31
40
  try:
32
- d = yaml.safe_load(open('$file'))
41
+ d = yaml.safe_load(open(sys.argv[1]))
33
42
  val = d
34
- for k in '$key'.split('.'):
43
+ for k in sys.argv[2].split('.'):
35
44
  if not isinstance(val, dict): sys.exit(1)
36
45
  val = val.get(k)
37
46
  if val is None: sys.exit(1)
38
47
  print(val)
39
48
  except: sys.exit(1)
40
- " 2>/dev/null
49
+ " "$file" "$key" 2>/dev/null
41
50
  }
42
51
 
43
52
  log_info() { echo " [INFO] $*"; }
@@ -111,3 +120,4 @@ migrate_detect_version() {
111
120
  fi
112
121
  return 0
113
122
  }
123
+
@@ -8,15 +8,14 @@ MIGRATE_VERSION_PLAN_LOADED=1
8
8
  migrate_build_plan() {
9
9
  echo "migrate-version: building migration plan from $REGISTRY …"
10
10
 
11
- MIGRATION_PLAN="$($PYTHON -c "
11
+ MIGRATION_PLAN="$("$PYTHON" -c "
12
12
  import yaml, json, sys
13
13
 
14
- with open('$REGISTRY') as f:
14
+ with open(sys.argv[1]) as f:
15
15
  parts = f.read().split('---')
16
16
  fm = yaml.safe_load(parts[1])
17
17
 
18
- detected = '$DETECTED'
19
- installed = '$INSTALLED'
18
+ detected, installed = sys.argv[2:4]
20
19
 
21
20
  plan = []
22
21
  for m in sorted(fm.get('migrations', []), key=lambda x: x.get('order', 99)):
@@ -30,7 +29,7 @@ for m in sorted(fm.get('migrations', []), key=lambda x: x.get('order', 99)):
30
29
 
31
30
  applicable = [m for m in plan if m['since_version'] > detected and m['since_version'] <= installed]
32
31
  print(json.dumps(applicable))
33
- " 2>/dev/null)"
32
+ " "$REGISTRY" "$DETECTED" "$INSTALLED")"
34
33
 
35
34
  MIGRATION_COUNT="$(echo "$MIGRATION_PLAN" | $PYTHON -c "import json,sys; print(len(json.load(sys.stdin)))" 2>/dev/null)"
36
35
 
@@ -79,7 +78,7 @@ for m in json.load(sys.stdin):
79
78
  echo "$MIGRATION_PLAN" | $PYTHON -c "
80
79
  import json, sys, os
81
80
  for m in json.load(sys.stdin):
82
- bundle_path = os.path.join('$MIGRATIONS_DIR', m['file'])
81
+ bundle_path = os.path.join(sys.argv[1], m['file'])
83
82
  if not os.path.exists(bundle_path):
84
83
  print(f\" WARN {m['id']}: bundle file not found ({m['file']})\")
85
84
  continue
@@ -112,8 +111,9 @@ for m in json.load(sys.stdin):
112
111
  for a in fm.get('actions_needed', []):
113
112
  v = a.get('verify', '')
114
113
  if v: print(f\" verify: {v}\")
115
- " 2>/dev/null
114
+ " "$MIGRATIONS_DIR" 2>/dev/null
116
115
 
117
116
  echo ""
118
117
  echo "Run without --dry-run to apply these migrations."
119
118
  }
119
+
@@ -28,6 +28,8 @@ migrate_version_main() {
28
28
 
29
29
  INSTALLED="$(migrate_installed_version)"
30
30
  migrate_detect_version || exit $?
31
+ migrate_validate_version "$DETECTED" detected || exit $?
32
+ migrate_validate_version "$INSTALLED" installed || exit $?
31
33
 
32
34
  echo "migrate-version: detected=$DETECTED installed=$INSTALLED"
33
35
 
@@ -51,3 +53,4 @@ migrate_version_main() {
51
53
  migrate_finalize
52
54
  exit 0
53
55
  }
56
+
@@ -17,6 +17,7 @@ if _LIB_DIR not in sys.path:
17
17
 
18
18
  from link_utils import LINK_RE, EXTERNAL_RE # noqa: E402
19
19
  from srp_engine_links import rewrite_links_for_pi # noqa: E402
20
+ from srp_engine_okf import render_okf_concept # noqa: E402
20
21
 
21
22
  # End-user setup (bigpowers setup) may run on a python3 without PyYAML.
22
23
  # Prefer PyYAML for exact parsing; fall back to the dependency-free
@@ -133,66 +134,6 @@ def iterate_skills(repo_root):
133
134
  skills.append(skill_md)
134
135
  return sorted(skills)
135
136
 
136
- def render_okf_concept(skill_data, okf_wiki_skills):
137
- name = skill_data['name']
138
- model = skill_data['model']
139
- effort = skill_data['effort']
140
- description = skill_data['description']
141
-
142
- os.makedirs(okf_wiki_skills, exist_ok=True)
143
- okf_file = os.path.join(okf_wiki_skills, f"{name}.md")
144
-
145
- phase = "utility"
146
- critical_path_skills = {
147
- "survey-context", "elaborate-spec", "model-domain", "define-language",
148
- "design-interface", "plan-work", "plan-release", "slice-tasks",
149
- "kickoff-branch", "develop-tdd", "verify-work", "audit-code",
150
- "commit-message", "release-branch", "security-review",
151
- "assess-impact", "investigate-bug", "validate-fix", "dispatch-agents",
152
- "request-review", "respond-review", "hook-commits", "guard-git",
153
- "wire-observability", "wire-ci", "smoke-test", "deploy", "enforce-first"
154
- }
155
- if name in critical_path_skills:
156
- phase = "critical-path"
157
-
158
- title = name.replace("-", " ").title()
159
- desc_escaped = description.replace("\\", "\\\\").replace('"', '\\"')
160
-
161
- lines = [
162
- "---",
163
- "okf_kind: concept",
164
- "type: Skill",
165
- f"id: {name}",
166
- f'title: "{title}"',
167
- f"name: {name}",
168
- "category: skills"
169
- ]
170
- if model:
171
- lines.append(f"model: {model}")
172
- lines.extend([
173
- f"effort: {effort}",
174
- f"phase: {phase}",
175
- f'description: "{desc_escaped}"',
176
- "references: []",
177
- "---",
178
- "",
179
- f"# {title}",
180
- "",
181
- f"**Phase:** {phase}"
182
- ])
183
- if model:
184
- lines.append(f"**Model:** {model}")
185
- lines.extend([
186
- f"**Effort:** {effort}",
187
- "",
188
- description,
189
- "",
190
- f"> Auto-generated by sync-skills.sh --okf (e39s04) from skills/{name}/SKILL.md"
191
- ])
192
-
193
- with open(okf_file, 'w', encoding='utf-8') as f:
194
- f.write('\n'.join(lines) + '\n')
195
-
196
137
  def resolve_bash():
197
138
  """Return an explicit path to a usable bash interpreter.
198
139
 
@@ -0,0 +1,65 @@
1
+ """OKF concept-page rendering for srp-engine (sync-skills.sh --okf)."""
2
+ # story: e39s04
3
+ # story: BUG-2026-10-30-compliance-akita-masked
4
+ import os
5
+
6
+
7
+ def render_okf_concept(skill_data, okf_wiki_skills):
8
+ name = skill_data['name']
9
+ model = skill_data['model']
10
+ effort = skill_data['effort']
11
+ description = skill_data['description']
12
+
13
+ os.makedirs(okf_wiki_skills, exist_ok=True)
14
+ okf_file = os.path.join(okf_wiki_skills, f"{name}.md")
15
+
16
+ phase = "utility"
17
+ critical_path_skills = {
18
+ "survey-context", "elaborate-spec", "model-domain", "define-language",
19
+ "design-interface", "plan-work", "plan-release", "slice-tasks",
20
+ "kickoff-branch", "develop-tdd", "verify-work", "audit-code",
21
+ "commit-message", "release-branch", "security-review",
22
+ "assess-impact", "investigate-bug", "validate-fix", "dispatch-agents",
23
+ "request-review", "respond-review", "hook-commits", "guard-git",
24
+ "wire-observability", "wire-ci", "smoke-test", "deploy", "enforce-first"
25
+ }
26
+ if name in critical_path_skills:
27
+ phase = "critical-path"
28
+
29
+ title = name.replace("-", " ").title()
30
+ desc_escaped = description.replace("\\", "\\\\").replace('"', '\\"')
31
+
32
+ lines = [
33
+ "---",
34
+ "okf_kind: concept",
35
+ "type: Skill",
36
+ f"id: {name}",
37
+ f'title: "{title}"',
38
+ f"name: {name}",
39
+ "category: skills"
40
+ ]
41
+ if model:
42
+ lines.append(f"model: {model}")
43
+ lines.extend([
44
+ f"effort: {effort}",
45
+ f"phase: {phase}",
46
+ f'description: "{desc_escaped}"',
47
+ "references: []",
48
+ "---",
49
+ "",
50
+ f"# {title}",
51
+ "",
52
+ f"**Phase:** {phase}"
53
+ ])
54
+ if model:
55
+ lines.append(f"**Model:** {model}")
56
+ lines.extend([
57
+ f"**Effort:** {effort}",
58
+ "",
59
+ description,
60
+ "",
61
+ f"> Auto-generated by sync-skills.sh --okf (e39s04) from skills/{name}/SKILL.md"
62
+ ])
63
+
64
+ with open(okf_file, 'w', encoding='utf-8') as f:
65
+ f.write('\n'.join(lines) + '\n')