pi-crew 0.9.26 → 0.9.27
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/CHANGELOG.md +87 -0
- package/NOTICE.md +14 -0
- package/assets/crew-vibes.ttf +0 -0
- package/assets/runner-spritesheet.png +0 -0
- package/dist/build-meta.json +255 -40
- package/dist/index.mjs +1564 -241
- package/dist/index.mjs.map +4 -4
- package/docs/perf/optimization-plan-2026-07-verified.md +396 -0
- package/package.json +9 -2
- package/scripts/bench-check.mjs +96 -0
- package/scripts/bench-cold-start.mjs +216 -0
- package/scripts/build-bundle.mjs +84 -0
- package/scripts/build-crew-vibes-font.py +328 -0
- package/scripts/check-all-skills.ts +294 -0
- package/scripts/check-bundle-staleness.mjs +97 -0
- package/scripts/check-conflict-markers.mjs +91 -0
- package/scripts/check-lazy-imports.mjs +28 -0
- package/scripts/install-crew-vibes-font.mjs +91 -0
- package/scripts/postinstall.mjs +47 -0
- package/scripts/profile-startup.mjs +125 -0
- package/scripts/release-smoke.mjs +74 -0
- package/scripts/run-bench.mjs +47 -0
- package/scripts/run-real-chain.ts +41 -0
- package/scripts/test-issue-29-crash.ts +111 -0
- package/scripts/test-issue-29-e2e.ts +183 -0
- package/scripts/test-issue-29-real-runtime.ts +330 -0
- package/scripts/test-issue-29-real-tasks.ts +387 -0
- package/scripts/test-issue-29-team-tool.ts +105 -0
- package/scripts/test-runner.mjs +74 -0
- package/scripts/verify-flicker-fix.ts +109 -0
- package/scripts/verify-skill.ts +550 -0
- package/scripts/watch-bundle.mjs +259 -0
- package/scripts/watchdog-harness.ts +119 -0
- package/src/agents/discover-agents.ts +6 -1
- package/src/config/defaults.ts +6 -0
- package/src/extension/crew-vibes/cat-frames.ts +18 -0
- package/src/extension/crew-vibes/config.ts +195 -0
- package/src/extension/crew-vibes/figures.ts +94 -0
- package/src/extension/crew-vibes/font-detect.ts +58 -0
- package/src/extension/crew-vibes/index.ts +396 -0
- package/src/extension/crew-vibes/provider-usage.ts +330 -0
- package/src/extension/crew-vibes/render.ts +206 -0
- package/src/extension/crew-vibes/speed.ts +286 -0
- package/src/extension/knowledge-injection.ts +12 -3
- package/src/extension/management.ts +23 -3
- package/src/extension/register.ts +7 -0
- package/src/runtime/child-pi.ts +117 -10
- package/src/runtime/crew-agent-records.ts +10 -3
- package/src/runtime/retry-executor.ts +4 -1
- package/src/runtime/task-runner/state-helpers.ts +9 -30
- package/src/runtime/team-runner.ts +5 -3
- package/src/state/atomic-write.ts +153 -49
- package/src/state/event-log.ts +16 -10
- package/src/state/locks.ts +7 -8
- package/src/state/mailbox.ts +15 -4
- package/src/state/state-store.ts +39 -10
- package/src/teams/discover-teams.ts +56 -1
- package/src/utils/safe-paths.ts +2 -1
- package/src/workflows/discover-workflows.ts +72 -1
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Check All Skills Script
|
|
5
|
+
*
|
|
6
|
+
* Runs verify-skill.ts against all skills and produces a summary report.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node scripts/check-all-skills.ts
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { spawnSync } from "child_process";
|
|
13
|
+
import * as fs from "fs";
|
|
14
|
+
import * as path from "path";
|
|
15
|
+
|
|
16
|
+
interface SkillSummary {
|
|
17
|
+
name: string;
|
|
18
|
+
path: string;
|
|
19
|
+
status: "pass" | "fail" | "warning";
|
|
20
|
+
warnings: string[];
|
|
21
|
+
errors: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface Report {
|
|
25
|
+
total: number;
|
|
26
|
+
passed: number;
|
|
27
|
+
failed: number;
|
|
28
|
+
warningsOnly: number;
|
|
29
|
+
skills: SkillSummary[];
|
|
30
|
+
timestamp: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const SKILLS_DIR = path.join(process.cwd(), "skills");
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get all skill directories
|
|
37
|
+
*/
|
|
38
|
+
function getSkillDirs(): string[] {
|
|
39
|
+
if (!fs.existsSync(SKILLS_DIR)) {
|
|
40
|
+
console.error("Skills directory not found: " + SKILLS_DIR);
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const dirs: string[] = [];
|
|
45
|
+
const entries = fs.readdirSync(SKILLS_DIR, { withFileTypes: true });
|
|
46
|
+
|
|
47
|
+
for (const entry of entries) {
|
|
48
|
+
if (entry.isDirectory()) {
|
|
49
|
+
const skillPath = path.join(SKILLS_DIR, entry.name, "SKILL.md");
|
|
50
|
+
if (fs.existsSync(skillPath)) {
|
|
51
|
+
dirs.push(entry.name);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return dirs.sort();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Run verify-skill.ts for a single skill
|
|
61
|
+
*/
|
|
62
|
+
function verifySingleSkill(skillName: string): {
|
|
63
|
+
passed: boolean;
|
|
64
|
+
warnings: string[];
|
|
65
|
+
errors: string[];
|
|
66
|
+
} {
|
|
67
|
+
const skillPath = path.join(SKILLS_DIR, skillName, "SKILL.md");
|
|
68
|
+
|
|
69
|
+
const result = spawnSync("node", ["scripts/verify-skill.ts", skillPath], {
|
|
70
|
+
cwd: process.cwd(),
|
|
71
|
+
encoding: "utf-8",
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const output = result.stdout + result.stderr;
|
|
75
|
+
const warnings: string[] = [];
|
|
76
|
+
const errors: string[] = [];
|
|
77
|
+
|
|
78
|
+
// Parse output
|
|
79
|
+
const warningMatches = output.match(/[^\n]*⚠️[^\n]*/g);
|
|
80
|
+
const errorMatches = output.match(/[^\n]+FAIL[^\n]+/g);
|
|
81
|
+
|
|
82
|
+
if (warningMatches) {
|
|
83
|
+
warnings.push(...warningMatches.map((w) => w.trim()));
|
|
84
|
+
}
|
|
85
|
+
if (errorMatches) {
|
|
86
|
+
errors.push(...errorMatches.map((e) => e.trim()));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const passed = result.status === 0;
|
|
90
|
+
|
|
91
|
+
return { passed, warnings, errors };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Generate markdown report
|
|
96
|
+
*/
|
|
97
|
+
function generateMarkdownReport(report: Report): string {
|
|
98
|
+
const lines: string[] = [];
|
|
99
|
+
|
|
100
|
+
lines.push("# Skill Verification Report");
|
|
101
|
+
lines.push("");
|
|
102
|
+
lines.push("Generated: " + report.timestamp);
|
|
103
|
+
lines.push("");
|
|
104
|
+
|
|
105
|
+
// Summary table
|
|
106
|
+
lines.push("## Summary");
|
|
107
|
+
lines.push("");
|
|
108
|
+
lines.push("| Status | Count |");
|
|
109
|
+
lines.push("|--------|-------|");
|
|
110
|
+
lines.push("| PASS | " + report.passed + " |");
|
|
111
|
+
lines.push("| FAIL | " + report.failed + " |");
|
|
112
|
+
lines.push("| Warnings | " + report.warningsOnly + " |");
|
|
113
|
+
lines.push("| **Total** | **" + report.total + "** |");
|
|
114
|
+
lines.push("");
|
|
115
|
+
|
|
116
|
+
// Skill list by status
|
|
117
|
+
lines.push("## Skills by Status");
|
|
118
|
+
lines.push("");
|
|
119
|
+
|
|
120
|
+
// Passed skills
|
|
121
|
+
const passedSkills = report.skills.filter((s) => s.status === "pass");
|
|
122
|
+
if (passedSkills.length > 0) {
|
|
123
|
+
lines.push("### Passing Skills");
|
|
124
|
+
lines.push("");
|
|
125
|
+
for (const skill of passedSkills) {
|
|
126
|
+
lines.push("- " + skill.name);
|
|
127
|
+
}
|
|
128
|
+
lines.push("");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Failed skills
|
|
132
|
+
const failedSkills = report.skills.filter((s) => s.status === "fail");
|
|
133
|
+
if (failedSkills.length > 0) {
|
|
134
|
+
lines.push("### Failing Skills");
|
|
135
|
+
lines.push("");
|
|
136
|
+
for (const skill of failedSkills) {
|
|
137
|
+
lines.push("- **" + skill.name + "**");
|
|
138
|
+
for (const error of skill.errors) {
|
|
139
|
+
lines.push(" - " + error);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
lines.push("");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Warning skills
|
|
146
|
+
const warningSkills = report.skills.filter((s) => s.status === "warning");
|
|
147
|
+
if (warningSkills.length > 0) {
|
|
148
|
+
lines.push("### Skills with Warnings");
|
|
149
|
+
lines.push("");
|
|
150
|
+
for (const skill of warningSkills) {
|
|
151
|
+
lines.push("- **" + skill.name + "**");
|
|
152
|
+
for (const warning of skill.warnings) {
|
|
153
|
+
lines.push(" - " + warning);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
lines.push("");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Recommendations
|
|
160
|
+
lines.push("## Recommendations");
|
|
161
|
+
lines.push("");
|
|
162
|
+
if (failedSkills.length > 0) {
|
|
163
|
+
lines.push("1. Fix " + failedSkills.length + " failing skills to have proper RED/GREEN gates");
|
|
164
|
+
lines.push("2. Add trigger sections (When to Activate) if missing");
|
|
165
|
+
lines.push("3. Add anti-patterns sections to prevent common mistakes");
|
|
166
|
+
lines.push("");
|
|
167
|
+
}
|
|
168
|
+
if (warningSkills.length > 0) {
|
|
169
|
+
lines.push("4. Review " + warningSkills.length + " skills with warnings for improvements");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return lines.join("\n");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Generate JSON report
|
|
177
|
+
*/
|
|
178
|
+
function generateJsonReport(report: Report): string {
|
|
179
|
+
return JSON.stringify(report, null, 2);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Main entry point
|
|
184
|
+
*/
|
|
185
|
+
async function main() {
|
|
186
|
+
console.log("Checking all skills...\n");
|
|
187
|
+
|
|
188
|
+
const skillNames = getSkillDirs();
|
|
189
|
+
console.log("Found " + skillNames.length + " skills\n");
|
|
190
|
+
|
|
191
|
+
const report: Report = {
|
|
192
|
+
total: skillNames.length,
|
|
193
|
+
passed: 0,
|
|
194
|
+
failed: 0,
|
|
195
|
+
warningsOnly: 0,
|
|
196
|
+
skills: [],
|
|
197
|
+
timestamp: new Date().toISOString(),
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
for (const skillName of skillNames) {
|
|
201
|
+
process.stdout.write(".");
|
|
202
|
+
|
|
203
|
+
const result = verifySingleSkill(skillName);
|
|
204
|
+
|
|
205
|
+
let status: "pass" | "fail" | "warning" = "pass";
|
|
206
|
+
if (!result.passed && result.errors.length > 0) {
|
|
207
|
+
status = "fail";
|
|
208
|
+
report.failed++;
|
|
209
|
+
} else if (result.warnings.length > 0) {
|
|
210
|
+
status = "warning";
|
|
211
|
+
report.warningsOnly++;
|
|
212
|
+
} else {
|
|
213
|
+
report.passed++;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
report.skills.push({
|
|
217
|
+
name: skillName,
|
|
218
|
+
path: path.join("skills", skillName, "SKILL.md"),
|
|
219
|
+
status,
|
|
220
|
+
warnings: result.warnings,
|
|
221
|
+
errors: result.errors,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
console.log("\n\n");
|
|
226
|
+
|
|
227
|
+
// Print summary
|
|
228
|
+
console.log("=== Skill Verification Summary ===");
|
|
229
|
+
console.log("Total: " + report.total);
|
|
230
|
+
console.log("Passed: " + report.passed);
|
|
231
|
+
console.log("Failed: " + report.failed);
|
|
232
|
+
console.log("Warnings only: " + report.warningsOnly);
|
|
233
|
+
console.log("");
|
|
234
|
+
|
|
235
|
+
// List failed skills
|
|
236
|
+
if (report.failed > 0) {
|
|
237
|
+
console.log("Failing skills:");
|
|
238
|
+
for (const skill of report.skills.filter((s) => s.status === "fail")) {
|
|
239
|
+
console.log(" - " + skill.name);
|
|
240
|
+
for (const error of skill.errors) {
|
|
241
|
+
console.log(" " + error);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
console.log("");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// List skills with warnings
|
|
248
|
+
if (report.warningsOnly > 0) {
|
|
249
|
+
console.log("Skills with warnings:");
|
|
250
|
+
for (const skill of report.skills.filter((s) => s.status === "warning")) {
|
|
251
|
+
console.log(" - " + skill.name);
|
|
252
|
+
for (const warning of skill.warnings) {
|
|
253
|
+
console.log(" " + warning);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
console.log("");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Write reports
|
|
260
|
+
const reportDir = path.join(process.cwd(), "reports");
|
|
261
|
+
if (!fs.existsSync(reportDir)) {
|
|
262
|
+
fs.mkdirSync(reportDir, { recursive: true });
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
266
|
+
const mdReportPath = path.join(reportDir, "skill-verification-" + timestamp + ".md");
|
|
267
|
+
const jsonReportPath = path.join(reportDir, "skill-verification-" + timestamp + ".json");
|
|
268
|
+
const latestMdPath = path.join(reportDir, "skill-verification-latest.md");
|
|
269
|
+
const latestJsonPath = path.join(reportDir, "skill-verification-latest.json");
|
|
270
|
+
|
|
271
|
+
fs.writeFileSync(mdReportPath, generateMarkdownReport(report));
|
|
272
|
+
fs.writeFileSync(jsonReportPath, generateJsonReport(report));
|
|
273
|
+
fs.writeFileSync(latestMdPath, generateMarkdownReport(report));
|
|
274
|
+
fs.writeFileSync(latestJsonPath, generateJsonReport(report));
|
|
275
|
+
|
|
276
|
+
console.log("Reports written to:");
|
|
277
|
+
console.log(" - " + mdReportPath);
|
|
278
|
+
console.log(" - " + jsonReportPath);
|
|
279
|
+
console.log(" - " + latestMdPath + " (latest)");
|
|
280
|
+
console.log(" - " + latestJsonPath + " (latest)");
|
|
281
|
+
|
|
282
|
+
// Exit with appropriate code
|
|
283
|
+
if (report.failed > 0) {
|
|
284
|
+
process.exit(1);
|
|
285
|
+
} else if (report.warningsOnly > 0) {
|
|
286
|
+
process.exit(2);
|
|
287
|
+
}
|
|
288
|
+
process.exit(0);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
main().catch((err) => {
|
|
292
|
+
console.error("Fatal error:", err);
|
|
293
|
+
process.exit(1);
|
|
294
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CI gate: detect stale dist/index.mjs vs src/.
|
|
4
|
+
*
|
|
5
|
+
* Compares the newest mtime in src/ against dist/index.mjs mtime. If
|
|
6
|
+
* src/ is newer, the bundle is stale (someone edited source without
|
|
7
|
+
* rebuilding). Exits non-zero in that case.
|
|
8
|
+
*
|
|
9
|
+
* Why: in v0.9.17 the entrypoint prefers the bundle (with strip-types
|
|
10
|
+
* fallback). Stale bundles silently run old code — see Phase 5 H2
|
|
11
|
+
* investigation (2026-07-01) for the regression risk.
|
|
12
|
+
*
|
|
13
|
+
* Skips if dist/index.mjs does not exist (the strip-types fallback path
|
|
14
|
+
* is fine and the build:bundle automation will produce it).
|
|
15
|
+
*
|
|
16
|
+
* Exits:
|
|
17
|
+
* 0 — dist is fresh OR absent
|
|
18
|
+
* 1 — dist is stale (run `npm run build:bundle`)
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { execSync } from "node:child_process";
|
|
22
|
+
import { existsSync, statSync } from "node:fs";
|
|
23
|
+
|
|
24
|
+
const distPath = "dist/index.mjs";
|
|
25
|
+
if (!existsSync(distPath)) {
|
|
26
|
+
console.log("[check-bundle-staleness] dist/index.mjs absent — strip-types fallback will be used. OK.");
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const distMtimeMs = statSync(distPath).mtimeMs;
|
|
31
|
+
|
|
32
|
+
// `git ls-files -o -- src` lists untracked files in src/. We want every
|
|
33
|
+
// tracked file's mtime plus the newest untracked one. Cheap heuristic:
|
|
34
|
+
// use `find` style by asking git for the full list, then statSync each.
|
|
35
|
+
let trackedFiles;
|
|
36
|
+
try {
|
|
37
|
+
trackedFiles = execSync(`git ls-files src`, { encoding: "utf-8" })
|
|
38
|
+
.split("\n")
|
|
39
|
+
.filter(Boolean);
|
|
40
|
+
} catch {
|
|
41
|
+
// Not a git repo (e.g. npm pack test) — skip.
|
|
42
|
+
console.log("[check-bundle-staleness] not a git repo; skipping staleness check.");
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let newestMtimeMs = 0;
|
|
47
|
+
let newestFile = "";
|
|
48
|
+
for (const f of trackedFiles) {
|
|
49
|
+
try {
|
|
50
|
+
const mt = statSync(f).mtimeMs;
|
|
51
|
+
if (mt > newestMtimeMs) {
|
|
52
|
+
newestMtimeMs = mt;
|
|
53
|
+
newestFile = f;
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
// File listed but missing on disk — ignore.
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Add untracked .ts files in src/ (devs editing a new file)
|
|
61
|
+
try {
|
|
62
|
+
const untracked = execSync(`git ls-files -o --exclude-standard src`, { encoding: "utf-8" })
|
|
63
|
+
.split("\n")
|
|
64
|
+
.filter(Boolean);
|
|
65
|
+
for (const f of untracked) {
|
|
66
|
+
if (!f.endsWith(".ts")) continue;
|
|
67
|
+
try {
|
|
68
|
+
const mt = statSync(f).mtimeMs;
|
|
69
|
+
if (mt > newestMtimeMs) {
|
|
70
|
+
newestMtimeMs = mt;
|
|
71
|
+
newestFile = f;
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
// ignore
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
// git may fail; ignore
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (newestMtimeMs === 0) {
|
|
82
|
+
console.log("[check-bundle-staleness] no src/*.ts files found; skipping.");
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (newestMtimeMs > distMtimeMs) {
|
|
87
|
+
console.error(
|
|
88
|
+
`[check-bundle-staleness] FAIL: dist/index.mjs is stale.\n` +
|
|
89
|
+
` Newest src file: ${newestFile} (mtime=${newestMtimeMs.toFixed(0)})\n` +
|
|
90
|
+
` dist/index.mjs mtime: ${distMtimeMs.toFixed(0)}\n` +
|
|
91
|
+
` → run \`npm run build:bundle\` to refresh.`,
|
|
92
|
+
);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const ageSec = (distMtimeMs - newestMtimeMs) / 1000;
|
|
97
|
+
console.log(`[check-bundle-staleness] OK: dist/index.mjs is ${ageSec.toFixed(1)}s newer than newest src/ file.`);
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CI gate: detect unresolved git merge conflict markers in source.
|
|
4
|
+
*
|
|
5
|
+
* Scans tracked source files for `<<<<<<<`, `=======` (lone), and
|
|
6
|
+
* `>>>>>>>` patterns that indicate a botched merge. Fails CI if any
|
|
7
|
+
* are found so they can't reach the bundle (which esbuild would
|
|
8
|
+
* choke on with a parse error).
|
|
9
|
+
*
|
|
10
|
+
* History: 2026-07-01 CI #28498831579 (Ubuntu) failed because
|
|
11
|
+
* src/runtime/child-pi.ts and src/extension/pi-api.ts shipped with
|
|
12
|
+
* unresolved conflict markers left over from earlier stash/pop cycles
|
|
13
|
+
* during Phase 2-5 implementation. The esbuild transformer in the
|
|
14
|
+
* test runner hit `Expected identifier but found "<<"` at
|
|
15
|
+
* src/runtime/child-pi.ts:1073 — a syntax error caused by the markers.
|
|
16
|
+
*
|
|
17
|
+
* Scope:
|
|
18
|
+
* - Tracks files in src/, test/, scripts/, workflows/, teams/,
|
|
19
|
+
* agents/, themes/ — anywhere a conflict would land.
|
|
20
|
+
* - Excludes test/unit/conflict-detect.test.ts which INTENTIONALLY
|
|
21
|
+
* contains `<<<<<<< HEAD` etc. as test fixtures (test the
|
|
22
|
+
detector itself).
|
|
23
|
+
* - Detects both conflict-style (`<<<<<<<`, `=======`, `>>>>>>>`)
|
|
24
|
+
* and diff3-style (`|||||||`) markers.
|
|
25
|
+
*
|
|
26
|
+
* Exits:
|
|
27
|
+
* 0 — clean (no markers found, or only allowed locations)
|
|
28
|
+
* 1 — markers found (with file:line listing)
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { execSync } from "node:child_process";
|
|
32
|
+
import { readFileSync } from "node:fs";
|
|
33
|
+
|
|
34
|
+
const MARKER_PATTERNS = [
|
|
35
|
+
/^<<<<<<< /m, // ours/incoming/conflict markers
|
|
36
|
+
/^=======$/m, // conflict separator (lone, not bash =======)
|
|
37
|
+
/^>>>>>>> /m, // theirs/outgoing markers
|
|
38
|
+
/^\|\|\|\|\|\|\| /m, // diff3 base marker
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const SCAN_PATHS = ["src/", "test/", "scripts/", "workflows/", "teams/", "agents/", "themes/"];
|
|
42
|
+
const EXCLUDE_FILES = new Set([
|
|
43
|
+
// Intentionally contains conflict markers as test fixtures.
|
|
44
|
+
"test/unit/conflict-detect.test.ts",
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
function listTrackedFiles() {
|
|
48
|
+
try {
|
|
49
|
+
const all = execSync("git ls-files", { encoding: "utf-8" });
|
|
50
|
+
return all
|
|
51
|
+
.split("\n")
|
|
52
|
+
.filter(Boolean)
|
|
53
|
+
.filter((f) => SCAN_PATHS.some((p) => f.startsWith(p) || f === p.replace(/\/$/, "")))
|
|
54
|
+
.filter((f) => !EXCLUDE_FILES.has(f));
|
|
55
|
+
} catch {
|
|
56
|
+
console.error("[conflict-markers] not a git repo; skipping check.");
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const files = listTrackedFiles();
|
|
62
|
+
const offenders = [];
|
|
63
|
+
|
|
64
|
+
for (const file of files) {
|
|
65
|
+
let content;
|
|
66
|
+
try {
|
|
67
|
+
content = readFileSync(file, "utf-8");
|
|
68
|
+
} catch {
|
|
69
|
+
// File listed but unreadable — skip silently.
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const lines = content.split(/\r?\n/);
|
|
73
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
74
|
+
const line = lines[i];
|
|
75
|
+
if (MARKER_PATTERNS.some((re) => re.test(line))) {
|
|
76
|
+
offenders.push({ file, line: i + 1, text: line });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (offenders.length === 0) {
|
|
82
|
+
console.log(`[conflict-markers] OK: scanned ${files.length} files, no markers found.`);
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
console.error(`[conflict-markers] FAIL: ${offenders.length} conflict marker(s) found.`);
|
|
87
|
+
for (const o of offenders) {
|
|
88
|
+
console.error(` ${o.file}:${o.line}: ${o.text.trim().slice(0, 80)}`);
|
|
89
|
+
}
|
|
90
|
+
console.error("\nResolve with: git checkout --ours|--theirs or git add ... after manual edit.");
|
|
91
|
+
process.exit(1);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
const out = execSync(
|
|
5
|
+
`git grep -nE "await import\\(" -- "src/**/*.ts"`,
|
|
6
|
+
{ encoding: "utf-8" },
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
const bad = [];
|
|
10
|
+
const fileCache = new Map();
|
|
11
|
+
|
|
12
|
+
for (const line of out.split("\n").filter(Boolean)) {
|
|
13
|
+
if (line.includes("// LAZY:")) continue;
|
|
14
|
+
const m = line.match(/^([^:]+):(\d+):/);
|
|
15
|
+
if (!m) continue;
|
|
16
|
+
const [, file, lineNum] = m;
|
|
17
|
+
if (!fileCache.has(file)) fileCache.set(file, readFileSync(file, "utf-8").split(/\r?\n/));
|
|
18
|
+
const lines = fileCache.get(file);
|
|
19
|
+
const prevLine = lines[Number(lineNum) - 2] ?? "";
|
|
20
|
+
if (!prevLine.includes("// LAZY:")) bad.push(line);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (bad.length > 0) {
|
|
24
|
+
console.error("Dynamic imports without `// LAZY:` marker:\n" + bad.join("\n"));
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
console.log("All dynamic imports have `// LAZY:` marker.");
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Installs the bundled crew-vibes.ttf (PUA glyphs U+E700..U+E70A) into the
|
|
4
|
+
* user fonts directory so the crew-vibes speed + capacity figures render
|
|
5
|
+
* instead of tofu boxes. Cross-platform mirror of pi-speeed's install-font.
|
|
6
|
+
*
|
|
7
|
+
* macOS ~/Library/Fonts/crew-vibes.ttf
|
|
8
|
+
* Linux ~/.local/share/fonts/crew-vibes.ttf (+ fc-cache -f)
|
|
9
|
+
* Windows %LOCALAPPDATA%\Microsoft\Windows\Fonts\crew-vibes.ttf (per-user)
|
|
10
|
+
*
|
|
11
|
+
* Invoked by the npm `postinstall` hook and by `npm run install:crew-font`.
|
|
12
|
+
* Best-effort: never fails the install.
|
|
13
|
+
*/
|
|
14
|
+
import { spawnSync } from "node:child_process";
|
|
15
|
+
import { copyFileSync, existsSync, mkdirSync } from "node:fs";
|
|
16
|
+
import { homedir, platform } from "node:os";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
|
|
20
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const source = join(__dirname, "..", "assets", "crew-vibes.ttf");
|
|
22
|
+
const FONT_NAME = "crew-vibes.ttf";
|
|
23
|
+
|
|
24
|
+
function log(message) {
|
|
25
|
+
console.log(`[pi-crew] ${message}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function installFont() {
|
|
29
|
+
if (!existsSync(source)) {
|
|
30
|
+
log(`crew-vibes font missing: ${source}`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const os = platform();
|
|
35
|
+
let targetDir;
|
|
36
|
+
|
|
37
|
+
if (os === "darwin") {
|
|
38
|
+
targetDir = join(homedir(), "Library", "Fonts");
|
|
39
|
+
} else if (os === "linux") {
|
|
40
|
+
targetDir = join(homedir(), ".local", "share", "fonts");
|
|
41
|
+
} else if (os === "win32") {
|
|
42
|
+
const localAppData = process.env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local");
|
|
43
|
+
targetDir = join(localAppData, "Microsoft", "Windows", "Fonts");
|
|
44
|
+
} else {
|
|
45
|
+
log(`Skipping font install on unsupported platform: ${os}`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
mkdirSync(targetDir, { recursive: true });
|
|
50
|
+
const target = join(targetDir, FONT_NAME);
|
|
51
|
+
copyFileSync(source, target);
|
|
52
|
+
log(`Installed crew-vibes font to ${target}`);
|
|
53
|
+
|
|
54
|
+
if (os === "linux") {
|
|
55
|
+
// Refresh fontconfig cache so newly installed font is immediately
|
|
56
|
+
// available to all terminals. -fv for verbose + forced rebuild.
|
|
57
|
+
const result = spawnSync("fc-cache", ["-fv", targetDir], { stdio: "ignore" });
|
|
58
|
+
if (result.status === 0) log("Refreshed font cache (fc-cache -fv)");
|
|
59
|
+
else log("fontconfig fc-cache not available; restart terminal if font is not visible");
|
|
60
|
+
|
|
61
|
+
// Verify font is actually indexed by fontconfig
|
|
62
|
+
const verify = spawnSync("fc-match", [":charset=E700"], { encoding: "utf8" });
|
|
63
|
+
if (verify.stdout && verify.stdout.includes(FONT_NAME)) {
|
|
64
|
+
log("Font verified: fc-match U+E700 -> crew-vibes.ttf");
|
|
65
|
+
} else {
|
|
66
|
+
log("WARNING: fc-match did not resolve U+E700 to crew-vibes.ttf");
|
|
67
|
+
log(" Run: fc-cache -fv " + targetDir);
|
|
68
|
+
log(" Then restart your terminal.");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (os === "win32") {
|
|
73
|
+
// Register the per-user font in HKCU so Windows Terminal and other
|
|
74
|
+
// apps pick it up for PUA glyph fallback. Copying alone is not enough.
|
|
75
|
+
const regValue = "Crew Vibes (TrueType)";
|
|
76
|
+
const regResult = spawnSync("reg", ["add", "HKCU\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Fonts", "/v", regValue, "/t", "REG_SZ", "/d", target, "/f"], { stdio: "ignore" });
|
|
77
|
+
if (regResult.status === 0) log("Registered crew-vibes font in Windows registry (HKCU)");
|
|
78
|
+
else log("Windows registry registration skipped (reg.exe unavailable); glyphs may not render until font is installed via Settings");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (os === "darwin" || os === "win32") {
|
|
82
|
+
log("Restart your terminal (or select a Crew-Vibes-capable font) if glyphs still show as boxes");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
installFont();
|
|
88
|
+
} catch (error) {
|
|
89
|
+
log(`Font install failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
90
|
+
process.exitCode = 0;
|
|
91
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Cross-platform postinstall orchestrator.
|
|
4
|
+
*
|
|
5
|
+
* 1. Build the ESM bundle (best-effort; on failure we log a fallback and
|
|
6
|
+
* let Pi fall back to strip-types loading).
|
|
7
|
+
* 2. Install the bundled crew-vibes.ttf into the user fonts directory so
|
|
8
|
+
* the crew-vibes speed/capacity PUA glyphs render.
|
|
9
|
+
*
|
|
10
|
+
* Replaces the old `postinstall` shell chain so the font install runs on
|
|
11
|
+
* every platform without relying on shell-specific chaining (`;`/`&&`).
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync } from "node:fs";
|
|
14
|
+
import { spawnSync } from "node:child_process";
|
|
15
|
+
import { dirname, join } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
|
|
18
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const root = join(__dirname, "..");
|
|
20
|
+
|
|
21
|
+
function run(scriptRel) {
|
|
22
|
+
const abs = join(root, scriptRel);
|
|
23
|
+
if (!existsSync(abs)) return 1;
|
|
24
|
+
const result = spawnSync(process.execPath, [abs], { stdio: "inherit" });
|
|
25
|
+
return result.status ?? 1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function main() {
|
|
29
|
+
try {
|
|
30
|
+
// Dev clones ship scripts/build-bundle.mjs and devDeps (esbuild) so the
|
|
31
|
+
// bundle rebuilds; published packages omit both and rely on committed
|
|
32
|
+
// dist/index.mjs, so this best-effort build simply no-ops.
|
|
33
|
+
const bundleStatus = run("scripts/build-bundle.mjs");
|
|
34
|
+
if (bundleStatus !== 0) {
|
|
35
|
+
console.warn(
|
|
36
|
+
"[pi-crew] postinstall: bundle build skipped or failed; using committed dist/ (or strip-types fallback). Run npm run build:bundle to retry.",
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
// Font install is best-effort and must never fail the install.
|
|
40
|
+
run("scripts/install-crew-vibes-font.mjs");
|
|
41
|
+
} catch (err) {
|
|
42
|
+
// Postinstall must NEVER fail the install (SEC-M2).
|
|
43
|
+
console.warn("[pi-crew] postinstall: best-effort step failed:", err instanceof Error ? err.message : err);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
main();
|