@phi-code-admin/phi-code 0.95.0 → 0.96.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 (36) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/dist/cli/args.d.ts +1 -1
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +43 -1
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/core/api-key-store.d.ts +20 -4
  7. package/dist/core/api-key-store.d.ts.map +1 -1
  8. package/dist/core/api-key-store.js +37 -7
  9. package/dist/core/api-key-store.js.map +1 -1
  10. package/dist/core/compaction/compaction.d.ts +16 -0
  11. package/dist/core/compaction/compaction.d.ts.map +1 -1
  12. package/dist/core/compaction/compaction.js +43 -19
  13. package/dist/core/compaction/compaction.js.map +1 -1
  14. package/dist/core/json-utils.d.ts +11 -0
  15. package/dist/core/json-utils.d.ts.map +1 -0
  16. package/dist/core/json-utils.js +15 -0
  17. package/dist/core/json-utils.js.map +1 -0
  18. package/dist/core/model-registry.d.ts +6 -1
  19. package/dist/core/model-registry.d.ts.map +1 -1
  20. package/dist/core/model-registry.js +10 -9
  21. package/dist/core/model-registry.js.map +1 -1
  22. package/dist/migrations.d.ts.map +1 -1
  23. package/dist/migrations.js +2 -2
  24. package/dist/migrations.js.map +1 -1
  25. package/extensions/phi/benchmark.ts +32 -28
  26. package/extensions/phi/memory.ts +1 -1
  27. package/extensions/phi/models.ts +4 -3
  28. package/extensions/phi/orchestrator.ts +198 -12
  29. package/extensions/phi/providers/candidate-fanout.ts +180 -0
  30. package/extensions/phi/providers/debug-build-commands.ts +27 -0
  31. package/extensions/phi/providers/escalation.ts +16 -0
  32. package/extensions/phi/providers/explore-fanout.ts +1 -1
  33. package/extensions/phi/providers/telemetry.ts +66 -0
  34. package/extensions/phi/providers/test-discovery.ts +119 -0
  35. package/extensions/phi/providers/triage.ts +15 -0
  36. package/package.json +3 -3
@@ -15,6 +15,72 @@ export interface PhaseRecord {
15
15
  verdict: string | null;
16
16
  retried: boolean;
17
17
  blockedRetried: boolean;
18
+ /** Wall-clock of this phase attempt (ms); absent on legacy records. */
19
+ durationMs?: number;
20
+ }
21
+
22
+ /** Aggregate .phi/runs.jsonl records into a readable markdown summary. */
23
+ export function summarizeRuns(records: RunRecord[]): string {
24
+ if (records.length === 0) return "No runs recorded yet — run /fix, /debug or /build first.";
25
+ const byMode = new Map<string, RunRecord[]>();
26
+ for (const r of records) {
27
+ const list = byMode.get(r.mode) ?? [];
28
+ list.push(r);
29
+ byMode.set(r.mode, list);
30
+ }
31
+ const pct = (n: number, d: number) => (d ? `${Math.round((100 * n) / d)}%` : "–");
32
+ const avg = (xs: number[]) => (xs.length ? Math.round(xs.reduce((a, b) => a + b, 0) / xs.length) : 0);
33
+ let out = `**Run telemetry** — ${records.length} run(s)\n\n`;
34
+ out +=
35
+ "| mode | runs | green/finished | blocked | unverified | avg duration | avg sandbox execs |\n|---|---|---|---|---|---|---|\n";
36
+ for (const [mode, rs] of byMode) {
37
+ const green = rs.filter((r) => /GREEN|FIXED|finished/i.test(r.outcome)).length;
38
+ const blocked = rs.filter((r) => /BLOCKED/i.test(r.outcome)).length;
39
+ const unv = rs.filter((r) => /UNVERIFIED/i.test(r.outcome)).length;
40
+ out += `| ${mode} | ${rs.length} | ${pct(green, rs.length)} | ${pct(blocked, rs.length)} | ${pct(unv, rs.length)} | ${Math.round(avg(rs.map((x) => x.durationMs)) / 1000)}s | ${avg(rs.map((x) => x.sandboxExecs))} |\n`;
41
+ }
42
+ // /fix specifics: how often the shot alone was enough (the promise metric).
43
+ const fixes = byMode.get("fix") ?? [];
44
+ if (fixes.length) {
45
+ const greenShot = fixes.filter((r) => /GREEN at single-shot cost/i.test(r.outcome)).length;
46
+ const escalated = fixes.filter((r) => r.phases.some((p) => p.key === "localize" || p.key === "reproduce")).length;
47
+ out += `\n/fix: green at single-shot cost ${pct(greenShot, fixes.length)}, escalated ${pct(escalated, fixes.length)}.\n`;
48
+ }
49
+ // Slowest phase kinds (durationMs is absent on legacy records).
50
+ const durs = new Map<string, number[]>();
51
+ for (const r of records)
52
+ for (const ph of r.phases)
53
+ if (typeof ph.durationMs === "number") {
54
+ const l = durs.get(ph.key) ?? [];
55
+ l.push(ph.durationMs);
56
+ durs.set(ph.key, l);
57
+ }
58
+ if (durs.size) {
59
+ const rows = [...durs]
60
+ .map(([k, xs]) => ({ k, avg: avg(xs), n: xs.length }))
61
+ .sort((a, b) => b.avg - a.avg)
62
+ .slice(0, 5);
63
+ out += `\nSlowest phases (avg): ${rows.map((r) => `${r.k} ${Math.round(r.avg / 1000)}s ×${r.n}`).join(", ")}.\n`;
64
+ }
65
+ const timeouts = records.flatMap((r) => r.phases).filter((p) => p.verdict === "TIMEOUT").length;
66
+ if (timeouts) out += `\n⏰ ${timeouts} phase timeout(s) recorded.\n`;
67
+ return out;
68
+ }
69
+
70
+ /** Parse a runs.jsonl blob (tolerant: bad lines are skipped). */
71
+ export function parseRunsJsonl(blob: string): RunRecord[] {
72
+ const out: RunRecord[] = [];
73
+ for (const line of blob.split("\n")) {
74
+ const t = line.trim();
75
+ if (!t) continue;
76
+ try {
77
+ const r = JSON.parse(t);
78
+ if (r && typeof r.mode === "string" && Array.isArray(r.phases)) out.push(r as RunRecord);
79
+ } catch {
80
+ /* skip */
81
+ }
82
+ }
83
+ return out;
18
84
  }
19
85
 
20
86
  export interface RunRecord {
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Targeted-test discovery — the oracle upgrade the flask-4992 measurement
3
+ * demanded: the driver oracle validated the agent's own reproduction (exit 0)
4
+ * while the project's REAL tests for the touched module failed. Running the
5
+ * full suite is usually too slow/foreign (django ≠ pytest); the right-sized
6
+ * check is the EXISTING test files that belong to the modules the change
7
+ * touched. Pure logic with fs seams so every heuristic is unit-tested.
8
+ */
9
+
10
+ import { existsSync, readFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+
13
+ export interface DiscoverySeams {
14
+ /** Does this repo-relative path exist? */
15
+ exists(relPath: string): boolean;
16
+ /** package.json content (for JS runner detection); null when absent. */
17
+ readPackageJson(): { devDependencies?: Record<string, string>; dependencies?: Record<string, string> } | null;
18
+ }
19
+
20
+ const PY_EXT = /\.py$/;
21
+ const JS_EXT = /\.(ts|tsx|js|jsx|mjs)$/;
22
+
23
+ function posix(p: string): string {
24
+ return p.replace(/\\/g, "/");
25
+ }
26
+
27
+ /** Candidate test-file locations for one changed source file. */
28
+ export function testCandidatesFor(changedFile: string): string[] {
29
+ const f = posix(changedFile);
30
+ const dir = f.includes("/") ? f.slice(0, f.lastIndexOf("/")) : "";
31
+ const base = f.slice(f.lastIndexOf("/") + 1);
32
+ const stem = base.replace(/\.[^.]+$/, "");
33
+ const d = (s: string) => (dir ? `${dir}/${s}` : s);
34
+
35
+ if (PY_EXT.test(base)) {
36
+ if (base.startsWith("test_")) return [f]; // a test itself
37
+ return [
38
+ d(`test_${base}`),
39
+ d(`tests/test_${base}`),
40
+ `tests/test_${stem}.py`,
41
+ `test/test_${stem}.py`,
42
+ // package-level tests dir next to the module's parent (e.g. pkg/mod/x.py → pkg/tests/test_x.py)
43
+ dir.includes("/") ? `${dir.slice(0, dir.lastIndexOf("/"))}/tests/test_${stem}.py` : "",
44
+ ].filter(Boolean);
45
+ }
46
+
47
+ if (JS_EXT.test(base)) {
48
+ if (/\.(test|spec)\./.test(base)) return [f];
49
+ const exts = ["ts", "tsx", "js", "mjs"];
50
+ const out: string[] = [];
51
+ for (const e of exts) {
52
+ out.push(
53
+ d(`${stem}.test.${e}`),
54
+ d(`__tests__/${stem}.test.${e}`),
55
+ `test/${stem}.test.${e}`,
56
+ `tests/${stem}.test.${e}`,
57
+ );
58
+ }
59
+ return out;
60
+ }
61
+
62
+ return [];
63
+ }
64
+
65
+ export interface TargetedTests {
66
+ /** Existing test files that cover the changed modules. */
67
+ files: string[];
68
+ /** A runnable command for them, or undefined when none could be built. */
69
+ command?: string;
70
+ }
71
+
72
+ /**
73
+ * Discover the existing targeted tests for a set of changed files and build one
74
+ * command to run them. Python → pytest; JS → vitest/jest when present in
75
+ * package.json. Deduped, capped (a huge change should not queue a full suite by
76
+ * the back door).
77
+ */
78
+ export function discoverTargetedTests(changedFiles: string[], seams: DiscoverySeams, cap = 5): TargetedTests {
79
+ const found: string[] = [];
80
+ for (const cf of changedFiles) {
81
+ for (const cand of testCandidatesFor(cf)) {
82
+ if (!found.includes(cand) && seams.exists(cand)) {
83
+ found.push(cand);
84
+ break; // first existing candidate per changed file is enough
85
+ }
86
+ }
87
+ if (found.length >= cap) break;
88
+ }
89
+ if (found.length === 0) return { files: [] };
90
+
91
+ if (found.every((f) => PY_EXT.test(f))) {
92
+ return { files: found, command: `python -m pytest ${found.join(" ")} -x -q` };
93
+ }
94
+ const pkg = seams.readPackageJson();
95
+ const deps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
96
+ if (deps.vitest) return { files: found, command: `npx vitest run ${found.join(" ")}` };
97
+ if (deps.jest) return { files: found, command: `npx jest ${found.join(" ")}` };
98
+ return { files: found };
99
+ }
100
+
101
+ /** Real-fs seams for a repo root. */
102
+ export function fsSeamsFor(cwd: string): DiscoverySeams {
103
+ return {
104
+ exists: (rel) => {
105
+ try {
106
+ return existsSync(join(cwd, rel));
107
+ } catch {
108
+ return false;
109
+ }
110
+ },
111
+ readPackageJson: () => {
112
+ try {
113
+ return JSON.parse(readFileSync(join(cwd, "package.json"), "utf-8"));
114
+ } catch {
115
+ return null;
116
+ }
117
+ },
118
+ };
119
+ }
@@ -54,6 +54,21 @@ export function estimateFiles(text: string): number {
54
54
  * decision out. Order matters — a forced mode wins, then a real failing state
55
55
  * (cheapest useful oracle), then build-scale, else single shot.
56
56
  */
57
+ const BUG_SHAPE =
58
+ /\b(traceback|exception|stack ?trace|segfault|error:|fails?\b|failing|broken|crash(es|ed)?|bug\b|régression|regression|ne (marche|fonctionne) (pas|plus)|plante)\b/i;
59
+
60
+ /**
61
+ * Does a free-form user message look like a bug report? Used to suggest /fix
62
+ * once (the measured-best default: never worse than a single shot, oracle-
63
+ * verified). Deliberately conservative — suggestion, never interception.
64
+ */
65
+ export function looksLikeBugReport(text: string): boolean {
66
+ const t = text.trim();
67
+ if (!t || t.startsWith("/")) return false;
68
+ if (t.length < 15) return false;
69
+ return BUG_SHAPE.test(t);
70
+ }
71
+
57
72
  export function triage(signals: TriageSignals): TriageDecision {
58
73
  if (signals.forced) {
59
74
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phi-code-admin/phi-code",
3
- "version": "0.95.0",
3
+ "version": "0.96.1",
4
4
  "description": "Coding agent CLI with persistent memory, sub-agents, intelligent routing, and orchestration",
5
5
  "type": "module",
6
6
  "piConfig": {
@@ -63,11 +63,11 @@
63
63
  "marked": "^15.0.12",
64
64
  "minimatch": "^10.2.3",
65
65
  "phi-code-agent": "^0.74.0",
66
- "phi-code-ai": "^0.74.0",
66
+ "phi-code-ai": "^0.74.4",
67
67
  "phi-code-tui": "^0.74.0",
68
68
  "proper-lockfile": "^4.1.2",
69
69
  "sigma-agents": "^0.1.7",
70
- "sigma-memory": "^0.2.2",
70
+ "sigma-memory": "^0.2.9",
71
71
  "sigma-skills": "^0.1.2",
72
72
  "strip-ansi": "^7.1.0",
73
73
  "typebox": "^1.1.24",