Skip to content

hyp status is a triage summary; the inventory moves behind --full (LLP 0212) - #716

Open
bgmcmullen wants to merge 3 commits into
masterfrom
status-triage-summary
Open

hyp status is a triage summary; the inventory moves behind --full (LLP 0212)#716
bgmcmullen wants to merge 3 commits into
masterfrom
status-triage-summary

Conversation

@bgmcmullen

Copy link
Copy Markdown
Contributor

hyp --help states this command's job: "Start with hyp status for whether this install is working." On a joined machine it printed 50 lines, and that was not what most of them answered.

Every subsystem that landed a never-silent requirement landed it here, each correctly and each unaware of the others, so the screen became an inventory: ten active plugins lines, three rosters, a nine-entry client-action ledger of which six read [done], a 64-character etag, two absolute paths, a raw byte count, datasets: 1. All of it addressable elsewhere (hyp plugin list, hyp config show, hyp daemon status, hyp query status), and all of it printed unconditionally. On the observed machine one real problem, Claude Desktop enabled but not attached, appeared four times inside it: as a roster line, as an attach action, as a backfill action, and as a diagnostic.

Before / after

Before (50 lines)
hypaware
  overall:  healthy
  config:   /Users/you/.hyp/hypaware-config.json (ok)
  daemon:   installed, loaded, running, state=healthy, pid=32521, mode=foreground
  active plugins:
    - @hypaware/central  [central · locked]
    ... 9 more ...
  sources:
    - ai-gateway  (@hypaware/ai-gateway)  [started]  [central · locked]
    - hermes  (@hypaware/hermes)  [started]  [local]
  sinks:
    ... 2 lines ...
  clients:
    ... 4 lines ...
    syncing: claude · claude-desktop · codex · hermes · openclaw · raw-anthropic · raw-openai - local-only: (none)
  recent clients:
    - cli  (claude)  last seen 2m ago, 1978 rows
  cache:           /Users/you/.hyp/hypaware/cache
  cache retention: 120 days
  cache size:      847360677 bytes
  datasets:        1
  recent errors:   0
  new folders:     sync without asking (`hyp policy folders ask` to be asked instead)
  local config (not applied):
    - plugins.@hypaware/ai-gateway  (collides with central)
  remote config:
    running etag:  "f208d9861fd092babe1114cacc696953db74f676f985e39ca4a616cc1d2e73f1"
  client actions:
    ... 9 lines, 6 of them [done] ...
  diagnostics:
    [WARN ] client_attach_missing: '@hypaware/claude-desktop' is enabled but claude-desktop settings show no HypAware marker - run 'hyp claude-desktop install'
        repair: hyp claude-desktop install

After:

╭──────────────────────────────────────────────────────────────────────────╮
│ HypAware  healthy                                                        │
│ daemon    running (foreground, pid 32521)                                │
│ capture   Claude, Codex, OpenClaw, Claude Desktop (not attached), Hermes │
│ activity  claude/cli just now · 2,194 rows                               │
│ data      820 MB · 120-day retention · syncing to org                    │
╰──────────────────────────────────────────────────────────────────────────╯

  warning  '@hypaware/claude-desktop' is enabled but claude-desktop settings
           show no HypAware marker
           → hyp claude-desktop install
  note     @hypaware/ai-gateway is configured by your org, so your local
           plugins entry for it is ignored

hyp status --full for the full inventory, --json for everything

What the summary carries

Four rows, in the order the questions get asked: healthy → recording → where it goes → what needs you.

activity is promoted out of a trailing section because rows landing is the only proof of capture; every other row is a proxy for it. It comes from the daemon's status file (LLP 0164), so it costs no query and reads no cache.

A client carries its exceptions inline (not attached, local only), so one client is one mention. pending and done client actions are a ledger and stay in --full; failed and refused are attention, with the re-arm hint LLP 0186 settled.

Never-silent facts under a summary

A mandated fact now renders when it is true, not unconditionally: the summary states a fact when a reader could act on it, and the default it departs from is stated by its absence. --full remains the surface where everything is stated unconditionally, and --json where everything is stated in a fixed shape. LLP 0212 carries the full table (local-only clients, withheld directories, provenance tags, dropped entries, first-sync hold, folder ask, diagnostic kinds) and where each one lands.

Width

The summary wraps itself (stdout.columns, else COLUMNS, else 80) before drawing the frame, and hangs continuations in the value column. A framed block cannot rely on the terminal's own wrap: it happens after the right edge is placed, so a too-long row turns the rectangle into a staircase, and a soft-wrapped continuation restarts under the label instead of under the text it continues. Below 34 columns the frame drops and the gutter layout stands alone. 80-when-unknown is deliberate: a status screen is pasted into chat and captured in CI logs at least as often as it is read on a wide terminal.

Also in here

Three collector diagnostics (client_attach_missing, client_attach_stale, client_attached_not_configured) stop embedding their repair command in their message. The repair field carries it and both text surfaces render that field, so the embedded copy only ever printed the same command twice on one screen. No test pinned the wording.

formatBytesShort / friendlyClientLabel move out of wizard/fork.js into a shared src/core/cli/format.js, so the returning gate and status quote the same install in the same units and the same client spellings.

Docs

LLP 0212 records the decision. LLP 0031, 0164, 0186 and 0188 each gain an Extended-by: forward-ref for the part of their hyp status prose this narrows; none of what they settled is edited.

Testing

  • test/core/status-summary.test.js, 24 tests: the four rows, each conditional fact appearing exactly when true, attention ordering and dedup, colour never being the sole encoding, and no line exceeding the terminal width at every width from 20 to 200.
  • Seven existing status test files retargeted to renderStatusFull (mechanical rename; their assertions are unchanged, and they now guard that --full still prints the inventory verbatim).
  • status_diagnostics gains a case 5 asserting the default screen carries the problem and a runnable repair on a broken config, and that the plugin roster and the diagnostic kind are not on it; span assertions cover format=summary and format=full.
  • local_only_export_withhold asserts the withheld-folder count on the default screen and the old wording under --full.
  • npm test 3922 pass. Pre-existing failures on master, untouched: report-render* and npm run typecheck both fail on a missing marked dependency, plus one hyparquet-writer dictionary test.
  • Full smoke battery green, except package_bin_boot, which fails on master too: it asserts a help header (hyp - HypAware kernel CLI) the CLI stopped printing some time ago. Worth a separate fix.

🤖 Generated with Claude Code

…P 0212)

`hyp --help` states this command's job: "Start with 'hyp status' for
whether this install is working." On a joined machine it printed 50
lines, and that was not what most of them answered.

Every subsystem that landed a never-silent requirement landed it here,
each correctly and each unaware of the others, so the screen became an
inventory: ten plugin lines, three rosters, a nine-entry action ledger
of which six read [done], a 64-char etag, two absolute paths, a raw byte
count. On the observed machine one real problem (Claude Desktop enabled
but not attached) appeared four times inside it.

The default screen is now four rows in a frame - healthy, recording,
where the data goes, what needs you - then the attention items, and
nothing else. A conditional fact (a client not attached, a folder
withheld, a refused action) is rendered when it is true; the inventory
it used to sit in is `hyp status --full`, byte-identical to what
shipped before. `--json` is untouched: no key moves.

The summary wraps itself to the terminal (stdout.columns, else COLUMNS,
else 80) before drawing the frame, and hangs continuations in the value
column. A framed block cannot rely on the terminal's own wrap: that
happens after the right edge is placed, turning the rectangle into a
staircase, and a soft-wrapped continuation restarts under the label
rather than under the text it continues. Verified no line exceeds the
terminal width at every width from 20 to 200.

Also: three collector diagnostics stop embedding their repair command
in their message text. The `repair` field carries it and both text
surfaces render that field, so the embedded copy only ever printed the
same command twice on one screen.

LLP 0212 records the decision and carries the table of every
never-silent fact and where it now renders; 0031, 0164, 0186 and 0188
each gain an Extended-by forward-ref for the part of their `hyp status`
prose this narrows.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@bgmcmullen bgmcmullen added the neutral:adopt Foreign PR adopted into neutral's reconcile scope label Aug 11, 2026
@philcunliffe philcunliffe added the neutral:adopted Adoption completion record: merged while carrying neutral:adopt (LLP 0031) label Aug 11, 2026
- summariseCapture: mark local-only non-client sources (LLP 0188's
  clientSync.localOnly is keyed by picker id, which differs from the
  otel/otlp source name); state raw-anthropic/raw-openai's local-only
  count on the data row instead, since they have no capture row to
  attach to; fix the empty-parts fallback to exclude ai-gateway and use
  friendlyClientLabel like the main loop does.
- summariseData: gate "asking about new folders" on report.layered.hasCentral
  to match renderStatusFull's LLP 0106 enrolled-only rule.
- docs/ACCEPTANCE.md and README.md: update hyp status invocations and
  pass conditions that assumed the old always-on inventory text, now
  behind --full; rewrite the README troubleshooting paragraph for the
  summary + --full split.
- wizard/fork.js: collapse a doubled blank line.
- test/core/status-summary.test.js: add coverage for the above, and
  replace inline import(...) JSDoc types with a top-of-file @import.
@philcunliffe

Copy link
Copy Markdown
Contributor

Review round 1 of 25dc0da (adopted PR, LLP 0024/0058). Verdict: findings - 6 fixed and pushed as 17927c9, 2 left open for a human call.

The diff was read in full against merge base 773466d (21 files), alongside status.js, daemon/status.js, cli/style.js, plugin_catalog.js, wizard/provenance.js, wizard/sync_scope.js, daemon/types.d.ts, and LLPs 0212/0031/0164/0186/0188/0106.

Fixed in 17927c9

1. major - src/core/commands/status.js:724-742 - a local-only source that is not a client vanished from the default screen.
clientSync.localOnly is keyed by picker source id, but the (local only) mark was applied only to report.clients entries. report.clients comes from contributes.client (only claude, claude-desktop, codex, openclaw), while clientSync comes from catalog.pickerDescriptors (also hermes, otel, raw-anthropic, raw-openai). Rendering a report with clientSync = { syncing: ['claude'], localOnly: ['hermes','otel'] } produced:

│ capture   Claude, Hermes, Otlp                       │
│ data      100 B · 120-day retention · syncing to org │

Nothing said Hermes was withheld, and the data row affirmatively claimed "syncing to org" - a never-silent fact (LLP 0212 #never-silent) silently dropped on the new default surface. --full was unaffected. Second half of the bug: picker id otel never equals source name otlp, so a naive localOnly.has(s.name) would miss it too.
Fix: a SOURCE_PICKER_IDS map resolves source name to picker id, so non-client sources now carry the same inline (local only) mark; raw-anthropic/raw-openai (hidden pickers with no row of their own) are stated by count as N local only on the data row via a new unattributedLocalOnlyCount. Three tests added.

2. major - docs/ACCEPTANCE.md:42,85,158,271 and README.md:378-390 stated pass conditions against text that now only exists under --full.
The codex_desktop_capture procedure told a human to run hyp status and required codex [configured, attached]; the new default screen prints capture Codex and never emits that string, so the gate read as a failure on a working install. Same for the openclaw line and the recent clients: checks (the summary's row is activity).
Fix: those invocations now say hyp status --full, or are restated in summary terms where the summary is the more natural check; the README troubleshooting paragraph now describes the summary / --full / --json split matching core_commands.js:93-101.

4. minor - src/core/commands/status.js:798 printed asking about new folders on a solo machine.
renderStatusFull gates the same line on report.layered?.hasCentral && report.usagePolicy (status.js:441) per LLP 0106 #enrolled-only: with no server the hook is inert and the question is never asked. The summary printed it unconditionally, promising a prompt that never comes.
Fix: same gate applied; a test asserts silence on a solo host with folderAsk: 'ask'.

6. minor - src/core/commands/status.js:741-742 fallback leaked the raw ai-gateway id.
With ai-gateway active and no client plugins, the loop skipped ai-gateway, parts came out empty, and the fallback returned the unfriendlied raw id - contradicting the rule stated three lines above that the gateway is plumbing, not a thing being captured.
Fix: the fallback applies the same exclusion and friendlyClientLabel, falling through to nothing configured yet.

7. minor - test/core/status-summary.test.js:23,24,63 used inline import('...') JSDoc types, which CLAUDE.md forbids. Replaced with a top-of-file @import.

8. trivial - src/core/cli/wizard/fork.js:339 doubled blank line left by the formatBytesShort / friendlyClientLabel move. Collapsed.

Each fix was verified as landed in the committed tree (named file and symbol changed vs 25dc0da), not merely reported. npm test: 3927 pass, 3 fail - exactly the pre-existing failures this PR's description already documents (report-render* on the missing marked dep, one hyparquet-writer dictionary test). npm run typecheck: the same pre-existing marked error, nothing new.

Left open - these need your call, not a patch

3. minor - status.js:794 - stays on this machine is derived from report.layered?.hasCentral alone.
An unjoined install with a configured forwarding sink (e.g. @hypaware/s3) has layered === null, so the data row asserts the data stays local while every tick ships rows off the box. The pre-PR surface made no such claim - it printed the sink roster and only said "keeping captured data local only" when report.sinks was empty, so this is a new false statement rather than a pre-existing one.
Not fixed here because the obvious repair (a third phrasing for "exporting to N configured sinks") narrows what LLP 0212 #never-silent settled: "in both directions: syncing to org or stays on this machine". LLP 0212 is Accepted, so this wants a forward-ref amendment deciding the third state, not a silent code change.

5. minor - status.js:606 + :837 - the headline health word can contradict the attention section.
After hyp daemon stop, overall stays 'healthy' (a stopped daemon is not in degradingKinds, daemon/status.js:1130), so the screen reads HypAware healthy in green directly above warning the daemon is not running, so nothing is being recorded. hyp --help points readers here for "whether this install is working" and this PR makes that word the first thing they read. test/core/status-summary.test.js:172 pins this state but does not assert the health word, so the pairing is currently unchosen rather than chosen.
Deriving the word from overall and the attention list is a behaviour decision (and this attention kind is a fifth item not enumerated in LLP 0212 #attention's list), so it is yours rather than a mechanical fix.

Also checked, clean

  • Width and wrapping math (cli/format.js:97-117, status.js:597-660): rowWidth = columns - 4 - LABEL_WIDTH lands boxed()'s width + 4 > columns check exactly on the boundary, so the frame is never suppressed by its own content; the attention gutter sums to columns. wrapToWidth traced for long-token hard breaks with exact-multiple tails, empty words from doubled spaces, the width < 4 bailout, and multi-paragraph input. The Math.max(8, ...) floor overflows by one column only below 19 columns, outside the tested 20-200 range and not worth a change.
  • The never-silent table against the code, row by row: withheld dirs, first-sync hold, refused/failed actions, collision drops, centralQueryIgnored, badEtag, rollback, recentErrorCount all reach the summary. Only the local-only case was missing (finding 1).
  • @ref honesty: every anchor new code cites resolves. 0164#status-reads-it-from-the-status-file and 0188#never-silent have no matching heading in their docs, but both are pre-existing on master in 8+ files - not a regression from this PR, worth a sweep sometime.
  • Forward-refs: the four Extended-by: lines are additive header edits only; nothing Accepted was rewritten. One wording drift: 0188's Extended-by: puts local-only clients on the data row while 0212's own table puts them on capture; the code follows 0212.
  • Renames: no stray renderStatusText callers; all 8 sibling test files and both smokes migrated. renderStatusJson is byte-identical, matching LLP 0212 #consequences.
  • Diagnostic message trimming (daemon/status.js:924/946/967): all three still carry the command in repair, and both text surfaces render it, so nothing was lost.
  • Conventions: no em dashes, no semicolons, no @typedef, no .d.ts type-import specifiers in new files.

The head has moved to 17927c9, so the next reconcile tick reviews that head (round 2).

…dicate

- format.js: `otlp` and `otel` both map to `OpenTelemetry` in
  FRIENDLY_CLIENT_LABELS, so the summary prints the name the picker row
  offered instead of title-casing the source id into `Otlp`. Tests updated,
  including the fixture that used `otel` as a source name where the plugin
  actually contributes `otlp`.
- status.js: `unattributedLocalOnlyCount` now builds its already-attributed
  client set from configured clients only, matching what `summariseCapture`
  renders. A local-only client with its plugin disabled was absent from
  `capture` yet counted as attributed, which is exactly the silence the
  never-silent invariant forbids.
- LLP 0212 and LLP 0031: point at `renderStatusSummary` / `renderStatusFull`
  rather than the deleted `renderStatusText`.
- LLP 0188: the `Extended-by:` line described the `data` row as naming
  local-only clients; the code marks them inline on `capture` and only
  carries a bare count on `data` for pickers with no row of their own.
@philcunliffe

Copy link
Copy Markdown
Contributor

Review round 2 of 17927c9 (adopted PR, LLP 0024/0058). Verdict: findings - 4 minor, all 4 fixed and pushed as cc6d10e. No blockers. The substance of round 1's six fixes landed correctly.

Round 2 verified each round-1 fix in the committed tree, read the 25dc0da..17927c9 delta in full, and re-read the whole diff against merge base 773466d (23 files).

Findings, all fixed

1. minor - src/core/cli/format.js label map: the OTEL source rendered as Otlp on the default screen.
friendlyClientLabel title-cases anything absent from FRIENDLY_CLIENT_LABELS, and neither otlp nor otel had an entry. The @hypaware/otel plugin contributes a source literally named otlp (hypaware.plugin.json contributes.sources[0].name, and inferConfiguredSources at daemon/status.js:1341 hardcodes the same name), so every install with the bundled OTEL plugin enabled printed capture ..., Otlp. That plugin's own picker row declares "label": "OpenTelemetry" - the exact spelling the user picked it by, which is this helper's stated purpose (format.js:6-12). It escaped round 1 because the new test at status-summary.test.js:316-327 built a source named otel, which no plugin contributes, and the round-1 test at :151-165 pinned the wrong spelling.
Fixed: otel and otlp both map to OpenTelemetry (format.js:56-57); the two expectations updated and the fixture renamed to the real contributed name otlp.

2. minor - two LLP docs named a function this PR deleted.
There is no renderStatusText in the tree: it is renderStatusSummary (new) plus renderStatusFull (the preserved body). 0212:156 opened its Consequences with "renderStatusText becomes the summary", and 0031:316 routed Status provenance through renderStatusText/renderStatusJson. Both were dead pointers, and 0212 is the doc of record for this very rename, so it would have landed already wrong. Mechanical dead-pointer repair, which CLAUDE.md explicitly permits on an Accepted doc.
Fixed: 0212:153-159 now reads "renderStatusText is replaced by renderStatusSummary; the previous body is preserved verbatim as renderStatusFull..."; 0031:316 now names renderStatusFull/renderStatusJson. renderStatusText now survives nowhere in the repo except that one "was replaced by" mention.

3. minor - llp/0188-...decision.md:9 Extended-by: line named the wrong row.
It said the summary's data row "always says where rows go and names local-only clients whenever any exist". The code and LLP 0212's own never-silent table mark local-only clients inline on the capture row; the data row carries only a bare N local only count for pickers with no capture row of their own (summariseData, status.js:844-845). Round 1 saw this as harmless wording drift; after round 1's fix it was wrong in a second way too, since the data row demonstrably does not name anything. A reader auditing never-silent coverage forward from 0188 would look on the wrong row.
Fixed: reworded to name data for where rows go and capture for the inline local-only marks. The Extended-by: LLP 0212 link and the rest of the line are unchanged.

4. minor - src/core/commands/status.js:731: unattributedLocalOnlyCount and summariseCapture disagreed about which clients count as rendered.
summariseCapture renders report.clients.filter((c) => c.configured) (:760), but unattributedLocalOnlyCount built its "already attributed" set from all of report.clients (daemon/status.js:894, the full catalog roster), so an id could be in clientNames and still never reach the screen. Reachable case: an enrolled machine with {"name": "@hypaware/claude", "enabled": false} in the local layer plus a claude opt-out - classifyClientProvenance only checks membership in effective.plugins and ignores enabled (wizard/provenance.js:49), so localOnly contains claude, while activePlugins skips disabled entries (daemon/status.js:634) so configured is false. The capture row drops the client and the data row treats it as already stated. User-visible harm is small (a disabled plugin captures nothing), but these two predicates are the definition of "rendered" and "attributed" and the never-silent invariant only holds while they agree.
Fixed: the set is now built from .filter((c) => c.configured), matching :760 exactly.

Verified from round 1

  • 1 (local-only source vanishing from the default screen) - landed and correct. SOURCE_PICKER_IDS (status.js:715) resolves otlp to otel, the source loop builds a marks array carrying state and local only (:773-778), and unattributedLocalOnlyCount (:727-737) states hidden pickers by count on the data row. Findings 1 and 4 above sit on top of this fix rather than undoing it.
  • 2 (docs asserting --full-only text) - landed. docs/ACCEPTANCE.md:39/42 and :269/273 say hyp status --full for the [configured, attached] gates; :85-94 and :154-168 restated in summary terms. README.md:377-398 matches core_commands.js:93-104. Re-swept every remaining hyp status reference in README.md, docs/, .claude/, and the runtime strings in core_commands.js/remote_commands.js/wizard/index.js: all still true of the summary.
  • 4 (asking about new folders on a solo host) - landed. status.js:842 gates on report.layered?.hasCentral && ...folderAsk === 'ask', identical to renderStatusFull:444; the @ref LLP 0106#enrolled-only at :828 resolves. Test at status-summary.test.js:205, and the pre-existing test at :190 was correctly given a central layer so it still exercises the positive case.
  • 6 (raw ai-gateway id leaking through the fallback) - landed. The empty-parts fallback at :781-784 applies the same exclusion and friendlyClientLabel, falling through to nothing configured yet; pinned by the new test at :337.
  • 7 (inline import('...') JSDoc types) - landed. status-summary.test.js:8-10 declares a top-of-file @import; no inline import type remains in the diff.
  • 8 (doubled blank line in fork.js) - landed, wizard/fork.js:339-340 collapsed.

Still open from round 1 - human calls, not re-litigated

  • stays on this machine has no third state for a configured forwarding sink (status.js:841). Still open, with one new data point: the local_only_export_withhold smoke now pins the phrase (hypaware-core/smoke/flows/local_only_export_withhold.js:355) in a flow that instantiates a real @hypaware/central forward sink and ships rows to a fake central server in that same run (:135-181). A release gate now encodes the claim. Fixing it means amending LLP 0212 #never-silent (Accepted) with a third phrasing, which is a design call.
  • The headline health word can contradict the attention section (status.js:606 plus :837). Unchanged at this head; still a behaviour decision about deriving the word from overall and the attention list, and this attention kind is a fifth item not enumerated in LLP 0212 #attention's list.

Also checked, clean

  • The 25dc0da..17927c9 delta in full (5 files, +158/-36): nothing beyond the six fixes, no collateral edits, no regression to previously-reviewed code.
  • @ref honesty for every anchor the fixes cite: 0106#enrolled-only, 0069#requirements, 0200#decision, 0135#disclosure, and all five 0212 anchors (#decision, #rows, #width, #attention, #never-silent) resolve to real headings.
  • Frame and wrap math with colour on: boxed() measures with visibleWidth (cli/style.js:82,88), so the SGR escapes the summary paints do not skew the frame - the NO_COLOR-only width tests were not hiding a coloured-terminal misalignment. rowWidth = columns - 4 - LABEL_WIDTH still lands boxed's suppression check exactly on the boundary, and the attention gutter sums to columns.
  • Never-silent coverage end to end: withheld dirs, first-sync hold, refused/failed actions, collision and invalid-merge drops, centralQueryIgnored, badEtag, rollback (daemon/status.js:1040), and recentErrorCount (which fires the recent_errors diagnostic at daemon/status.js:1112) all reach the attention section.
  • Trimmed diagnostic messages: all three still carry the command in repair, both text surfaces render it, and nothing (test, smoke, or doc) greps for the removed fragment.
  • Smoke changes: status_diagnostics case 5 asserts the summary carries the health word, the sentence, a runnable repair, and the --full pointer while not carrying active plugins: or the diagnostic kind; the span assertions pin the new format=summary / format=full attribute values, and no other smoke asserts status text.
  • --full routing: runStatus checks --json before --full; init.js:126 sends the wizard finale to --full, which is right given the gate already prints a summary above it.
  • Conventions across the diff: no em dashes, no semicolons, no @typedef, no inline import types, no .d.ts type-import specifiers.

Note on the three "pre-existing" test failures

The PR description documents 3 failures (report-render* on a missing marked dep, one hyparquet-writer dictionary test). They did not reproduce in a clean worktree with a fresh npm install: npm test was 3968 passing / 0 failing / 1 skipped, and npm run typecheck clean. marked is a declared dependency at this commit and hyparquet-writer pins to 0.16.6 via the overrides block, so both look like artifacts of a stale node_modules (package version 1.19.0 installed vs 1.21.2 here) rather than anything real. Worth striking that caveat from the description before merge. CI is green on all 9 checks and remains the authority.

Head has moved to cc6d10e, so the round budget (2) is now spent at an unreviewed head: the next tick triages the two still-open round-1 items rather than opening a round 3.

@philcunliffe philcunliffe added the neutral:stuck neutral attempted this but cannot complete it autonomously — needs a human label Aug 12, 2026
@philcunliffe

Copy link
Copy Markdown
Contributor

Triage after the review budget (LLP 0017). Two review rounds ran on this adopted PR: round 1 on 25dc0da (8 findings, 6 fixed as 17927c9), round 2 on 17927c9 (4 findings, all 4 fixed as cc6d10e). Two residual findings remained, so the triage rung judged whether the PR can ship with them open. Verdict: it cannot - one is a true blocker. The PR is otherwise MERGEABLE with all 9 checks green at cc6d10e.

Blocker: the data row can claim stays on this machine while rows leave the box

summariseData derives the egress claim solely from report.layered?.hasCentral (src/core/commands/status.js:838), and hasCentral is set from resolveCentralLayerPath (src/core/daemon/status.js:502-505) - it is purely "did this machine run hyp join". That is orthogonal to whether a forwarding sink is configured. Sinks are a generic top-level sinks config key (src/core/config/schema.js:224-289), and the repo ships a real, tested @hypaware/s3 plugin whose entire purpose is landing cache partitions in the user's own bucket.

So: configure @hypaware/s3 in local config, never join an org. hasCentral is false, the sink driver ships every tick off-box, and the default hyp status screen prints stays on this machine. Nothing corrects it - summariseData (:830-845) has no branch for "sinks configured but not central", and collectAttention (:864+) has no diagnostic for it.

This is not hypothetical. The local_only_export_withhold smoke instantiates a real @hypaware/central forward sink directly via kernel.sinks.instantiate (hypaware-core/smoke/flows/local_only_export_withhold.js:135-181), bypassing the join machinery so hasCentral stays false, POSTs rows to a fake central server, and then asserts the default screen includes('stays on this machine') (:322-336) in that same run. A release gate now pins the false phrase in a flow that is simultaneously proving data leaves the box.

Why this blocks rather than defers: hyp --help points users at hyp status for "whether this install is working", this PR makes that screen the default surface, and the claim is a data-egress statement. A privacy-minded user reading it walks away believing nothing leaves the machine when it demonstrably does. Reachability is ordinary - no enrollment, just a documented first-class plugin.

What is needed to unstick

A design call, because the two-state phrasing is settled text inside Accepted LLP 0212 #never-silent (the "Where rows go (LLP 0188)" row). Per CLAUDE.md's LLP conventions that cannot be changed by a silent code edit: it needs a new request that @refs 0212, with an Extended-by: line added to 0212. Options:

  1. Three-way phrasing keyed off "any real configured forward sink exists": stays on this machine / syncing to org / exporting to N configured sink(s). Open sub-question: does "central" collapse into the same non-local bucket as other forward sinks, or stay distinct? A user both joined to an org and running a private S3 export arguably wants both stated.
  2. Name the sinks inline (mirroring how capture names local-only clients) rather than a bare count - more specific, costs brevity on a row with a tight width budget.
  3. Decide whether @hypaware/gascity's forward sink (same shape as central's, bundled and opt-in) is treated like central or like an arbitrary third-party sink.

Whichever way it goes, the local_only_export_withhold smoke assertion at :355 needs updating in the same change, since it currently defends the wrong string.

The non-blocking residual, for completeness

The headline health word can sit above an attention line that reads as contradicting it: healthy = report.overall !== 'degraded' (status.js:606), and overall (daemon/status.js:1122-1134) is deliberately scoped to "is the install correctly configured" - only config_missing / config_unreadable or severity: 'error' degrade it. So hyp daemon stop then hyp status prints a green HypAware healthy a few lines above warning the daemon is not running, so nothing is being recorded.

Triaged as a preference, not a blocker: overall's scoping is a pre-existing Accepted decision (LLP 0041 §failure-is-surfaced-not-fatal, cited inline at daemon/status.js:1127) that this PR only made more prominent, and the true state is stated unconditionally on the same short screen, which is precisely what LLP 0212's summary/attention pairing is for. Worth a follow-up LLP 0212 extension deriving the word from overall plus a defined subset of attention severities. It would not have parked this PR on its own, but if you are amending 0212 for the blocker anyway, it may be cheapest to settle both in one pass.

How to unstick

Reply with a comment on this PR (or push to the branch). Neutral monitors this thread and will re-engage with your guidance on its next tick.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

neutral:adopt Foreign PR adopted into neutral's reconcile scope neutral:adopted Adoption completion record: merged while carrying neutral:adopt (LLP 0031) neutral:stuck neutral attempted this but cannot complete it autonomously — needs a human

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants