@renzrollon/interlock 1.1.1 → 1.2.1
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/.claude-plugin/icon.png +0 -0
- package/.claude-plugin/plugin.json +109 -1
- package/CHANGELOG.md +150 -11
- package/bin/interlock +421 -53
- package/bin/interlock-run +119 -7
- package/hooks/guard-relaunch.mjs +115 -0
- package/hooks/guard-tests.mjs +51 -3
- package/hooks/hooks.json +1 -0
- package/hooks/mod.mjs +655 -0
- package/hooks/preflight.mjs +124 -13
- package/hooks/recorder.mjs +189 -0
- package/lib/agent-usage.mjs +524 -0
- package/lib/doctor.mjs +511 -46
- package/lib/eval-history.mjs +233 -0
- package/lib/evals-capture.mjs +5 -2
- package/lib/evals-triage.mjs +1 -1
- package/lib/graph/cli.mjs +28 -5
- package/lib/graph/retrieve/context.mjs +8 -3
- package/lib/host/acp.mjs +77 -14
- package/lib/host/claude-cli.mjs +391 -21
- package/lib/host/claude-env.mjs +179 -0
- package/lib/host/codex.mjs +9 -1
- package/lib/host/effort.mjs +163 -0
- package/lib/host/model-map.mjs +43 -0
- package/lib/host/qwen.mjs +9 -1
- package/lib/host/registry.mjs +44 -10
- package/lib/host.mjs +46 -5
- package/lib/interrupted.mjs +167 -0
- package/lib/launch-ledger.mjs +198 -0
- package/lib/launch-rule.mjs +174 -0
- package/lib/limits.mjs +149 -41
- package/lib/meter-quiet.mjs +45 -0
- package/lib/notify-defaults.mjs +6 -0
- package/lib/notify.mjs +41 -25
- package/lib/plan-fingerprint.mjs +80 -11
- package/lib/prompts/implementer.mjs +1 -1
- package/lib/prompts/verify.mjs +24 -1
- package/lib/receipt.mjs +59 -8
- package/lib/report-html.mjs +89 -6
- package/lib/report.mjs +313 -12
- package/lib/resume-card.mjs +90 -2
- package/lib/run-log.mjs +192 -5
- package/lib/run.mjs +1177 -84
- package/lib/state-home.mjs +298 -0
- package/lib/verify.mjs +37 -0
- package/lib/waves.mjs +53 -16
- package/package.json +4 -2
- package/shared/TEST-PROFILE.md +10 -5
- package/skills/evals/SKILL.md +3 -3
- package/skills/fix-tests/SKILL.md +3 -3
- package/skills/spec/SKILL.md +1 -6
- package/skills/spec/scripts/compact-instructions.mjs +30 -0
- package/types/index.d.ts +28 -0
- package/workflows/ship.js +75 -51
- package/.claude-plugin/marketplace.json +0 -19
package/lib/prompts/verify.mjs
CHANGED
|
@@ -29,6 +29,27 @@ function retryPreamble(fixAttempt, fixAttemptsRemaining, errors) {
|
|
|
29
29
|
)
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
// Shell-quoted, because a planned command is one argument to the wrapper and
|
|
33
|
+
// routinely holds spaces, `&&` or a pipe. The same rule `workflows/ship.js`
|
|
34
|
+
// quotes a relay's argv with.
|
|
35
|
+
const quote = a => (/[^\w@%+=:,./-]/.test(String(a)) ? `'${String(a).replace(/'/g, `'\\''`)}'` : String(a))
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* One planned step as the line the agent runs: the command, verbatim, inside
|
|
39
|
+
* `interlock verify exec`, which passes its output and exit code through and
|
|
40
|
+
* records how long it took. The inter-wave budget is charged from that record —
|
|
41
|
+
* the CLI's own clock around the command — rather than from the relay and agent
|
|
42
|
+
* time between the step's dispatch and its judgement.
|
|
43
|
+
*
|
|
44
|
+
* The line is the shell command and nothing else. The briefing says to run each
|
|
45
|
+
* line exactly as written, so a `kind:` label in front of it would be run too —
|
|
46
|
+
* and fail. The kind is already on the line, as `--kind`.
|
|
47
|
+
*/
|
|
48
|
+
function execLine(s) {
|
|
49
|
+
const cwd = typeof s.cwd === 'string' && s.cwd.trim() && s.cwd.trim() !== '.' ? ` --cwd ${quote(s.cwd.trim())}` : ''
|
|
50
|
+
return ` interlock verify exec --kind ${quote(s.kind)}${cwd} --command ${quote(s.command)}`
|
|
51
|
+
}
|
|
52
|
+
|
|
32
53
|
/**
|
|
33
54
|
* @param {object} input
|
|
34
55
|
* @param {string} input.change
|
|
@@ -51,7 +72,7 @@ export function assembleVerifyPrompt({
|
|
|
51
72
|
fixAttemptsRemaining,
|
|
52
73
|
errors
|
|
53
74
|
}) {
|
|
54
|
-
const planned = (Array.isArray(steps) ? steps : []).map(
|
|
75
|
+
const planned = (Array.isArray(steps) ? steps : []).map(execLine).join('\n')
|
|
55
76
|
// A run id the CLI already knows is stated; otherwise the agent is told where
|
|
56
77
|
// to read it, rather than being left to invent one and spill into nowhere.
|
|
57
78
|
const spillRunId = runId
|
|
@@ -66,6 +87,8 @@ export function assembleVerifyPrompt({
|
|
|
66
87
|
`Run these verification steps for change "${change}" and report what happened.\n\n` +
|
|
67
88
|
retry +
|
|
68
89
|
planned +
|
|
90
|
+
`\n\nRun each line above exactly as written, as one shell command: \`interlock verify exec\` runs the command unchanged — its ` +
|
|
91
|
+
`output and exit code are the command's own — and records how long the check took.` +
|
|
69
92
|
// The repair clause is the one sentence a retry must not inherit: "do not
|
|
70
93
|
// repair on a first failure" is exactly wrong on the second attempt, which
|
|
71
94
|
// exists to repair.
|
package/lib/receipt.mjs
CHANGED
|
@@ -103,8 +103,14 @@ export function tailFromManifest(manifest) {
|
|
|
103
103
|
* degradations?: string[],
|
|
104
104
|
* planFingerprint?: string,
|
|
105
105
|
* leftoverTaskIds?: string[],
|
|
106
|
-
* host?: {id?: string, billing?: string, hooks?: boolean, usage?: boolean
|
|
107
|
-
* }
|
|
106
|
+
* host?: {id?: string, billing?: string, hooks?: boolean, usage?: boolean,
|
|
107
|
+
* cacheAccounting?: boolean|'hook'},
|
|
108
|
+
* lanesStoppedByHost?: number, schemaResultsMissing?: number,
|
|
109
|
+
* toolsDeniedInLanes?: number, modelSubstitutions?: number,
|
|
110
|
+
* permissionPrompts?: number, autoModeDenials?: number,
|
|
111
|
+
* surface?: string, stateHome?: string
|
|
112
|
+
* }} input the six counts are passed only when the run observed agents at all
|
|
113
|
+
* (a host record or a sidecar); leaving one out records it as unobserved
|
|
108
114
|
*/
|
|
109
115
|
export function buildReceipt(input) {
|
|
110
116
|
const source = input || {}
|
|
@@ -143,6 +149,12 @@ export function buildReceipt(input) {
|
|
|
143
149
|
// outcome corpus's comparisons are only meaningful if the run said so itself.
|
|
144
150
|
const host = source.host && typeof source.host === 'object' ? source.host : null
|
|
145
151
|
|
|
152
|
+
// A count the run measured, zero included, or `undefined`. Never a default:
|
|
153
|
+
// a run that saw no host record and no sidecar observed no agent, and a zero
|
|
154
|
+
// here would file it beside a run that watched every lane and found nothing.
|
|
155
|
+
const measured = value => (typeof value === 'number' && Number.isFinite(value) ? value : undefined)
|
|
156
|
+
const named = value => (typeof value === 'string' && value.length > 0 ? value : undefined)
|
|
157
|
+
|
|
146
158
|
return {
|
|
147
159
|
type: 'run-receipt',
|
|
148
160
|
change: source.change,
|
|
@@ -155,7 +167,12 @@ export function buildReceipt(input) {
|
|
|
155
167
|
// Recorded per run so a reader can PARTITION the corpus rather than
|
|
156
168
|
// average across it: cache figures exist on some hosts and not others,
|
|
157
169
|
// and that asymmetry is stated rather than engineered away.
|
|
158
|
-
cacheAccounting: host.cacheAccounting
|
|
170
|
+
cacheAccounting: host.cacheAccounting,
|
|
171
|
+
// The effort capability the host declared (flag, negotiated or
|
|
172
|
+
// unsupported), so a reader can tell a run whose effort was applied
|
|
173
|
+
// from one where it could not be. `null` when the manifest predates
|
|
174
|
+
// the capability, never the assumed `flag`.
|
|
175
|
+
effort: host.effort ?? null
|
|
159
176
|
}
|
|
160
177
|
: undefined,
|
|
161
178
|
waves: (Array.isArray(summary.waves) ? summary.waves : []).map(wave => ({
|
|
@@ -230,7 +247,21 @@ export function buildReceipt(input) {
|
|
|
230
247
|
(paths
|
|
231
248
|
? 'the executed plan could not be read back at close'
|
|
232
249
|
: 'the run did not reach its close, so the executed plan was never read'),
|
|
233
|
-
degradations: source.degradations
|
|
250
|
+
degradations: source.degradations,
|
|
251
|
+
// What the run observed about its agents (design D8), copied by name. The
|
|
252
|
+
// close counts them off the host records and the sidecar; this only
|
|
253
|
+
// carries what it was handed, so the banners and the counts cannot come
|
|
254
|
+
// apart through a second derivation here.
|
|
255
|
+
lanesStoppedByHost: measured(source.lanesStoppedByHost),
|
|
256
|
+
schemaResultsMissing: measured(source.schemaResultsMissing),
|
|
257
|
+
toolsDeniedInLanes: measured(source.toolsDeniedInLanes),
|
|
258
|
+
modelSubstitutions: measured(source.modelSubstitutions),
|
|
259
|
+
permissionPrompts: measured(source.permissionPrompts),
|
|
260
|
+
autoModeDenials: measured(source.autoModeDenials),
|
|
261
|
+
// Where the run ran and where its corpora went (design D14), as `run start`
|
|
262
|
+
// recorded them — never re-resolved at close, so one run names one home.
|
|
263
|
+
surface: named(source.surface),
|
|
264
|
+
stateHome: named(source.stateHome)
|
|
234
265
|
}
|
|
235
266
|
}
|
|
236
267
|
|
|
@@ -278,6 +309,17 @@ export function degradationLines({ banners = [], closing = null, hostBanners = [
|
|
|
278
309
|
return [...new Set(out)]
|
|
279
310
|
}
|
|
280
311
|
|
|
312
|
+
// Whether two absolute paths name the same directory for the purpose of the
|
|
313
|
+
// `state home:` row. Import-free like the rest of this module, so it compares
|
|
314
|
+
// text: trailing separators are ignored and nothing touches the filesystem. A
|
|
315
|
+
// home recorded through a symlink and a cwd reached through its real path read
|
|
316
|
+
// as different, which prints a row naming a real path — never a lost one.
|
|
317
|
+
function samePath(a, b) {
|
|
318
|
+
if (typeof a !== 'string' || typeof b !== 'string') return false
|
|
319
|
+
const trim = p => (p.length > 1 ? p.replace(/[\\/]+$/, '') || p.slice(0, 1) : p)
|
|
320
|
+
return trim(a) === trim(b)
|
|
321
|
+
}
|
|
322
|
+
|
|
281
323
|
/**
|
|
282
324
|
* The summary's first line, exactly as `formatRunSummary` prints it.
|
|
283
325
|
*
|
|
@@ -302,10 +344,10 @@ export function summaryHeadline({ change, halted, leftoverTaskIds } = {}) {
|
|
|
302
344
|
* Silence is the failure mode it exists to remove: a summary with no banner
|
|
303
345
|
* section is indistinguishable from a run that degraded and hid it.
|
|
304
346
|
*
|
|
305
|
-
* `runId`, `root`, `projectSlug`, `push`, `unarchived` and
|
|
306
|
-
* default so this stays callable exactly as it was before
|
|
307
|
-
* the halt resume card added them — the
|
|
308
|
-
* way.
|
|
347
|
+
* `runId`, `root`, `projectSlug`, `stateHome`, `push`, `unarchived` and
|
|
348
|
+
* `resumeCard` all default so this stays callable exactly as it was before
|
|
349
|
+
* design D7/D8/D9, the halt resume card and the state home added them — the
|
|
350
|
+
* receipt module stays import-free either way.
|
|
309
351
|
*/
|
|
310
352
|
export function formatRunSummary({
|
|
311
353
|
change,
|
|
@@ -316,6 +358,7 @@ export function formatRunSummary({
|
|
|
316
358
|
runId = undefined,
|
|
317
359
|
root = undefined,
|
|
318
360
|
projectSlug = undefined,
|
|
361
|
+
stateHome = undefined,
|
|
319
362
|
push = null,
|
|
320
363
|
unarchived = null,
|
|
321
364
|
resumeCard = null
|
|
@@ -390,6 +433,14 @@ export function formatRunSummary({
|
|
|
390
433
|
)
|
|
391
434
|
if (projectSlug !== undefined && projectSlug !== null) lines.push(` project: ${projectSlug}`)
|
|
392
435
|
if (root !== undefined && root !== null) lines.push(` cwd: ${root}`)
|
|
436
|
+
// Where the run's trajectory, outcome line, metrics and resume card went
|
|
437
|
+
// (design D14), directly under the directory the close ran in, and only when
|
|
438
|
+
// the two differ: a main-checkout run's summary stays byte-identical, and a
|
|
439
|
+
// linked-worktree run tells its reader that its records are not in the tree
|
|
440
|
+
// they are looking at.
|
|
441
|
+
if (typeof stateHome === 'string' && stateHome.length > 0 && !samePath(stateHome, root)) {
|
|
442
|
+
lines.push(` state home: ${stateHome}`)
|
|
443
|
+
}
|
|
393
444
|
|
|
394
445
|
// The push outcome (design D5): a row only when a push was actually
|
|
395
446
|
// attempted — an unconfigured relay is an optional feature left off, not a
|
package/lib/report-html.mjs
CHANGED
|
@@ -28,10 +28,11 @@
|
|
|
28
28
|
// through the `html` tagged template below, which escapes unless a fragment is
|
|
29
29
|
// explicitly marked `raw`. There is no second interpolation path.
|
|
30
30
|
|
|
31
|
-
// The
|
|
32
|
-
//
|
|
33
|
-
// is how a reader comes to believe
|
|
34
|
-
|
|
31
|
+
// The only things this renderer imports: wording it shares with the text
|
|
32
|
+
// surface — an absence, and the surface partition's bucket labels. Two surfaces
|
|
33
|
+
// describing the same fact in different words is how a reader comes to believe
|
|
34
|
+
// they are looking at two different facts.
|
|
35
|
+
import { NO_EVAL_RESULT, SURFACE_BUCKETS } from './report.mjs'
|
|
35
36
|
|
|
36
37
|
const ESCAPES = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }
|
|
37
38
|
|
|
@@ -195,10 +196,79 @@ function evalHistoryCorpus(e) {
|
|
|
195
196
|
)
|
|
196
197
|
}
|
|
197
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Trajectories in the home repository's other worktrees: counted, never read.
|
|
201
|
+
*
|
|
202
|
+
* A scan that could not be made renders as a word at full weight with its
|
|
203
|
+
* reason, never as a zero — a count nobody took is not a count of nothing.
|
|
204
|
+
*/
|
|
205
|
+
function unreadWorktrees(u) {
|
|
206
|
+
if (!u) return ''
|
|
207
|
+
const counted = u.runs !== null && u.runs !== undefined && u.worktrees !== null && u.worktrees !== undefined
|
|
208
|
+
const count = counted
|
|
209
|
+
? raw(html`<span class="num">${u.runs}</span> file(s) across <span class="num">${u.worktrees}</span> worktree(s), not read`)
|
|
210
|
+
: raw('<span class="word">NOT COUNTED</span>')
|
|
211
|
+
|
|
212
|
+
const notes = []
|
|
213
|
+
if (!counted) notes.push(`could not be counted: ${u.reason || 'no reason was given'}`)
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
html`<div class="corpus"><div class="corpus-head"><h3>Trajectories in other linked worktrees</h3>
|
|
217
|
+
<div class="corpus-count">${count}</div></div>
|
|
218
|
+
<p class="reason">Counted by file name from the repository's worktree list. None was opened, and no
|
|
219
|
+
figure on this page includes them.</p>` +
|
|
220
|
+
(notes.length ? html`<ul class="named">${raw(notes.map(n => html`<li>${n}</li>`).join(''))}</ul>` : '') +
|
|
221
|
+
(u.unscannable && u.unscannable.length
|
|
222
|
+
? html`<div class="named-head">Worktrees not counted, named rather than skipped</div><ul class="named">` +
|
|
223
|
+
raw(u.unscannable.map(w => html`<li><span class="file">${w.worktree}</span> — ${w.reason}</li>`).join('')) +
|
|
224
|
+
'</ul>'
|
|
225
|
+
: '') +
|
|
226
|
+
'</div>'
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** The partition of the scanned runs by the surface each `run-start` recorded. Every bucket, zeros included. */
|
|
231
|
+
function surfacePartition(surfaces, scanned) {
|
|
232
|
+
if (!surfaces) return ''
|
|
233
|
+
return (
|
|
234
|
+
html`<div class="named-head">Surface each run-start recorded — over ${scanned} scanned</div>` +
|
|
235
|
+
html`<dl class="facets surfaces">${raw(
|
|
236
|
+
SURFACE_BUCKETS.map(b => html`<div><dt>${b.label}</dt><dd class="num">${surfaces[b.key] ?? 0}</dd></div>`).join('')
|
|
237
|
+
)}</dl>`
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Said only when the home is not simply the one git named: an unresolved home,
|
|
243
|
+
* or a surface the resolver could not establish. Never left to the header row.
|
|
244
|
+
*/
|
|
245
|
+
function stateHomeNotice(h) {
|
|
246
|
+
if (!h) return ''
|
|
247
|
+
if (h.resolvedFrom === 'fallback') {
|
|
248
|
+
return html`<p class="reason">STATE HOME UNRESOLVED: ${h.reason || 'no reason was given'} — every corpus was read from the root.</p>`
|
|
249
|
+
}
|
|
250
|
+
if (h.reason) return html`<p class="reason">Surface unknown: ${h.reason}</p>`
|
|
251
|
+
return ''
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const RESOLVED_FROM_WORDS = Object.freeze({
|
|
255
|
+
explicit: 'explicit — --state-home or INTERLOCK_STATE_HOME',
|
|
256
|
+
git: 'git',
|
|
257
|
+
lane: 'lane worktree path',
|
|
258
|
+
fallback: 'unresolved — the root'
|
|
259
|
+
})
|
|
260
|
+
|
|
198
261
|
function coverageSection(coverage) {
|
|
199
262
|
const { trajectories: t, outcomes: o, metrics: m } = coverage
|
|
200
263
|
|
|
201
264
|
const trajectoryNotes = []
|
|
265
|
+
if (t.interruptedNotesWithoutTrajectory) {
|
|
266
|
+
const n = t.interruptedNotesWithoutTrajectory
|
|
267
|
+
trajectoryNotes.push(`${n} interrupted-run note${n === 1 ? '' : 's'} name${n === 1 ? 's' : ''} no scanned trajectory`)
|
|
268
|
+
}
|
|
269
|
+
for (const u of t.interruptedNotesUnreadable || []) {
|
|
270
|
+
trajectoryNotes.push(`interrupted-run note unreadable: ${u.file} — ${u.reason}`)
|
|
271
|
+
}
|
|
202
272
|
if (t.truncated) trajectoryNotes.push(`scan capped — ${t.notScanned} not read`)
|
|
203
273
|
if (t.filteredOut) trajectoryNotes.push(`${t.filteredOut} outside the filter`)
|
|
204
274
|
if (t.skippedLines) trajectoryNotes.push(`${t.skippedLines} torn line(s) skipped`)
|
|
@@ -215,9 +285,15 @@ function coverageSection(coverage) {
|
|
|
215
285
|
${proportionMark(t.scanned, t.total)}
|
|
216
286
|
<dl class="facets">
|
|
217
287
|
<div><dt>with run-start</dt><dd class="num">${t.withRunStart}</dd></div>
|
|
218
|
-
<div><dt>with terminal</dt><dd class="num">${t.withTerminal}</dd><dd class="aside">run-complete or run-halt</dd></div
|
|
219
|
-
|
|
288
|
+
<div><dt>with terminal</dt><dd class="num">${t.withTerminal}</dd><dd class="aside">run-complete or run-halt</dd></div>` +
|
|
289
|
+
(t.withoutTerminal
|
|
290
|
+
? html`<div><dt>without terminal</dt><dd class="num">${t.withoutTerminal.total}</dd><dd class="aside">of ${t.scanned} scanned</dd></div>
|
|
291
|
+
<div><dt>interrupted</dt><dd class="num">${t.withoutTerminal.interrupted}</dd><dd class="aside">a SessionEnd note names the run</dd></div>
|
|
292
|
+
<div><dt>unexplained</dt><dd class="num">${t.withoutTerminal.unexplained}</dd><dd class="aside">no note names the run</dd></div>`
|
|
293
|
+
: '') +
|
|
294
|
+
html`<div><dt>with receipt</dt><dd class="num">${t.withReceipt}</dd><dd class="aside">every receipt-derived indicator rests on this</dd></div>
|
|
220
295
|
</dl>` +
|
|
296
|
+
surfacePartition(coverage.surfaces, t.scanned) +
|
|
221
297
|
(trajectoryNotes.length
|
|
222
298
|
? html`<ul class="named">${raw(trajectoryNotes.map(n => html`<li>${n}</li>`).join(''))}</ul>`
|
|
223
299
|
: '') +
|
|
@@ -227,6 +303,8 @@ function coverageSection(coverage) {
|
|
|
227
303
|
'</ul>'
|
|
228
304
|
: '') +
|
|
229
305
|
'</div>' +
|
|
306
|
+
// --- trajectories the home's repository holds elsewhere, counted not read
|
|
307
|
+
unreadWorktrees(coverage.unreadInLinkedWorktrees) +
|
|
230
308
|
// --- outcomes
|
|
231
309
|
html`<div class="corpus"><div class="corpus-head"><h3>Outcome records</h3>
|
|
232
310
|
<div class="corpus-count">${o.exists ? raw(html`<span class="num">${o.records}</span> record(s)`) : raw('<span class="word">ABSENT</span>')}</div></div>` +
|
|
@@ -576,6 +654,7 @@ export function renderReportHtml(report) {
|
|
|
576
654
|
const filterBits = []
|
|
577
655
|
if (filter.since) filterBits.push(`since ${filter.since}`)
|
|
578
656
|
if (filter.change) filterBits.push(`change ${filter.change}`)
|
|
657
|
+
const home = r.stateHome && typeof r.stateHome === 'object' ? r.stateHome : null
|
|
579
658
|
|
|
580
659
|
const body = r.coverage
|
|
581
660
|
? coverageSection(r.coverage) + (r.indicators ? indicatorsSection(r.indicators) : '')
|
|
@@ -596,6 +675,9 @@ export function renderReportHtml(report) {
|
|
|
596
675
|
<div><div class="kicker">Interlock · instrument readout</div><h1>Interlock Report</h1></div>
|
|
597
676
|
<div class="meta">
|
|
598
677
|
<span class="k">ROOT</span><span>${r.root || '.'}</span>
|
|
678
|
+
<span class="k">STATE HOME</span><span>${home && home.home ? home.home : 'unstated'}</span>
|
|
679
|
+
<span class="k">SURFACE</span><span>${home && home.surface ? home.surface : 'unstated'}</span>
|
|
680
|
+
<span class="k">HOME FROM</span><span>${home ? RESOLVED_FROM_WORDS[home.resolvedFrom] || home.resolvedFrom || 'unstated' : 'unstated'}</span>
|
|
599
681
|
<span class="k">SCHEMA</span><span>${r.schema || 'unknown'}</span>
|
|
600
682
|
<span class="k">SCAN CAP</span><span>${filter.maxRuns === undefined ? 'unstated' : filter.maxRuns} run(s)</span>
|
|
601
683
|
<span class="k">FILTER</span><span>${filterBits.length ? filterBits.join(', ') : 'none'}</span>
|
|
@@ -613,6 +695,7 @@ export function renderReportHtml(report) {
|
|
|
613
695
|
<div><span></span><span>Both are findings, recorded at the same standing as a measured value.</span></div>
|
|
614
696
|
</div>
|
|
615
697
|
</div>
|
|
698
|
+
${raw(stateHomeNotice(home))}
|
|
616
699
|
${raw(body)}
|
|
617
700
|
<div class="foot"><span>${r.note || 'reports only'}</span><span>Exit status is always 0.</span></div>
|
|
618
701
|
</main></body></html>
|