@prestyj/cli 5.3.0 → 5.4.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/dist/app-sidecar.js +696 -59
- package/dist/app-sidecar.js.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/core/agent-session-compaction.test.js +138 -0
- package/dist/core/agent-session-compaction.test.js.map +1 -1
- package/dist/core/agent-session.d.ts +71 -3
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +280 -47
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/autopilot-cycle.d.ts +69 -17
- package/dist/core/autopilot-cycle.d.ts.map +1 -1
- package/dist/core/autopilot-cycle.js +82 -13
- package/dist/core/autopilot-cycle.js.map +1 -1
- package/dist/core/autopilot-cycle.test.js +206 -10
- package/dist/core/autopilot-cycle.test.js.map +1 -1
- package/dist/core/autopilot-gate.d.ts +48 -22
- package/dist/core/autopilot-gate.d.ts.map +1 -1
- package/dist/core/autopilot-gate.js +110 -1
- package/dist/core/autopilot-gate.js.map +1 -1
- package/dist/core/autopilot-gate.test.js +139 -3
- package/dist/core/autopilot-gate.test.js.map +1 -1
- package/dist/core/autopilot-verdict.d.ts +6 -0
- package/dist/core/autopilot-verdict.d.ts.map +1 -1
- package/dist/core/autopilot-verdict.js +75 -1
- package/dist/core/autopilot-verdict.js.map +1 -1
- package/dist/core/autopilot-verdict.test.js +63 -0
- package/dist/core/autopilot-verdict.test.js.map +1 -1
- package/dist/core/bash-spawn-benchmark.d.ts +2 -0
- package/dist/core/bash-spawn-benchmark.d.ts.map +1 -0
- package/dist/core/bash-spawn-benchmark.js +139 -0
- package/dist/core/bash-spawn-benchmark.js.map +1 -0
- package/dist/core/compaction/compactor.test.js +1 -1
- package/dist/core/compaction/compactor.test.js.map +1 -1
- package/dist/core/fast-apply-benchmark.d.ts +1 -1
- package/dist/core/fast-apply-benchmark.js +2 -2
- package/dist/core/fast-apply-benchmark.js.map +1 -1
- package/dist/core/hashline-edit-benchmark.d.ts +33 -1
- package/dist/core/hashline-edit-benchmark.d.ts.map +1 -1
- package/dist/core/hashline-edit-benchmark.js +193 -21
- package/dist/core/hashline-edit-benchmark.js.map +1 -1
- package/dist/core/hashline-edit-benchmark.test.js +51 -2
- package/dist/core/hashline-edit-benchmark.test.js.map +1 -1
- package/dist/core/mcp/deferred-catalog.d.ts +28 -0
- package/dist/core/mcp/deferred-catalog.d.ts.map +1 -0
- package/dist/core/mcp/deferred-catalog.js +72 -0
- package/dist/core/mcp/deferred-catalog.js.map +1 -0
- package/dist/core/mcp/resolve-stdio.d.ts +19 -3
- package/dist/core/mcp/resolve-stdio.d.ts.map +1 -1
- package/dist/core/mcp/resolve-stdio.js +136 -5
- package/dist/core/mcp/resolve-stdio.js.map +1 -1
- package/dist/core/mcp/resolve-stdio.test.js +82 -1
- package/dist/core/mcp/resolve-stdio.test.js.map +1 -1
- package/dist/core/nolan-context.d.ts +26 -2
- package/dist/core/nolan-context.d.ts.map +1 -1
- package/dist/core/nolan-context.js +46 -4
- package/dist/core/nolan-context.js.map +1 -1
- package/dist/core/nolan-context.test.js +53 -12
- package/dist/core/nolan-context.test.js.map +1 -1
- package/dist/core/nolan-prompt.d.ts +2 -15
- package/dist/core/nolan-prompt.d.ts.map +1 -1
- package/dist/core/nolan-prompt.js +96 -15
- package/dist/core/nolan-prompt.js.map +1 -1
- package/dist/core/nolan-prompt.test.js +102 -7
- package/dist/core/nolan-prompt.test.js.map +1 -1
- package/dist/core/persistent-shell.d.ts +27 -0
- package/dist/core/persistent-shell.d.ts.map +1 -0
- package/dist/core/persistent-shell.js +150 -0
- package/dist/core/persistent-shell.js.map +1 -0
- package/dist/core/persistent-shell.test.d.ts +2 -0
- package/dist/core/persistent-shell.test.d.ts.map +1 -0
- package/dist/core/persistent-shell.test.js +84 -0
- package/dist/core/persistent-shell.test.js.map +1 -0
- package/dist/core/process-manager-dev-server-repro.test.js +34 -6
- package/dist/core/process-manager-dev-server-repro.test.js.map +1 -1
- package/dist/core/progress/engine.d.ts +23 -0
- package/dist/core/progress/engine.d.ts.map +1 -0
- package/dist/core/progress/engine.js +131 -0
- package/dist/core/progress/engine.js.map +1 -0
- package/dist/core/progress/engine.test.d.ts +2 -0
- package/dist/core/progress/engine.test.d.ts.map +1 -0
- package/dist/core/progress/engine.test.js +136 -0
- package/dist/core/progress/engine.test.js.map +1 -0
- package/dist/core/progress/git-xp.d.ts +16 -0
- package/dist/core/progress/git-xp.d.ts.map +1 -0
- package/dist/core/progress/git-xp.js +106 -0
- package/dist/core/progress/git-xp.js.map +1 -0
- package/dist/core/progress/git-xp.test.d.ts +2 -0
- package/dist/core/progress/git-xp.test.d.ts.map +1 -0
- package/dist/core/progress/git-xp.test.js +88 -0
- package/dist/core/progress/git-xp.test.js.map +1 -0
- package/dist/core/progress/ranks.d.ts +21 -0
- package/dist/core/progress/ranks.d.ts.map +1 -0
- package/dist/core/progress/ranks.js +141 -0
- package/dist/core/progress/ranks.js.map +1 -0
- package/dist/core/progress/ranks.test.d.ts +2 -0
- package/dist/core/progress/ranks.test.d.ts.map +1 -0
- package/dist/core/progress/ranks.test.js +59 -0
- package/dist/core/progress/ranks.test.js.map +1 -0
- package/dist/core/progress/rebuild.d.ts +9 -0
- package/dist/core/progress/rebuild.d.ts.map +1 -0
- package/dist/core/progress/rebuild.js +119 -0
- package/dist/core/progress/rebuild.js.map +1 -0
- package/dist/core/progress/rebuild.test.d.ts +2 -0
- package/dist/core/progress/rebuild.test.d.ts.map +1 -0
- package/dist/core/progress/rebuild.test.js +88 -0
- package/dist/core/progress/rebuild.test.js.map +1 -0
- package/dist/core/progress/store.d.ts +35 -0
- package/dist/core/progress/store.d.ts.map +1 -0
- package/dist/core/progress/store.js +200 -0
- package/dist/core/progress/store.js.map +1 -0
- package/dist/core/progress/store.test.d.ts +2 -0
- package/dist/core/progress/store.test.d.ts.map +1 -0
- package/dist/core/progress/store.test.js +108 -0
- package/dist/core/progress/store.test.js.map +1 -0
- package/dist/core/progress/types.d.ts +108 -0
- package/dist/core/progress/types.d.ts.map +1 -0
- package/dist/core/progress/types.js +3 -0
- package/dist/core/progress/types.js.map +1 -0
- package/dist/core/project-discovery.d.ts.map +1 -1
- package/dist/core/project-discovery.js +40 -9
- package/dist/core/project-discovery.js.map +1 -1
- package/dist/core/project-discovery.test.d.ts +2 -0
- package/dist/core/project-discovery.test.d.ts.map +1 -0
- package/dist/core/project-discovery.test.js +104 -0
- package/dist/core/project-discovery.test.js.map +1 -0
- package/dist/core/session-history.d.ts +51 -0
- package/dist/core/session-history.d.ts.map +1 -0
- package/dist/core/session-history.js +156 -0
- package/dist/core/session-history.js.map +1 -0
- package/dist/core/session-history.test.d.ts +2 -0
- package/dist/core/session-history.test.d.ts.map +1 -0
- package/dist/core/session-history.test.js +101 -0
- package/dist/core/session-history.test.js.map +1 -0
- package/dist/core/session-manager.d.ts +34 -0
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +67 -0
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-manager.test.js +140 -1
- package/dist/core/session-manager.test.js.map +1 -1
- package/dist/core/settings-manager.d.ts +1 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +4 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/system-prompt.d.ts.map +1 -1
- package/dist/system-prompt.js +12 -8
- package/dist/system-prompt.js.map +1 -1
- package/dist/system-prompt.test.js +15 -20
- package/dist/system-prompt.test.js.map +1 -1
- package/dist/tools/bash.d.ts +1 -0
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +27 -2
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/edit.d.ts +9 -2
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js +91 -10
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/edit.test.js +97 -0
- package/dist/tools/edit.test.js.map +1 -1
- package/dist/tools/plan-mode.test.js +25 -0
- package/dist/tools/plan-mode.test.js.map +1 -1
- package/dist/tools/prompt-hints.d.ts.map +1 -1
- package/dist/tools/prompt-hints.js +3 -0
- package/dist/tools/prompt-hints.js.map +1 -1
- package/dist/tools/subagent.d.ts +2 -0
- package/dist/tools/subagent.d.ts.map +1 -1
- package/dist/tools/subagent.js +213 -194
- package/dist/tools/subagent.js.map +1 -1
- package/dist/tools/subagent.test.d.ts +2 -0
- package/dist/tools/subagent.test.d.ts.map +1 -0
- package/dist/tools/subagent.test.js +73 -0
- package/dist/tools/subagent.test.js.map +1 -0
- package/dist/tools/tool-search.d.ts +15 -0
- package/dist/tools/tool-search.d.ts.map +1 -0
- package/dist/tools/tool-search.js +35 -0
- package/dist/tools/tool-search.js.map +1 -0
- package/dist/ui/components/Footer.js +3 -3
- package/dist/ui/components/Footer.js.map +1 -1
- package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
- package/dist/ui/hooks/useAgentLoop.js +22 -9
- package/dist/ui/hooks/useAgentLoop.js.map +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from "vitest";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import { PersistentShell } from "./persistent-shell.js";
|
|
4
|
+
const isWindows = os.platform() === "win32";
|
|
5
|
+
const d = describe.skipIf(isWindows);
|
|
6
|
+
d("PersistentShell", () => {
|
|
7
|
+
let shell = null;
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
shell?.kill();
|
|
10
|
+
shell = null;
|
|
11
|
+
});
|
|
12
|
+
function make() {
|
|
13
|
+
shell = new PersistentShell(os.tmpdir(), { ...process.env, TERM: "dumb" }, 1024 * 1024);
|
|
14
|
+
return shell;
|
|
15
|
+
}
|
|
16
|
+
const signal = () => new AbortController().signal;
|
|
17
|
+
it("runs a command and returns output + exit code", async () => {
|
|
18
|
+
const sh = make();
|
|
19
|
+
const res = await sh.run("echo hello", 10_000, signal());
|
|
20
|
+
expect(res.exitCode).toBe(0);
|
|
21
|
+
expect(res.output).toBe("hello");
|
|
22
|
+
});
|
|
23
|
+
it("propagates non-zero exit codes", async () => {
|
|
24
|
+
const sh = make();
|
|
25
|
+
const res = await sh.run("exit 3", 10_000, signal());
|
|
26
|
+
expect(res.exitCode).toBe(3);
|
|
27
|
+
});
|
|
28
|
+
it("persists cd and env vars across calls — the point of the feature", async () => {
|
|
29
|
+
const sh = make();
|
|
30
|
+
await sh.run("cd / && export GG_PSH_TEST=alive", 10_000, signal());
|
|
31
|
+
const res = await sh.run('pwd; echo "$GG_PSH_TEST"', 10_000, signal());
|
|
32
|
+
expect(res.output).toBe("/\nalive");
|
|
33
|
+
});
|
|
34
|
+
it("stdin-reading commands don't eat the sentinel or hang", async () => {
|
|
35
|
+
const sh = make();
|
|
36
|
+
const res = await sh.run("cat", 5_000, signal());
|
|
37
|
+
expect(res.exitCode).toBe(0);
|
|
38
|
+
// Session still healthy afterwards.
|
|
39
|
+
const next = await sh.run("echo ok", 5_000, signal());
|
|
40
|
+
expect(next.output).toBe("ok");
|
|
41
|
+
});
|
|
42
|
+
it("timeout kills the session; the next call gets a fresh shell", async () => {
|
|
43
|
+
const sh = make();
|
|
44
|
+
await sh.run("export GG_PSH_STATE=set", 10_000, signal());
|
|
45
|
+
const timedOut = await sh.run("sleep 30", 300, signal());
|
|
46
|
+
expect(timedOut.exitCode).toBe("TIMEOUT");
|
|
47
|
+
// Fresh shell: the exported var from before the timeout is gone.
|
|
48
|
+
const res = await sh.run('echo "[$GG_PSH_STATE]"', 10_000, signal());
|
|
49
|
+
expect(res.output).toBe("[]");
|
|
50
|
+
});
|
|
51
|
+
it("over-cap output still finds the sentinel — no hang, session survives", async () => {
|
|
52
|
+
shell = new PersistentShell(os.tmpdir(), { ...process.env, TERM: "dumb" }, 512);
|
|
53
|
+
const sh = shell;
|
|
54
|
+
await sh.run("export GG_PSH_CAP=kept", 10_000, signal());
|
|
55
|
+
// ~40KB of output, way past the 512-byte cap.
|
|
56
|
+
const res = await sh.run('yes x | head -n 20000; echo "end"', 10_000, signal());
|
|
57
|
+
expect(res.exitCode).toBe(0);
|
|
58
|
+
expect(res.output).toContain("[Output capped at 512 bytes]");
|
|
59
|
+
// Session state survived — the command completed instead of timing out.
|
|
60
|
+
const after = await sh.run('echo "$GG_PSH_CAP"', 10_000, signal());
|
|
61
|
+
expect(after.output).toBe("kept");
|
|
62
|
+
});
|
|
63
|
+
it("does not leak error listeners across many runs", async () => {
|
|
64
|
+
const sh = make();
|
|
65
|
+
for (let i = 0; i < 15; i++) {
|
|
66
|
+
await sh.run("true", 10_000, signal());
|
|
67
|
+
}
|
|
68
|
+
// Access the internal child to count listeners — the leak showed up as
|
|
69
|
+
// MaxListenersExceededWarning after ~10 calls before the fix.
|
|
70
|
+
const child = sh.child;
|
|
71
|
+
expect(child.listenerCount("error")).toBeLessThanOrEqual(1);
|
|
72
|
+
expect(child.listenerCount("exit")).toBeLessThanOrEqual(1);
|
|
73
|
+
});
|
|
74
|
+
it("rejects a concurrent call while busy instead of interleaving", async () => {
|
|
75
|
+
const sh = make();
|
|
76
|
+
const slow = sh.run("sleep 0.5; echo done", 10_000, signal());
|
|
77
|
+
const busy = await sh.run("echo nope", 10_000, signal());
|
|
78
|
+
expect(busy.exitCode).toBe(1);
|
|
79
|
+
expect(busy.output).toContain("busy");
|
|
80
|
+
const done = await slow;
|
|
81
|
+
expect(done.output).toBe("done");
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
//# sourceMappingURL=persistent-shell.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"persistent-shell.test.js","sourceRoot":"","sources":["../../src/core/persistent-shell.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -22,6 +22,25 @@ async function waitForProcessExit(pid) {
|
|
|
22
22
|
}
|
|
23
23
|
throw new Error(`Process ${pid} was still alive after shutdown.`);
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Terminate a real spawned process group with the *unmocked* process.kill.
|
|
27
|
+
* Used by tests that stub the manager's kill machinery and would otherwise
|
|
28
|
+
* leak the OS process they spawned. POSIX kills the detached group (-pid);
|
|
29
|
+
* Windows falls back to the single pid.
|
|
30
|
+
*/
|
|
31
|
+
function killRealProcessTree(pid) {
|
|
32
|
+
try {
|
|
33
|
+
process.kill(process.platform === "win32" ? pid : -pid, "SIGKILL");
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
try {
|
|
37
|
+
process.kill(pid, "SIGKILL");
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// Already gone.
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
25
44
|
function isProcessAlive(pid) {
|
|
26
45
|
try {
|
|
27
46
|
process.kill(pid, 0);
|
|
@@ -71,12 +90,21 @@ describe("ProcessManager dev-server lifecycle repro", () => {
|
|
|
71
90
|
});
|
|
72
91
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "gg-win-taskkill-"));
|
|
73
92
|
const started = await manager.start(`${JSON.stringify(process.execPath)} -e "setInterval(()=>{},1000)"`, tmpDir);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
93
|
+
try {
|
|
94
|
+
const stopped = await manager.stop(started.id);
|
|
95
|
+
expect(stopped).toBe(`Process ${started.id} already exited`);
|
|
96
|
+
manager.shutdownAll();
|
|
97
|
+
expect(taskkill).toHaveBeenCalledWith("taskkill", ["/pid", String(started.pid), "/T", "/F"], {
|
|
98
|
+
stdio: "ignore",
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
// This test deliberately mocks `kill` and `spawnSync`, so neither the
|
|
103
|
+
// simulated stop() nor shutdownAll() actually signals the real child
|
|
104
|
+
// spawned by start(). Reap it for real here — otherwise every run of
|
|
105
|
+
// this suite orphans a live `node -e setInterval` process forever.
|
|
106
|
+
killRealProcessTree(started.pid);
|
|
107
|
+
}
|
|
80
108
|
});
|
|
81
109
|
it("starts, reads, and stops a long-running Node HTTP server through the worker background path", async () => {
|
|
82
110
|
manager = new ProcessManager();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"process-manager-dev-server-repro.test.js","sourceRoot":"","sources":["../../src/core/process-manager-dev-server-repro.test.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"process-manager-dev-server-repro.test.js","sourceRoot":"","sources":["../../src/core/process-manager-dev-server-repro.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC7D,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD,KAAK,UAAU,aAAa,CAC1B,OAAuB,EACvB,EAAU,EACV,SAAsC;IAEtC,IAAI,QAAQ,GAAG,EAAE,CAAC;IAClB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QAC/B,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;QAC5C,QAAQ,IAAI,MAAM,CAAC,MAAM,CAAC;QAC1B,IAAI,SAAS,CAAC,QAAQ,CAAC;YAAE,OAAO,QAAQ,CAAC;QACzC,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC;IAC3D,CAAC;IACD,MAAM,IAAI,KAAK,CAAC,uCAAuC,QAAQ,EAAE,CAAC,CAAC;AACrE,CAAC;AAED,KAAK,UAAU,kBAAkB,CAAC,GAAW;IAC3C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QAC/B,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC;YAAE,OAAO;QACjC,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC;IAC3D,CAAC;IACD,MAAM,IAAI,KAAK,CAAC,WAAW,GAAG,kCAAkC,CAAC,CAAC;AACpE,CAAC;AAED;;;;;GAKG;AACH,SAAS,mBAAmB,CAAC,GAAW;IACtC,IAAI,CAAC;QACH,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;IACrE,CAAC;IAAC,MAAM,CAAC;QACP,IAAI,CAAC;YACH,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;QAC/B,CAAC;QAAC,MAAM,CAAC;YACP,gBAAgB;QAClB,CAAC;IACH,CAAC;AACH,CAAC;AAED,SAAS,cAAc,CAAC,GAAW;IACjC,IAAI,CAAC;QACH,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QACrB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,SAAS,kBAAkB,CAAC,MAAc;IACxC,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,wBAAwB,CAAC,CAAC;IACrD,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,iCAAiC,MAAM,EAAE,CAAC,CAAC;IACvE,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AAC1B,CAAC;AAED,QAAQ,CAAC,2CAA2C,EAAE,GAAG,EAAE;IACzD,IAAI,OAAuB,CAAC;IAE5B,SAAS,CAAC,GAAG,EAAE;QACb,OAAO,EAAE,WAAW,EAAE,CAAC;IACzB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6DAA6D,EAAE,KAAK,IAAI,EAAE;QAC3E,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC;QACtD,OAAO,CAAC,GAAG,CAAC,uBAAuB,GAAG,cAAc,CAAC;QACrD,OAAO,GAAG,IAAI,cAAc,EAAE,CAAC;QAC/B,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,YAAY,CAAC,CAAC,CAAC;QACtE,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,KAAK,CACjC,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,sEAAsE,EACzG,MAAM,CACP,CAAC;YACF,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC;YAC7F,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;YACrC,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;QAC/C,CAAC;gBAAS,CAAC;YACT,IAAI,SAAS,KAAK,SAAS;gBAAE,OAAO,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC;;gBACnE,OAAO,CAAC,GAAG,CAAC,uBAAuB,GAAG,SAAS,CAAC;QACvD,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0DAA0D,EAAE,KAAK,IAAI,EAAE;QACxE,MAAM,QAAQ,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;QACzB,OAAO,GAAG,IAAI,cAAc,CAAC;YAC3B,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,GAAG,EAAE;gBACf,MAAM,IAAI,KAAK,CAAC,gBAAgB,CAAC,CAAC;YACpC,CAAC,CAAwB;YACzB,SAAS,EAAE,QAAiB;SAC7B,CAAC,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,kBAAkB,CAAC,CAAC,CAAC;QAC5E,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,KAAK,CACjC,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,gCAAgC,EACnE,MAAM,CACP,CAAC;QACF,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;YAC/C,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,WAAW,OAAO,CAAC,EAAE,iBAAiB,CAAC,CAAC;YAC7D,OAAO,CAAC,WAAW,EAAE,CAAC;YACtB,MAAM,CAAC,QAAQ,CAAC,CAAC,oBAAoB,CAAC,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE;gBAC3F,KAAK,EAAE,QAAQ;aAChB,CAAC,CAAC;QACL,CAAC;gBAAS,CAAC;YACT,sEAAsE;YACtE,qEAAqE;YACrE,qEAAqE;YACrE,mEAAmE;YACnE,mBAAmB,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACnC,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6FAA6F,EAAE,KAAK,IAAI,EAAE;QAC3G,OAAO,GAAG,IAAI,cAAc,EAAE,CAAC;QAC/B,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,sBAAsB,CAAC,CAAC,CAAC;QAChF,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;QACpD,MAAM,EAAE,CAAC,SAAS,CAChB,OAAO,EACP,iCAAiC;YAC/B,mEAAmE;YACnE,yCAAyC;YACzC,uCAAuC;YACvC,sDAAsD;YACtD,OAAO;YACP,4EAA4E;YAC5E,iCAAiC;YACjC,wCAAwC;YACxC,8BAA8B;YAC9B,0CAA0C;YAC1C,OAAO,CACV,CAAC;QAEF,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,QAAQ,IAAI,OAAO,EAAE,EAAE,MAAM,CAAC,CAAC;QAC9E,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;QACvC,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QAE1C,MAAM,OAAO,GAAG,MAAM,aAAa,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,MAAM,EAAE,EAAE,CAClE,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAAC,CACpC,CAAC;QACF,MAAM,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,kBAAkB,CAAC,CAAC;QAE9C,MAAM,SAAS,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;QAC7D,MAAM,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACvC,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,QAAQ,EAAE,CAAC;QACtC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,kBAAkB,CAAC,CAAC;QAEvD,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAC/C,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,WAAW,OAAO,CAAC,EAAE,UAAU,CAAC,CAAC;QAEtD,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;QACzD,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACpC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;QACtC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,oBAAoB,CAAC,CAAC;IACvD,CAAC,EAAE,MAAM,CAAC,CAAC;IAEX,MAAM,OAAO,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;IAE5D,OAAO,CACL,8DAA8D,EAC9D,KAAK,IAAI,EAAE;QACT,OAAO,GAAG,IAAI,cAAc,EAAE,CAAC;QAC/B,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,yBAAyB,CAAC,CAAC,CAAC;QACnF,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;QACzD,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;QAEtD,MAAM,EAAE,CAAC,SAAS,CAChB,YAAY,EACZ,mDAAmD,GAAG,gCAAgC,CACvF,CAAC;QACF,MAAM,EAAE,CAAC,SAAS,CAChB,aAAa,EACb,+CAA+C;YAC7C,0CAA0C,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,oDAAoD;YAC1H,uEAAuE;YACvE,gCAAgC,CACnC,CAAC;QAEF,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,KAAK,CACjC,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC,EAAE,EACtE,MAAM,CACP,CAAC;QACF,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE,CAC/D,IAAI,CAAC,QAAQ,CAAC,kBAAkB,CAAC,CAClC,CAAC;QACF,MAAM,aAAa,GAAG,kBAAkB,CAAC,MAAM,CAAC,CAAC;QACjD,MAAM,CAAC,cAAc,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAEjD,OAAO,CAAC,WAAW,EAAE,CAAC;QAEtB,MAAM,kBAAkB,CAAC,aAAa,CAAC,CAAC;QACxC,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;QACzD,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACpC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;QAC/C,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,kBAAkB,CAAC,CAAC;IACrD,CAAC,EACD,MAAM,CACP,CAAC;AACJ,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { LevelUpEvent, ProgressFile, ScoredCommit } from "./types.js";
|
|
2
|
+
export interface AwardResult {
|
|
3
|
+
file: ProgressFile;
|
|
4
|
+
gained: number;
|
|
5
|
+
levelUp: LevelUpEvent | null;
|
|
6
|
+
}
|
|
7
|
+
/** Streak multiplier for the current streak length (in days). */
|
|
8
|
+
export declare function streakMultiplier(days: number): number;
|
|
9
|
+
/** Raw XP for a commit of a given size (before multipliers/caps). */
|
|
10
|
+
export declare function commitXp(linesChanged: number): number;
|
|
11
|
+
/** Update streak for an XP event happening now. Returns the active multiplier. */
|
|
12
|
+
export declare function applyStreak(file: ProgressFile, now: number): number;
|
|
13
|
+
/**
|
|
14
|
+
* Award XP for one completed prompt (caller ensures the run finished, not canceled, ≥1 turn).
|
|
15
|
+
* Diminishing returns: after 15 prompts in a rolling hour, +2 instead of +10.
|
|
16
|
+
*/
|
|
17
|
+
export declare function awardPrompt(file: ProgressFile, now: number, cwd?: string): AwardResult;
|
|
18
|
+
/**
|
|
19
|
+
* Award XP for new commits. Dedupes by patch-id (reverts/recommits score 0) and caps
|
|
20
|
+
* XP-earning commits at 6 per rolling hour (extra commits still count in totals).
|
|
21
|
+
*/
|
|
22
|
+
export declare function awardCommits(file: ProgressFile, commits: ScoredCommit[], now: number, cwd?: string): AwardResult;
|
|
23
|
+
//# sourceMappingURL=engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../../src/core/progress/engine.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAY3E,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,YAAY,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,YAAY,GAAG,IAAI,CAAC;CAC9B;AAED,iEAAiE;AACjE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMrD;AAED,qEAAqE;AACrE,wBAAgB,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAErD;AAYD,kFAAkF;AAClF,wBAAgB,WAAW,CAAC,IAAI,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAUnE;AAkDD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,WAAW,CAYtF;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,YAAY,EAClB,OAAO,EAAE,YAAY,EAAE,EACvB,GAAG,EAAE,MAAM,EACX,GAAG,CAAC,EAAE,MAAM,GACX,WAAW,CAmBb"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// Pure XP engine — all functions take a ProgressFile and a `now`, mutate a copy, and
|
|
2
|
+
// return the gain. No I/O here; the store handles persistence.
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import { levelForXp, rankForLevel } from "./ranks.js";
|
|
5
|
+
import { dayKey } from "./store.js";
|
|
6
|
+
const HOUR_MS = 60 * 60 * 1000;
|
|
7
|
+
const PROMPT_XP = 10;
|
|
8
|
+
const PROMPT_XP_DIMINISHED = 2;
|
|
9
|
+
const PROMPTS_PER_HOUR_FULL = 15;
|
|
10
|
+
const COMMIT_BASE_XP = 20;
|
|
11
|
+
const COMMIT_SIZE_XP_CAP = 80;
|
|
12
|
+
const COMMITS_PER_HOUR = 6;
|
|
13
|
+
const DAILY_SOFT_CAP = 500;
|
|
14
|
+
const DAILY_OVERCAP_FACTOR = 0.25;
|
|
15
|
+
/** Streak multiplier for the current streak length (in days). */
|
|
16
|
+
export function streakMultiplier(days) {
|
|
17
|
+
if (days >= 30)
|
|
18
|
+
return 1.5;
|
|
19
|
+
if (days >= 14)
|
|
20
|
+
return 1.4;
|
|
21
|
+
if (days >= 7)
|
|
22
|
+
return 1.25;
|
|
23
|
+
if (days >= 3)
|
|
24
|
+
return 1.1;
|
|
25
|
+
return 1;
|
|
26
|
+
}
|
|
27
|
+
/** Raw XP for a commit of a given size (before multipliers/caps). */
|
|
28
|
+
export function commitXp(linesChanged) {
|
|
29
|
+
return COMMIT_BASE_XP + Math.min(COMMIT_SIZE_XP_CAP, Math.floor(Math.sqrt(linesChanged) * 4));
|
|
30
|
+
}
|
|
31
|
+
function pruneRolling(file, now) {
|
|
32
|
+
file.rolling.promptTimes = file.rolling.promptTimes.filter((t) => now - t < HOUR_MS);
|
|
33
|
+
file.rolling.commitTimes = file.rolling.commitTimes.filter((t) => now - t < HOUR_MS);
|
|
34
|
+
const today = dayKey(new Date(now));
|
|
35
|
+
if (file.rolling.dayKey !== today) {
|
|
36
|
+
file.rolling.dayKey = today;
|
|
37
|
+
file.rolling.dayXp = 0;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Update streak for an XP event happening now. Returns the active multiplier. */
|
|
41
|
+
export function applyStreak(file, now) {
|
|
42
|
+
const today = dayKey(new Date(now));
|
|
43
|
+
const last = file.streak.lastActiveDay;
|
|
44
|
+
if (last !== today) {
|
|
45
|
+
const yesterday = dayKey(new Date(now - 24 * HOUR_MS));
|
|
46
|
+
file.streak.current = last === yesterday ? file.streak.current + 1 : 1;
|
|
47
|
+
file.streak.lastActiveDay = today;
|
|
48
|
+
if (file.streak.current > file.streak.best)
|
|
49
|
+
file.streak.best = file.streak.current;
|
|
50
|
+
}
|
|
51
|
+
return streakMultiplier(file.streak.current);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Apply streak multiplier + daily soft cap to a base amount and credit it.
|
|
55
|
+
* Splits the streak bonus out into its own xpBySource bucket.
|
|
56
|
+
*/
|
|
57
|
+
function creditXp(file, base, source, now) {
|
|
58
|
+
const multiplier = applyStreak(file, now);
|
|
59
|
+
const withStreak = base * multiplier;
|
|
60
|
+
// Daily soft cap: XP beyond 500/day earns at 25%.
|
|
61
|
+
const room = Math.max(0, DAILY_SOFT_CAP - file.rolling.dayXp);
|
|
62
|
+
const full = Math.min(withStreak, room);
|
|
63
|
+
const overflow = withStreak - full;
|
|
64
|
+
const gained = Math.round(full + overflow * DAILY_OVERCAP_FACTOR);
|
|
65
|
+
if (gained <= 0)
|
|
66
|
+
return 0;
|
|
67
|
+
// Attribute the capped gain proportionally between base and streak bonus.
|
|
68
|
+
const capRatio = withStreak > 0 ? gained / withStreak : 0;
|
|
69
|
+
const baseShare = Math.round(base * capRatio);
|
|
70
|
+
const bonusShare = gained - baseShare;
|
|
71
|
+
file.xp += gained;
|
|
72
|
+
file.rolling.dayXp += gained;
|
|
73
|
+
file.xpBySource[source] += baseShare;
|
|
74
|
+
file.xpBySource.streakBonus += bonusShare;
|
|
75
|
+
return gained;
|
|
76
|
+
}
|
|
77
|
+
function markProject(file, cwd) {
|
|
78
|
+
if (!cwd)
|
|
79
|
+
return;
|
|
80
|
+
const hash = crypto.createHash("sha256").update(cwd).digest("hex").slice(0, 16);
|
|
81
|
+
if (!file.totals.projects.includes(hash))
|
|
82
|
+
file.totals.projects.push(hash);
|
|
83
|
+
}
|
|
84
|
+
function finishAward(file, levelBefore, gained) {
|
|
85
|
+
const levelAfter = levelForXp(file.xp);
|
|
86
|
+
let levelUp = null;
|
|
87
|
+
if (levelAfter > levelBefore) {
|
|
88
|
+
levelUp = { from: levelBefore, to: levelAfter, rankName: rankForLevel(levelAfter).name };
|
|
89
|
+
}
|
|
90
|
+
file.lastEvent = { nonce: crypto.randomUUID(), levelUp };
|
|
91
|
+
return { file, gained, levelUp };
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Award XP for one completed prompt (caller ensures the run finished, not canceled, ≥1 turn).
|
|
95
|
+
* Diminishing returns: after 15 prompts in a rolling hour, +2 instead of +10.
|
|
96
|
+
*/
|
|
97
|
+
export function awardPrompt(file, now, cwd) {
|
|
98
|
+
const levelBefore = levelForXp(file.xp);
|
|
99
|
+
pruneRolling(file, now);
|
|
100
|
+
const base = file.rolling.promptTimes.length >= PROMPTS_PER_HOUR_FULL ? PROMPT_XP_DIMINISHED : PROMPT_XP;
|
|
101
|
+
file.rolling.promptTimes.push(now);
|
|
102
|
+
file.totals.prompts += 1;
|
|
103
|
+
markProject(file, cwd);
|
|
104
|
+
const gained = creditXp(file, base, "prompts", now);
|
|
105
|
+
return finishAward(file, levelBefore, gained);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Award XP for new commits. Dedupes by patch-id (reverts/recommits score 0) and caps
|
|
109
|
+
* XP-earning commits at 6 per rolling hour (extra commits still count in totals).
|
|
110
|
+
*/
|
|
111
|
+
export function awardCommits(file, commits, now, cwd) {
|
|
112
|
+
const levelBefore = levelForXp(file.xp);
|
|
113
|
+
pruneRolling(file, now);
|
|
114
|
+
let gained = 0;
|
|
115
|
+
for (const commit of commits) {
|
|
116
|
+
if (commit.patchId && file.patchIds.includes(commit.patchId))
|
|
117
|
+
continue;
|
|
118
|
+
if (commit.patchId)
|
|
119
|
+
file.patchIds.push(commit.patchId);
|
|
120
|
+
file.totals.commits += 1;
|
|
121
|
+
file.totals.linesShipped += commit.linesChanged;
|
|
122
|
+
if (file.rolling.commitTimes.length >= COMMITS_PER_HOUR)
|
|
123
|
+
continue;
|
|
124
|
+
file.rolling.commitTimes.push(now);
|
|
125
|
+
gained += creditXp(file, commitXp(commit.linesChanged), "commits", now);
|
|
126
|
+
}
|
|
127
|
+
if (commits.length > 0)
|
|
128
|
+
markProject(file, cwd);
|
|
129
|
+
return finishAward(file, levelBefore, gained);
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=engine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.js","sourceRoot":"","sources":["../../../src/core/progress/engine.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.test.d.ts","sourceRoot":"","sources":["../../../src/core/progress/engine.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { awardCommits, awardPrompt, applyStreak, commitXp, streakMultiplier } from "./engine.js";
|
|
3
|
+
import { createEmptyProgress, dayKey } from "./store.js";
|
|
4
|
+
const T0 = Date.parse("2026-07-01T12:00:00Z");
|
|
5
|
+
const HOUR = 60 * 60 * 1000;
|
|
6
|
+
const DAY = 24 * HOUR;
|
|
7
|
+
function fresh() {
|
|
8
|
+
return createEmptyProgress(new Date(T0));
|
|
9
|
+
}
|
|
10
|
+
function commit(sha, lines) {
|
|
11
|
+
return { sha, patchId: `pid-${sha}`, linesChanged: lines };
|
|
12
|
+
}
|
|
13
|
+
describe("awardPrompt", () => {
|
|
14
|
+
it("gives +10 for a normal prompt", () => {
|
|
15
|
+
const { file, gained } = awardPrompt(fresh(), T0);
|
|
16
|
+
expect(gained).toBe(10);
|
|
17
|
+
expect(file.xp).toBe(10);
|
|
18
|
+
expect(file.totals.prompts).toBe(1);
|
|
19
|
+
expect(file.xpBySource.prompts).toBe(10);
|
|
20
|
+
});
|
|
21
|
+
it("diminishes to +2 after 15 prompts in a rolling hour", () => {
|
|
22
|
+
let file = fresh();
|
|
23
|
+
for (let i = 0; i < 15; i++) {
|
|
24
|
+
file = awardPrompt(file, T0 + i * 1000).file;
|
|
25
|
+
}
|
|
26
|
+
const { gained } = awardPrompt(file, T0 + 16_000);
|
|
27
|
+
expect(gained).toBe(2);
|
|
28
|
+
});
|
|
29
|
+
it("recovers full XP once old prompts age out of the hour", () => {
|
|
30
|
+
let file = fresh();
|
|
31
|
+
for (let i = 0; i < 15; i++) {
|
|
32
|
+
file = awardPrompt(file, T0 + i * 1000).file;
|
|
33
|
+
}
|
|
34
|
+
const { gained } = awardPrompt(file, T0 + HOUR + 20_000);
|
|
35
|
+
expect(gained).toBe(10);
|
|
36
|
+
});
|
|
37
|
+
it("tracks distinct projects by hash", () => {
|
|
38
|
+
let file = fresh();
|
|
39
|
+
file = awardPrompt(file, T0, "/a").file;
|
|
40
|
+
file = awardPrompt(file, T0 + 1, "/b").file;
|
|
41
|
+
file = awardPrompt(file, T0 + 2, "/a").file;
|
|
42
|
+
expect(file.totals.projects).toHaveLength(2);
|
|
43
|
+
});
|
|
44
|
+
it("sets a fresh lastEvent nonce on every award", () => {
|
|
45
|
+
const r1 = awardPrompt(fresh(), T0);
|
|
46
|
+
const nonce1 = r1.file.lastEvent?.nonce;
|
|
47
|
+
const r2 = awardPrompt(r1.file, T0 + 1000);
|
|
48
|
+
expect(nonce1).toBeTruthy();
|
|
49
|
+
expect(r2.file.lastEvent?.nonce).not.toBe(nonce1);
|
|
50
|
+
});
|
|
51
|
+
it("reports levelUp when crossing a boundary", () => {
|
|
52
|
+
const file = fresh();
|
|
53
|
+
file.xp = 300; // level 2 needs 303
|
|
54
|
+
const { levelUp } = awardPrompt(file, T0);
|
|
55
|
+
expect(levelUp).toEqual({ from: 1, to: 2, rankName: "Tinkerer" });
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
describe("commitXp", () => {
|
|
59
|
+
it("scales sub-linearly and caps at 100", () => {
|
|
60
|
+
expect(commitXp(0)).toBe(20);
|
|
61
|
+
expect(commitXp(25)).toBe(40); // 20 + √25×4
|
|
62
|
+
expect(commitXp(400)).toBe(100); // capped
|
|
63
|
+
expect(commitXp(1_000_000)).toBe(100);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
describe("awardCommits", () => {
|
|
67
|
+
it("scores a commit by size", () => {
|
|
68
|
+
const { file, gained } = awardCommits(fresh(), [commit("a", 25)], T0);
|
|
69
|
+
expect(gained).toBe(40);
|
|
70
|
+
expect(file.totals.commits).toBe(1);
|
|
71
|
+
expect(file.totals.linesShipped).toBe(25);
|
|
72
|
+
expect(file.xpBySource.commits).toBe(40);
|
|
73
|
+
});
|
|
74
|
+
it("dedupes by patch-id — revert/recommit earns 0", () => {
|
|
75
|
+
const first = awardCommits(fresh(), [commit("a", 25)], T0);
|
|
76
|
+
const second = awardCommits(first.file, [{ ...commit("b", 25), patchId: "pid-a" }], T0 + 1000);
|
|
77
|
+
expect(second.gained).toBe(0);
|
|
78
|
+
expect(second.file.totals.commits).toBe(1); // not even counted
|
|
79
|
+
});
|
|
80
|
+
it("caps XP-earning commits at 6 per rolling hour", () => {
|
|
81
|
+
const commits = Array.from({ length: 8 }, (_, i) => commit(`c${i}`, 4));
|
|
82
|
+
const { file, gained } = awardCommits(fresh(), commits, T0);
|
|
83
|
+
expect(gained).toBe(6 * 28); // 20 + √4×4 = 28 each
|
|
84
|
+
expect(file.totals.commits).toBe(8); // totals still count all
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
describe("streaks", () => {
|
|
88
|
+
it("multiplier tiers", () => {
|
|
89
|
+
expect(streakMultiplier(1)).toBe(1);
|
|
90
|
+
expect(streakMultiplier(3)).toBe(1.1);
|
|
91
|
+
expect(streakMultiplier(7)).toBe(1.25);
|
|
92
|
+
expect(streakMultiplier(14)).toBe(1.4);
|
|
93
|
+
expect(streakMultiplier(30)).toBe(1.5);
|
|
94
|
+
});
|
|
95
|
+
it("increments on consecutive days and resets after a gap", () => {
|
|
96
|
+
const file = fresh();
|
|
97
|
+
applyStreak(file, T0);
|
|
98
|
+
expect(file.streak.current).toBe(1);
|
|
99
|
+
applyStreak(file, T0 + DAY);
|
|
100
|
+
expect(file.streak.current).toBe(2);
|
|
101
|
+
applyStreak(file, T0 + DAY + 1000); // same day — no change
|
|
102
|
+
expect(file.streak.current).toBe(2);
|
|
103
|
+
applyStreak(file, T0 + 4 * DAY); // gap
|
|
104
|
+
expect(file.streak.current).toBe(1);
|
|
105
|
+
expect(file.streak.best).toBe(2);
|
|
106
|
+
});
|
|
107
|
+
it("applies the multiplier to prompt XP", () => {
|
|
108
|
+
const file = fresh();
|
|
109
|
+
file.streak = { current: 6, best: 6, lastActiveDay: dayKey(new Date(T0 - DAY)) };
|
|
110
|
+
const { gained } = awardPrompt(file, T0); // day 7 → ×1.25
|
|
111
|
+
expect(gained).toBe(Math.round(10 * 1.25));
|
|
112
|
+
expect(file.xpBySource.prompts).toBe(10);
|
|
113
|
+
expect(file.xpBySource.streakBonus).toBe(gained - 10);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
describe("daily soft cap", () => {
|
|
117
|
+
it("earns at 25% beyond 500 XP/day", () => {
|
|
118
|
+
const file = fresh();
|
|
119
|
+
file.rolling.dayXp = 500;
|
|
120
|
+
const { gained } = awardPrompt(file, T0);
|
|
121
|
+
expect(gained).toBe(Math.round(10 * 0.25));
|
|
122
|
+
});
|
|
123
|
+
it("splits an award straddling the cap", () => {
|
|
124
|
+
const file = fresh();
|
|
125
|
+
file.rolling.dayXp = 495;
|
|
126
|
+
const { gained } = awardPrompt(file, T0); // 5 full + 5×0.25
|
|
127
|
+
expect(gained).toBe(6);
|
|
128
|
+
});
|
|
129
|
+
it("resets on day rollover", () => {
|
|
130
|
+
const file = fresh();
|
|
131
|
+
file.rolling.dayXp = 500;
|
|
132
|
+
const { gained } = awardPrompt(file, T0 + DAY);
|
|
133
|
+
expect(gained).toBe(10);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
//# sourceMappingURL=engine.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.test.js","sourceRoot":"","sources":["../../../src/core/progress/engine.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ScoredCommit } from "./types.js";
|
|
2
|
+
/** Stable key for a repo root, used in ProgressFile.repos. */
|
|
3
|
+
export declare function repoKey(repoRoot: string): string;
|
|
4
|
+
export interface DetectResult {
|
|
5
|
+
repoRoot: string;
|
|
6
|
+
head: string;
|
|
7
|
+
commits: ScoredCommit[];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Detect new, scoreable commits since `lastHead`.
|
|
11
|
+
* - `lastHead` undefined (first time seeing this repo) → record HEAD, score nothing.
|
|
12
|
+
* - Merge commits excluded; commits authored before the run window excluded.
|
|
13
|
+
* - Any git failure → null (silent).
|
|
14
|
+
*/
|
|
15
|
+
export declare function detectNewCommits(cwd: string, lastHead: string | undefined, runStartedAt: number): Promise<DetectResult | null>;
|
|
16
|
+
//# sourceMappingURL=git-xp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-xp.d.ts","sourceRoot":"","sources":["../../../src/core/progress/git-xp.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAqB/C,8DAA8D;AAC9D,wBAAgB,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,YAAY,EAAE,CAAC;CACzB;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAsC9B"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Detect and score new commits made during an agent run. All failures are silent —
|
|
2
|
+
// progress must never break a run.
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
|
+
const GIT_TIMEOUT_MS = 5_000;
|
|
6
|
+
const MAX_COMMITS_PER_DETECT = 50;
|
|
7
|
+
/** Commits authored earlier than runStart - 5min are treated as imports (pull), not work. */
|
|
8
|
+
const AUTHOR_WINDOW_SLACK_MS = 5 * 60 * 1000;
|
|
9
|
+
function git(cwd, args, input) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const opts = { cwd, timeout: GIT_TIMEOUT_MS, maxBuffer: 10 * 1024 * 1024 };
|
|
12
|
+
const child = execFile("git", args, opts, (err, stdout) => {
|
|
13
|
+
if (err)
|
|
14
|
+
reject(err);
|
|
15
|
+
else
|
|
16
|
+
resolve(String(stdout));
|
|
17
|
+
});
|
|
18
|
+
if (input !== undefined && child.stdin) {
|
|
19
|
+
child.stdin.write(input);
|
|
20
|
+
child.stdin.end();
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/** Stable key for a repo root, used in ProgressFile.repos. */
|
|
25
|
+
export function repoKey(repoRoot) {
|
|
26
|
+
return crypto.createHash("sha256").update(repoRoot).digest("hex").slice(0, 16);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Detect new, scoreable commits since `lastHead`.
|
|
30
|
+
* - `lastHead` undefined (first time seeing this repo) → record HEAD, score nothing.
|
|
31
|
+
* - Merge commits excluded; commits authored before the run window excluded.
|
|
32
|
+
* - Any git failure → null (silent).
|
|
33
|
+
*/
|
|
34
|
+
export async function detectNewCommits(cwd, lastHead, runStartedAt) {
|
|
35
|
+
try {
|
|
36
|
+
const repoRoot = (await git(cwd, ["rev-parse", "--show-toplevel"])).trim();
|
|
37
|
+
const head = (await git(cwd, ["rev-parse", "HEAD"])).trim();
|
|
38
|
+
if (!repoRoot || !head)
|
|
39
|
+
return null;
|
|
40
|
+
if (!lastHead || lastHead === head) {
|
|
41
|
+
return { repoRoot, head, commits: [] };
|
|
42
|
+
}
|
|
43
|
+
const sinceIso = new Date(runStartedAt - AUTHOR_WINDOW_SLACK_MS).toISOString();
|
|
44
|
+
let shas;
|
|
45
|
+
try {
|
|
46
|
+
const out = await git(cwd, [
|
|
47
|
+
"rev-list",
|
|
48
|
+
`${lastHead}..HEAD`,
|
|
49
|
+
"--no-merges",
|
|
50
|
+
`--since=${sinceIso}`,
|
|
51
|
+
`--max-count=${MAX_COMMITS_PER_DETECT}`,
|
|
52
|
+
]);
|
|
53
|
+
shas = out
|
|
54
|
+
.split("\n")
|
|
55
|
+
.map((s) => s.trim())
|
|
56
|
+
.filter(Boolean);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// lastHead may be gone (rebase/gc) — reset the baseline without scoring.
|
|
60
|
+
return { repoRoot, head, commits: [] };
|
|
61
|
+
}
|
|
62
|
+
const commits = [];
|
|
63
|
+
for (const sha of shas.reverse()) {
|
|
64
|
+
const scored = await scoreCommit(cwd, sha);
|
|
65
|
+
if (scored)
|
|
66
|
+
commits.push(scored);
|
|
67
|
+
}
|
|
68
|
+
return { repoRoot, head, commits };
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function scoreCommit(cwd, sha) {
|
|
75
|
+
try {
|
|
76
|
+
// Lines changed: numstat sums added+deleted; binary files show "-" and count 0.
|
|
77
|
+
const numstat = await git(cwd, ["show", "--numstat", "--format=", sha]);
|
|
78
|
+
let linesChanged = 0;
|
|
79
|
+
for (const line of numstat.split("\n")) {
|
|
80
|
+
const parts = line.split("\t");
|
|
81
|
+
if (parts.length < 3)
|
|
82
|
+
continue;
|
|
83
|
+
const added = parseInt(parts[0], 10);
|
|
84
|
+
const deleted = parseInt(parts[1], 10);
|
|
85
|
+
if (Number.isFinite(added))
|
|
86
|
+
linesChanged += added;
|
|
87
|
+
if (Number.isFinite(deleted))
|
|
88
|
+
linesChanged += deleted;
|
|
89
|
+
}
|
|
90
|
+
// Patch-id dedupe: revert/recommit of the same diff scores 0.
|
|
91
|
+
let patchId = "";
|
|
92
|
+
try {
|
|
93
|
+
const diff = await git(cwd, ["show", sha]);
|
|
94
|
+
const out = await git(cwd, ["patch-id", "--stable"], diff);
|
|
95
|
+
patchId = out.trim().split(" ")[0] ?? "";
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
patchId = "";
|
|
99
|
+
}
|
|
100
|
+
return { sha, patchId, linesChanged };
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=git-xp.js.map
|