@raresportan/marram 0.2.2 → 0.2.3
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/package.json +1 -1
- package/src/core/config.mjs +1 -0
- package/src/core/gate-state.mjs +19 -5
- package/src/core/gate.mjs +2 -2
- package/src/core/hook.mjs +19 -13
package/package.json
CHANGED
package/src/core/config.mjs
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// progress: /lines from a stage worth showing as it runs/, // besides ✓ and ✗ lines
|
|
7
7
|
// sleepGapMs: 30000, // how long a pause counts as the machine sleeping, so a stage that failed runs again
|
|
8
8
|
// tiers: { fast: [stage, …], default: [stage, …], full: [stage, …] }, // stage: { name, run: [command, …args], env? }
|
|
9
|
+
// prePush: "full", // the gate a push must pass (marram hook pre-push): "default" unless set
|
|
9
10
|
// },
|
|
10
11
|
// review: { classes: "the mistake-classes doc", rungs: ["type", "lint", …] },
|
|
11
12
|
// };
|
package/src/core/gate-state.mjs
CHANGED
|
@@ -78,9 +78,23 @@ export function updateState(cwd, change) {
|
|
|
78
78
|
});
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
/**
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
81
|
+
/**
|
|
82
|
+
* `{ passed, tier, stage?, log?, at }` for a tree `tier` (the default gate, or the full one) passed, or failed less than
|
|
83
|
+
* 15 minutes ago. A full pass counts for the default gate.
|
|
84
|
+
*/
|
|
85
|
+
export function verdictOf(cwd, tree, tier = "default") {
|
|
86
|
+
const trees = readState(cwd).trees;
|
|
87
|
+
const own = current(trees[verdictKey(tree, tier)]);
|
|
88
|
+
// The full gate runs every default stage, so its pass answers for the default gate too.
|
|
89
|
+
if (own === undefined && tier === "default" && trees[verdictKey(tree, "full")]?.passed) return trees[verdictKey(tree, "full")];
|
|
90
|
+
return own;
|
|
86
91
|
}
|
|
92
|
+
|
|
93
|
+
/** A verdict that still counts: a pass, or a failure from the last FAILURE_TTL_MS. */
|
|
94
|
+
const current = (verdict) => (verdict === undefined || verdict.passed || Date.now() - verdict.at < FAILURE_TTL_MS ? verdict : undefined);
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Where a tier's verdict on `tree` is kept: the default gate's under the tree, the full gate's beside it, so a default run
|
|
98
|
+
* after a full pass doesn't replace it.
|
|
99
|
+
*/
|
|
100
|
+
export const verdictKey = (tree, tier) => (tier === "full" ? `${tree}:full` : tree);
|
package/src/core/gate.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import { spawn } from "node:child_process";
|
|
|
12
12
|
import { createWriteStream } from "node:fs";
|
|
13
13
|
import { mkdir, rename, writeFile } from "node:fs/promises";
|
|
14
14
|
import { join } from "node:path";
|
|
15
|
-
import { treeOf, updateState } from "./gate-state.mjs";
|
|
15
|
+
import { treeOf, updateState, verdictKey } from "./gate-state.mjs";
|
|
16
16
|
|
|
17
17
|
const TIERS = ["fast", "default", "full"];
|
|
18
18
|
|
|
@@ -77,7 +77,7 @@ export async function gate(root, config, [tier = "default"]) {
|
|
|
77
77
|
// A fast pass doesn't make a change done, so only the default and full gates record a verdict.
|
|
78
78
|
const tree = tier === "fast" ? undefined : treeOf(root);
|
|
79
79
|
const verdict = (passed, extra = {}) => {
|
|
80
|
-
if (tree !== undefined) updateState(root, (state) => { state.trees[tree] = { passed, tier, at: Date.now(), ...extra }; });
|
|
80
|
+
if (tree !== undefined) updateState(root, (state) => { state.trees[verdictKey(tree, tier)] = { passed, tier, at: Date.now(), ...extra }; });
|
|
81
81
|
};
|
|
82
82
|
|
|
83
83
|
console.log(`Gate (${tier}). Logs: ${dir}`);
|
package/src/core/hook.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Runs the gate where work is declared done, so nothing finishes or ships while it's red. Usage:
|
|
2
|
-
// marram hook pre-push git's pre-push hook: what's pushed must have passed.
|
|
2
|
+
// marram hook pre-push git's pre-push hook: what's pushed must have passed `gate.prePush`, the default gate
|
|
3
|
+
// unless that names "full".
|
|
3
4
|
// marram hook claude-turn Claude Code's UserPromptSubmit: notes the code a turn starts from.
|
|
4
5
|
// marram hook claude-stop Claude Code's Stop: a turn that changed code ends only once the gate passes, sent back at
|
|
5
6
|
// most 3 times a turn.
|
|
@@ -15,16 +16,16 @@ const MAX_SENT_BACK = 3;
|
|
|
15
16
|
const MARRAM = join(import.meta.dirname, "..", "..", "bin", "marram.mjs");
|
|
16
17
|
const stdin = () => readFileSync(0, "utf8");
|
|
17
18
|
|
|
18
|
-
/**
|
|
19
|
-
async function judge(root, tree, show) {
|
|
20
|
-
const known = verdictOf(root, tree);
|
|
19
|
+
/** `tier`'s verdict on `tree`, running that gate when there is none. `show` sends its progress to the terminal. */
|
|
20
|
+
async function judge(root, tree, show, tier = "default") {
|
|
21
|
+
const known = verdictOf(root, tree, tier);
|
|
21
22
|
if (known !== undefined) return { ...known, reused: true };
|
|
22
|
-
const gate = spawn(process.execPath, [MARRAM, "gate"], { cwd: root, stdio: ["ignore", show ? "inherit" : "ignore", "inherit"] });
|
|
23
|
+
const gate = spawn(process.execPath, [MARRAM, "gate", ...(tier === "default" ? [] : [tier])], { cwd: root, stdio: ["ignore", show ? "inherit" : "ignore", "inherit"] });
|
|
23
24
|
// A hook that's killed (its timeout) passes it on, so the gate can stop what it started.
|
|
24
25
|
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) process.once(signal, () => gate.kill(signal));
|
|
25
26
|
await new Promise((done) => gate.once("close", done));
|
|
26
27
|
// A gate that changed files as it ran records no pass; it counts as failed with no stage.
|
|
27
|
-
return verdictOf(root, tree) ?? { passed: false };
|
|
28
|
+
return verdictOf(root, tree, tier) ?? { passed: false };
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
/** What an agent is told when the gate fails: where it failed, and the end of that stage's log. */
|
|
@@ -45,19 +46,24 @@ function failure(verdict) {
|
|
|
45
46
|
].join("\n");
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
async function prePush(root) {
|
|
49
|
+
async function prePush(root, config) {
|
|
50
|
+
const tier = config.gate?.prePush ?? "default";
|
|
51
|
+
if (tier !== "default" && tier !== "full") {
|
|
52
|
+
console.error(`gate.prePush is "${tier}": a push can require the default gate or the full one.`);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
49
55
|
// stdin: one line per ref pushed, "<local ref> <local sha> <remote ref> <remote sha>".
|
|
50
56
|
for (const line of stdin().split("\n").filter(Boolean)) {
|
|
51
57
|
const [ref, sha] = line.split(" ");
|
|
52
58
|
if (/^0+$/.test(sha)) continue; // a deletion
|
|
53
59
|
const tree = git(root, ["rev-parse", `${sha}^{tree}`]);
|
|
54
|
-
if (verdictOf(root, tree)?.passed) continue;
|
|
60
|
+
if (verdictOf(root, tree, tier)?.passed) continue;
|
|
55
61
|
if (treeOf(root) !== tree) {
|
|
56
|
-
console.error(`${ref} hasn't passed the gate, and the working tree isn't that commit, so it can't be checked here.\nCommit or stash your changes, check it out and push again (or \`git push --no-verify\` to skip the gate).`);
|
|
62
|
+
console.error(`${ref} hasn't passed the ${tier} gate, and the working tree isn't that commit, so it can't be checked here.\nCommit or stash your changes, check it out and push again (or \`git push --no-verify\` to skip the gate).`);
|
|
57
63
|
process.exit(1);
|
|
58
64
|
}
|
|
59
|
-
console.error(`Gating ${ref} before it's pushed (\`git push --no-verify\` skips this).`);
|
|
60
|
-
const verdict = await judge(root, tree, true);
|
|
65
|
+
console.error(`Gating ${ref} (the ${tier} gate) before it's pushed (\`git push --no-verify\` skips this).`);
|
|
66
|
+
const verdict = await judge(root, tree, true, tier);
|
|
61
67
|
if (!verdict.passed) {
|
|
62
68
|
console.error(failure(verdict));
|
|
63
69
|
process.exit(1);
|
|
@@ -105,10 +111,10 @@ async function finish(root) {
|
|
|
105
111
|
|
|
106
112
|
const modes = { "pre-push": prePush, "claude-turn": claudeTurn, "claude-stop": claudeStop, finish };
|
|
107
113
|
|
|
108
|
-
export async function hook(root,
|
|
114
|
+
export async function hook(root, config, [mode]) {
|
|
109
115
|
if (!(mode in modes)) {
|
|
110
116
|
console.error(`Usage: marram hook ${Object.keys(modes).join(" | ")}`);
|
|
111
117
|
process.exit(1);
|
|
112
118
|
}
|
|
113
|
-
await modes[mode](root);
|
|
119
|
+
await modes[mode](root, config);
|
|
114
120
|
}
|