Public: anyone can read this; the crew can change it.
Sentinel: The guard parked 29 runs in trevato/forge in a day #2163
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/2000-sentinel-the-guard-parked-29-runs-in-tre → main 1 commit fork
2 files
changed +33
−3
src/crew/sentinel.ts +12 −3
| @@ -233,15 +233,24 @@ export function ledgerStreaks(db: Database, now: number): Finding[] { | ||
| 233 | 233 | lookFor: "A retry decision that is not stop, or a green check after a stop.", |
| 234 | 234 | }); |
| 235 | 235 | } |
| 236 | // A skip is a run the guard saved — nothing new to act on — not a run it | |
| 237 | // refused; only the decisions that parked work are counted. | |
| 236 | 238 | const parks = new Map<string, number>(); |
| 237 | for (const d of decisionsSince(db, "guard", new Date(now - DAY_MS).toISOString())) | |
| 238 | parks.set(d.repoId, (parks.get(d.repoId) ?? 0) + 1); | |
| 239 | const skips = new Map<string, number>(); | |
| 240 | for (const d of decisionsSince(db, "guard", new Date(now - DAY_MS).toISOString())) { | |
| 241 | const into = d.answer === "skipped" ? skips : parks; | |
| 242 | into.set(d.repoId, (into.get(d.repoId) ?? 0) + 1); | |
| 243 | } | |
| 239 | 244 | for (const [repoId, n] of parks) { |
| 240 | 245 | if (n < 20) continue; |
| 246 | const skipped = skips.get(repoId) ?? 0; | |
| 241 | 247 | out.push({ |
| 242 | 248 | key: `parks:${repoId}`, |
| 243 | 249 | title: `The guard parked ${n} runs in ${repoSlug(db, repoId)} in a day`, |
| 244 | evidence: [`${n} \`guard\` decisions since ${new Date(now - DAY_MS).toISOString()}`], | |
| 250 | evidence: [ | |
| 251 | `${n} \`guard\` decisions that parked a run since ${new Date(now - DAY_MS).toISOString()}`, | |
| 252 | ...(skipped > 0 ? [`${skipped} skips of a run with nothing new are not counted`] : []), | |
| 253 | ], | |
| 245 | 254 | pillar: "Throughput: the crew spends on work, not on being refused", |
| 246 | 255 | lookFor: "Fewer than 20 guard decisions a day for the repository.", |
| 247 | 256 | }); |
test/sentinel.test.ts +21
| @@ -274,6 +274,27 @@ describe("the guard", () => { | ||
| 274 | 274 | park(); |
| 275 | 275 | expect(parks()?.evidence[0]).toContain("20 `guard` decisions"); |
| 276 | 276 | }); |
| 277 | ||
| 278 | test("a skip is a run saved, not a park: skips alone are never named", () => { | |
| 279 | app.db.query("DELETE FROM decision WHERE kind = 'guard'").run(); | |
| 280 | const guard = (answer: string) => | |
| 281 | recordDecision(app.db, { | |
| 282 | kind: "guard", | |
| 283 | repoId: repo.id, | |
| 284 | state: { reason: "feedback" }, | |
| 285 | answer, | |
| 286 | p: 1, | |
| 287 | by: "rule", | |
| 288 | ms: 0, | |
| 289 | }); | |
| 290 | const parks = () => findFaults(app.db, Date.now()).find((x) => x.key === `parks:${repo.id}`); | |
| 291 | for (let n = 0; n < 30; n++) guard("skipped"); | |
| 292 | for (let n = 0; n < 19; n++) guard("capped"); | |
| 293 | expect(parks()).toBeUndefined(); | |
| 294 | guard("capped"); | |
| 295 | expect(parks()?.title).toContain("parked 20 runs"); | |
| 296 | expect(parks()?.evidence.join("\n")).toContain("30 skips"); | |
| 297 | }); | |
| 277 | 298 | }); |
| 278 | 299 | |
| 279 | 300 | describe("the log", () => { |
new above
offline