@bongos/core 1.21.69 → 1.21.71

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.
Files changed (68) hide show
  1. package/.bongos-core.json +119 -64
  2. package/.claude/skills/bongos-feedback/SKILL.md +10 -46
  3. package/.claude/skills/goal-create/SKILL.md +21 -3
  4. package/clients/bongos-client/README.md +1 -1
  5. package/clients/bongos-client/bongos-client.global.js +8 -2
  6. package/clients/bongos-client/index.cjs +8 -2
  7. package/clients/bongos-client/index.d.ts +15 -6
  8. package/clients/bongos-client/index.mjs +8 -2
  9. package/docs/adr/0368-red-and-blue-teams-practice-on-a-range-never-on-live.md +99 -0
  10. package/docs/adr/README.md +1 -0
  11. package/docs/api/openapi.json +196 -19
  12. package/docs/api-reference.md +9 -6
  13. package/docs/copy-inventory.md +25 -21
  14. package/docs/copy-registry.json +76 -30
  15. package/docs/file-map.md +1 -1
  16. package/docs/module-api-changelog.md +4 -0
  17. package/docs/page-readings.json +8 -8
  18. package/docs/recipes/core-release-pipeline.md +6 -0
  19. package/docs/recipes/upgrading-the-core.md +2 -0
  20. package/modules/hall-ui/public/settings-software-update.js +77 -2
  21. package/modules/hall-ui/public/settings.html +1 -1
  22. package/modules/lifecycle/db-tasks.js +12 -0
  23. package/modules/lifecycle/goal-spec-task.js +129 -0
  24. package/modules/lifecycle/routes/goals.js +7 -9
  25. package/modules/lifecycle/routes/task-write-routes.js +11 -1
  26. package/modules/lifecycle/upkeep-home.js +55 -0
  27. package/modules/platform-identity/instance-feedback.js +108 -7
  28. package/modules/platform-identity/routes/sso.js +71 -0
  29. package/package-lock.json +2 -2
  30. package/package.json +1 -1
  31. package/release-notes.json +42 -0
  32. package/scripts/gds/claude-materialize.js +125 -31
  33. package/scripts/gds/claude-md-template.js +47 -10
  34. package/scripts/gds/cli-lib.js +4 -4
  35. package/scripts/gds/feedback-send.js +146 -73
  36. package/scripts/gds/paste-token.js +1 -1
  37. package/scripts/gds/provision-net.js +1 -8
  38. package/scripts/gds/provision-units.js +28 -2
  39. package/scripts/gds/reauth.js +1 -1
  40. package/scripts/gds/run-unit-tests.js +3 -0
  41. package/scripts/gds/setup.js +1 -1
  42. package/scripts/gds/start.js +4 -4
  43. package/scripts/gds/surface.js +15 -3
  44. package/scripts/gds/task.js +32 -5
  45. package/scripts/gds/upgrade-operator.js +73 -0
  46. package/scripts/gds/upgrade.js +7 -2
  47. package/scripts/hall-preview/server.js +3 -0
  48. package/src/bongos/platform-visibility-gate.js +3 -0
  49. package/src/bongos/routes/core-update.js +98 -4
  50. package/src/bongos/routes/feedback.js +100 -76
  51. package/src/bongos/upgrade-request.js +158 -0
  52. package/src/module-api.js +1 -1
  53. package/tests/claude_materialize.mjs +195 -18
  54. package/tests/cli_allowlist_permissions.mjs +6 -2
  55. package/tests/cli_surface.mjs +2 -2
  56. package/tests/fixtures/page-ledger.mjs +5 -0
  57. package/tests/goal_admission_override.mjs +1 -1
  58. package/tests/goal_category.mjs +1 -1
  59. package/tests/goal_create_spec_entry.mjs +61 -0
  60. package/tests/goal_spec_task.mjs +197 -0
  61. package/tests/goal_spec_task_db.mjs +95 -0
  62. package/tests/instance_feedback.mjs +267 -68
  63. package/tests/provision_settings_apply.mjs +25 -1
  64. package/tests/software_update.mjs +18 -1
  65. package/tests/start_env_guard.mjs +2 -2
  66. package/tests/upgrade_operator.mjs +105 -0
  67. package/tests/upgrade_request.mjs +348 -0
  68. package/tests/upkeep_home.mjs +156 -0
@@ -33,7 +33,7 @@ const { spawnSync } = require('node:child_process');
33
33
  const { VERBS } = require('../../bin/bongos.js'); // side-effect-free require (guarded main)
34
34
  const ic = require('../../src/instance-config'); // neutral configDir/envPrefix fallbacks (never a founder literal)
35
35
  const { isCorePackageName, CORE_PACKAGE_NAMES } = require('../../src/module-loader/catalog'); // one definition of "this checkout IS the core"
36
- const { renderInstanceClaudeMd } = require('./claude-md-template'); // task 2104 — the starter CLAUDE.md
36
+ const { renderInstanceClaudeMd, refreshStarterCliLines } = require('./claude-md-template'); // task 2104 — the starter CLAUDE.md
37
37
 
38
38
  // script basename (e.g. "ship.js") → dedicated bongos verb, for verbs that map to a bare script with
39
39
  // NO prefixArgs. Verbs whose script is driven by prefixArgs (box → api.js) are EXCLUDED so a generic
@@ -61,16 +61,40 @@ const GDS_INVOCATION_RE = new RegExp(LAUNCHER + String.raw`\s+"?(?:\$CLAUDE_PROJ
61
61
  // A non-gds core script under scripts/ (rare in skills, but keep the transform total).
62
62
  const OTHER_SCRIPT_RE = new RegExp(LAUNCHER + String.raw`\s+"?(?:\$CLAUDE_PROJECT_DIR\/|\.\/)?scripts\/((?!gds\/)[a-zA-Z0-9_./-]+\.js)"?`, 'g');
63
63
 
64
- // Pure: rewrite core-script invocations in a skill/settings command string to the bongos bin.
65
- // Idempotent (a string with no matching invocation is returned unchanged).
64
+ // How a project runs the CLI (task 1004621). The `bongos` bin ships inside the @bongos/core
65
+ // dependency (node_modules/.bin/bongos), which is NOT on PATH, so a bare `bongos ship` written
66
+ // into a project's skills failed with `command not found` on every session's first call and the
67
+ // assistant had to recover by retrying through npx. `npx` finds the project's own bin right after
68
+ // `npm install`, with no setup, on every OS and shell — so that is the spelling a project reads.
69
+ const PROJECT_CLI = 'npx bongos';
70
+
71
+ // A hand-written bare `bongos <verb>` in skill prose (e.g. "`bongos paste-token <token>`"), so it
72
+ // takes the npx spelling too. Case-sensitive on purpose ("Bongos status" is a product noun), only a
73
+ // KNOWN verb, and never when already prefixed by `npx ` or glued to a path/name (`@bongos/core`,
74
+ // `bongos-feedback`, `bongos: command not found` all stay). The `npx ` lookbehind is what keeps a
75
+ // second run from writing `npx npx bongos`.
76
+ function bareVerbRe(verbs) {
77
+ const names = Object.keys(verbs || {}).filter((v) => /^[a-z][a-z-]*$/.test(v));
78
+ if (!names.length) return null;
79
+ names.sort((a, b) => b.length - a.length); // `paste-token` before a shorter prefix of it
80
+ return new RegExp(String.raw`(?<![\w@/.\-])(?<!npx )bongos (${names.join('|')})(?![\w-])`, 'g');
81
+ }
82
+
83
+ // Pure: rewrite core-script invocations in a skill/settings command string to the project's CLI
84
+ // spelling (`npx bongos <verb>`, else `npx bongos exec scripts/gds/<x>.js`), and give any bare
85
+ // `bongos <verb>` the same npx prefix. Idempotent: its own output contains neither a
86
+ // `node scripts/…` invocation nor an un-prefixed `bongos <verb>`, so a second run changes nothing.
66
87
  function bongosify(text, verbs = VERBS) {
67
88
  const map = scriptToVerb(verbs);
68
- return String(text == null ? '' : text)
89
+ const bare = bareVerbRe(verbs);
90
+ let out = String(text == null ? '' : text);
91
+ if (bare) out = out.replace(bare, (_whole, verb) => `${PROJECT_CLI} ${verb}`);
92
+ return out
69
93
  .replace(GDS_INVOCATION_RE, (_whole, script) => {
70
94
  const verb = map[`${script}.js`];
71
- return verb ? `bongos ${verb}` : `bongos exec scripts/gds/${script}.js`;
95
+ return verb ? `${PROJECT_CLI} ${verb}` : `${PROJECT_CLI} exec scripts/gds/${script}.js`;
72
96
  })
73
- .replace(OTHER_SCRIPT_RE, (_whole, rel) => `bongos exec scripts/${rel}`);
97
+ .replace(OTHER_SCRIPT_RE, (_whole, rel) => `${PROJECT_CLI} exec scripts/${rel}`);
74
98
  }
75
99
 
76
100
  // ---- instance rebrand (task 2103) -----------------------------------------
@@ -340,34 +364,38 @@ function copyTreeTransformed(srcDir, destDir, { dryRun, onFile, overwrite = true
340
364
  // path and is the agent's escape hatch, so allow-listing it would be a blanket
341
365
  // grant wearing a narrow rule's clothes. The one script the core's own list
342
366
  // blesses (`api.js`) is named in full instead.
367
+ //
368
+ // EVERY RULE IS THE `npx bongos` SPELLING (task 1004621): a project has no bare
369
+ // `bongos` on PATH, so its skills, its CLAUDE.md and Settings → CLI access
370
+ // (task 1004575) all say `npx bongos …`, and a bare-`bongos` rule would allow a
371
+ // command nothing runs. An instance that took the older bare starter gains the
372
+ // npx twin of each bare rule it still holds on its next upgrade
373
+ // (topUpNpxAllowTwins below).
343
374
  const STARTER_COMMANDS = [
344
- 'bongos claim:*',
345
- 'bongos ship:*',
346
- 'bongos release:*',
347
- 'bongos reauth:*',
348
- 'bongos paste-token:*',
349
- // What Settings → CLI access hands out since task 1004575: a clone has no
350
- // bare `bongos` on PATH, so the copied command runs through npx.
375
+ 'npx bongos claim:*',
376
+ 'npx bongos ship:*',
377
+ 'npx bongos release:*',
378
+ 'npx bongos reauth:*',
351
379
  'npx bongos paste-token:*',
352
- 'bongos exec scripts/gds/api.js:*',
380
+ 'npx bongos exec scripts/gds/api.js:*',
353
381
  // The read-only subset (task 1003992, ADR 0304). `git branch` and `git fetch`
354
382
  // are exact forms on purpose: a flag turns either into a write or an exec. No
355
383
  // `git diff|log|show` rule, also on purpose: the harness already runs their
356
384
  // read-only forms unprompted, and a prefix rule would switch off its flag check
357
385
  // (`--output=<file>` writes anywhere, `diff --no-index` reads any file).
358
386
  // tests/cli_allowlist_permissions.mjs pins this list to the core's, rule for rule.
359
- 'bongos start:*',
360
- 'bongos status:*',
361
- 'bongos recall:*',
362
- 'bongos exec scripts/gds/me.js:*',
363
- 'bongos doctor:*',
364
- 'bongos exec scripts/gds/preflight.js:*',
365
- 'bongos exec scripts/gds/skill-preflight.js:*',
366
- 'bongos exec scripts/gds/ship-check.js:*',
367
- 'bongos exec scripts/gds/fitness.js:*',
368
- 'bongos exec scripts/gds/strand-watch.js:*',
369
- 'bongos exec scripts/gds/stale-worktrees.js:*',
370
- 'bongos exec scripts/gds/read-session-export.js:*',
387
+ 'npx bongos start:*',
388
+ 'npx bongos status:*',
389
+ 'npx bongos recall:*',
390
+ 'npx bongos exec scripts/gds/me.js:*',
391
+ 'npx bongos doctor:*',
392
+ 'npx bongos exec scripts/gds/preflight.js:*',
393
+ 'npx bongos exec scripts/gds/skill-preflight.js:*',
394
+ 'npx bongos exec scripts/gds/ship-check.js:*',
395
+ 'npx bongos exec scripts/gds/fitness.js:*',
396
+ 'npx bongos exec scripts/gds/strand-watch.js:*',
397
+ 'npx bongos exec scripts/gds/stale-worktrees.js:*',
398
+ 'npx bongos exec scripts/gds/read-session-export.js:*',
371
399
  'git status:*',
372
400
  'git branch --list',
373
401
  'git branch --show-current',
@@ -473,6 +501,37 @@ function topUpStarterDeny(existingText, wanted) {
473
501
  return { text: `${JSON.stringify(next, null, 2)}\n`, added, reason: null };
474
502
  }
475
503
 
504
+ // topUpNpxAllowTwins — the `npx bongos` twin of every bare-`bongos` allow rule an instance's OWN
505
+ // settings.json holds (task 1004621). The skills an upgrade lands now say `npx bongos ship`, and a
506
+ // `Bash(bongos ship:*)` rule does not match that, so without the twin the upgrade would trade a
507
+ // `command not found` for a permission prompt on every blessed command. This is NOT the shell-rule
508
+ // top-up task 1004158 ruled out: nothing new is offered. Each twin is a rule the owner already
509
+ // holds, respelled for the one launcher that reaches the same project-local bin — a rule they
510
+ // removed gets no twin, and nothing they wrote is removed or rewritten. Same { text, added, reason }
511
+ // contract as topUpStarterDeny; an unreadable shape is left alone and reported.
512
+ const BARE_BONGOS_RULE_RE = /^(\w+)\(bongos (.+)\)$/;
513
+ function topUpNpxAllowTwins(existingText) {
514
+ const isObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
515
+ const cannotCheck = (reason) => ({ text: null, added: [], reason });
516
+ let doc;
517
+ try { doc = JSON.parse(existingText); } catch (err) { return cannotCheck(`not valid JSON (${err.message})`); }
518
+ if (!isObject(doc)) return cannotCheck('its top level is not a JSON object');
519
+ const perms = doc.permissions === undefined ? {} : doc.permissions;
520
+ if (!isObject(perms)) return cannotCheck('its "permissions" is not an object');
521
+ const allow = perms.allow === undefined ? [] : perms.allow;
522
+ if (!Array.isArray(allow)) return cannotCheck('its "permissions.allow" is not an array');
523
+ const have = new Set(allow);
524
+ const added = [];
525
+ for (const rule of allow) {
526
+ const m = typeof rule === 'string' && rule.match(BARE_BONGOS_RULE_RE);
527
+ const twin = m && `${m[1]}(${PROJECT_CLI} ${m[2]})`;
528
+ if (twin && !have.has(twin)) { have.add(twin); added.push(twin); }
529
+ }
530
+ if (!added.length) return { text: null, added: [], reason: null };
531
+ const next = { ...doc, permissions: { ...perms, allow: [...allow, ...added] } };
532
+ return { text: `${JSON.stringify(next, null, 2)}\n`, added, reason: null };
533
+ }
534
+
476
535
  // The starter an instance gets when the core root carries no settings.json — a
477
536
  // package-only install. Permissions ONLY: no hooks (they are topology), no env.
478
537
  function renderStarterSettings(branding = {}) {
@@ -778,9 +837,26 @@ function materializeClaude({ coreRoot, instanceDir, dryRun = false, overwrite =
778
837
  // The read is guarded: existsSync passing does not mean readable (a directory at that path,
779
838
  // a locked or root-owned file), and this runs AFTER the pin moved and the migrations ran, so
780
839
  // a throw here would leave a half-applied upgrade. It fails soft, like its sibling above.
840
+ // The npx twins of the owner's own bare-`bongos` allow rules ride the same single read and
841
+ // write (task 1004621): applied first, so the deny top-up below works on — and writes — the
842
+ // text that already carries them. An unparseable file trips both the same way and is
843
+ // reported once, by the deny half.
781
844
  let topUp;
782
- try { topUp = topUpStarterDeny(fs.readFileSync(settingsDest, 'utf8'), credentialDeny(toBranding)); }
783
- catch (err) { topUp = { text: null, added: [], reason: `it could not be read (${err.code || err.message})` }; }
845
+ let twins = { text: null, added: [], reason: null };
846
+ try {
847
+ const raw = fs.readFileSync(settingsDest, 'utf8');
848
+ twins = topUpNpxAllowTwins(raw);
849
+ topUp = topUpStarterDeny(twins.text || raw, credentialDeny(toBranding));
850
+ } catch (err) { topUp = { text: null, added: [], reason: `it could not be read (${err.code || err.message})` }; }
851
+ if (twins.added.length) {
852
+ summary.settingsAllowTwinsAdded = twins.added;
853
+ summary.notes.push(`.claude/settings.json kept every rule it already had and GAINED the \`npx bongos\` twin of ${twins.added.length} bare-\`bongos\` allow rule(s) it held — the skills now say \`npx bongos …\`, which a bare rule does not match: ${twins.added.join(', ')}`);
854
+ log(` ${dryRun ? '[dry-run] would add' : '✓ added'} the npx twin of ${twins.added.length} bare-bongos allow rule(s) to .claude/settings.json (skills now run \`npx bongos …\`; every rule you wrote is kept): ${twins.added.join(', ')}`);
855
+ if (!topUp.text && !topUp.reason) {
856
+ if (!dryRun) fs.writeFileSync(settingsDest, twins.text);
857
+ if (!dryRun) addWritten('.claude/settings.json'); // real run only — the same reasoning as the deny write below
858
+ }
859
+ }
784
860
  if (topUp.text) {
785
861
  if (!dryRun) fs.writeFileSync(settingsDest, topUp.text);
786
862
  // Reported as written only on the REAL run, deliberately. writtenPaths doubles as the set
@@ -803,7 +879,7 @@ function materializeClaude({ coreRoot, instanceDir, dryRun = false, overwrite =
803
879
  summary.settingsDenyUncheckable = topUp.reason;
804
880
  summary.notes.push(`.claude/settings.json could not be checked for missing deny rules — ${topUp.reason}. It was left exactly as it is, so if it predates task 1003966 its local agent can still read keys and session tokens; compare its permissions.deny against the starter by hand, or fix the file and upgrade again.`);
805
881
  log(` ! .claude/settings.json could NOT be checked for missing deny rules — ${topUp.reason}. Left untouched; its deny half may still be missing.`);
806
- } else {
882
+ } else if (!twins.added.length) {
807
883
  summary.skippedFiles.push('settings.json');
808
884
  log(' • .claude/settings.json already present — left untouched (the instance owns it; it already carries every credential deny rule. The starter\'s shelf rules are not offered here — they guard a workflow, not a secret, and are starter-only: task 1004158).');
809
885
  }
@@ -859,9 +935,27 @@ function materializeClaude({ coreRoot, instanceDir, dryRun = false, overwrite =
859
935
  // branding. Written only when ABSENT — never clobber a CLAUDE.md the instance has
860
936
  // grown (safe for init, adopt, and upgrade alike; the self-copy guard above already
861
937
  // keeps this from ever touching the core/OTB checkout's own CLAUDE.md).
938
+ // One narrow exception (task 1004621): the starter's OWN sentences that told the assistant to
939
+ // type a bare `bongos …` — not on PATH in a project — are swapped for their `npx bongos …` form
940
+ // where they still read exactly as the starter wrote them (refreshStarterCliLines). Not in
941
+ // adopt mode, which never touches what the owner already had.
862
942
  const claudeMdPath = path.join(instanceDir, 'CLAUDE.md');
863
943
  if (fs.existsSync(claudeMdPath)) {
864
- summary.notes.push('CLAUDE.md already present — left untouched (the instance owns it).');
944
+ let refreshed = { changed: 0 };
945
+ if (overwrite) {
946
+ try { refreshed = refreshStarterCliLines(fs.readFileSync(claudeMdPath, 'utf8')); } catch { /* unreadable: leave it, as before */ }
947
+ }
948
+ if (refreshed.changed) {
949
+ if (!dryRun) fs.writeFileSync(claudeMdPath, refreshed.text);
950
+ // Real run only, like the settings top-up: it preserves content, so the upgrade pre-flight's
951
+ // "materialize overwrites this anyway" waiver must not cover an owner's uncommitted edit.
952
+ if (!dryRun) addWritten('CLAUDE.md');
953
+ summary.claudeMdCliLines = refreshed.changed;
954
+ summary.notes.push(`CLAUDE.md kept as the instance's own, except ${refreshed.changed} unedited starter sentence(s) now say \`npx bongos …\` — the CLI is not on PATH in a project, so a bare \`bongos\` failed with command not found.`);
955
+ log(` ${dryRun ? '[dry-run] would update' : '✓ updated'} ${refreshed.changed} unedited starter sentence(s) in CLAUDE.md to \`npx bongos …\` (everything else untouched)`);
956
+ } else {
957
+ summary.notes.push('CLAUDE.md already present — left untouched (the instance owns it).');
958
+ }
865
959
  } else {
866
960
  if (!dryRun) {
867
961
  fs.mkdirSync(instanceDir, { recursive: true });
@@ -1018,4 +1112,4 @@ if (require.main === module) {
1018
1112
  process.exit(s.refused.length ? 1 : 0);
1019
1113
  }
1020
1114
 
1021
- module.exports = { scriptToVerb, bongosify, transformSettings, readSettingsTransform, renderStarterSettings, STARTER_ALLOW, copyTreeTransformed, materializeClaude, materializeWillRewrite, materializeModuleSkillsIntoCore, isCoreCheckoutRoot, readModuleSkillsManifest, MODULE_SKILLS_MANIFEST, readInstanceSkillsManifest, INSTANCE_SKILLS_MANIFEST, GDS_INVOCATION_RE, buildRebrandRules, rebrandText, disabledModuleSkills, moduleSkillRoster, moduleSkillSources, vendoredSkillGate, SKILL_NAME_RE, VENDOR_DEFAULT_POLICY };
1115
+ module.exports = { scriptToVerb, bongosify, PROJECT_CLI, topUpNpxAllowTwins, transformSettings, readSettingsTransform, renderStarterSettings, STARTER_ALLOW, copyTreeTransformed, materializeClaude, materializeWillRewrite, materializeModuleSkillsIntoCore, isCoreCheckoutRoot, readModuleSkillsManifest, MODULE_SKILLS_MANIFEST, readInstanceSkillsManifest, INSTANCE_SKILLS_MANIFEST, GDS_INVOCATION_RE, buildRebrandRules, rebrandText, disabledModuleSkills, moduleSkillRoster, moduleSkillSources, vendoredSkillGate, SKILL_NAME_RE, VENDOR_DEFAULT_POLICY };
@@ -56,15 +56,15 @@ function renderInstanceClaudeMd(branding = {}) {
56
56
 
57
57
  Read this at the start of every session. Keep it lean — it loads in full every time.
58
58
 
59
- ## First run — before any \`bongos\` command
59
+ ## First run — before any \`npx bongos\` command
60
60
 
61
- **If \`node_modules/@bongos/core\` is missing, run \`npm install\` first.** The \`bongos\` CLI ships *inside* that dependency, so until the install completes there is no \`bongos\` command and every instruction below fails with \`command not found\`:
61
+ **If \`node_modules/@bongos/core\` is missing, run \`npm install\` first.** The \`bongos\` CLI ships *inside* that dependency and is **not on your PATH**, so it always runs through npx — \`npx bongos <command>\`, never a bare \`bongos\` (that fails with \`command not found\`). Until the install completes, every \`npx bongos\` instruction below fails too:
62
62
 
63
63
  \`\`\`bash
64
64
  npm install
65
65
  \`\`\`
66
66
 
67
- Once per machine, per clone — it is not committed (\`node_modules/\` is ignored on purpose), so a fresh \`git clone\` always needs it. If a \`bongos\` command then still misbehaves, \`bongos doctor\` checks the toolchain and session and prints the fix.
67
+ Once per machine, per clone — it is not committed (\`node_modules/\` is ignored on purpose), so a fresh \`git clone\` always needs it. If an \`npx bongos\` command then still misbehaves, \`npx bongos doctor\` checks the toolchain and session and prints the fix.
68
68
 
69
69
  ## What this is
70
70
 
@@ -74,17 +74,17 @@ ${ident}
74
74
 
75
75
  **Every change is backed by a claimed Bongos task. No exceptions.** Code, docs, migrations, config — if you are about to change project state and do not hold an active claim covering it, stop: create or find the task, claim it, then make the change. There is no "tiny fix" or "just a doc" loophole; the database is the complete ledger of what happened to this project.
76
76
 
77
- - **Start every session with \`bongos start\`** (or \`/builder-start\`) — it lists the tasks you can claim right now, parallel-safe against everyone else's active claims.
78
- - **\`bongos claim <id>\`** locks a task to your session. If it refuses (already claimed, not ready, dependencies not shipped), pick another.
79
- - **\`bongos ship\`** runs the smoke + grade, then merges and deploys; **\`bongos release <id>\`** drops a claim with no credit. Ship or release before you sign off.
77
+ - **Start every session with \`npx bongos start\`** (or \`/builder-start\`) — it lists the tasks you can claim right now, parallel-safe against everyone else's active claims.
78
+ - **\`npx bongos claim <id>\`** locks a task to your session. If it refuses (already claimed, not ready, dependencies not shipped), pick another.
79
+ - **\`npx bongos ship\`** runs the smoke + grade, then merges and deploys; **\`npx bongos release <id>\`** drops a claim with no credit. Ship or release before you sign off.
80
80
  - **Dependencies gate everything** — a task becomes claimable only once every task it depends on has shipped.
81
81
  - **Code you write or touch carries a doc-comment** — each module a header, each public function/class a line or two, saying *what* it does and *why* it exists (never how), in the words someone would search for. Agents find code by searching text, so meaning written into the source is what makes it findable.
82
82
 
83
83
  ## Where things live
84
84
 
85
- - **Live work state** (tasks, versions, ranks, ${currency}): the Bongos DB — \`bongos start\` / \`bongos status\`, or ${hallLine}.${statusLine}
85
+ - **Live work state** (tasks, versions, ranks, ${currency}): the Bongos DB — \`npx bongos start\` / \`npx bongos status\`, or ${hallLine}.${statusLine}
86
86
  - **This instance's identity** (product, world, brand, addresses): \`config/branding.json\`. **Which features are on**: \`config/modules.json\`.
87
- - **The platform you stand on** (rank ladder, the ship/grade/deploy pipeline, permissions, migrations) is vendored **Cloud Bongos core** — build the product, not the platform under it. Landing code is Bongos's job: claim a task and run \`bongos ship\`, never a raw \`git push\`/deploy.
87
+ - **The platform you stand on** (rank ladder, the ship/grade/deploy pipeline, permissions, migrations) is vendored **Cloud Bongos core** — build the product, not the platform under it. Landing code is Bongos's job: claim a task and run \`npx bongos ship\`, never a raw \`git push\`/deploy.
88
88
 
89
89
  ## Permissions
90
90
 
@@ -96,8 +96,45 @@ Builders are rewarded in **${currency}** for shipped work.
96
96
 
97
97
  ---
98
98
 
99
- *This is a starter, authored by \`bongos init\`. Grow it as the project grows — add your architecture, versioning methodology, and hard-won gotchas — but keep it lean: every line spends context budget in every session.*
99
+ *This is a starter, authored by \`npx bongos init\`. Grow it as the project grows — add your architecture, versioning methodology, and hard-won gotchas — but keep it lean: every line spends context budget in every session.*
100
100
  `;
101
101
  }
102
102
 
103
- module.exports = { renderInstanceClaudeMd };
103
+ // The starter's own sentences as an earlier `bongos init` wrote them, each beside its `npx bongos`
104
+ // form (task 1004621). Every pair is branding-independent text this template authored, so an exact
105
+ // match can only be a line the owner never changed.
106
+ const LEGACY_CLI_LINES = [
107
+ ['## First run — before any `bongos` command\n\n**If `node_modules/@bongos/core` is missing, run `npm install` first.** The `bongos` CLI ships *inside* that dependency, so until the install completes there is no `bongos` command and every instruction below fails with `command not found`:',
108
+ '## First run — before any `npx bongos` command\n\n**If `node_modules/@bongos/core` is missing, run `npm install` first.** The `bongos` CLI ships *inside* that dependency and is **not on your PATH**, so it always runs through npx — `npx bongos <command>`, never a bare `bongos` (that fails with `command not found`). Until the install completes, every `npx bongos` instruction below fails too:'],
109
+ ['If a `bongos` command then still misbehaves, `bongos doctor` checks', 'If an `npx bongos` command then still misbehaves, `npx bongos doctor` checks'],
110
+ ['- **Start every session with `bongos start`** (or `/builder-start`)', '- **Start every session with `npx bongos start`** (or `/builder-start`)'],
111
+ ['- **`bongos claim <id>`** locks a task to your session.', '- **`npx bongos claim <id>`** locks a task to your session.'],
112
+ ['- **`bongos ship`** runs the smoke + grade, then merges and deploys; **`bongos release <id>`** drops', '- **`npx bongos ship`** runs the smoke + grade, then merges and deploys; **`npx bongos release <id>`** drops'],
113
+ ['the Bongos DB — `bongos start` / `bongos status`, or', 'the Bongos DB — `npx bongos start` / `npx bongos status`, or'],
114
+ ['claim a task and run `bongos ship`, never', 'claim a task and run `npx bongos ship`, never'],
115
+ ['*This is a starter, authored by `bongos init`.', '*This is a starter, authored by `npx bongos init`.'],
116
+ ];
117
+
118
+ // refreshStarterCliLines — bring an EXISTING project's CLAUDE.md onto the `npx bongos` spelling
119
+ // (task 1004621). The starter is written only when CLAUDE.md is absent, so every project scaffolded
120
+ // before this kept telling its assistant to type a bare `bongos start` — a command that is not on
121
+ // PATH — in the one file loaded every session. The instance owns the file, so this replaces ONLY
122
+ // the starter's own sentences, matched exactly: a line the owner reworded is left alone, nothing
123
+ // is added or removed, and a file grown from the starter keeps everything else byte for byte.
124
+ // Pure: returns { text, changed } where `changed` counts the sentences replaced.
125
+ function refreshStarterCliLines(text) {
126
+ let out = String(text == null ? '' : text);
127
+ let changed = 0;
128
+ // A Windows checkout (git autocrlf) holds the file with CRLF endings; match it in its own.
129
+ const eol = out.includes('\r\n') ? (s) => s.replace(/\n/g, '\r\n') : (s) => s;
130
+ for (const [legacyLf, currentLf] of LEGACY_CLI_LINES) {
131
+ const legacy = eol(legacyLf);
132
+ const current = eol(currentLf);
133
+ if (!out.includes(legacy)) continue;
134
+ out = out.split(legacy).join(current);
135
+ changed += 1;
136
+ }
137
+ return { text: out, changed };
138
+ }
139
+
140
+ module.exports = { renderInstanceClaudeMd, refreshStarterCliLines, LEGACY_CLI_LINES };
@@ -512,7 +512,7 @@ function printUnauthGuidanceAndExit() {
512
512
  console.error(` Recover the fast (UI-first) way — run ${surfaceAction('reauth')}`);
513
513
  console.error(' (opens your Settings page; under "CLI Access" click "Re-issue CLI');
514
514
  console.error(' token", copy the one-liner, paste it back — no terminal Device Flow.)');
515
- console.error(' Fallback (browser sign-in): bongos login <instance-url>');
515
+ console.error(' Fallback (browser sign-in): npx bongos login <instance-url>');
516
516
  } else {
517
517
  console.error(' Recover the fast (UI-first) way — run /builder-reauth, or:');
518
518
  console.error(' node scripts/gds/reauth.js');
@@ -609,7 +609,7 @@ function printInstanceMismatchAndExit({ expected, actual }) {
609
609
  console.error(` Your session is signed in to: ${actual}`);
610
610
  console.error('');
611
611
  console.error(' Sign in to THIS instance, then re-run your command:');
612
- console.error(` bongos login ${expected}`);
612
+ console.error(` npx bongos login ${expected}`);
613
613
  console.error(` (or, from a checkout without the bongos bin: node scripts/gds/login.js ${expected})`);
614
614
  console.error('');
615
615
  console.error(' Intentionally targeting another instance? Set the base explicitly, e.g.');
@@ -764,8 +764,8 @@ async function requireSession() {
764
764
  if (isTerminalSurface()) {
765
765
  // A terminal reader may have no instance configured at all — naming one is the whole point
766
766
  // of `login`, and `/builder-*` does not exist outside a Claude Code session (task 1003730).
767
- console.error(` • First time here → run bongos login <instance-url> (sign in with a browser click)`);
768
- console.error(` • Signed in before / expired → run bongos reauth (re-issue your CLI token)`);
767
+ console.error(` • First time here → run npx bongos login <instance-url> (sign in with a browser click)`);
768
+ console.error(` • Signed in before / expired → run npx bongos reauth (re-issue your CLI token)`);
769
769
  } else {
770
770
  console.error(` • First time on the project → run /builder-setup (one-time GitHub sign-in that also registers you)`);
771
771
  console.error(` • Signed in before / expired → run /builder-reauth (fast UI re-issue of your CLI token)`);
@@ -1,40 +1,58 @@
1
1
  #!/usr/bin/env node
2
- // Send feedback about Bongos itself to the Cloud Bongos maintainers (task 1004462).
2
+ // Send feedback about Bongos itself to the Cloud Bongos maintainers (tasks 1004462, 1004591).
3
3
  //
4
4
  // Usage:
5
5
  // bongos feedback "the claim command hung for two minutes"
6
6
  // bongos feedback --idea "let me filter the task list by goal"
7
- // bongos feedback --message-file notes.md
8
- // [--json]
7
+ // bongos feedback --message-file notes.md (or --message-file - for stdin)
8
+ // [--wait] send in the foreground and print the outcome (debugging)
9
9
  //
10
- // Not the /feedback skill, which loads a locally recorded walkthrough into a
11
- // session. This SENDS: it posts to the builder's OWN project (POST /feedback,
12
- // src/bongos/routes/feedback.js), which relays it to the hub. A bug lands on the
13
- // hub as a 'backlog' task in the maintenance goal awaiting a maintainer's nod; an
14
- // idea lands in the hub's idea inbox. Only the typed text and version stamps
15
- // travel — never code, a transcript or file contents.
10
+ // Straight to the hub, never through the project: a project with no hub link can
11
+ // still send. The builder's OWN project vouches for who sent it — the CLI asks it
12
+ // for a one-time receipt (src/bongos/routes/feedback.js) and the hub redeems that
13
+ // receipt before filing (modules/platform-identity/instance-feedback.js). So a
14
+ // signed-in builder does nothing extra.
15
+ //
16
+ // Fast and quiet by design (the owner, 2026-10-06): the command prints one line
17
+ // and returns at once; a detached background process does the network. No task
18
+ // number comes back. Only the typed text (secret-scrubbed) and version stamps travel.
16
19
 
17
20
  'use strict';
18
21
 
22
+ const fs = require('node:fs');
19
23
  const os = require('node:os');
20
- const { cliClient, requireSession, argText, hasFlag, errCode, errDetails, cliExit } = require('./cli-lib');
24
+ const path = require('node:path');
25
+ const crypto = require('node:crypto');
26
+ const { spawn } = require('node:child_process');
27
+ const { cliClient, requireSession, loadSessionSync, argText, hasFlag, errCode, cliExit } = require('./cli-lib');
28
+ const ic = require('../../src/instance-config');
21
29
 
22
30
  const USAGE = [
23
31
  'usage:',
24
- ' bongos feedback "what went wrong" report a problem with Bongos',
25
- ' bongos feedback --idea "what you wish for" suggest an improvement',
26
- ' bongos feedback --message-file notes.md send a longer note from a file',
32
+ ' npx bongos feedback "what went wrong" report a problem with Bongos',
33
+ ' npx bongos feedback --idea "what you wish for" suggest an improvement',
34
+ ' npx bongos feedback --message-file notes.md send a longer note from a file (- for stdin)',
27
35
  '',
28
36
  'Goes to the Cloud Bongos maintainers. Only the text you type and the Bongos',
29
37
  'versions you run are sent — no code, chat or files from your project.',
30
38
  ].join('\n');
31
39
 
32
- // The CLI release, as `bongos version` reports it: config/bongos-cli.json where
33
- // the core checkout carries it (an instance does not — task 1824), else the
34
- // package version. Read from disk, never required, so a vanilla core still runs.
40
+ const SENT_LINE = 'Sent to the Bongos team.';
41
+ const REPEAT_WINDOW_MS = 6 * 60 * 60 * 1000;
42
+
43
+ // Plain lines for the refusals the builder can see (everything else happens in
44
+ // the background and is written to the outcome file, never to the terminal).
45
+ const REFUSALS = {
46
+ message_required: 'Nothing to send — add your feedback after the command, in quotes.',
47
+ too_long: 'That is too long to send — keep it under 4,000 characters, or split it into two.',
48
+ local_only: 'This project runs only on your computer, so Cloud Bongos cannot confirm who sent the report. Nothing was sent.',
49
+ no_scrubber: 'Feedback was not sent: the secret filter that checks every report is missing from this install.',
50
+ };
51
+
52
+ function stateDir() { return path.join(os.homedir(), '.config', ic.FALLBACK_DIR || 'cloudbongos'); }
53
+
54
+ // The CLI release, as `bongos version` reports it. Read from disk, never required.
35
55
  function cliVersion() {
36
- const fs = require('node:fs');
37
- const path = require('node:path');
38
56
  for (const rel of ['../../config/bongos-cli.json', '../../package.json']) {
39
57
  try {
40
58
  const f = path.join(__dirname, rel);
@@ -44,17 +62,19 @@ function cliVersion() {
44
62
  return null;
45
63
  }
46
64
 
47
- // The message: --message / --message-file, else every positional word joined.
48
- function readMessage(args) {
49
- if (args.includes('--message') || args.includes('--message-file')) return argText('--message', args);
50
- return args.filter((a) => !a.startsWith('--')).join(' ');
65
+ // The message: --message / --message-file (incl. - for stdin), else every
66
+ // positional word joined, else whatever was piped in.
67
+ function readMessage(args, io = {}) {
68
+ if (args.includes('--message') || args.includes('--message-file')) return argText('--message', args, io).value || '';
69
+ const words = args.filter((a) => !a.startsWith('--')).join(' ');
70
+ if (words) return words;
71
+ const stdinIsPiped = io.stdinIsPiped !== undefined ? io.stdinIsPiped : !process.stdin.isTTY;
72
+ if (!stdinIsPiped) return '';
73
+ try { return (io.readStdin || (() => fs.readFileSync(0, 'utf8')))(); } catch { return ''; }
51
74
  }
52
75
 
53
76
  // Every report goes through the transcript-grade secret scrubber before it leaves
54
- // this machine (task 1004467). An assistant may file unasked when a builder vents,
55
- // so nobody reviews the text first — the scrubber is the check that does not
56
- // depend on the assistant's judgment. FAIL CLOSED: no scrubber, nothing is sent.
57
- // It also strips the builder's environment secrets by value, whatever their shape.
77
+ // this machine (task 1004467). FAIL CLOSED: no scrubber, nothing is sent.
58
78
  function scrubReport(message, env = process.env) {
59
79
  let scrub;
60
80
  try { scrub = require('../../modules/security/secret-scrub.js'); } catch { return null; }
@@ -63,69 +83,122 @@ function scrubReport(message, env = process.env) {
63
83
  return { text, removed };
64
84
  }
65
85
 
66
- // Plain-language lines for each refusal the route can give.
67
- const REFUSALS = {
68
- message_required: 'Nothing to send — add your feedback after the command, in quotes.',
69
- validation_failed: 'That could not be sent as written — keep it under 4,000 characters, or split it into two.',
70
- not_hub_connected: 'This project is not connected to Cloud Bongos, so feedback cannot be relayed from here.\n'
71
- + 'Report it to the Bongos maintainers on the Bongos core repository instead.',
72
- membership_required: 'Cloud Bongos does not yet know you as a member of this project.\n'
73
- + 'Sign in to this project once through Cloud Bongos in a browser, then try again.',
74
- rate_limited: 'Too much feedback from you or this project in the last hour. Try again later.',
75
- feedback_unavailable: 'Cloud Bongos is not taking bug reports right now (no version is being built). Try again later.',
76
- hub_unreachable: 'Could not reach Cloud Bongos. Check your connection and try again.',
77
- };
86
+ // A project the hub cannot reach can never vouch for a report — say so up front
87
+ // rather than send something that is certain to be dropped.
88
+ function isLocalBase(base) {
89
+ let host;
90
+ try { host = new URL(base).hostname.toLowerCase(); } catch { return true; }
91
+ if (require('node:net').isIP(host.replace(/^\[|\]$/g, ''))) return true;
92
+ return !host.includes('.') || /(^|\.)(localhost|local|internal|test|invalid)$/.test(host);
93
+ }
94
+
95
+ // Repeat guard: the same report (same first line) inside REPEAT_WINDOW_MS is not
96
+ // sent twice, so an assistant filing on every rant cannot spam the maintainers.
97
+ function repeatKey(message) {
98
+ const first = String(message).split('\n').map((l) => l.trim().toLowerCase()).find(Boolean) || '';
99
+ return crypto.createHash('sha256').update(first).digest('hex').slice(0, 16);
100
+ }
101
+ function checkAndRecordRepeat(message, { file = path.join(stateDir(), 'feedback-sent.json'), now = Date.now() } = {}) {
102
+ let seen = {};
103
+ try { seen = JSON.parse(fs.readFileSync(file, 'utf8')) || {}; } catch { /* first report */ }
104
+ for (const [k, t] of Object.entries(seen)) if (!(now - t < REPEAT_WINDOW_MS)) delete seen[k];
105
+ const key = repeatKey(message);
106
+ if (Object.prototype.hasOwnProperty.call(seen, key)) return true;
107
+ seen[key] = now;
108
+ try {
109
+ fs.mkdirSync(path.dirname(file), { recursive: true });
110
+ fs.writeFileSync(file, JSON.stringify(seen), { mode: 0o600 });
111
+ } catch { /* a guard that cannot write still lets the report through */ }
112
+ return false;
113
+ }
114
+
115
+ // deliver — the network half: receipt from the project, then the report to the
116
+ // hub. Returns an outcome object; never prints. Runs in the background child.
117
+ async function deliver({ message, kind }, deps = {}) {
118
+ const api = deps.api || await cliClient();
119
+ const base = deps.base || ic.resolveApiBase({ session: loadSessionSync() });
120
+ const doFetch = deps.fetchImpl || fetch;
121
+ const sha256 = crypto.createHash('sha256').update(message, 'utf8').digest('hex');
122
+ const r = await api.request('POST', '/feedback/receipt', { body: { sha256, kind } });
123
+ if (!r.ok) return { ok: false, step: 'receipt', code: errCode(r.data) || `http_${r.status}` };
124
+ const origin = new URL(base).origin;
125
+ const hub = String((r.data && r.data.hub) || origin).replace(/\/+$/, '');
126
+ const res = await doFetch(`${hub}/api/bongos/sso/feedback/direct`, {
127
+ method: 'POST',
128
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
129
+ body: JSON.stringify({
130
+ origin, receipt: r.data.receipt, message, kind,
131
+ context: { cli_version: cliVersion(), os: `${os.platform()} ${os.release()}` },
132
+ }),
133
+ signal: AbortSignal.timeout(15000),
134
+ });
135
+ if (res.status === 202) return { ok: true, hub };
136
+ let body = null;
137
+ try { body = await res.json(); } catch { /* non-JSON */ }
138
+ return { ok: false, step: 'hub', code: errCode(body) || `http_${res.status}` };
139
+ }
140
+
141
+ // The background child: read the queued report, deliver it, record the outcome
142
+ // where a curious builder (or a maintainer helping them) can look.
143
+ async function runDeliverChild(file) {
144
+ let job;
145
+ try { job = JSON.parse(fs.readFileSync(file, 'utf8')); } finally { fs.rmSync(path.dirname(file), { recursive: true, force: true }); }
146
+ let outcome;
147
+ try { outcome = await deliver(job); } catch (e) { outcome = { ok: false, step: 'network', code: String(e && e.message || e).slice(0, 200) }; }
148
+ try {
149
+ fs.writeFileSync(path.join(stateDir(), 'feedback-last.json'), JSON.stringify({ at: new Date().toISOString(), ...outcome }), { mode: 0o600 });
150
+ } catch { /* nowhere to record — the report itself is what mattered */ }
151
+ return outcome.ok ? 0 : 1;
152
+ }
153
+
154
+ // Hand the job to a detached child and return. The job travels in a 0600 temp
155
+ // file, never argv (visible in ps) or env.
156
+ function spawnDelivery(job, { spawnImpl = spawn } = {}) {
157
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'bongos-feedback-'));
158
+ const file = path.join(dir, 'job.json');
159
+ fs.writeFileSync(file, JSON.stringify(job), { mode: 0o600 });
160
+ const child = spawnImpl(process.execPath, [__filename, '--deliver', file], { detached: true, stdio: 'ignore' });
161
+ if (child && child.unref) child.unref();
162
+ return file;
163
+ }
78
164
 
79
- async function main(argv = process.argv.slice(2)) {
165
+ async function main(argv = process.argv.slice(2), deps = {}) {
80
166
  if (hasFlag('--help', argv) || hasFlag('-h', argv)) { console.log(USAGE); return 0; }
81
- const json = hasFlag('--json', argv);
82
167
  const kind = hasFlag('--idea', argv) ? 'idea' : 'bug';
83
- const message = String(readMessage(argv) || '').trim();
84
- if (!message) { console.error(USAGE); return 2; }
168
+ const message = String(readMessage(argv, deps.io) || '').trim();
169
+ if (!message) { console.error(REFUSALS.message_required); return 2; }
170
+ if (message.length > 4000) { console.error(REFUSALS.too_long); return 2; }
85
171
 
86
172
  const scrubbed = scrubReport(message);
87
- if (!scrubbed) {
88
- console.error('Feedback was not sent: the secret filter that checks every report is missing from this install.');
89
- return 1;
90
- }
173
+ if (!scrubbed) { console.error(REFUSALS.no_scrubber); return 1; }
91
174
 
92
- await requireSession();
93
- const api = await cliClient();
94
- const res = await api.request('POST', '/feedback', {
95
- body: { message: scrubbed.text, kind, context: { cli_version: cliVersion(), os: `${os.platform()} ${os.release()}` } },
96
- });
97
- if (scrubbed.removed > 0 && !json) {
98
- console.log(`Removed ${scrubbed.removed} secret-looking value(s) from the report before sending.`);
99
- }
175
+ const session = await (deps.requireSession || requireSession)();
176
+ const base = deps.base || ic.resolveApiBase({ session });
177
+ if (isLocalBase(base)) { console.error(REFUSALS.local_only); return 1; }
100
178
 
101
- if (json) { console.log(JSON.stringify(res.data, null, 2)); return res.ok ? 0 : 1; }
102
- if (!res.ok) {
103
- const code = errCode(res.data);
104
- const details = errDetails(res.data) || {};
105
- let line = REFUSALS[code] || `Feedback was not sent (${code || `HTTP ${res.status}`}).`;
106
- if (code === 'rate_limited' && details.retry_after_seconds) {
107
- line += ` (in about ${Math.ceil(details.retry_after_seconds / 60)} min)`;
108
- }
109
- console.error(line);
110
- return 1;
179
+ const job = { message: scrubbed.text, kind };
180
+ if ((deps.checkRepeat || checkAndRecordRepeat)(job.message)) {
181
+ console.log('Already sent — the Bongos team has this one.');
182
+ return 0;
111
183
  }
112
- const d = res.data || {};
113
- if (d.landed === 'task') {
114
- console.log(`Thanks — sent to the Cloud Bongos maintainers as task ${d.task_id}.`);
115
- console.log('It waits for a maintainer to approve it before anyone works on it.');
116
- if (d.hub_origin) console.log(`${String(d.hub_origin).replace(/\/+$/, '')}/builders#/task/${d.task_id}`);
117
- } else {
118
- console.log(`Thanks — your idea is in the Cloud Bongos idea inbox (idea ${d.idea_id}) for the maintainers to review.`);
184
+ if (hasFlag('--wait', argv)) {
185
+ const outcome = await (deps.deliver || deliver)(job);
186
+ console.log(outcome.ok ? SENT_LINE : `Not sent (${outcome.step}: ${outcome.code}).`);
187
+ return outcome.ok ? 0 : 1;
119
188
  }
189
+ (deps.spawnDelivery || spawnDelivery)(job);
190
+ console.log(SENT_LINE);
120
191
  return 0;
121
192
  }
122
193
 
123
194
  if (require.main === module) {
195
+ const argv = process.argv.slice(2);
196
+ const run = argv[0] === '--deliver' ? runDeliverChild(argv[1]) : main(argv);
124
197
  // cliExit, never process.exit, after a request (task 1002734 — the Windows abort).
125
- main().then((code) => cliExit(code), (err) => {
198
+ run.then((code) => cliExit(code), (err) => {
126
199
  console.error(`Feedback was not sent: ${err && err.message ? err.message : err}`);
127
200
  cliExit(1);
128
201
  });
129
202
  }
130
203
 
131
- module.exports = { main, readMessage, REFUSALS, scrubReport };
204
+ module.exports = { main, deliver, readMessage, REFUSALS, scrubReport, isLocalBase, repeatKey, checkAndRecordRepeat, SENT_LINE };