bigpowers 2.88.17 → 2.89.0
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/.gemini/extensions/bigpowers/gemini-extension.json +2 -2
- package/.pi/package.json +2 -2
- package/CHANGELOG.md +18 -0
- package/CLAUDE.md +1 -1
- package/CONVENTIONS.md +0 -1
- package/README.md +1 -1
- package/SKILL-INDEX.md +43 -44
- package/bigpowers-mcp/build/lib/git-context.js +52 -36
- package/bigpowers-mcp/build/lib/git-context.js.map +1 -1
- package/bigpowers-mcp/src/lib/git-context.ts +9 -5
- package/dashboard/src/web/client.html +15 -8
- package/dashboard/src/web/server.js +2 -1
- package/package.json +1 -1
- package/scripts/bp-read-agents.sh +7 -5
- package/scripts/generate-skill-index.sh +1 -1
- package/scripts/lib/__pycache__/link_utils.cpython-310.pyc +0 -0
- package/scripts/lib/__pycache__/srp_engine_links.cpython-310.pyc +0 -0
- package/scripts/lib/__pycache__/srp_engine_okf.cpython-310.pyc +0 -0
- package/scripts/lib/doc-fetch-cache.sh +1 -1
- package/scripts/lib/golden-suite-gates.sh +6 -0
- package/scripts/lib/migrate-version-common.sh +15 -5
- package/scripts/lib/migrate-version-plan.sh +7 -7
- package/scripts/lib/migrate-version-run.sh +3 -0
- package/scripts/lib/sync-post.sh +1 -1
- package/scripts/validate-skill-catalog.sh +1 -1
- package/skills-lock.json +0 -5
- package/tests/security/agents-import.cjs +36 -0
- package/tests/security/dashboard-bind.cjs +31 -0
- package/tests/security/dashboard-render.cjs +60 -0
- package/tests/security/git-context.cjs +44 -0
- package/tests/security/migration.cjs +76 -0
- package/.agents/skills/context7-mcp/SKILL.md +0 -55
- package/.cline/skills/context7-mcp/SKILL.md +0 -55
- package/.codebuddy/skills/context7-mcp/SKILL.md +0 -55
- package/.codex/skills/context7-mcp/SKILL.md +0 -55
- package/.continue/rules/context7-mcp.md +0 -0
- package/.copilot/skills/context7-mcp/SKILL.md +0 -55
- package/.cursor/rules/context7-mcp.mdc +0 -54
- package/.gemini/extensions/bigpowers/commands/prompts/context7-mcp.md +0 -54
- package/.gemini/extensions/bigpowers/skills/context7-mcp/SKILL.md +0 -56
- package/.hermes/skills/context7-mcp/SKILL.md +0 -55
- package/.kilocode/rules/context7-mcp.md +0 -55
- package/.mimocode/skills/context7-mcp/SKILL.md +0 -55
- package/.opencode/skills/context7-mcp/SKILL.md +0 -55
- package/.pi/prompts/context7-mcp.md +0 -54
- package/.pi/skills/context7-mcp/SKILL.md +0 -55
- package/.qwen/skills/context7-mcp/SKILL.md +0 -55
- package/.trae/skills/context7-mcp/SKILL.md +0 -55
- package/.windsurf/rules/context7-mcp.md +0 -55
- package/.zcode/skills/context7-mcp/SKILL.md +0 -55
- package/skills/context7-mcp/SKILL.md +0 -57
|
@@ -21,23 +21,32 @@ EOF
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
migrate_installed_version() {
|
|
24
|
-
node -e
|
|
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(
|
|
41
|
+
d = yaml.safe_load(open(sys.argv[1]))
|
|
33
42
|
val = d
|
|
34
|
-
for k in
|
|
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(
|
|
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 =
|
|
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
|
-
"
|
|
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(
|
|
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
|
+
|
package/scripts/lib/sync-post.sh
CHANGED
|
@@ -73,7 +73,7 @@ sync_post_run() {
|
|
|
73
73
|
bash "$REPO_ROOT/scripts/build-skill-index.sh" || true
|
|
74
74
|
fi
|
|
75
75
|
|
|
76
|
-
local CURSOR_KEEP="
|
|
76
|
+
local CURSOR_KEEP="" mdc mdc_base
|
|
77
77
|
for mdc in "$CURSOR_RULES"/*.mdc; do
|
|
78
78
|
[[ -e "$mdc" ]] || continue
|
|
79
79
|
mdc_base=$(basename "$mdc")
|
|
@@ -18,7 +18,7 @@ VIOLATIONS=0
|
|
|
18
18
|
WARNINGS=0
|
|
19
19
|
|
|
20
20
|
# Documented naming exceptions (craft-skill REFERENCE.md)
|
|
21
|
-
VERB_NOUN_EXCEPTIONS="grill-me grill-with-docs
|
|
21
|
+
VERB_NOUN_EXCEPTIONS="grill-me grill-with-docs using-bigpowers deploy generate-allure-report"
|
|
22
22
|
|
|
23
23
|
catalog_usage() {
|
|
24
24
|
echo "Usage: bash scripts/validate-skill-catalog.sh [--archive] [--strict] [--skill <name>]"
|
package/skills-lock.json
CHANGED
|
@@ -41,11 +41,6 @@
|
|
|
41
41
|
"sha256": "5dc23c0642af9d2b",
|
|
42
42
|
"path": "skills/compose-workflow/SKILL.md"
|
|
43
43
|
},
|
|
44
|
-
"context7-mcp": {
|
|
45
|
-
"description": "Fetch current library docs via Context7 MCP instead of training data. Use when user asks about frameworks, APIs, setup, or code examples for React, Next.js, Prisma, etc.",
|
|
46
|
-
"sha256": "f58dcec5aff6a0bf",
|
|
47
|
-
"path": "skills/context7-mcp/SKILL.md"
|
|
48
|
-
},
|
|
49
44
|
"craft-skill": {
|
|
50
45
|
"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
46
|
"sha256": "7acb068f458fa1b4",
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// story: e21s03
|
|
2
|
+
const assert = require('assert');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { execFileSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'bp-agents-'));
|
|
9
|
+
const script = path.resolve(__dirname, '../../scripts/bp-read-agents.sh');
|
|
10
|
+
const commands = [
|
|
11
|
+
"echo'; printf IMPORT_RAN > import-marker; #",
|
|
12
|
+
"node -e 'console.log(\"quoted value\")'",
|
|
13
|
+
'echo $(touch substitution-marker) `touch backtick-marker` "$HOME"; echo done',
|
|
14
|
+
"printf '%s\\n' 'spaces & symbols | < >'",
|
|
15
|
+
'echo "deploy" && echo finished'
|
|
16
|
+
];
|
|
17
|
+
const names = ['PREFLIGHT', 'TEST', 'BUILD', 'LINT', 'DEPLOY'];
|
|
18
|
+
const sections = ['Preflight', 'Test', 'Build', 'Lint', 'Deploy'];
|
|
19
|
+
try {
|
|
20
|
+
for (const source of ['AGENTS.md', 'CLAUDE.md', 'CURSOR.md']) {
|
|
21
|
+
fs.writeFileSync(path.join(root, source), commands.map((c, i) =>
|
|
22
|
+
`## ${sections[i]}\n\n\`\`\`bash\n${c}\n\`\`\`\n`).join('\n'));
|
|
23
|
+
const output = execFileSync('bash', ['-c',
|
|
24
|
+
'eval "$(bash "$1")"; printf "%s\\0" "$BP_PREFLIGHT" "$BP_TEST" "$BP_BUILD" "$BP_LINT" "$BP_DEPLOY"',
|
|
25
|
+
'import-test', script], { cwd: root, encoding: 'utf8' });
|
|
26
|
+
for (const marker of ['import-marker', 'substitution-marker', 'backtick-marker']) {
|
|
27
|
+
assert(!fs.existsSync(path.join(root, marker)), `${source}: importing executed ${marker}`);
|
|
28
|
+
}
|
|
29
|
+
assert.deepStrictEqual(output.split('\0').slice(0, -1), commands, `${source}: exact values`);
|
|
30
|
+
const printed = execFileSync('bash', [script, '--print'], { cwd: root, encoding: 'utf8' });
|
|
31
|
+
for (const command of commands) assert(printed.includes(command));
|
|
32
|
+
fs.unlinkSync(path.join(root, source));
|
|
33
|
+
}
|
|
34
|
+
assert.strictEqual(execFileSync('bash', [script], { cwd: root, encoding: 'utf8' }), '');
|
|
35
|
+
console.log(`PASS: ${names.length} command exports round-trip without execution in all 3 source formats`);
|
|
36
|
+
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const http = require('http');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const vm = require('vm');
|
|
6
|
+
|
|
7
|
+
// Replace filesystem watching and Express routing at the I/O boundary, but
|
|
8
|
+
// exercise a real TCP listener with the arguments supplied by createServer.
|
|
9
|
+
const filename = path.resolve(__dirname, '../../dashboard/src/web/server.js');
|
|
10
|
+
const moduleStub = { exports: {} };
|
|
11
|
+
const sandbox = {
|
|
12
|
+
module: moduleStub, __dirname: path.dirname(filename), console, setTimeout, clearTimeout,
|
|
13
|
+
require(name) {
|
|
14
|
+
if (name === 'path') return path;
|
|
15
|
+
if (name === '../loaders/watcher') return { watch: () => ({ on() {} }) };
|
|
16
|
+
if (name.startsWith('../loaders/')) return {};
|
|
17
|
+
assert.strictEqual(name, 'express');
|
|
18
|
+
return () => ({ get() {}, listen: (...args) => http.createServer().listen(...args) });
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
vm.runInNewContext(fs.readFileSync(filename, 'utf8'), sandbox, { filename });
|
|
22
|
+
const app = moduleStub.exports.createServer('/unused', 0);
|
|
23
|
+
const server = app.listen(0, () => {
|
|
24
|
+
try {
|
|
25
|
+
assert.strictEqual(server.address().address, '127.0.0.1');
|
|
26
|
+
assert(server.address().port > 0, 'ephemeral port and callback remain supported');
|
|
27
|
+
console.log('PASS: dashboard binds only to IPv4 loopback');
|
|
28
|
+
} catch (error) { console.error(error); process.exitCode = 1; }
|
|
29
|
+
finally { server.close(); }
|
|
30
|
+
});
|
|
31
|
+
server.on('error', error => { console.error(error); process.exitCode = 1; });
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const vm = require('vm');
|
|
5
|
+
|
|
6
|
+
// Execute the shipped client and feed its public fetch/SSE inputs. Capture
|
|
7
|
+
// generated HTML at the DOM boundary; this is not a browser execution test.
|
|
8
|
+
const nodes = new Map(['pipeline', 'metrics', 'state', 'epics', 'ledger', 'status']
|
|
9
|
+
.map(id => [id, { innerHTML: '', classList: { add() {}, remove() {} } }]));
|
|
10
|
+
const events = new Map();
|
|
11
|
+
let stream;
|
|
12
|
+
const payload = '<img src=x onerror="window.__bpXss=1"> & \'quoted\'';
|
|
13
|
+
const escaped = '<img src=x onerror="window.__bpXss=1"> & 'quoted'';
|
|
14
|
+
const snapshot = {
|
|
15
|
+
state: { activeFlow: payload, activeEpic: payload, activeStory: payload,
|
|
16
|
+
gitBranch: payload, release: { version: payload } },
|
|
17
|
+
projectMetrics: { totalBcps: payload, avgBcpPerHour: 1.5 },
|
|
18
|
+
currentVelocity: { avgBcpPerHour: 2 },
|
|
19
|
+
epics: [{ id: payload, title: payload, stories: [] }],
|
|
20
|
+
cycleTimes: [{ id: payload, bcps: payload, cycleMin: payload, bcpPerHour: 3 }]
|
|
21
|
+
};
|
|
22
|
+
const sandbox = {
|
|
23
|
+
console,
|
|
24
|
+
document: {
|
|
25
|
+
getElementById: id => nodes.get(id), querySelectorAll: () => [],
|
|
26
|
+
addEventListener() {}, createElement: () => ({ getContext: () => ({}) })
|
|
27
|
+
},
|
|
28
|
+
window: { addEventListener: (name, fn) => events.set(name, fn) },
|
|
29
|
+
fetch: async () => ({ json: async () => JSON.parse(JSON.stringify(snapshot)) }),
|
|
30
|
+
EventSource: class { constructor() { stream = this; } },
|
|
31
|
+
setInterval() { throw new Error('unexpected polling fallback'); }
|
|
32
|
+
};
|
|
33
|
+
const filename = path.resolve(__dirname, '../../dashboard/src/web/client.html');
|
|
34
|
+
const html = fs.readFileSync(filename, 'utf8');
|
|
35
|
+
for (const match of html.matchAll(/<script>([\s\S]*?)<\/script>/g)) {
|
|
36
|
+
vm.runInNewContext(match[1], sandbox, { filename });
|
|
37
|
+
}
|
|
38
|
+
function assertEscaped() {
|
|
39
|
+
for (const [panel, count] of [['state', 4], ['metrics', 2], ['epics', 2], ['ledger', 3]]) {
|
|
40
|
+
const output = nodes.get(panel).innerHTML;
|
|
41
|
+
assert(!output.includes('<img'), `${panel}: project metadata became HTML`);
|
|
42
|
+
assert.strictEqual(output.split(escaped).length - 1, count, `${panel}: literal text preserved`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function main() {
|
|
46
|
+
events.get('load')();
|
|
47
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
48
|
+
assertEscaped();
|
|
49
|
+
assert(stream, 'initial fetch should connect SSE');
|
|
50
|
+
stream.onmessage({ data: JSON.stringify(snapshot) });
|
|
51
|
+
assertEscaped();
|
|
52
|
+
stream.onmessage({ data: JSON.stringify({ state: { gitBranch: 'feature/example' },
|
|
53
|
+
projectMetrics: { totalBcps: 0, avgBcpPerHour: 0 } }) });
|
|
54
|
+
assert(nodes.get('state').innerHTML.includes('feature/example'));
|
|
55
|
+
assert(nodes.get('metrics').innerHTML.includes('>0</span>'));
|
|
56
|
+
assert(nodes.get('epics').innerHTML.includes('No epics available'));
|
|
57
|
+
assert(nodes.get('ledger').innerHTML.includes('No cycle-time data available'));
|
|
58
|
+
console.log('PASS: fetch and SSE render all project metadata as escaped text');
|
|
59
|
+
}
|
|
60
|
+
main().catch(error => { console.error(error); process.exitCode = 1; });
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// story: e32s04
|
|
2
|
+
const assert = require('assert');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { execFileSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'bp-diff-'));
|
|
9
|
+
const git = (...args) => execFileSync('git', args, { cwd: root, stdio: 'pipe' });
|
|
10
|
+
const put = (name, value) => {
|
|
11
|
+
fs.mkdirSync(path.dirname(path.join(root, name)), { recursive: true });
|
|
12
|
+
fs.writeFileSync(path.join(root, name), value);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
async function main() {
|
|
16
|
+
const { getGitContext } = await import('../../bigpowers-mcp/build/lib/git-context.js');
|
|
17
|
+
git('init', '-q');
|
|
18
|
+
const secret = 'specs/m-secret.pem';
|
|
19
|
+
const before = 'specs/a-public.md';
|
|
20
|
+
const after = 'specs/z-public.md';
|
|
21
|
+
const outside = 'private/readme.md';
|
|
22
|
+
for (const file of [secret, before, after, outside]) put(file, 'baseline\n');
|
|
23
|
+
git('add', '.');
|
|
24
|
+
git('-c', 'user.name=Test', '-c', 'user.email=test@example.invalid',
|
|
25
|
+
'commit', '-qm', 'fixture');
|
|
26
|
+
for (const allowed of [[after], [before, after], [before], []]) {
|
|
27
|
+
git('reset', '--hard', 'HEAD');
|
|
28
|
+
put(secret, 'DUMMY_SECRET_VALUE\n');
|
|
29
|
+
put(outside, 'OUT_OF_SCOPE_VALUE\n');
|
|
30
|
+
for (const file of allowed) put(file, 'PUBLIC_CHANGE\n');
|
|
31
|
+
const result = getGitContext(root, 'diff');
|
|
32
|
+
assert.deepStrictEqual(Object.keys(result.diffs).sort(), [...allowed].sort());
|
|
33
|
+
assert.deepStrictEqual(Object.values(result.changed_files).flat().sort(), [...allowed].sort());
|
|
34
|
+
assert(!JSON.stringify(result).includes('DUMMY_SECRET_VALUE'));
|
|
35
|
+
assert(!JSON.stringify(result).includes('OUT_OF_SCOPE_VALUE'));
|
|
36
|
+
for (const file of allowed) assert(result.diffs[file].includes('+PUBLIC_CHANGE'));
|
|
37
|
+
}
|
|
38
|
+
git('reset', '--hard', 'HEAD');
|
|
39
|
+
assert.deepStrictEqual(getGitContext(root, 'diff').diffs, {});
|
|
40
|
+
console.log('PASS: secret diffs excluded in first, middle, last and only positions');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
main().catch(error => { console.error(error); process.exitCode = 1; })
|
|
44
|
+
.finally(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// story: e44s03 e44s06
|
|
2
|
+
const assert = require('assert');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { execFileSync, spawnSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'bp-migration-'));
|
|
9
|
+
const repo = path.resolve(__dirname, '../..');
|
|
10
|
+
const project = path.join(root, 'project');
|
|
11
|
+
const specs = path.join(project, 'specs');
|
|
12
|
+
const marker = path.join(root, 'executed-marker');
|
|
13
|
+
const git = (...args) => execFileSync('git', args, { cwd: project, stdio: 'pipe' });
|
|
14
|
+
function stamp(version) {
|
|
15
|
+
fs.writeFileSync(path.join(specs, 'state.yaml'),
|
|
16
|
+
`bigpowers_version: ${JSON.stringify(version)}\nactive_flow: null\n`);
|
|
17
|
+
git('add', '.');
|
|
18
|
+
git('-c', 'user.name=Test', '-c', 'user.email=test@example.invalid',
|
|
19
|
+
'commit', '--allow-empty', '-qm', 'fixture');
|
|
20
|
+
}
|
|
21
|
+
function preview(script) {
|
|
22
|
+
const before = fs.readFileSync(path.join(specs, 'state.yaml'), 'utf8');
|
|
23
|
+
const result = spawnSync('bash', [script, '--project', project, '--dry-run'],
|
|
24
|
+
{ cwd: project, encoding: 'utf8' });
|
|
25
|
+
assert(!result.error, String(result.error));
|
|
26
|
+
assert(!fs.existsSync(marker), 'dry run executed version metadata');
|
|
27
|
+
assert.strictEqual(fs.readFileSync(path.join(specs, 'state.yaml'), 'utf8'), before);
|
|
28
|
+
assert.deepStrictEqual(fs.readdirSync(specs), ['state.yaml'], 'dry run wrote files');
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
function copyFile(relative, destination) {
|
|
32
|
+
const target = path.join(destination, relative);
|
|
33
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
34
|
+
fs.copyFileSync(path.join(repo, relative), target);
|
|
35
|
+
}
|
|
36
|
+
function installFixture(destination) {
|
|
37
|
+
for (const relative of ['package.json', 'scripts/migrate-version.sh',
|
|
38
|
+
'scripts/lib/python-env.sh', ...['common', 'plan', 'run', 'post', 'execute', 'transforms']
|
|
39
|
+
.map(name => `scripts/lib/migrate-version-${name}.sh`)]) copyFile(relative, destination);
|
|
40
|
+
const migrations = path.join(destination, 'specs/migrations');
|
|
41
|
+
fs.mkdirSync(migrations, { recursive: true });
|
|
42
|
+
fs.writeFileSync(path.join(migrations, 'registry.okf.md'),
|
|
43
|
+
'---\nmigrations:\n - id: test\n since_version: "2.80.0"\n order: 1\n---\n');
|
|
44
|
+
fs.writeFileSync(path.join(migrations, 'test.okf.md'),
|
|
45
|
+
'---\ntransforms: []\nactions_needed: []\n---\n');
|
|
46
|
+
return path.join(destination, 'scripts/migrate-version.sh');
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
fs.mkdirSync(specs, { recursive: true });
|
|
50
|
+
git('init', '-q', '-b', 'main');
|
|
51
|
+
const script = installFixture(path.join(root, 'installation'));
|
|
52
|
+
stamp(`0.0.0'; open('${marker}', 'w').write('DUMMY'); #`);
|
|
53
|
+
const hostile = preview(script);
|
|
54
|
+
assert.notStrictEqual(hostile.status, 0, 'invalid version must fail closed');
|
|
55
|
+
assert(/invalid.*version/i.test(hostile.stderr), hostile.stderr);
|
|
56
|
+
for (const version of ['1.0.0', '2.0.0', '2.20.0', '2.20.0-rc.1+build.2']) {
|
|
57
|
+
stamp(version);
|
|
58
|
+
const result = preview(script);
|
|
59
|
+
assert.strictEqual(result.status, 0, result.stderr);
|
|
60
|
+
assert(result.stdout.includes(`Detected version: ${version}`), result.stdout);
|
|
61
|
+
}
|
|
62
|
+
// The same data boundary applies to legitimate installation/project paths.
|
|
63
|
+
const installation = path.join(root, "install's directory");
|
|
64
|
+
const quotedScript = installFixture(installation);
|
|
65
|
+
stamp('1.0.0');
|
|
66
|
+
const quotedProject = path.join(root, "project's directory");
|
|
67
|
+
fs.symlinkSync(project, quotedProject, 'dir');
|
|
68
|
+
const result = spawnSync('bash', [quotedScript,
|
|
69
|
+
'--project', quotedProject, '--dry-run'], { encoding: 'utf8' });
|
|
70
|
+
assert.strictEqual(result.status, 0, result.stderr);
|
|
71
|
+
assert(result.stdout.includes('Detected version: 1.0.0'), result.stdout);
|
|
72
|
+
assert(result.stdout.includes('Transforms preview:'), result.stdout);
|
|
73
|
+
assert(result.stdout.includes('-- test: test --'), result.stdout);
|
|
74
|
+
assert(!result.stdout.includes('installed=unknown'), result.stdout);
|
|
75
|
+
console.log('PASS: migration preview rejects executable stamps and supports quoted paths');
|
|
76
|
+
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: context7-mcp
|
|
3
|
-
model: haiku
|
|
4
|
-
description: "Fetch current library docs via Context7 MCP instead of training data. Use when user asks about frameworks, APIs, setup, or code examples for React, Next.js, Prisma, etc."
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# Context7 MCP
|
|
8
|
-
|
|
9
|
-
> **HARD GATE** — Max **3** Context7 tool calls per user question (`resolve-library-id` + `query-docs` count toward the cap). On quota/rate-limit errors, emit an explicit **CONTEXT7_UNAVAILABLE** block — do NOT silently answer from training data.
|
|
10
|
-
>
|
|
11
|
-
> **HARD GATE** — Before HTTP fetch, check `bash scripts/lib/doc-fetch-cache.sh get "<libraryId>:<query>"`. Cache hit within TTL → use cached body (no round-trip). On ETag mismatch after conditional refresh, replace cache entry.
|
|
12
|
-
|
|
13
|
-
## When to Use
|
|
14
|
-
|
|
15
|
-
- Setup/configuration questions ("How do I configure Next.js middleware?")
|
|
16
|
-
- Code involving libraries ("Write a Prisma query for…")
|
|
17
|
-
- API references ("What are the Supabase auth methods?")
|
|
18
|
-
- User mentions specific frameworks (React, Vue, Svelte, Express, Tailwind, etc.)
|
|
19
|
-
|
|
20
|
-
## Bounded Retry (max 3x)
|
|
21
|
-
|
|
22
|
-
| Attempt | Action |
|
|
23
|
-
|---------|--------|
|
|
24
|
-
| 1 | `resolve-library-id` → pick best match |
|
|
25
|
-
| 2 | `query-docs` with selected `libraryId` |
|
|
26
|
-
| 3 | Retry `query-docs` once with refined query (narrower scope) |
|
|
27
|
-
|
|
28
|
-
After 3 failures, stop and print:
|
|
29
|
-
|
|
30
|
-
```
|
|
31
|
-
CONTEXT7_UNAVAILABLE
|
|
32
|
-
Reason: <quota|rate-limit|no-match|timeout>
|
|
33
|
-
Action: Ask user to retry later, paste official docs URL, or run `bts docs <lib>`.
|
|
34
|
-
Do NOT substitute training-data answers without labeling them UNVERIFIED.
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
## Fetch Cache (ETag-revalidated)
|
|
38
|
-
|
|
39
|
-
1. **Cache key:** `"<libraryId>:<normalized-query>"` (lowercase, trimmed).
|
|
40
|
-
2. **Read:** `bash scripts/lib/doc-fetch-cache.sh get "<key>"` — exit 0 → use cached body.
|
|
41
|
-
3. **Miss / stale:** call `query-docs`; store via `doc-fetch-cache.sh put`.
|
|
42
|
-
4. **TTL:** 300s default (`DOC_CACHE_TTL`). Stale entries refresh on next fetch; honor `ETag` when MCP returns it.
|
|
43
|
-
|
|
44
|
-
`bts docs <lib>` shares the same cache helper when invoked from this skill.
|
|
45
|
-
|
|
46
|
-
## Process
|
|
47
|
-
|
|
48
|
-
1. `resolve-library-id` with `libraryName` + full user `query`.
|
|
49
|
-
2. Select match: name similarity, reputation, benchmark score; prefer version-specific IDs when user names a version.
|
|
50
|
-
3. `query-docs` with `libraryId` + specific question (one concept per call).
|
|
51
|
-
4. Answer using fetched docs; cite library/version when relevant.
|
|
52
|
-
|
|
53
|
-
## Verify
|
|
54
|
-
|
|
55
|
-
→ verify: `test -f scripts/lib/doc-fetch-cache.sh`
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: context7-mcp
|
|
3
|
-
model: haiku
|
|
4
|
-
description: "Fetch current library docs via Context7 MCP instead of training data. Use when user asks about frameworks, APIs, setup, or code examples for React, Next.js, Prisma, etc."
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# Context7 MCP
|
|
8
|
-
|
|
9
|
-
> **HARD GATE** — Max **3** Context7 tool calls per user question (`resolve-library-id` + `query-docs` count toward the cap). On quota/rate-limit errors, emit an explicit **CONTEXT7_UNAVAILABLE** block — do NOT silently answer from training data.
|
|
10
|
-
>
|
|
11
|
-
> **HARD GATE** — Before HTTP fetch, check `bash scripts/lib/doc-fetch-cache.sh get "<libraryId>:<query>"`. Cache hit within TTL → use cached body (no round-trip). On ETag mismatch after conditional refresh, replace cache entry.
|
|
12
|
-
|
|
13
|
-
## When to Use
|
|
14
|
-
|
|
15
|
-
- Setup/configuration questions ("How do I configure Next.js middleware?")
|
|
16
|
-
- Code involving libraries ("Write a Prisma query for…")
|
|
17
|
-
- API references ("What are the Supabase auth methods?")
|
|
18
|
-
- User mentions specific frameworks (React, Vue, Svelte, Express, Tailwind, etc.)
|
|
19
|
-
|
|
20
|
-
## Bounded Retry (max 3x)
|
|
21
|
-
|
|
22
|
-
| Attempt | Action |
|
|
23
|
-
|---------|--------|
|
|
24
|
-
| 1 | `resolve-library-id` → pick best match |
|
|
25
|
-
| 2 | `query-docs` with selected `libraryId` |
|
|
26
|
-
| 3 | Retry `query-docs` once with refined query (narrower scope) |
|
|
27
|
-
|
|
28
|
-
After 3 failures, stop and print:
|
|
29
|
-
|
|
30
|
-
```
|
|
31
|
-
CONTEXT7_UNAVAILABLE
|
|
32
|
-
Reason: <quota|rate-limit|no-match|timeout>
|
|
33
|
-
Action: Ask user to retry later, paste official docs URL, or run `bts docs <lib>`.
|
|
34
|
-
Do NOT substitute training-data answers without labeling them UNVERIFIED.
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
## Fetch Cache (ETag-revalidated)
|
|
38
|
-
|
|
39
|
-
1. **Cache key:** `"<libraryId>:<normalized-query>"` (lowercase, trimmed).
|
|
40
|
-
2. **Read:** `bash scripts/lib/doc-fetch-cache.sh get "<key>"` — exit 0 → use cached body.
|
|
41
|
-
3. **Miss / stale:** call `query-docs`; store via `doc-fetch-cache.sh put`.
|
|
42
|
-
4. **TTL:** 300s default (`DOC_CACHE_TTL`). Stale entries refresh on next fetch; honor `ETag` when MCP returns it.
|
|
43
|
-
|
|
44
|
-
`bts docs <lib>` shares the same cache helper when invoked from this skill.
|
|
45
|
-
|
|
46
|
-
## Process
|
|
47
|
-
|
|
48
|
-
1. `resolve-library-id` with `libraryName` + full user `query`.
|
|
49
|
-
2. Select match: name similarity, reputation, benchmark score; prefer version-specific IDs when user names a version.
|
|
50
|
-
3. `query-docs` with `libraryId` + specific question (one concept per call).
|
|
51
|
-
4. Answer using fetched docs; cite library/version when relevant.
|
|
52
|
-
|
|
53
|
-
## Verify
|
|
54
|
-
|
|
55
|
-
→ verify: `test -f scripts/lib/doc-fetch-cache.sh`
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: context7-mcp
|
|
3
|
-
model: haiku
|
|
4
|
-
description: "Fetch current library docs via Context7 MCP instead of training data. Use when user asks about frameworks, APIs, setup, or code examples for React, Next.js, Prisma, etc."
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# Context7 MCP
|
|
8
|
-
|
|
9
|
-
> **HARD GATE** — Max **3** Context7 tool calls per user question (`resolve-library-id` + `query-docs` count toward the cap). On quota/rate-limit errors, emit an explicit **CONTEXT7_UNAVAILABLE** block — do NOT silently answer from training data.
|
|
10
|
-
>
|
|
11
|
-
> **HARD GATE** — Before HTTP fetch, check `bash scripts/lib/doc-fetch-cache.sh get "<libraryId>:<query>"`. Cache hit within TTL → use cached body (no round-trip). On ETag mismatch after conditional refresh, replace cache entry.
|
|
12
|
-
|
|
13
|
-
## When to Use
|
|
14
|
-
|
|
15
|
-
- Setup/configuration questions ("How do I configure Next.js middleware?")
|
|
16
|
-
- Code involving libraries ("Write a Prisma query for…")
|
|
17
|
-
- API references ("What are the Supabase auth methods?")
|
|
18
|
-
- User mentions specific frameworks (React, Vue, Svelte, Express, Tailwind, etc.)
|
|
19
|
-
|
|
20
|
-
## Bounded Retry (max 3x)
|
|
21
|
-
|
|
22
|
-
| Attempt | Action |
|
|
23
|
-
|---------|--------|
|
|
24
|
-
| 1 | `resolve-library-id` → pick best match |
|
|
25
|
-
| 2 | `query-docs` with selected `libraryId` |
|
|
26
|
-
| 3 | Retry `query-docs` once with refined query (narrower scope) |
|
|
27
|
-
|
|
28
|
-
After 3 failures, stop and print:
|
|
29
|
-
|
|
30
|
-
```
|
|
31
|
-
CONTEXT7_UNAVAILABLE
|
|
32
|
-
Reason: <quota|rate-limit|no-match|timeout>
|
|
33
|
-
Action: Ask user to retry later, paste official docs URL, or run `bts docs <lib>`.
|
|
34
|
-
Do NOT substitute training-data answers without labeling them UNVERIFIED.
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
## Fetch Cache (ETag-revalidated)
|
|
38
|
-
|
|
39
|
-
1. **Cache key:** `"<libraryId>:<normalized-query>"` (lowercase, trimmed).
|
|
40
|
-
2. **Read:** `bash scripts/lib/doc-fetch-cache.sh get "<key>"` — exit 0 → use cached body.
|
|
41
|
-
3. **Miss / stale:** call `query-docs`; store via `doc-fetch-cache.sh put`.
|
|
42
|
-
4. **TTL:** 300s default (`DOC_CACHE_TTL`). Stale entries refresh on next fetch; honor `ETag` when MCP returns it.
|
|
43
|
-
|
|
44
|
-
`bts docs <lib>` shares the same cache helper when invoked from this skill.
|
|
45
|
-
|
|
46
|
-
## Process
|
|
47
|
-
|
|
48
|
-
1. `resolve-library-id` with `libraryName` + full user `query`.
|
|
49
|
-
2. Select match: name similarity, reputation, benchmark score; prefer version-specific IDs when user names a version.
|
|
50
|
-
3. `query-docs` with `libraryId` + specific question (one concept per call).
|
|
51
|
-
4. Answer using fetched docs; cite library/version when relevant.
|
|
52
|
-
|
|
53
|
-
## Verify
|
|
54
|
-
|
|
55
|
-
→ verify: `test -f scripts/lib/doc-fetch-cache.sh`
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: context7-mcp
|
|
3
|
-
model: haiku
|
|
4
|
-
description: "Fetch current library docs via Context7 MCP instead of training data. Use when user asks about frameworks, APIs, setup, or code examples for React, Next.js, Prisma, etc."
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# Context7 MCP
|
|
8
|
-
|
|
9
|
-
> **HARD GATE** — Max **3** Context7 tool calls per user question (`resolve-library-id` + `query-docs` count toward the cap). On quota/rate-limit errors, emit an explicit **CONTEXT7_UNAVAILABLE** block — do NOT silently answer from training data.
|
|
10
|
-
>
|
|
11
|
-
> **HARD GATE** — Before HTTP fetch, check `bash scripts/lib/doc-fetch-cache.sh get "<libraryId>:<query>"`. Cache hit within TTL → use cached body (no round-trip). On ETag mismatch after conditional refresh, replace cache entry.
|
|
12
|
-
|
|
13
|
-
## When to Use
|
|
14
|
-
|
|
15
|
-
- Setup/configuration questions ("How do I configure Next.js middleware?")
|
|
16
|
-
- Code involving libraries ("Write a Prisma query for…")
|
|
17
|
-
- API references ("What are the Supabase auth methods?")
|
|
18
|
-
- User mentions specific frameworks (React, Vue, Svelte, Express, Tailwind, etc.)
|
|
19
|
-
|
|
20
|
-
## Bounded Retry (max 3x)
|
|
21
|
-
|
|
22
|
-
| Attempt | Action |
|
|
23
|
-
|---------|--------|
|
|
24
|
-
| 1 | `resolve-library-id` → pick best match |
|
|
25
|
-
| 2 | `query-docs` with selected `libraryId` |
|
|
26
|
-
| 3 | Retry `query-docs` once with refined query (narrower scope) |
|
|
27
|
-
|
|
28
|
-
After 3 failures, stop and print:
|
|
29
|
-
|
|
30
|
-
```
|
|
31
|
-
CONTEXT7_UNAVAILABLE
|
|
32
|
-
Reason: <quota|rate-limit|no-match|timeout>
|
|
33
|
-
Action: Ask user to retry later, paste official docs URL, or run `bts docs <lib>`.
|
|
34
|
-
Do NOT substitute training-data answers without labeling them UNVERIFIED.
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
## Fetch Cache (ETag-revalidated)
|
|
38
|
-
|
|
39
|
-
1. **Cache key:** `"<libraryId>:<normalized-query>"` (lowercase, trimmed).
|
|
40
|
-
2. **Read:** `bash scripts/lib/doc-fetch-cache.sh get "<key>"` — exit 0 → use cached body.
|
|
41
|
-
3. **Miss / stale:** call `query-docs`; store via `doc-fetch-cache.sh put`.
|
|
42
|
-
4. **TTL:** 300s default (`DOC_CACHE_TTL`). Stale entries refresh on next fetch; honor `ETag` when MCP returns it.
|
|
43
|
-
|
|
44
|
-
`bts docs <lib>` shares the same cache helper when invoked from this skill.
|
|
45
|
-
|
|
46
|
-
## Process
|
|
47
|
-
|
|
48
|
-
1. `resolve-library-id` with `libraryName` + full user `query`.
|
|
49
|
-
2. Select match: name similarity, reputation, benchmark score; prefer version-specific IDs when user names a version.
|
|
50
|
-
3. `query-docs` with `libraryId` + specific question (one concept per call).
|
|
51
|
-
4. Answer using fetched docs; cite library/version when relevant.
|
|
52
|
-
|
|
53
|
-
## Verify
|
|
54
|
-
|
|
55
|
-
→ verify: `test -f scripts/lib/doc-fetch-cache.sh`
|
|
File without changes
|