agent-horde 0.1.5 → 0.1.6
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/README.md +21 -8
- package/docs/REFERENCE.md +55 -16
- package/package.json +1 -1
- package/src/args.ts +18 -3
- package/src/build-info.ts +65 -0
- package/src/collaboration-report.ts +23 -9
- package/src/dashboard-html.ts +11 -3
- package/src/ledger/collaboration.ts +36 -6
- package/src/ledger/facts.ts +52 -5
- package/src/ledger/findings.ts +91 -1
- package/src/ledger/run-signals.ts +17 -2
- package/src/member.ts +15 -3
- package/src/prompt.ts +73 -16
- package/src/run/completions.ts +6 -1
- package/src/run/finalize.ts +6 -7
- package/src/run/launch.ts +1 -1
- package/src/run/repair.ts +15 -7
- package/src/run/schedule.ts +43 -13
- package/src/run/setup.ts +2 -1
- package/src/run/solved.ts +125 -15
- package/src/run/supervise.ts +74 -9
- package/src/run/types.ts +6 -0
- package/src/run-graph.ts +9 -2
- package/src/tool/actions.ts +149 -13
- package/src/tool/collaboration-actions.ts +75 -25
- package/src/tool/params.ts +10 -7
- package/src/tool/register.ts +11 -6
package/README.md
CHANGED
|
@@ -88,7 +88,7 @@ pi install npm:agent-horde
|
|
|
88
88
|
Or from the repository, pinned to a tag:
|
|
89
89
|
|
|
90
90
|
```
|
|
91
|
-
pi install git:github.com/eliyastein/agent-horde@v0.1.
|
|
91
|
+
pi install git:github.com/eliyastein/agent-horde@v0.1.6
|
|
92
92
|
```
|
|
93
93
|
|
|
94
94
|
To hack on it, clone and install in place. pi loads the files from the clone, so edits take effect
|
|
@@ -122,13 +122,23 @@ When you already know how the work splits, use `plan` to fix the partition at la
|
|
|
122
122
|
Each layer becomes a seeded idea owned by one member, recorded in the run's `PLAN.md`, and that
|
|
123
123
|
member works only its layer. Layers are assigned round-robin, so more layers than members gives
|
|
124
124
|
some members two. Members without a layer become verifiers, and an unpartitioned run of three or
|
|
125
|
-
more members makes its last member the verifier. `plan` suits
|
|
126
|
-
work by output type, such as spec, prototype, and generator,
|
|
125
|
+
more members makes its last member the verifier (`--no-verifier` turns that off). `plan` suits
|
|
126
|
+
"analyse each X" tasks; split design work by output type, such as spec, prototype, and generator,
|
|
127
|
+
rather than by theme.
|
|
128
|
+
|
|
129
|
+
A run normally winds down once a validated answer exists and every idea is closed. `--until-stop`
|
|
130
|
+
keeps the whole horde working until you stop it: a validated answer is banked as the current one
|
|
131
|
+
and the proposal slot reopens, and a member whose session finishes gets a fresh session told to
|
|
132
|
+
extend, stress-test, or deepen the work.
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
/agent-horde --dashboard --until-stop 8 find every way an unauthenticated request reaches the database
|
|
136
|
+
```
|
|
127
137
|
|
|
128
138
|
| command | effect |
|
|
129
139
|
|---|---|
|
|
130
140
|
| `/agent-horde status` | the active run and each member |
|
|
131
|
-
| `/agent-horde stop-clean [postmortem]` | members wrap up, then the run finalizes |
|
|
141
|
+
| `/agent-horde stop-clean [postmortem]` | members wrap up, then the run finalizes; this is how an `--until-stop` run ends |
|
|
132
142
|
| `/agent-horde stop-hard` | terminate members now |
|
|
133
143
|
| `/agent-horde pause`, `resume` | suspend and resume member processes |
|
|
134
144
|
| `/agent-horde help` | every flag and target |
|
|
@@ -172,11 +182,13 @@ die, runs the repair phase, freezes acceptance, and writes the report.
|
|
|
172
182
|
- Ideas, findings, facts, and resources are deduplicated on identifiers first, then wording; a
|
|
173
183
|
duplicate returns the existing id and records a corroborator.
|
|
174
184
|
- Every finding names a concern slug; a look-alike of an existing slug is refused.
|
|
175
|
-
- A review records the revision it checked, its scope, a verdict, and
|
|
176
|
-
artifacts need a tested or reproduced review from a
|
|
177
|
-
derived one.
|
|
185
|
+
- A review records the revision it checked, its scope, a verdict, evidence, and any limits
|
|
186
|
+
outside the target's criteria. Runnable artifacts need a tested or reproduced review from a
|
|
187
|
+
non-owner; documents need a sourced or derived one. A reviewer can carry its review forward to
|
|
188
|
+
an unchanged revision instead of redoing it.
|
|
178
189
|
- Solved is two-phase: one member proposes, a different member validates, and the run winds down
|
|
179
|
-
only when every idea it created is closed.
|
|
190
|
+
only when every idea it created is closed. Under `--until-stop` the answer is banked instead
|
|
191
|
+
and the horde keeps going until `stop-clean` or `stop-hard`.
|
|
180
192
|
- Peer messages, reviews, and evidence enter a member's context as data. They cannot authorize
|
|
181
193
|
tools or override the task.
|
|
182
194
|
|
|
@@ -194,6 +206,7 @@ linked to the page that shows it, so a run reads left to right: who claimed what
|
|
|
194
206
|
landed under which ideas, who reviewed whom, and where actions were rejected. It is server-rendered
|
|
195
207
|
SVG/HTML with no scripts. The download icon on every page exports the whole dashboard as a zip of
|
|
196
208
|
static pages that opens from disk, after a warning that it may carry sensitive data from the machine.
|
|
209
|
+
Every page's footer names the agent-horde version and, from a checkout, the commit it runs at.
|
|
197
210
|
|
|
198
211
|
## Benchmark: FinanceBench
|
|
199
212
|
|
package/docs/REFERENCE.md
CHANGED
|
@@ -7,7 +7,7 @@ at one problem beat a single linear one: research, audits, design exploration, R
|
|
|
7
7
|
## Usage
|
|
8
8
|
|
|
9
9
|
```
|
|
10
|
-
/agent-horde [--dashboard[=<port>]] [--no-verifier] <count> <prompt>
|
|
10
|
+
/agent-horde [--dashboard[=<port>]] [--no-verifier] [--until-stop] <count> <prompt>
|
|
11
11
|
/agent-horde plan [--dashboard] <count> <layer; layer; ...>
|
|
12
12
|
/agent-horde status [target]
|
|
13
13
|
/agent-horde stop-hard [target] SIGTERM, SIGKILL after 5s (alias: stop)
|
|
@@ -72,7 +72,9 @@ own when the parent is gone, even after a SIGKILL or OOM kill.
|
|
|
72
72
|
HEAD, never writes to `.horde/`, and stays up after the run until the session ends. A busy port
|
|
73
73
|
refuses the run before any run directory is created. Live pages reload every three seconds
|
|
74
74
|
(`?refresh=0` disables); expandable entries are URL-backed, so they stay open across refreshes.
|
|
75
|
-
Every page has a JSON twin under `/api/<same path>`.
|
|
75
|
+
Every page has a JSON twin under `/api/<same path>`. The footer of every page names the
|
|
76
|
+
agent-horde version from its package.json and, when it runs from a git checkout, the commit it is
|
|
77
|
+
at (`-dirty` when the tree has uncommitted changes), linked to the commit on GitHub.
|
|
76
78
|
|
|
77
79
|
| page | shows |
|
|
78
80
|
|------|-------|
|
|
@@ -154,17 +156,19 @@ from the same actor as an idempotent retry. The parent appends its own lifecycle
|
|
|
154
156
|
| `list`, `findings`, `facts`, `claims`, `resources`, `concerns` | print a ledger |
|
|
155
157
|
| `post` | append a message to BOARD.md |
|
|
156
158
|
| `idea` | add an open checklist item; a near-duplicate returns the existing id |
|
|
157
|
-
| `claim`, `done`, `drop`, `rule-out` | take, finish, release, or close idea #id |
|
|
159
|
+
| `claim`, `done`, `drop`, `rule-out` | take, finish, release, or close idea #id; `done` with a confirmed-negative result line is refused in favour of `rule-out` |
|
|
158
160
|
| `finding` | file under a claimed `idea` with a `concern` and `support`; `importance` and `confidence` are optional; `deltaOf: F#` adds distinct evidence, `supersedes: F#` corrects |
|
|
159
161
|
| `verify` | record your own reproduction of F#id with the support level you reached |
|
|
160
|
-
| `
|
|
162
|
+
| `amend` | rewrite the body prose of your own F#id (`text`) with a `note` saying why; the revision changes, reviews stay pinned to what they checked, earlier verification notes are kept as history of the previous revision, and the legacy `verified=` label and corroborators are dropped |
|
|
163
|
+
| `fact` | record a checkable fact; `deltaOf: T#` extends one; a fact about what an open idea or contested claim is still auditing is recorded as provisional on it |
|
|
161
164
|
| `assert`, `confirm`, `contest`, `retract`, `supersede` | create or move claim C#id |
|
|
162
165
|
| `resource` | claim a shared dataset, harness, corpus, or sandbox by name, with an optional `cleanup` command |
|
|
163
166
|
| `solved`, `confirm-solved`, `contest-solved` | propose, validate, or refute the goal answer |
|
|
164
167
|
| `message`, `inbox`, `ack`, `respond`, `resolve`, `defer` | targeted threads: question, blocker, review-request, challenge, handoff, decision |
|
|
165
168
|
| `attach` | add supporting, contradicting, or extending evidence to a target |
|
|
166
|
-
| `review` | record a revision-pinned check with explicit `checked` and `unchecked` scope, a verdict, and evidence refs; `evidenceText` attaches and reviews in one call |
|
|
167
|
-
| `
|
|
169
|
+
| `review` | record a revision-pinned check with explicit `checked` and `unchecked` (in-criteria gaps) scope, `limits` outside the criteria, a verdict, and evidence refs; `evidenceText` attaches and reviews in one call |
|
|
170
|
+
| `reattest` | carry your earlier review of F#/A# forward to its current revision, with evidence that what you checked is unchanged; refused across a changed artifact criteria or kind, a changed finding claim line, or a revision no `amend` started from |
|
|
171
|
+
| `artifact` | publish a file under `artifacts/` with `artifactKind`, `criteria`, and revision-pinned `dependsOn`; republishing a revision other artifacts pin needs `text` naming the change |
|
|
168
172
|
| `resource-state` | publish building, ready, broken, or retired state with a lease, ports, and health evidence |
|
|
169
173
|
| `inspect`, `collaboration` | one record in full (`F#`, `I#`, `R#`, `A#`, `E#`, `V#`, `Q#`, `answer`), or a paged index |
|
|
170
174
|
|
|
@@ -191,18 +195,40 @@ from the same actor as an idempotent retry. The parent appends its own lifecycle
|
|
|
191
195
|
validated and every idea this run created or its members hold is done or ruled out. With no
|
|
192
196
|
other member running, or no validation within `PI_HORDE_SOLVED_CONFIRM_MS`, an uncontested
|
|
193
197
|
proposal commits as `solved-unvalidated`. Findings no other member reproduced hold wind-down
|
|
194
|
-
while other members run, unless `PI_HORDE_ALLOW_UNVERIFIED=1`.
|
|
198
|
+
while other members run, unless `PI_HORDE_ALLOW_UNVERIFIED=1`. A proposal pins the revision of
|
|
199
|
+
every finding it cites. An answer that cites a superseded F# or quotes a provisional T# is
|
|
200
|
+
refused at `solved` and at `confirm-solved`; a pending proposal whose cited finding is
|
|
201
|
+
superseded or amended later is withdrawn (`solved-withdrawn`) for re-proposal. A contest that names idea ids reopens the proposal under a new revision
|
|
202
|
+
(`solved-reopened`) once every named idea is done or ruled out, with the citations it pinned;
|
|
203
|
+
if a cited finding changed or a quoted fact is still provisional by then, it is withdrawn instead. Repair serves a request about
|
|
204
|
+
the pending answer (`target=answer`) with the blockers.
|
|
195
205
|
- **Reviews are revision-pinned.** A runnable artifact needs a full-scope `tested` or
|
|
196
206
|
`reproduced` review from a non-owner. A `document` artifact (.md, .txt, .rst, .adoc, .pdf)
|
|
197
|
-
accepts a full-scope `sourced` or `derived` review against its criteria.
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
207
|
+
accepts a full-scope `sourced` or `derived` review against its criteria. `unchecked` names gaps
|
|
208
|
+
inside the target's stated claim or criteria and keeps acceptance open; `limits` names
|
|
209
|
+
disclosures outside the criteria and is recorded on the acceptance (`Accepted with limits` in
|
|
210
|
+
ACCEPTANCE.md and COLLABORATION.md), never as a blocker. A `partially-supports` verdict refutes
|
|
211
|
+
nothing and opens no challenge: the acceptance names the review and what it left unchecked, and
|
|
212
|
+
another member's full-scope review settles it. Only `refutes` opens a challenge. Changing
|
|
213
|
+
criteria or dependencies needs changed bytes and a fresh review; republishing a revision that
|
|
214
|
+
other artifacts pin needs `text` naming the change, and the owner of each dependent is handed a
|
|
215
|
+
repin request when a pinned dependency is superseded or re-hashed. `reattest` carries a
|
|
216
|
+
reviewer's earlier review forward to the current revision with evidence that the checked
|
|
217
|
+
behaviour is unchanged; the parent prefers prior reviewers when assigning a re-review. Open
|
|
218
|
+
blockers and challenges block acceptance; `defer` keeps the debt visible, the recipient can
|
|
219
|
+
still `respond` to a deferred request, and the requester then resolves it. A challenger can
|
|
220
|
+
withdraw its own blocking challenge on an F# or A# before any response by recording a
|
|
221
|
+
supporting review of the current revision and resolving with evidence on it; limits that
|
|
222
|
+
review discloses remain acceptance debt. Acceptance names an open blocker as `blocked by Q#`.
|
|
223
|
+
- **Verifier.** The verifier's idea claims are refused while other members' findings await its
|
|
224
|
+
review at their current revision; the pending F# list follows every review, reattest and board
|
|
225
|
+
post. A verifier whose session closes with critical or high-importance findings unreviewed is
|
|
226
|
+
reset once more into a fresh session that owes them, within `PI_HORDE_RESET_CAP`. SUMMARY.md
|
|
227
|
+
derives its coverage sentence from the review records and names the unreviewed F#.
|
|
228
|
+
- **Reproductions.** A finding with an independent corroborator or a full-scope review is
|
|
229
|
+
settled: a claim whose text overlaps it is told to bring a new vector as `deltaOf`, and a
|
|
230
|
+
third duplicate report is dropped. A finding that cites a runnable under `artifacts/` no R# or
|
|
231
|
+
A# covers is told to register it.
|
|
206
232
|
- **Inbox.** Targeted messages are inserted at turn boundaries, for at most
|
|
207
233
|
`PI_HORDE_INBOX_TURNS` extra turns per session. A recipient answers with `respond`; `ack` is an
|
|
208
234
|
optional first step that signals intent. Delivery is a context insertion, not
|
|
@@ -226,6 +252,17 @@ highest-importance unchecked assertions first with `horde review`, contest or su
|
|
|
226
252
|
not reproduce, and claim ideas only when nothing is left to review. `--no-verifier` turns this
|
|
227
253
|
off.
|
|
228
254
|
|
|
255
|
+
`--until-stop` keeps the whole horde working until you stop it. A validated solve does not wind
|
|
256
|
+
the run down: the answer is banked in ANSWER.md (and under `answers/`) as round N and the proposal
|
|
257
|
+
slot reopens, so a materially better answer can be proposed later. A member whose session finishes
|
|
258
|
+
is given a fresh session that reads the board, findings, and answer and adds ideas that extend,
|
|
259
|
+
stress-test, or deepen the work; these continuations count against no reset cap and are not
|
|
260
|
+
resets in the report. A session that finished within `PI_HORDE_CONTINUE_COOLDOWN_MS` (default
|
|
261
|
+
60000) of starting waits out the remainder first, so a member that finds nothing to do cannot spin.
|
|
262
|
+
Crash and stall resets apply as usual. `stop-clean` and `stop-hard` end the run as usual and
|
|
263
|
+
`stop-clean` produces the report, deliverable, and postmortem; the last banked answer stands unless
|
|
264
|
+
a later proposal was validated too.
|
|
265
|
+
|
|
229
266
|
## Finding metadata
|
|
230
267
|
|
|
231
268
|
`importance` (critical, high, medium, low, info), `confidence` (high, medium, low), and
|
|
@@ -253,6 +290,7 @@ carries a one-line diagnosis and, when generated, the post-mortem text.
|
|
|
253
290
|
| variable | default | effect |
|
|
254
291
|
|----------|---------|--------|
|
|
255
292
|
| `PI_HORDE_DASHBOARD_PORT` | 7433 | dashboard port when `--dashboard` names none |
|
|
293
|
+
| `PI_HORDE_CONTINUE_COOLDOWN_MS` | 60000 | `--until-stop`: a finished session younger than this waits out the remainder before its fresh session starts |
|
|
256
294
|
| `PI_HORDE_MAX_MINUTES` | off | clean stop at a wall-clock deadline |
|
|
257
295
|
| `PI_HORDE_CANARY_MS` | 60000 | time for member-1 to produce output before a retry |
|
|
258
296
|
| `PI_HORDE_STALL_MS` | 1800000 | reset a member with no ledger write for this long; `0` disables |
|
|
@@ -295,6 +333,7 @@ watchdog.ts child exits when its parent pid is gone
|
|
|
295
333
|
widget.ts TUI widget above the editor
|
|
296
334
|
citations.ts R#/F#/T# and file-path citation checks for findings and generated artifacts
|
|
297
335
|
dashboard.ts localhost HTTP server; dashboard-data.ts loads pages, dashboard-html.ts renders
|
|
336
|
+
build-info.ts the running package's version and checkout commit, for the dashboard footer
|
|
298
337
|
collaboration-report.ts acceptance checks, frozen snapshots, collaboration metrics
|
|
299
338
|
run-graph.ts events.jsonl projected into lanes, phases, nodes, and edges with dashboard links
|
|
300
339
|
run-graph-svg.ts the swimlane SVG and its legend; run-graph-mermaid.ts the Mermaid export and GRAPH.md
|
package/package.json
CHANGED
package/src/args.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const MAX_MEMBERS = 12;
|
|
2
2
|
|
|
3
3
|
export const USAGE = [
|
|
4
|
-
"usage: /agent-horde [--dashboard[=<port>]] [--no-verifier] <count> <prompt>",
|
|
4
|
+
"usage: /agent-horde [--dashboard[=<port>]] [--no-verifier] [--until-stop] <count> <prompt>",
|
|
5
5
|
" spawn <count> members with the same prompt",
|
|
6
6
|
" /agent-horde status [target] show the active run",
|
|
7
7
|
" /agent-horde stop-hard [target] terminate members now (SIGTERM, then SIGKILL)",
|
|
@@ -20,6 +20,9 @@ export const USAGE = [
|
|
|
20
20
|
"PI_HORDE_DASHBOARD_PORT); --dashboard=<port> picks the port.",
|
|
21
21
|
"--no-verifier skips the default verifier role that runs of 3 or more members assign",
|
|
22
22
|
"to their last member.",
|
|
23
|
+
"--until-stop keeps the whole horde working until you run stop-clean or stop-hard: a",
|
|
24
|
+
"validated solve is banked as the current answer and a member that finishes gets a fresh",
|
|
25
|
+
"session told to add new ideas.",
|
|
23
26
|
].join("\n");
|
|
24
27
|
|
|
25
28
|
/** Which members a control command applies to. */
|
|
@@ -49,6 +52,8 @@ type HordeArgs =
|
|
|
49
52
|
dashboard?: DashboardOption | undefined;
|
|
50
53
|
/** Present (true) only when `--no-verifier` was given. */
|
|
51
54
|
noVerifier?: boolean | undefined;
|
|
55
|
+
/** Present (true) only when `--until-stop` was given. */
|
|
56
|
+
untilStop?: boolean | undefined;
|
|
52
57
|
};
|
|
53
58
|
|
|
54
59
|
export type HordeRunArgs = Extract<HordeArgs, { kind: "run" }>;
|
|
@@ -102,6 +107,7 @@ function parseStopRest(
|
|
|
102
107
|
|
|
103
108
|
const DASHBOARD_FLAG = /^--dashboard(?:=(\S*))?(?=\s|$)/;
|
|
104
109
|
const NO_VERIFIER_FLAG = /^--no-verifier(?=\s|$)/;
|
|
110
|
+
const UNTIL_STOP_FLAG = /^--until-stop(?=\s|$)/;
|
|
105
111
|
const DASHBOARD_ERROR = "invalid --dashboard; use --dashboard or --dashboard=<port 1-65535>";
|
|
106
112
|
|
|
107
113
|
/** Launch flags found at one position of the command; `rest` is the remaining text. */
|
|
@@ -109,6 +115,7 @@ interface LaunchFlags {
|
|
|
109
115
|
rest: string;
|
|
110
116
|
dashboard?: DashboardOption | undefined;
|
|
111
117
|
noVerifier?: boolean | undefined;
|
|
118
|
+
untilStop?: boolean | undefined;
|
|
112
119
|
error?: string | undefined;
|
|
113
120
|
}
|
|
114
121
|
|
|
@@ -120,7 +127,7 @@ function parseDashboardFlag(port: string | undefined): DashboardOption | { error
|
|
|
120
127
|
return { port: value };
|
|
121
128
|
}
|
|
122
129
|
|
|
123
|
-
/** Strip leading `--dashboard[=<port>]
|
|
130
|
+
/** Strip leading `--dashboard[=<port>]`, `--no-verifier`, and `--until-stop` tokens, in any order. */
|
|
124
131
|
function takeLaunchFlags(text: string): LaunchFlags {
|
|
125
132
|
const flags: LaunchFlags = { rest: text.trim() };
|
|
126
133
|
for (;;) {
|
|
@@ -138,6 +145,12 @@ function takeLaunchFlags(text: string): LaunchFlags {
|
|
|
138
145
|
flags.rest = flags.rest.slice(noVerifier[0].length).trim();
|
|
139
146
|
continue;
|
|
140
147
|
}
|
|
148
|
+
const untilStop = UNTIL_STOP_FLAG.exec(flags.rest);
|
|
149
|
+
if (untilStop) {
|
|
150
|
+
flags.untilStop = true;
|
|
151
|
+
flags.rest = flags.rest.slice(untilStop[0].length).trim();
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
141
154
|
return flags;
|
|
142
155
|
}
|
|
143
156
|
}
|
|
@@ -148,6 +161,7 @@ function mergeLaunchFlags(leading: LaunchFlags, trailing: LaunchFlags): LaunchFl
|
|
|
148
161
|
rest: trailing.rest,
|
|
149
162
|
dashboard: leading.dashboard ?? trailing.dashboard,
|
|
150
163
|
noVerifier: leading.noVerifier === true || trailing.noVerifier === true ? true : undefined,
|
|
164
|
+
untilStop: leading.untilStop === true || trailing.untilStop === true ? true : undefined,
|
|
151
165
|
error: leading.error ?? trailing.error,
|
|
152
166
|
};
|
|
153
167
|
}
|
|
@@ -173,6 +187,7 @@ function runArgs(count: number, prompt: string, flags: LaunchFlags, plan?: strin
|
|
|
173
187
|
if (plan) args.plan = plan;
|
|
174
188
|
if (flags.dashboard) args.dashboard = flags.dashboard;
|
|
175
189
|
if (flags.noVerifier) args.noVerifier = true;
|
|
190
|
+
if (flags.untilStop) args.untilStop = true;
|
|
176
191
|
return args;
|
|
177
192
|
}
|
|
178
193
|
|
|
@@ -180,7 +195,7 @@ export function parseHordeArgs(raw: string): HordeArgs {
|
|
|
180
195
|
const leading = takeLaunchFlags(raw);
|
|
181
196
|
if (leading.error) return { kind: "usage", error: leading.error };
|
|
182
197
|
const trimmed = leading.rest;
|
|
183
|
-
const hasFlags = leading.dashboard !== undefined || leading.noVerifier === true;
|
|
198
|
+
const hasFlags = leading.dashboard !== undefined || leading.noVerifier === true || leading.untilStop === true;
|
|
184
199
|
if (trimmed === "") {
|
|
185
200
|
return hasFlags ? { kind: "usage", error: "count is required" } : { kind: "usage" };
|
|
186
201
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
/** What is running: the package version and, from a git checkout, the commit it is at. */
|
|
7
|
+
export interface BuildInfo {
|
|
8
|
+
name: string;
|
|
9
|
+
version: string;
|
|
10
|
+
/** Short hash of HEAD, suffixed `-dirty` when the tree has uncommitted changes; absent outside a checkout. */
|
|
11
|
+
commit?: string | undefined;
|
|
12
|
+
/** The project's web repository, for linking the commit. */
|
|
13
|
+
repository?: string | undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const GIT_TIMEOUT_MS = 2_000;
|
|
17
|
+
|
|
18
|
+
/** The package directory: the parent of `src/`, which is also where `package.json` lives. */
|
|
19
|
+
export function packageRoot(): string {
|
|
20
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function repositoryUrl(raw: unknown): string | undefined {
|
|
24
|
+
const url = typeof raw === "string" ? raw : typeof raw === "object" && raw !== null ? (raw as { url?: unknown }).url : undefined;
|
|
25
|
+
if (typeof url !== "string") return undefined;
|
|
26
|
+
const web = url.replace(/^git\+/, "").replace(/\.git$/, "");
|
|
27
|
+
return web.startsWith("https://") ? web : undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The commit of the checkout rooted at `root`, or undefined when `root` is not itself a checkout (no parent repo is consulted). */
|
|
31
|
+
function describeCommit(root: string): string | undefined {
|
|
32
|
+
if (!fs.existsSync(path.join(root, ".git"))) return undefined;
|
|
33
|
+
try {
|
|
34
|
+
const out = execFileSync("git", ["describe", "--always", "--dirty", "--exclude=*", "--abbrev=7"], {
|
|
35
|
+
cwd: root,
|
|
36
|
+
encoding: "utf8",
|
|
37
|
+
timeout: GIT_TIMEOUT_MS,
|
|
38
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
39
|
+
}).trim();
|
|
40
|
+
return /^[0-9a-f]{7,40}(?:-dirty)?$/.test(out) ? out : undefined;
|
|
41
|
+
} catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Reads the package at `root`; throws when its package.json is missing or malformed. */
|
|
47
|
+
export function readBuildInfo(root: string): BuildInfo {
|
|
48
|
+
const file = path.join(root, "package.json");
|
|
49
|
+
let pkg: { name?: unknown; version?: unknown; repository?: unknown };
|
|
50
|
+
try {
|
|
51
|
+
pkg = JSON.parse(fs.readFileSync(file, "utf8")) as typeof pkg;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
throw new Error(`cannot read ${file}: ${error instanceof Error ? error.message : String(error)}; reinstall agent-horde`);
|
|
54
|
+
}
|
|
55
|
+
if (typeof pkg.name !== "string" || typeof pkg.version !== "string") throw new Error(`${file} has no name or version; reinstall agent-horde`);
|
|
56
|
+
return { name: pkg.name, version: pkg.version, commit: describeCommit(root), repository: repositoryUrl(pkg.repository) };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let cached: BuildInfo | undefined;
|
|
60
|
+
|
|
61
|
+
/** The running package's build, read once per process. */
|
|
62
|
+
export function buildInfo(): BuildInfo {
|
|
63
|
+
cached ??= readBuildInfo(packageRoot());
|
|
64
|
+
return cached;
|
|
65
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { artifactAccepted, blockingMessages, collaborationState, openMessages } from "./ledger/collaboration.js";
|
|
3
|
+
import { artifactAccepted, artifactLimits, blockingMessages, collaborationState, openMessages, partialArtifactReviews } from "./ledger/collaboration.js";
|
|
4
4
|
import type { ArtifactRecord, CollaborationState, PeerMessage } from "./ledger/collaboration.js";
|
|
5
5
|
import { readEvents } from "./ledger/events.js";
|
|
6
6
|
import { findingReviewState, findingRevision, listFindingsForRun } from "./ledger/findings.js";
|
|
@@ -13,6 +13,8 @@ export interface CollaborationReport {
|
|
|
13
13
|
sequence: number;
|
|
14
14
|
state: CollaborationState;
|
|
15
15
|
issues: string[];
|
|
16
|
+
/** Limits the accepting reviews disclosed outside each accepted artifact's criteria. */
|
|
17
|
+
limits: string[];
|
|
16
18
|
acceptedArtifacts: string[];
|
|
17
19
|
acceptedDocuments: string[];
|
|
18
20
|
acceptedRunnableArtifacts: string[];
|
|
@@ -55,9 +57,14 @@ function artifactProblem(hordeDir: string, runId: string, state: CollaborationSt
|
|
|
55
57
|
// "needs a review" sent them after a review that already existed.
|
|
56
58
|
const blocker = blockingMessages(state).find((m) => m.target === artifact.id);
|
|
57
59
|
if (blocker) return `blocked by ${blocker.id} (${blocker.state} blocking ${blocker.kind} from ${blocker.sender})`;
|
|
58
|
-
|
|
60
|
+
if (artifactAccepted(state, artifact)) return undefined;
|
|
61
|
+
// A partial review is coverage debt, not an objection: the acceptance names what it left
|
|
62
|
+
// unchecked so a full review, or one covering that scope, can settle it.
|
|
63
|
+
const partial = partialArtifactReviews(state, artifact).map((r) => `${r.id} (${r.actor}) left unchecked: ${r.unchecked.trim()}`);
|
|
64
|
+
const need = artifact.kind === "document"
|
|
59
65
|
? "needs a full independent review against document criteria"
|
|
60
66
|
: "needs a full independent tested/reproduced review";
|
|
67
|
+
return partial.length ? `${need}; ${partial.join("; ")}` : need;
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
export function loadCollaborationReport(hordeDir: string, runId: string): CollaborationReport {
|
|
@@ -65,10 +72,15 @@ export function loadCollaborationReport(hordeDir: string, runId: string): Collab
|
|
|
65
72
|
const state = collaborationState(hordeDir, runId);
|
|
66
73
|
const issues = blockingMessages(state).map((m) => `${m.id} ${m.target}: ${m.state} blocking ${m.kind}`);
|
|
67
74
|
const acceptedArtifacts: string[] = [];
|
|
75
|
+
const limits: string[] = [];
|
|
68
76
|
for (const artifact of state.artifacts) {
|
|
69
77
|
const problem = artifactProblem(hordeDir, runId, state, artifact);
|
|
70
|
-
if (problem)
|
|
71
|
-
|
|
78
|
+
if (problem) {
|
|
79
|
+
issues.push(`${artifact.id}@${artifact.hash}: ${problem}; exclude from recommended ${artifact.kind === "document" ? "document" : "runnable"} outputs`);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
acceptedArtifacts.push(artifact.id);
|
|
83
|
+
limits.push(...artifactLimits(state, artifact).map((limit) => `${artifact.id}@${artifact.hash}: accepted with limits outside its criteria; ${limit}`));
|
|
72
84
|
}
|
|
73
85
|
for (const f of listFindingsForRun(hordeDir, runId)) {
|
|
74
86
|
if (f.supersededBy !== undefined) continue;
|
|
@@ -83,7 +95,7 @@ export function loadCollaborationReport(hordeDir: string, runId: string): Collab
|
|
|
83
95
|
const delays = messages.flatMap((m) => Object.values(m.deliveries).slice(0, 1).map((time) => Date.parse(time) - Date.parse(m.createdAt)));
|
|
84
96
|
const acceptedDocuments = state.artifacts.filter((a) => a.kind === "document" && acceptedArtifacts.includes(a.id)).map((a) => a.id);
|
|
85
97
|
const acceptedRunnableArtifacts = acceptedArtifacts.filter((id) => !acceptedDocuments.includes(id));
|
|
86
|
-
return { runId, sequence: state.seq, state, issues, acceptedArtifacts, acceptedDocuments, acceptedRunnableArtifacts, metrics: {
|
|
98
|
+
return { runId, sequence: state.seq, state, issues, limits, acceptedArtifacts, acceptedDocuments, acceptedRunnableArtifacts, metrics: {
|
|
87
99
|
events: events.length, rejected: events.filter((e) => e.outcome === "rejected").length,
|
|
88
100
|
unresolved: openMessages(state).length, blocking: blockingMessages(state).length,
|
|
89
101
|
resolvedChallenges: messages.filter((m) => m.kind === "challenge" && m.state === "resolved").length,
|
|
@@ -116,9 +128,10 @@ function collaborationMarkdown(report: CollaborationReport): string {
|
|
|
116
128
|
return [
|
|
117
129
|
`# Collaboration ${report.runId}`, `\nEvent high-water mark: ${report.sequence}. Delivery records context insertion, not comprehension.`,
|
|
118
130
|
"\n## Acceptance issues", ...(report.issues.length ? report.issues.map((i) => `- ${i}`) : ["- No protocol-level issues recorded; task acceptance still requires coverage and review."]),
|
|
131
|
+
"\n## Accepted with limits", ...(report.limits.length ? report.limits.map((l) => `- ${l}`) : ["- none disclosed"]),
|
|
119
132
|
"\n## Questions, challenges and handoffs", ...state.messages.map(messageMarkdown),
|
|
120
|
-
"\n## Artifact revisions", ...state.artifacts.map((a) => `- ${a.id} ${a.path}@${a.hash} owner=${a.owner} kind=${a.kind ?? "runnable"} snapshot=${a.snapshot} accepted=${report.acceptedArtifacts.includes(a.id)}\n Criteria: ${a.criteria}\n Dependencies: ${JSON.stringify(a.dependencies)}`),
|
|
121
|
-
"\n## Reviews", ...state.reviews.map((r) => `- ${r.id} ${r.target}@${r.revision} @${r.actor} ${r.verdict} [${r.support}] evidence=${r.evidence.join(",")}\n Checked: ${r.checked}\n Unchecked: ${r.unchecked || "none declared"}\n ${r.text}`),
|
|
133
|
+
"\n## Artifact revisions", ...state.artifacts.map((a) => `- ${a.id} ${a.path}@${a.hash} owner=${a.owner} kind=${a.kind ?? "runnable"} snapshot=${a.snapshot} accepted=${report.acceptedArtifacts.includes(a.id)}\n Criteria: ${a.criteria}\n Dependencies: ${JSON.stringify(a.dependencies)}${a.changeNote ? `\n Change: ${a.changeNote}` : ""}`),
|
|
134
|
+
"\n## Reviews", ...state.reviews.map((r) => `- ${r.id} ${r.target}@${r.revision} @${r.actor} ${r.verdict} [${r.support}] evidence=${r.evidence.join(",")}${r.reattests ? ` reattests=${r.reattests}` : ""}\n Checked: ${r.checked}\n Unchecked: ${r.unchecked || "none declared"}\n Limits: ${r.limits || "none disclosed"}\n ${r.text}`),
|
|
122
135
|
"\n## Evidence", ...state.evidence.map((e) => `- ${e.id} ${e.target}@${e.revision} @${e.actor} ${e.relation}${e.path ? ` ${e.path}@${e.hash}` : ""}\n ${e.text}`),
|
|
123
136
|
"\n## Resources", ...state.resources.map((r) => `- ${r.target} ${r.state} generation=${r.generation} @${r.owner}/${r.attempt} ports=${r.ports.join(",")} lease=${new Date(r.leaseUntil).toISOString()}\n Health: ${r.health}`),
|
|
124
137
|
"\n## Metrics", ...Object.entries(report.metrics).map(([key, value]) => `- ${key}: ${value}`), "",
|
|
@@ -139,12 +152,13 @@ export function freezeAcceptance(hordeDir: string, runId: string, startIdeaId =
|
|
|
139
152
|
if (check.status === "disputed") issues.push(`${check.target}: unresolved challenge or refuting review`);
|
|
140
153
|
}
|
|
141
154
|
const runDir = path.join(hordeDir, "runs", runId);
|
|
142
|
-
const snapshot = { report, findings, findingReviews, ideas, issues };
|
|
155
|
+
const snapshot = { report, findings, findingReviews, ideas, issues, limits: report.limits };
|
|
143
156
|
const hash = hashText(JSON.stringify(snapshot));
|
|
144
157
|
const status = issues.length ? "incomplete" : "reviewed";
|
|
145
158
|
writeFileAtomic(path.join(runDir, "COLLABORATION.md"), collaborationMarkdown(report));
|
|
146
159
|
writeFileAtomic(path.join(runDir, "ACCEPTANCE.json"), JSON.stringify({ ...snapshot, hash, status }, null, 2));
|
|
147
|
-
|
|
160
|
+
const limitsSection = report.limits.length ? `\n\n## Accepted with limits\n\n${report.limits.map((l) => `- ${l}`).join("\n")}` : "";
|
|
161
|
+
writeFileAtomic(path.join(runDir, "ACCEPTANCE.md"), `# Acceptance ${runId}\n\nStatus: ${status}\nSnapshot: ${hash}\nEvent sequence: ${report.sequence}\n\n${issues.map((i) => `- ${i}`).join("\n")}${limitsSection}\n`);
|
|
148
162
|
const dir = path.join(runDir, "snapshot");
|
|
149
163
|
fs.mkdirSync(dir, { recursive: true });
|
|
150
164
|
for (const name of ["BOARD", "FINDINGS", "IDEAS", "FACTS", "CLAIMS", "RESOURCES", "CONCERNS"]) {
|
package/src/dashboard-html.ts
CHANGED
|
@@ -24,6 +24,7 @@ import type {
|
|
|
24
24
|
TranscriptVm,
|
|
25
25
|
} from "./dashboard-data.js";
|
|
26
26
|
import { RUN_FILES } from "./dashboard-data.js";
|
|
27
|
+
import { buildInfo } from "./build-info.js";
|
|
27
28
|
import { detailKey, formatClock, formatElapsed, formatTokens, oneLine } from "./format.js";
|
|
28
29
|
import { renderRunGraphMermaid } from "./run-graph-mermaid.js";
|
|
29
30
|
import { GRAPH_CSS, graphLegendHtml, renderRunGraphSvg } from "./run-graph-svg.js";
|
|
@@ -54,7 +55,7 @@ const CSS =
|
|
|
54
55
|
"th,td{border:1px solid #000;padding:2px 6px;vertical-align:top;text-align:left}" +
|
|
55
56
|
"th{background:#eee}pre{white-space:pre-wrap;word-break:break-word;margin:0}" +
|
|
56
57
|
"nav a{margin-right:8px}.r{text-align:right}h1,h2{font-size:13px;margin:8px 0 4px}" +
|
|
57
|
-
"details>summary{cursor:pointer}a{color:#000}" +
|
|
58
|
+
"details>summary{cursor:pointer}a{color:#000}footer{margin-top:16px;color:#666}footer a{color:#666}" +
|
|
58
59
|
".export{position:absolute;top:8px;right:8px;line-height:0}" +
|
|
59
60
|
".modal{display:none;position:fixed;inset:0;background:rgba(0,0,0,.7)}" +
|
|
60
61
|
".modal:target{display:flex;align-items:center;justify-content:center}" +
|
|
@@ -161,7 +162,7 @@ export function renderCollaboration(vm: CollaborationVm, ctx: RenderContext): st
|
|
|
161
162
|
anchor(m.id), e(`${m.sender} -> ${m.to}`), e(`${m.state}${m.blocking ? " BLOCKING" : ""} / ${formatElapsed(Date.now() - Date.parse(m.createdAt))}`),
|
|
162
163
|
e(`${m.target}@${m.revision}`), pre(messageThread(m)),
|
|
163
164
|
]));
|
|
164
|
-
const reviews = table(["review", "target revision", "verdict", "scope / evidence"], state.reviews.map((r) => [`${anchor(r.id)} @${e(r.actor)}`, e(`${r.target}@${r.revision}`), e(`${r.verdict} [${r.support}]`), pre(`Checked: ${r.checked}\nUnchecked: ${r.unchecked || "none declared"}\nEvidence: ${r.evidence.join(", ")}\n${r.text}`)]));
|
|
165
|
+
const reviews = table(["review", "target revision", "verdict", "scope / evidence"], state.reviews.map((r) => [`${anchor(r.id)} @${e(r.actor)}`, e(`${r.target}@${r.revision}`), e(`${r.verdict} [${r.support}]`), pre(`Checked: ${r.checked}\nUnchecked: ${r.unchecked || "none declared"}\nLimits: ${r.limits || "none disclosed"}\nEvidence: ${r.evidence.join(", ")}${r.reattests ? `\nReattests: ${r.reattests}` : ""}\n${r.text}`)]));
|
|
165
166
|
const artifacts = table(["artifact", "hash / dependencies", "acceptance"], state.artifacts.map((a) => [`${anchor(a.id)} ${e(`${a.path} @${a.owner} (${a.kind ?? "runnable"})`)}`, pre(`${a.hash}\n${JSON.stringify(a.dependencies)}`), e(report.acceptedArtifacts.includes(a.id) ? "accepted" : "excluded / needs review")]));
|
|
166
167
|
const events = vm.events.map((event) => `<div id="${e(event.eventId)}">${details(`${event.eventId} ${event.at} ${event.actor}/${event.attempt} ${event.action} ${event.outcome}`, pre(JSON.stringify(event, null, 2)), `event:${report.runId}:${event.eventId}`)}</div>`).join("");
|
|
167
168
|
return page(`Collaboration ${report.runId}`, [{ text: "runs", href: "/" }, { text: report.runId, href: runHref(report.runId) }, { text: "collaboration" }],
|
|
@@ -230,13 +231,20 @@ function nativeDetails(body: string): string {
|
|
|
230
231
|
return body.replace(/<details data-expand="[a-f0-9]+">/g, "<details>");
|
|
231
232
|
}
|
|
232
233
|
|
|
234
|
+
/** Which agent-horde produced the page: the package version and, from a checkout, the commit. */
|
|
235
|
+
function footer(): string {
|
|
236
|
+
const { name, version, commit, repository } = buildInfo();
|
|
237
|
+
const at = commit ? ` at ${repository ? link(`${repository}/commit/${commit.replace(/-dirty$/, "")}`, commit) : e(commit)}` : "";
|
|
238
|
+
return `<footer>${e(name)} ${e(version)}${at}</footer>`;
|
|
239
|
+
}
|
|
240
|
+
|
|
233
241
|
export function page(title: string, crumbs: Crumb[], body: string, ctx: RenderContext, head = ""): string {
|
|
234
242
|
body = ctx.export ? nativeDetails(body) : persistentDetails(body, ctx.href);
|
|
235
243
|
const meta = ctx.refresh?.on ? `<meta http-equiv="refresh" content="${ctx.refresh.seconds}">` : head;
|
|
236
244
|
return (
|
|
237
245
|
`<!doctype html><html><head><meta charset="utf-8"><title>${e(title)}</title>${meta}` +
|
|
238
246
|
`<style>${CSS}</style></head><body>${exportControl(ctx)}${nav(ctx)}${crumbLine(crumbs)}${refreshLine(ctx)}` +
|
|
239
|
-
`<h1>${e(title)}</h1>${body}</body></html>`
|
|
247
|
+
`<h1>${e(title)}</h1>${body}${footer()}</body></html>`
|
|
240
248
|
);
|
|
241
249
|
}
|
|
242
250
|
|
|
@@ -46,6 +46,8 @@ export interface ArtifactRecord {
|
|
|
46
46
|
snapshot: string;
|
|
47
47
|
criteria: string;
|
|
48
48
|
dependencies: Array<{ target: string; revision: string }>;
|
|
49
|
+
/** The owner's statement of what a republish changed, required once other artifacts pin it. */
|
|
50
|
+
changeNote?: string;
|
|
49
51
|
/** Projected from the publishing event; absent in hand-built records. */
|
|
50
52
|
publishedAt?: string;
|
|
51
53
|
}
|
|
@@ -57,9 +59,14 @@ export interface ReviewRecord {
|
|
|
57
59
|
verdict: "supports" | "partially-supports" | "refutes" | "inconclusive";
|
|
58
60
|
support: string;
|
|
59
61
|
checked: string;
|
|
62
|
+
/** Gaps inside the target's stated claim or criteria; non-empty keeps acceptance open. */
|
|
60
63
|
unchecked: string;
|
|
64
|
+
/** Disclosures outside the criteria; recorded on the acceptance as limits, never blockers. */
|
|
65
|
+
limits?: string;
|
|
61
66
|
text: string;
|
|
62
67
|
evidence: string[];
|
|
68
|
+
/** The reviewer's earlier review of a previous revision that this one carries forward. */
|
|
69
|
+
reattests?: string;
|
|
63
70
|
}
|
|
64
71
|
export interface ResourceContract {
|
|
65
72
|
target: string;
|
|
@@ -153,12 +160,35 @@ export function blockingMessages(state: CollaborationState): PeerMessage[] {
|
|
|
153
160
|
return state.messages.filter((m) => m.blocking && m.state !== "resolved");
|
|
154
161
|
}
|
|
155
162
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const reviews = state.reviews.filter((r) => r.target === artifact.id && r.revision === artifact.hash);
|
|
163
|
+
/** Each non-owner's latest review of the artifact's current revision. */
|
|
164
|
+
export function latestArtifactReviews(state: CollaborationState, artifact: ArtifactRecord): ReviewRecord[] {
|
|
159
165
|
const latest = new Map<string, ReviewRecord>();
|
|
160
|
-
for (const review of reviews)
|
|
161
|
-
|
|
166
|
+
for (const review of state.reviews) {
|
|
167
|
+
if (review.target === artifact.id && review.revision === artifact.hash && review.actor !== artifact.owner) latest.set(review.actor, review);
|
|
168
|
+
}
|
|
169
|
+
return [...latest.values()];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function acceptingReview(artifact: ArtifactRecord, review: ReviewRecord): boolean {
|
|
162
173
|
const methods = artifact.kind === "document" ? ["sourced", "derived", "tested", "reproduced"] : ["tested", "reproduced"];
|
|
163
|
-
return
|
|
174
|
+
return review.verdict === "supports" && !review.unchecked && methods.includes(review.support);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function artifactAccepted(state: CollaborationState, artifact: ArtifactRecord): boolean {
|
|
178
|
+
if (blockingMessages(state).some((m) => m.target === artifact.id)) return false;
|
|
179
|
+
const latest = latestArtifactReviews(state, artifact);
|
|
180
|
+
if (latest.some((r) => r.verdict === "refutes")) return false;
|
|
181
|
+
return latest.some((r) => acceptingReview(artifact, r));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Limits the accepting reviews disclosed outside the criteria, each named with its review. */
|
|
185
|
+
export function artifactLimits(state: CollaborationState, artifact: ArtifactRecord): string[] {
|
|
186
|
+
return latestArtifactReviews(state, artifact)
|
|
187
|
+
.filter((r) => acceptingReview(artifact, r) && r.limits?.trim())
|
|
188
|
+
.map((r) => `${r.id} (${r.actor}): ${r.limits?.trim()}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Reviews of the current revision that left in-criteria scope unchecked: coverage debt, named. */
|
|
192
|
+
export function partialArtifactReviews(state: CollaborationState, artifact: ArtifactRecord): ReviewRecord[] {
|
|
193
|
+
return latestArtifactReviews(state, artifact).filter((r) => r.verdict !== "refutes" && r.unchecked.trim());
|
|
164
194
|
}
|