@monster-orange/den 0.4.9 → 0.4.10
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 +25 -0
- package/README.md +7 -7
- package/docs/distributions.md +7 -3
- package/package.json +1 -1
- package/src/shared/agents/records.ts +5 -0
- package/src/shared/agents/rotation.ts +9 -0
- package/src/shared/git/gh-pr-state.ts +19 -7
- package/src/shared/usage/report.ts +33 -14
- package/src/ui/cli/usage-command.ts +31 -14
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,31 @@ All notable changes to Den are documented here. Each release lists the
|
|
|
4
4
|
config and manifest changes a distribution must make to move to it under
|
|
5
5
|
**Migrations**.
|
|
6
6
|
|
|
7
|
+
## [0.4.10] - 2026-11-07
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **A confirmed merge stays confirmed.** Once `gh pr view` says an agent's
|
|
12
|
+
pull request is MERGED, its record keeps that (`prMergeConfirmed`), and
|
|
13
|
+
`usage` doesn't ask GitHub about it again. A later run that can't check
|
|
14
|
+
no longer drops it. A merged flag from before 0.4.10, with no
|
|
15
|
+
confirmation, is checked again.
|
|
16
|
+
- **`usage agents` counts only real asks.** "Asked GitHub for the state of
|
|
17
|
+
N pull requests" counts the times gh ran. When gh is not installed, it
|
|
18
|
+
says "Couldn't check N merges: gh is not installed." instead.
|
|
19
|
+
- **The PR column** shows the confirmed pull request, with "(merged)", when
|
|
20
|
+
the agent merged one it didn't open.
|
|
21
|
+
- **A bare `gh pr merge`** (the branch's own pull request) is confirmed by
|
|
22
|
+
the number gh gives: the check is now `gh pr view --json number,state`.
|
|
23
|
+
With no number, it is not marked; an empty ref is never a key.
|
|
24
|
+
- **Dropping a mark drops its rotation.** When gh no longer confirms a
|
|
25
|
+
merged flag, a "PR merged" rotation it made due is cleared too. A
|
|
26
|
+
rotation due after compactions is left alone.
|
|
27
|
+
|
|
28
|
+
### Migrations
|
|
29
|
+
|
|
30
|
+
None.
|
|
31
|
+
|
|
7
32
|
## [0.4.9] - 2026-11-07
|
|
8
33
|
|
|
9
34
|
A session ends when its pull request merges, or after 3 compactions. The
|
package/README.md
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
12
|
<picture>
|
|
13
|
-
<source media="(prefers-color-scheme: dark)" srcset="docs/images/0.4.
|
|
14
|
-
<img src="docs/images/0.4.
|
|
13
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/images/0.4.10/hero-dark.png">
|
|
14
|
+
<img src="docs/images/0.4.10/hero-light.png" width="100%" alt="Den's Graph page: the demo workspace's services grouped by application, all healthy, with the status chip and the left rail">
|
|
15
15
|
</picture>
|
|
16
16
|
|
|
17
17
|
## Set up by your agent
|
|
@@ -38,7 +38,7 @@ It doesn't replace your tools. Your repos, Docker, your test runners, your secre
|
|
|
38
38
|
yours. Den connects them and turns on good practice from the start.
|
|
39
39
|
|
|
40
40
|
<p align="center">
|
|
41
|
-
<img src="docs/images/0.4.
|
|
41
|
+
<img src="docs/images/0.4.10/up.gif" width="100%" alt="make up in the Graph page: every stack down, then demo-api, demo-web and demo-notes come up in dependency order until the status chip reads All 3 healthy">
|
|
42
42
|
</p>
|
|
43
43
|
|
|
44
44
|
## How it helps
|
|
@@ -68,10 +68,10 @@ yours. Den connects them and turns on good practice from the start.
|
|
|
68
68
|
lives in your own repo.
|
|
69
69
|
|
|
70
70
|
<p align="center">
|
|
71
|
-
<img src="docs/images/0.4.
|
|
72
|
-
<img src="docs/images/0.4.
|
|
73
|
-
<img src="docs/images/0.4.
|
|
74
|
-
<img src="docs/images/0.4.
|
|
71
|
+
<img src="docs/images/0.4.10/agents.png" width="49%" alt="The Agents tab: the demo workspace's sessions on the left, and one open in a terminal pane on the right, running demo-api's test and then the service">
|
|
72
|
+
<img src="docs/images/0.4.10/workspaces.png" width="49%" alt="The Workspaces page: the demo workspace's repos with their health, mode and Start, Stop, Restart and Logs actions">
|
|
73
|
+
<img src="docs/images/0.4.10/status-drawer.png" width="49%" alt="The status drawer: nothing needs attention, and every service in the demo workspace, by application, with its latency">
|
|
74
|
+
<img src="docs/images/0.4.10/edge.png" width="49%" alt="The Edge page: the edge container up, its ports and TLS, and every route it serves, Den's own and each stack's, with its upstream">
|
|
75
75
|
</p>
|
|
76
76
|
|
|
77
77
|
## Get started
|
package/docs/distributions.md
CHANGED
|
@@ -2482,7 +2482,7 @@ report. It works on den-claude for personas with Den's hooks on.
|
|
|
2482
2482
|
A merge is never taken from text. The agent must have run `gh pr merge`
|
|
2483
2483
|
as a command (after any `cd` or `VAR=value` prefix; not `--help`, not
|
|
2484
2484
|
`echo gh pr merge`), and it must have exited 0. Then `usage` asks
|
|
2485
|
-
`gh pr view <ref> --json state` in the agent's folder, and only
|
|
2485
|
+
`gh pr view <ref> --json number,state` in the agent's folder, and only
|
|
2486
2486
|
`MERGED` counts. With no gh, or on any error, it is not marked. That
|
|
2487
2487
|
query is the one thing `usage` asks outside this machine; it says so
|
|
2488
2488
|
when it does.
|
|
@@ -2762,9 +2762,13 @@ and its wall-clock time. It matches transcripts to agents by the
|
|
|
2762
2762
|
conversation id Den saved at start. It prints the share of agents with
|
|
2763
2763
|
both. Transcripts never leave the machine, and their text is never kept:
|
|
2764
2764
|
only counts. "Whether it merged" is checked: when an agent ran
|
|
2765
|
-
`gh pr merge`, `usage` asks `gh pr view <ref> --json state` to confirm it.
|
|
2765
|
+
`gh pr merge`, `usage` asks `gh pr view <ref> --json number,state` to confirm it.
|
|
2766
2766
|
That query, the pull request's number or URL, is the only thing it asks
|
|
2767
|
-
outside, and the output says when it asked
|
|
2767
|
+
outside, and the output says when it asked (or that it couldn't, because
|
|
2768
|
+
gh is not installed). A merge gh confirmed is kept in the record
|
|
2769
|
+
(`prMergeConfirmed`, the pull request's ref or, for a bare `gh pr merge`,
|
|
2770
|
+
the number gh gave) and not asked about again. A mark gh no longer
|
|
2771
|
+
confirms is dropped, with the "PR merged" rotation it made due.
|
|
2768
2772
|
|
|
2769
2773
|
`<cli> usage report [--days 7] [--json]` writes a weekly summary to
|
|
2770
2774
|
`<state dir>/usage/<date>.json` and `.md`, compared with the last one:
|
package/package.json
CHANGED
|
@@ -80,6 +80,11 @@ export interface AgentOutcome {
|
|
|
80
80
|
prOpened?: string;
|
|
81
81
|
/** That pull request merged in the run. */
|
|
82
82
|
prMerged?: boolean;
|
|
83
|
+
/**
|
|
84
|
+
* The pull request gh said is MERGED (0.4.10): a merge confirmed once
|
|
85
|
+
* stays, and is not asked again.
|
|
86
|
+
*/
|
|
87
|
+
prMergeConfirmed?: string;
|
|
83
88
|
/** The last test run it made passed (true) or failed (false). */
|
|
84
89
|
testsGreen?: boolean;
|
|
85
90
|
/** From its start to its last activity, in milliseconds. */
|
|
@@ -235,6 +235,15 @@ export function markDue(
|
|
|
235
235
|
return true;
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
/** Clear a rotation due for `reason` (a merge mark gh no longer confirms). */
|
|
239
|
+
export function clearDue(filesDir: string, reason: RotateReason): boolean {
|
|
240
|
+
const s = readRotation(filesDir);
|
|
241
|
+
if (!s || s.due !== reason) return false;
|
|
242
|
+
const { due: _d, dueAt: _a, ...rest } = s;
|
|
243
|
+
writeRotation(filesDir, rest);
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
|
|
238
247
|
/** Whether the state says a rotation is due, for this conversation. */
|
|
239
248
|
export function dueFor(
|
|
240
249
|
s: RotationState | null,
|
|
@@ -6,27 +6,39 @@
|
|
|
6
6
|
import { execFile } from "child_process";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* `gh pr view [<ref>] [-R <repo>] --json state
|
|
10
|
-
*
|
|
9
|
+
* `gh pr view [<ref>] [-R <repo>] --json number,state`: the state and the
|
|
10
|
+
* number (a bare ref is the branch's PR, and the number names it). Rejects
|
|
11
|
+
* on any error, or an answer that isn't that JSON. Only the ref and repo
|
|
12
|
+
* go out, to GitHub, by gh.
|
|
11
13
|
*/
|
|
12
14
|
export function ghPrState(env: NodeJS.ProcessEnv) {
|
|
13
15
|
return (ref: string, o: { repo?: string; cwd: string }) =>
|
|
14
|
-
new Promise<string
|
|
16
|
+
new Promise<{ state: string; number?: number }>((res, rej) => {
|
|
15
17
|
const args = [
|
|
16
18
|
"pr",
|
|
17
19
|
"view",
|
|
18
20
|
...(ref ? [ref] : []),
|
|
19
21
|
...(o.repo ? ["-R", o.repo] : []),
|
|
20
22
|
"--json",
|
|
21
|
-
"state",
|
|
22
|
-
"-q",
|
|
23
|
-
".state",
|
|
23
|
+
"number,state",
|
|
24
24
|
];
|
|
25
25
|
execFile(
|
|
26
26
|
"gh",
|
|
27
27
|
args,
|
|
28
28
|
{ cwd: o.cwd, env, timeout: 15_000, maxBuffer: 64 * 1024 },
|
|
29
|
-
(e, stdout) =>
|
|
29
|
+
(e, stdout) => {
|
|
30
|
+
if (e) return rej(e);
|
|
31
|
+
try {
|
|
32
|
+
const o = JSON.parse(String(stdout)) as Record<string, unknown>;
|
|
33
|
+
if (typeof o.state !== "string") throw new Error("no state");
|
|
34
|
+
res({
|
|
35
|
+
state: o.state,
|
|
36
|
+
...(typeof o.number === "number" ? { number: o.number } : {}),
|
|
37
|
+
});
|
|
38
|
+
} catch (err) {
|
|
39
|
+
rej(err);
|
|
40
|
+
}
|
|
41
|
+
},
|
|
30
42
|
);
|
|
31
43
|
});
|
|
32
44
|
}
|
|
@@ -102,6 +102,19 @@ export function outcomeOf(
|
|
|
102
102
|
return out;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
/**
|
|
106
|
+
* What a pull request check answers: its state ("MERGED"), and its number
|
|
107
|
+
* when gh gave one (a bare `gh pr merge` is the branch's PR: the number is
|
|
108
|
+
* how it is kept).
|
|
109
|
+
*/
|
|
110
|
+
export type PrStateAnswer = string | { state: string; number?: number };
|
|
111
|
+
|
|
112
|
+
/** Checks one pull request's state (gh pr view), in the agent's folder. */
|
|
113
|
+
export type PrStateCheck = (
|
|
114
|
+
ref: string,
|
|
115
|
+
o: { repo?: string; cwd: string },
|
|
116
|
+
) => Promise<PrStateAnswer | undefined>;
|
|
117
|
+
|
|
105
118
|
/**
|
|
106
119
|
* Whether one of the transcripts' `gh pr merge` claims is a pull request
|
|
107
120
|
* gh says is MERGED. A bare `gh pr merge` (the branch's PR) is checked by
|
|
@@ -111,12 +124,9 @@ async function mergeVerified(
|
|
|
111
124
|
rec: AgentRecord,
|
|
112
125
|
stats: readonly TranscriptStats[],
|
|
113
126
|
outcome: AgentOutcome,
|
|
114
|
-
prState?:
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
) => Promise<string | undefined>,
|
|
118
|
-
): Promise<boolean> {
|
|
119
|
-
if (!prState) return false;
|
|
127
|
+
prState?: PrStateCheck,
|
|
128
|
+
): Promise<string | undefined> {
|
|
129
|
+
if (!prState) return undefined;
|
|
120
130
|
const seen = new Set<string>();
|
|
121
131
|
for (const c of stats.flatMap((s) => s.prMerges)) {
|
|
122
132
|
const ref = c.ref || outcome.prOpened || "";
|
|
@@ -128,12 +138,17 @@ async function mergeVerified(
|
|
|
128
138
|
...(c.repo ? { repo: c.repo } : {}),
|
|
129
139
|
cwd: rec.cwd,
|
|
130
140
|
});
|
|
131
|
-
|
|
141
|
+
const state = typeof st === "string" ? st : st?.state;
|
|
142
|
+
if (state?.trim().toUpperCase() !== "MERGED") continue;
|
|
143
|
+
// Kept by its ref, or by the number gh gave; never by "".
|
|
144
|
+
const n = typeof st === "object" ? st.number : undefined;
|
|
145
|
+
const key = ref || (Number.isInteger(n) && n! > 0 ? String(n) : "");
|
|
146
|
+
if (key) return key;
|
|
132
147
|
} catch {
|
|
133
148
|
// Not checked: not marked.
|
|
134
149
|
}
|
|
135
150
|
}
|
|
136
|
-
return
|
|
151
|
+
return undefined;
|
|
137
152
|
}
|
|
138
153
|
|
|
139
154
|
/**
|
|
@@ -146,14 +161,11 @@ export async function agentUsage(
|
|
|
146
161
|
env?: NodeJS.ProcessEnv;
|
|
147
162
|
source?: (runtime: string) => UsageSource | undefined;
|
|
148
163
|
/**
|
|
149
|
-
* A pull request's state (`gh pr view <ref> --json state`), run in the
|
|
164
|
+
* A pull request's state (`gh pr view <ref> --json number,state`), run in the
|
|
150
165
|
* agent's folder: "MERGED" marks the outcome merged. Without it, or on
|
|
151
166
|
* any error, nothing is marked: a merge is never taken from text.
|
|
152
167
|
*/
|
|
153
|
-
prState?:
|
|
154
|
-
ref: string,
|
|
155
|
-
o: { repo?: string; cwd: string },
|
|
156
|
-
) => Promise<string | undefined>;
|
|
168
|
+
prState?: PrStateCheck;
|
|
157
169
|
} = {},
|
|
158
170
|
): Promise<AgentUsageRow[]> {
|
|
159
171
|
const pick = opts.source ?? sourceFor;
|
|
@@ -198,8 +210,15 @@ export async function agentUsage(
|
|
|
198
210
|
}
|
|
199
211
|
const usage = sumUsage(stats);
|
|
200
212
|
const outcome = outcomeOf(rec, stats);
|
|
201
|
-
|
|
213
|
+
// Confirmed once, kept: GitHub is not asked again.
|
|
214
|
+
const confirmed =
|
|
215
|
+
rec.outcome?.prMerged && rec.outcome.prMergeConfirmed
|
|
216
|
+
? rec.outcome.prMergeConfirmed
|
|
217
|
+
: await mergeVerified(rec, stats, outcome, opts.prState);
|
|
218
|
+
if (confirmed) {
|
|
202
219
|
outcome.prMerged = true;
|
|
220
|
+
outcome.prMergeConfirmed = confirmed;
|
|
221
|
+
}
|
|
203
222
|
rows.push({
|
|
204
223
|
...base,
|
|
205
224
|
transcripts: files.length,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* runtimes' own transcripts on this machine (usage sources, plugin kind
|
|
4
4
|
* `usage`). None of it is sent anywhere: it reads local files and writes
|
|
5
5
|
* local files. The one question it asks outside: when an agent ran
|
|
6
|
-
* `gh pr merge`, `gh pr view <ref> --json state` confirms the merge
|
|
6
|
+
* `gh pr merge`, `gh pr view <ref> --json number,state` confirms the merge
|
|
7
7
|
* (gh-pr-state.ts) before it counts.
|
|
8
8
|
*
|
|
9
9
|
* agents [-w <ws>] [--json] each agent's tokens and outcome (PR opened
|
|
@@ -22,8 +22,9 @@ import {
|
|
|
22
22
|
readRecord,
|
|
23
23
|
writeRecord,
|
|
24
24
|
} from "../../shared/agents/records.js";
|
|
25
|
-
import { markDue } from "../../shared/agents/rotation.js";
|
|
25
|
+
import { clearDue, markDue } from "../../shared/agents/rotation.js";
|
|
26
26
|
import { ghPrState } from "../../shared/git/gh-pr-state.js";
|
|
27
|
+
import type { PrStateCheck } from "../../shared/usage/report.js";
|
|
27
28
|
import { readAgentsConfig } from "../../shared/den-config.js";
|
|
28
29
|
import { getStateDir } from "../../shared/paths.js";
|
|
29
30
|
import { product } from "../../shared/product.js";
|
|
@@ -77,24 +78,31 @@ export interface UsageDeps {
|
|
|
77
78
|
rotateAfter?: () => { afterCompactions: number } | undefined;
|
|
78
79
|
/**
|
|
79
80
|
* A pull request's state, to confirm a `gh pr merge` the agent ran
|
|
80
|
-
* (default: `gh pr view <ref> --json state` in the agent's folder).
|
|
81
|
+
* (default: `gh pr view <ref> --json number,state` in the agent's folder).
|
|
81
82
|
*/
|
|
82
|
-
prState?:
|
|
83
|
-
ref: string,
|
|
84
|
-
o: { repo?: string; cwd: string },
|
|
85
|
-
) => Promise<string | undefined>;
|
|
83
|
+
prState?: PrStateCheck;
|
|
86
84
|
}
|
|
87
85
|
|
|
88
|
-
/**
|
|
86
|
+
/**
|
|
87
|
+
* The check, counting how often gh ran (`asked`) and how often it could not
|
|
88
|
+
* run because gh is not installed (`missing`); the summary says both.
|
|
89
|
+
*/
|
|
89
90
|
function counted(
|
|
90
91
|
f: NonNullable<UsageDeps["prState"]>,
|
|
91
|
-
): NonNullable<UsageDeps["prState"]> & { asked: number } {
|
|
92
|
+
): NonNullable<UsageDeps["prState"]> & { asked: number; missing: number } {
|
|
92
93
|
const g = Object.assign(
|
|
93
|
-
(ref: string, o: { repo?: string; cwd: string }) => {
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
async (ref: string, o: { repo?: string; cwd: string }) => {
|
|
95
|
+
try {
|
|
96
|
+
const st = await f(ref, o);
|
|
97
|
+
g.asked++;
|
|
98
|
+
return st;
|
|
99
|
+
} catch (e) {
|
|
100
|
+
if ((e as NodeJS.ErrnoException)?.code === "ENOENT") g.missing++;
|
|
101
|
+
else g.asked++;
|
|
102
|
+
throw e;
|
|
103
|
+
}
|
|
96
104
|
},
|
|
97
|
-
{ asked: 0 },
|
|
105
|
+
{ asked: 0, missing: 0 },
|
|
98
106
|
);
|
|
99
107
|
return g;
|
|
100
108
|
}
|
|
@@ -144,6 +152,9 @@ function save(stateDir: string, rows: readonly AgentUsageRow[]): void {
|
|
|
144
152
|
});
|
|
145
153
|
if (r.outcome?.prMerged)
|
|
146
154
|
markDue(agentFilesDir(stateDir, rec.id), "pr-merged", rec.resumeToken);
|
|
155
|
+
// A mark gh no longer confirms is dropped, and its rotation with it.
|
|
156
|
+
else if (rec.outcome?.prMerged)
|
|
157
|
+
clearDue(agentFilesDir(stateDir, rec.id), "pr-merged");
|
|
147
158
|
}
|
|
148
159
|
}
|
|
149
160
|
|
|
@@ -211,7 +222,9 @@ async function agents(
|
|
|
211
222
|
: "red",
|
|
212
223
|
r.outcome?.prOpened
|
|
213
224
|
? `${r.outcome.prOpened}${r.outcome.prMerged ? " (merged)" : ""}`
|
|
214
|
-
:
|
|
225
|
+
: r.outcome?.prMergeConfirmed
|
|
226
|
+
? `${r.outcome.prMergeConfirmed} (merged)`
|
|
227
|
+
: "-",
|
|
215
228
|
r.compactions === undefined ? "-" : String(r.compactions),
|
|
216
229
|
String(r.rotations),
|
|
217
230
|
]),
|
|
@@ -226,6 +239,10 @@ async function agents(
|
|
|
226
239
|
d.out(
|
|
227
240
|
`To confirm a merge, it asked GitHub for the state of ${d.prState.asked} pull request${d.prState.asked === 1 ? "" : "s"} (gh pr view).`,
|
|
228
241
|
);
|
|
242
|
+
if (d.prState.missing)
|
|
243
|
+
d.out(
|
|
244
|
+
`Couldn't check ${d.prState.missing} merge${d.prState.missing === 1 ? "" : "s"}: gh is not installed.`,
|
|
245
|
+
);
|
|
229
246
|
return 0;
|
|
230
247
|
}
|
|
231
248
|
|