@bongos/core 1.19.495 → 1.19.497

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.
@@ -3,9 +3,9 @@
3
3
  "generator": "scripts/gds/copy-inventory.js",
4
4
  "goal": 1000074,
5
5
  "counts": {
6
- "strings": 1612,
7
- "unique_ids": 1403,
8
- "certain": 1612,
6
+ "strings": 1615,
7
+ "unique_ids": 1406,
8
+ "certain": 1615,
9
9
  "heuristic": 0,
10
10
  "duplicate_ids": 150
11
11
  },
@@ -13,7 +13,7 @@
13
13
  "builders-hall": {
14
14
  "at": "modules/hall-ui/public",
15
15
  "files": 73,
16
- "strings": 1169
16
+ "strings": 1172
17
17
  },
18
18
  "status-dashboard": {
19
19
  "at": "modules/status-ui/public",
@@ -234,10 +234,10 @@
234
234
  "text": "Task {…}",
235
235
  "occurrences": 4,
236
236
  "at": [
237
- "modules/hall-ui/public/task.js:436",
238
- "modules/hall-ui/public/task.js:439",
239
- "modules/hall-ui/public/task.js:620",
240
- "modules/hall-ui/public/task.js:632"
237
+ "modules/hall-ui/public/task.js:454",
238
+ "modules/hall-ui/public/task.js:463",
239
+ "modules/hall-ui/public/task.js:691",
240
+ "modules/hall-ui/public/task.js:703"
241
241
  ]
242
242
  },
243
243
  {
@@ -315,8 +315,8 @@
315
315
  "text": "Copy prompt",
316
316
  "occurrences": 3,
317
317
  "at": [
318
- "modules/hall-ui/public/task.js:573",
319
- "modules/hall-ui/public/task.js:593",
318
+ "modules/hall-ui/public/task.js:617",
319
+ "modules/hall-ui/public/task.js:637",
320
320
  "modules/hall-ui/public/work.js:548"
321
321
  ]
322
322
  },
@@ -473,8 +473,8 @@
473
473
  "text": "Back to {…}",
474
474
  "occurrences": 2,
475
475
  "at": [
476
- "modules/hall-ui/public/task.js:170",
477
- "modules/hall-ui/public/task.js:179"
476
+ "modules/hall-ui/public/task.js:188",
477
+ "modules/hall-ui/public/task.js:197"
478
478
  ]
479
479
  },
480
480
  {
@@ -2333,7 +2333,7 @@
2333
2333
  "surface": "builders-hall",
2334
2334
  "text": "Awaiting the grader, or a confirmation, before it can ship.",
2335
2335
  "file": "modules/hall-ui/public/task.js",
2336
- "line": 563,
2336
+ "line": 607,
2337
2337
  "origin": "js-markup",
2338
2338
  "confidence": "certain"
2339
2339
  },
@@ -2405,7 +2405,7 @@
2405
2405
  "surface": "builders-hall",
2406
2406
  "text": "Back to {…}",
2407
2407
  "file": "modules/hall-ui/public/task.js",
2408
- "line": 170,
2408
+ "line": 188,
2409
2409
  "origin": "js-markup",
2410
2410
  "confidence": "certain"
2411
2411
  },
@@ -2414,7 +2414,7 @@
2414
2414
  "surface": "builders-hall",
2415
2415
  "text": "Back to {…}",
2416
2416
  "file": "modules/hall-ui/public/task.js",
2417
- "line": 179,
2417
+ "line": 197,
2418
2418
  "origin": "js-text-assign",
2419
2419
  "confidence": "certain"
2420
2420
  },
@@ -2706,6 +2706,15 @@
2706
2706
  "origin": "js-text-assign",
2707
2707
  "confidence": "certain"
2708
2708
  },
2709
+ {
2710
+ "id": "b3d0e2af5176",
2711
+ "surface": "builders-hall",
2712
+ "text": "Choose a goal first.",
2713
+ "file": "modules/hall-ui/public/task.js",
2714
+ "line": 666,
2715
+ "origin": "js-notice",
2716
+ "confidence": "certain"
2717
+ },
2709
2718
  {
2710
2719
  "id": "1c6b0e3ffc0e",
2711
2720
  "surface": "builders-hall",
@@ -2999,7 +3008,7 @@
2999
3008
  "surface": "builders-hall",
3000
3009
  "text": "Confirm that verification for #{…} actually passed?",
3001
3010
  "file": "modules/hall-ui/public/task.js",
3002
- "line": 608,
3011
+ "line": 652,
3003
3012
  "origin": "js-notice",
3004
3013
  "confidence": "certain"
3005
3014
  },
@@ -3125,7 +3134,7 @@
3125
3134
  "surface": "builders-hall",
3126
3135
  "text": "Copy prompt",
3127
3136
  "file": "modules/hall-ui/public/task.js",
3128
- "line": 573,
3137
+ "line": 617,
3129
3138
  "origin": "js-markup",
3130
3139
  "confidence": "certain"
3131
3140
  },
@@ -3134,7 +3143,7 @@
3134
3143
  "surface": "builders-hall",
3135
3144
  "text": "Copy prompt",
3136
3145
  "file": "modules/hall-ui/public/task.js",
3137
- "line": 593,
3146
+ "line": 637,
3138
3147
  "origin": "js-text-assign",
3139
3148
  "confidence": "certain"
3140
3149
  },
@@ -3359,7 +3368,7 @@
3359
3368
  "surface": "builders-hall",
3360
3369
  "text": "Could not load",
3361
3370
  "file": "modules/hall-ui/public/task.js",
3362
- "line": 639,
3371
+ "line": 710,
3363
3372
  "origin": "js-text-assign",
3364
3373
  "confidence": "certain"
3365
3374
  },
@@ -3503,7 +3512,7 @@
3503
3512
  "surface": "builders-hall",
3504
3513
  "text": "Could not load {…}",
3505
3514
  "file": "modules/hall-ui/public/task.js",
3506
- "line": 632,
3515
+ "line": 703,
3507
3516
  "origin": "js-markup",
3508
3517
  "confidence": "certain"
3509
3518
  },
@@ -4610,7 +4619,7 @@
4610
4619
  "surface": "builders-hall",
4611
4620
  "text": "Full log",
4612
4621
  "file": "modules/hall-ui/public/task.js",
4613
- "line": 425,
4622
+ "line": 443,
4614
4623
  "origin": "js-markup",
4615
4624
  "confidence": "certain"
4616
4625
  },
@@ -5411,7 +5420,7 @@
5411
5420
  "surface": "builders-hall",
5412
5421
  "text": "Last claimed by",
5413
5422
  "file": "modules/hall-ui/public/task.js",
5414
- "line": 503,
5423
+ "line": 549,
5415
5424
  "origin": "js-markup",
5416
5425
  "confidence": "certain"
5417
5426
  },
@@ -5847,6 +5856,15 @@
5847
5856
  "origin": "html-text",
5848
5857
  "confidence": "certain"
5849
5858
  },
5859
+ {
5860
+ "id": "e52ae74f0052",
5861
+ "surface": "builders-hall",
5862
+ "text": "Move to goal",
5863
+ "file": "modules/hall-ui/public/task.js",
5864
+ "line": 510,
5865
+ "origin": "js-markup",
5866
+ "confidence": "certain"
5867
+ },
5850
5868
  {
5851
5869
  "id": "ae84ebe1d735",
5852
5870
  "surface": "builders-hall",
@@ -6072,6 +6090,15 @@
6072
6090
  "origin": "js-markup",
6073
6091
  "confidence": "certain"
6074
6092
  },
6093
+ {
6094
+ "id": "421ae031594e",
6095
+ "surface": "builders-hall",
6096
+ "text": "No open goals in this version",
6097
+ "file": "modules/hall-ui/public/task.js",
6098
+ "line": 509,
6099
+ "origin": "js-markup",
6100
+ "confidence": "certain"
6101
+ },
6075
6102
  {
6076
6103
  "id": "2a6d7552e21f",
6077
6104
  "surface": "builders-hall",
@@ -6185,7 +6212,7 @@
6185
6212
  "surface": "builders-hall",
6186
6213
  "text": "Not found",
6187
6214
  "file": "modules/hall-ui/public/task.js",
6188
- "line": 627,
6215
+ "line": 698,
6189
6216
  "origin": "js-text-assign",
6190
6217
  "confidence": "certain"
6191
6218
  },
@@ -6464,7 +6491,7 @@
6464
6491
  "surface": "builders-hall",
6465
6492
  "text": "Open in Sessions",
6466
6493
  "file": "modules/hall-ui/public/task.js",
6467
- "line": 425,
6494
+ "line": 443,
6468
6495
  "origin": "js-markup",
6469
6496
  "confidence": "certain"
6470
6497
  },
@@ -7022,7 +7049,7 @@
7022
7049
  "surface": "builders-hall",
7023
7050
  "text": "Read the handoff notes",
7024
7051
  "file": "modules/hall-ui/public/task.js",
7025
- "line": 423,
7052
+ "line": 441,
7026
7053
  "origin": "js-markup",
7027
7054
  "confidence": "certain"
7028
7055
  },
@@ -7733,7 +7760,7 @@
7733
7760
  "surface": "builders-hall",
7734
7761
  "text": "Session log{…}",
7735
7762
  "file": "modules/hall-ui/public/task.js",
7736
- "line": 429,
7763
+ "line": 447,
7737
7764
  "origin": "js-markup",
7738
7765
  "confidence": "certain"
7739
7766
  },
@@ -7841,7 +7868,7 @@
7841
7868
  "surface": "builders-hall",
7842
7869
  "text": "Ship it",
7843
7870
  "file": "modules/hall-ui/public/task.js",
7844
- "line": 559,
7871
+ "line": 603,
7845
7872
  "origin": "js-markup",
7846
7873
  "confidence": "certain"
7847
7874
  },
@@ -7859,7 +7886,7 @@
7859
7886
  "surface": "builders-hall",
7860
7887
  "text": "Shipped {…}{…}, {…}.",
7861
7888
  "file": "modules/hall-ui/public/task.js",
7862
- "line": 538,
7889
+ "line": 584,
7863
7890
  "origin": "js-markup",
7864
7891
  "confidence": "certain"
7865
7892
  },
@@ -8228,7 +8255,7 @@
8228
8255
  "surface": "builders-hall",
8229
8256
  "text": "Task {…}",
8230
8257
  "file": "modules/hall-ui/public/task.js",
8231
- "line": 436,
8258
+ "line": 454,
8232
8259
  "origin": "js-text-assign",
8233
8260
  "confidence": "certain"
8234
8261
  },
@@ -8237,7 +8264,7 @@
8237
8264
  "surface": "builders-hall",
8238
8265
  "text": "Task {…}",
8239
8266
  "file": "modules/hall-ui/public/task.js",
8240
- "line": 439,
8267
+ "line": 463,
8241
8268
  "origin": "js-markup",
8242
8269
  "confidence": "certain"
8243
8270
  },
@@ -8246,7 +8273,7 @@
8246
8273
  "surface": "builders-hall",
8247
8274
  "text": "Task {…}",
8248
8275
  "file": "modules/hall-ui/public/task.js",
8249
- "line": 620,
8276
+ "line": 691,
8250
8277
  "origin": "js-markup",
8251
8278
  "confidence": "certain"
8252
8279
  },
@@ -8255,7 +8282,7 @@
8255
8282
  "surface": "builders-hall",
8256
8283
  "text": "Task {…}",
8257
8284
  "file": "modules/hall-ui/public/task.js",
8258
- "line": 632,
8285
+ "line": 703,
8259
8286
  "origin": "js-markup",
8260
8287
  "confidence": "certain"
8261
8288
  },
@@ -8291,7 +8318,7 @@
8291
8318
  "surface": "builders-hall",
8292
8319
  "text": "Tasks in the ledger are numbered separately from GitHub pull requests, so this may be a PR number rather than a task. The work board lists everything that does exist.",
8293
8320
  "file": "modules/hall-ui/public/task.js",
8294
- "line": 620,
8321
+ "line": 691,
8295
8322
  "origin": "js-markup",
8296
8323
  "confidence": "certain"
8297
8324
  },
@@ -8390,7 +8417,7 @@
8390
8417
  "surface": "builders-hall",
8391
8418
  "text": "The adversarial panel. Each worker reports pass, concerns or fail on its own; the gating workers must all pass before confirmed becomes {…}, and one concerns falls the task through to the manual confirm path.",
8392
8419
  "file": "modules/hall-ui/public/task.js",
8393
- "line": 373,
8420
+ "line": 391,
8394
8421
  "origin": "js-markup",
8395
8422
  "confidence": "certain"
8396
8423
  },
@@ -8471,7 +8498,7 @@
8471
8498
  "surface": "builders-hall",
8472
8499
  "text": "The grading panel could not run when this task was shipped, so no quality signal was recorded — an outage, not a verdict.",
8473
8500
  "file": "modules/hall-ui/public/task.js",
8474
- "line": 355,
8501
+ "line": 373,
8475
8502
  "origin": "js-markup",
8476
8503
  "confidence": "certain"
8477
8504
  },
@@ -8750,7 +8777,7 @@
8750
8777
  "surface": "builders-hall",
8751
8778
  "text": "The task feed did not answer. Nothing on the record has changed, only the read failed. Try again in a moment, or go back.",
8752
8779
  "file": "modules/hall-ui/public/task.js",
8753
- "line": 632,
8780
+ "line": 703,
8754
8781
  "origin": "js-markup",
8755
8782
  "confidence": "certain"
8756
8783
  },
@@ -9011,7 +9038,7 @@
9011
9038
  "surface": "builders-hall",
9012
9039
  "text": "This task has shipped and its credits have landed. Neither Ship it nor Confirm is available.",
9013
9040
  "file": "modules/hall-ui/public/task.js",
9014
- "line": 568,
9041
+ "line": 612,
9015
9042
  "origin": "js-markup",
9016
9043
  "confidence": "certain"
9017
9044
  },
@@ -11216,7 +11243,7 @@
11216
11243
  "surface": "builders-hall",
11217
11244
  "text": "verdict {…}",
11218
11245
  "file": "modules/hall-ui/public/task.js",
11219
- "line": 377,
11246
+ "line": 395,
11220
11247
  "origin": "js-markup",
11221
11248
  "confidence": "certain"
11222
11249
  },
@@ -11432,7 +11459,7 @@
11432
11459
  "surface": "builders-hall",
11433
11460
  "text": "{…} awarded",
11434
11461
  "file": "modules/hall-ui/public/task.js",
11435
- "line": 420,
11462
+ "line": 438,
11436
11463
  "origin": "js-markup",
11437
11464
  "confidence": "certain"
11438
11465
  },
@@ -11576,7 +11603,7 @@
11576
11603
  "surface": "builders-hall",
11577
11604
  "text": "{…} sessions",
11578
11605
  "file": "modules/hall-ui/public/task.js",
11579
- "line": 428,
11606
+ "line": 446,
11580
11607
  "origin": "js-markup",
11581
11608
  "confidence": "certain"
11582
11609
  },
@@ -11756,7 +11783,7 @@
11756
11783
  "surface": "builders-hall",
11757
11784
  "text": "{…}, no such task",
11758
11785
  "file": "modules/hall-ui/public/task.js",
11759
- "line": 620,
11786
+ "line": 691,
11760
11787
  "origin": "js-markup",
11761
11788
  "confidence": "certain"
11762
11789
  },
@@ -11819,7 +11846,7 @@
11819
11846
  "surface": "builders-hall",
11820
11847
  "text": "{…}{…} · {…}",
11821
11848
  "file": "modules/hall-ui/public/task.js",
11822
- "line": 500,
11849
+ "line": 546,
11823
11850
  "origin": "js-markup",
11824
11851
  "confidence": "certain"
11825
11852
  },
package/docs/file-map.md CHANGED
@@ -185,7 +185,7 @@
185
185
  │ ├── box.js ← (#598/ADR 0031) per-builder dev box lifecycle CLI: provision·park·wake·deprovision·sweep-idle·sweep-dormant·status (dry-run default, --apply to act; rank-gated)
186
186
  │ ├── smoke-box.sh ← (#598) hermetic box-lifecycle smoke (no DB/DO/network): runs tests/boxes.mjs + tests/cf_tunnel.mjs (#771 named tunnel) + route-rank + module-load
187
187
  │ ├── recall.js ← `/recall` CLI: two-call progressive-disclosure search (summary → `--expand <id>`) over Bongos retrieval layer (ADR 0060); `node scripts/gds/recall.js <query>`
188
- │ ├── capture.js ← file an idea_inbox row from the CLI (the ideas.capture kernel port); backs the "capture a tangent" flow across skills. Task 1002927 / R151 taught it the GRADES: `--full --fields-file f.json` files a Full Idea, `--spark` is capture-then-mark (two requests — the route refuses a pre-marked capture), and after a plain quick capture it OFFERS to develop further. That offer is TTY-gated (`--no-prompt` too): capture.js is run by agents with no terminal and reads its body from stdin, so an unguarded prompt would hang every non-interactive caller
188
+ │ ├── capture.js ← file an idea_inbox row from the CLI (the ideas.capture kernel port); backs the "capture a tangent" flow across skills. Task 1002927 / R151 taught it the GRADES: `--full --fields-file f.json` files a Full Idea, `--spark` is capture-then-mark (two requests — the route refuses a pre-marked capture), and after a plain quick capture it OFFERS to develop further. That offer is TTY-gated (`--no-prompt` too): capture.js is run by agents with no terminal and reads its body from stdin, so an unguarded prompt would hang every non-interactive caller. Task 1003070 / R09 (goal 1000071) taught it capture-time ROUTING: `--goal N` sends goal_id, `--goal none` opts out explicitly, and with neither it defaults to the caller's active claim's goal, else prints a one-line R07 suggester hint
189
189
  │ ├── spark.js ← (tasks 1002920 / R144 + 1002921 / R145 + 1002922 / R146) the whole spark handoff from the terminal: mark one of your OWN quick ideas as a Big Idea Spark (`--undo` withdraws it), `--list` is the SPARK QUEUE, `--template` prints the five Full Idea fields, `--develop <id> --fields-file <json>` is the PICKUP (any builder; author + developer both recorded). Author-only server-side for the mark (it is the consent); its own command because sparking acts on an EXISTING idea, which is why capture.js refuses grade='spark'
190
190
  │ ├── stale-worktrees.js ← (task 1003365) the per-claim worktrees whose branch already landed on origin/main or is gone — pure git facts, no API; `--line` is the SessionStart probe, `/worktree-clean` walks the list through worktree.js remove
191
191
  │ ├── worktree-claim-guard.js ← (task 1002579) the PURE decision behind worktree-per-claim, enforced at claim time: claiming in the MAIN CHECKOUT is refused (escape hatch `--here` → loud warning), a linked worktree passes, an unclassifiable tree fails open. Stops the ship-time "the main checkout is on <branch>, not main" strand at its cause
@@ -1427,5 +1427,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
1427
1427
  landed since 1.19.493 with no explicit bump. run 33697218855. (task 1002620)
1428
1428
  1.19.495 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1429
1429
  landed since 1.19.494 with no explicit bump. run 33766103175. (task 1002620)
1430
+ 1.19.496 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1431
+ landed since 1.19.495 with no explicit bump. run 33766363834. (task 1002620)
1432
+ 1.19.497 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1433
+ landed since 1.19.496 with no explicit bump. run 33767382661. (task 1002620)
1430
1434
  ---------------------------------------------------------------------------
1431
1435
  ```
@@ -20,6 +20,25 @@
20
20
  //
21
21
  // Requires (go-live): the Message Content PRIVILEGED intent (to read message
22
22
  // text) + DISCORD_IDEAS_CHANNEL_ID. Inert until both are set.
23
+ //
24
+ // ROUTING HINTS, NEVER ROUTING (task 1003068, BV1.R12; goal 1000071 criterion
25
+ // C5, deliberately not C1/C3). Unlike #bugs (discord-bugs.js, R11), this vector
26
+ // never routes: a Discord author is outside the trust boundary (ADR 0016,
27
+ // ADR 0032), so their message may SUGGEST a goal but must never BIND one. The
28
+ // suggester (modules/lifecycle/goal-suggest.js, resolved through the same
29
+ // `lifecycle` port #bugs already consumes) supplies a candidate goal; its id
30
+ // rides onto the row as the advisory `suggested_goal_id` hint (core_222) and its
31
+ // `version_id` as `suggested_version` — both read-only guesses for /idea-triage
32
+ // (R13), never `goalId`, which is what would make captureIdea actually route.
33
+ //
34
+ // suggested_version DEFAULTS even without a goal match: when the classifier
35
+ // finds no confident goal, `suggested_version` falls back to whichever open
36
+ // version most recently started `status='building'` — the version a filer is
37
+ // overwhelmingly likely to mean when they post right now, absent any other
38
+ // signal. A matched goal's OWN version always wins over the default (the two
39
+ // travel together elsewhere in this goal — a routed capture binds to the
40
+ // goal's version, never a caller-supplied one), so the fallback only fires
41
+ // when there is truly nothing more specific to say.
23
42
 
24
43
  // captureIdea is no longer a doorway export (retired at BV1.R78 / ADR 0093 §5).
25
44
  // We resolve the `ideas.capture` kernel PORT the ideas module registers — the
@@ -31,6 +50,7 @@ const auth = require('./auth');
31
50
  const attachments = require('./bug-attachments');
32
51
  const { createMessageAggregator } = require('./discord-message-buffer');
33
52
  const { LIMITS } = api;
53
+ const log = api.logger('discord');
34
54
 
35
55
  // Auto-pairing window for #ideas (task #979) — same default as #bugs. Env
36
56
  // DISCORD_IDEAS_PAIR_WINDOW_MS overrides.
@@ -68,10 +88,37 @@ const realDeps = {
68
88
  // Lazy: resolve the `ideas.capture` port at CALL time, so the ideas module's
69
89
  // registration (at its route-factory mount) need not precede this module's load.
70
90
  captureIdea: (...a) => api.resolve('ideas.capture')(...a),
91
+ // resolveOptional, not resolve: a vanilla instance without the lifecycle
92
+ // module has no goals to suggest, and that is a reason to skip the hint —
93
+ // never a reason to lose the idea (the discord-bugs.js precedent).
94
+ suggestGoalForCapture: async (...a) => {
95
+ const lifecycle = api.resolveOptional && api.resolveOptional('lifecycle');
96
+ if (!lifecycle || !lifecycle.suggestGoalForCapture) return null;
97
+ return lifecycle.suggestGoalForCapture(...a);
98
+ },
99
+ // Same resolveOptional posture — a vanilla instance without lifecycle has no
100
+ // versions to default to, which is a reason to skip the default, never the idea.
101
+ listVersions: async () => {
102
+ const lifecycle = api.resolveOptional && api.resolveOptional('lifecycle');
103
+ if (!lifecycle || !lifecycle.listVersions) return [];
104
+ return lifecycle.listVersions();
105
+ },
71
106
  insertAuditLog: db.insertAuditLog,
72
107
  persistImages: attachments.persistImages,
73
108
  };
74
109
 
110
+ // mostRecentBuildingVersionId — PURE: the id of whichever `status='building'`
111
+ // version most recently started, or null. The suggested_version fallback for a
112
+ // filing whose classifier found no confident goal (see header). `started_at` is
113
+ // a timestamp string/Date; String() comparison is wrong for it, so this compares
114
+ // parsed dates explicitly rather than relying on default sort.
115
+ function mostRecentBuildingVersionId(versions) {
116
+ const building = (Array.isArray(versions) ? versions : []).filter((v) => v && v.status === 'building');
117
+ if (building.length === 0) return null;
118
+ building.sort((a, b) => new Date(b.started_at) - new Date(a.started_at));
119
+ return building[0].id;
120
+ }
121
+
75
122
  // File one inbound message as an idea. NEVER throws (the caller is a gateway
76
123
  // event handler). Returns a small status object for logging/tests.
77
124
  async function recordInboundIdea({ discordUserId, content, imageUrls = [], messageId }, deps = realDeps) {
@@ -114,12 +161,34 @@ async function recordInboundIdea({ discordUserId, content, imageUrls = [], messa
114
161
  }
115
162
  bodyMd = bodyMd.slice(0, LIMITS.DESCRIPTION);
116
163
 
164
+ // HINTS ONLY (see header). Neither lookup throws into the filing — a
165
+ // suggester/version-list outage must not cost the idea, only the hints.
166
+ let hint = null;
167
+ try {
168
+ hint = await deps.suggestGoalForCapture({ title: derived.title, body: derived.bodyMd });
169
+ } catch (e) {
170
+ log.warn({ err: e && e.message }, 'idea goal suggestion failed — filing without a goal hint');
171
+ }
172
+ let suggestedVersion = hint ? hint.version_id : null;
173
+ if (!suggestedVersion) {
174
+ try {
175
+ suggestedVersion = mostRecentBuildingVersionId(await deps.listVersions());
176
+ } catch (e) {
177
+ log.warn({ err: e && e.message }, 'default-version lookup failed — filing without a version hint');
178
+ }
179
+ }
180
+
117
181
  const idea = await deps.captureIdea({
118
182
  title: derived.title,
119
183
  bodyMd,
120
184
  capturedBy: builder ? builder.id : null,
121
185
  // kind left null → inbox.captureIdea auto-classifies. We never pass
122
186
  // 'criterion-proposal' (the rank-gated kind) from this path.
187
+ // suggestedGoalId/suggestedVersion are ADVISORY-ONLY columns — this call
188
+ // deliberately never names the routing parameter (see header + F12 in
189
+ // tests/filing_abuse_matrix.mjs, which greps this exact call for it).
190
+ suggestedGoalId: hint ? hint.id : null,
191
+ suggestedVersion,
123
192
  });
124
193
 
125
194
  await auditDiscordBot(deps, {
@@ -144,4 +213,7 @@ function createIdeaAggregator({ onFlush, windowMs = pairWindowMs(), timers = glo
144
213
  return createMessageAggregator({ onFlush, windowMs, timers });
145
214
  }
146
215
 
147
- module.exports = { deriveIdea, recordInboundIdea, createIdeaAggregator, pairWindowMs, realDeps };
216
+ module.exports = {
217
+ deriveIdea, recordInboundIdea, createIdeaAggregator, pairWindowMs, realDeps,
218
+ mostRecentBuildingVersionId,
219
+ };
@@ -66,6 +66,24 @@
66
66
  return data;
67
67
  }
68
68
 
69
+ // task 1003063 (R10): mutating call for the move-to-goal affordance. Same
70
+ // posture as postJSON — resolves to the parsed 2xx body, throws an Error
71
+ // carrying { status, data } on a non-2xx so the caller can read the server's
72
+ // own refusal sentence (ADR 0016 enforces; this only surfaces it).
73
+ async function patchJSON(path, body) {
74
+ const res = await api.request('PATCH', path, { body: body || {} });
75
+ if (res.status === 401) { toAuth(); throw new Error('redirecting to auth'); }
76
+ let data = null;
77
+ try { data = res.data; } catch (_) { /* empty / non-JSON body */ }
78
+ if (!res.ok) {
79
+ const err = new Error(`${path} → ${res.status}`);
80
+ err.status = res.status;
81
+ err.data = data || {};
82
+ throw err;
83
+ }
84
+ return data;
85
+ }
86
+
69
87
  // Map the server's typed lifecycle-error to a builder-readable line. Codes are
70
88
  // exactly those POST /tasks/:id/ship + /confirm can return. The server is the
71
89
  // authority (ADR 0016); the UI only surfaces the refusal.
@@ -430,11 +448,17 @@
430
448
  }
431
449
 
432
450
  function renderTask(t, lastClaim, viewer, goal, extras) {
433
- const { grade = null, sessionLogs = null, claims = null } = extras || {};
451
+ const { grade = null, sessionLogs = null, claims = null, goalsList = null } = extras || {};
434
452
  document.title = `#${t.id} — ${t.title || 'Task'} — ${document.title.split(' — ').pop()}`;
435
453
  const sub = $('#task-sub');
436
454
  if (sub) sub.textContent = `Task ${t.id}`;
437
455
 
456
+ // Metic+ is the floor for both the lifecycle supervisor controls below and
457
+ // the move-to-goal affordance (task 1003063 / R10) — hoisted so the two
458
+ // gates read the same rank once, rather than risking drift between copies.
459
+ const rank = (viewer && viewer.rank) ? String(viewer.rank).toLowerCase() : '';
460
+ const isSupervisor = rank === 'metic' || rank === 'archon';
461
+
438
462
  const parts = [];
439
463
  parts.push(`<p class="rec-kind">Task ${statusPill(t.status)}</p>`);
440
464
  parts.push(`<h1 class="rec-title" id="task-h"><span class="rec-title__id">${escapeHtml(String(t.id))}</span>${escapeHtml(t.title || 'Untitled task')}</h1>`);
@@ -473,6 +497,28 @@
473
497
  parts.push(`<p class="rec-goal">Goal <a href="/goals#/goal/${encodeURIComponent(t.goal_id)}">${escapeHtml(String(t.goal_id))}${goalName}</a>${catChip}</p>`);
474
498
  }
475
499
 
500
+ // task 1003063 (R10): the reassignment affordance — so a wrong routing pick
501
+ // is one PATCH away, never a re-file. Metic+ only (the server's own floor on
502
+ // PATCH /tasks/:id, ADR 0090); goalsList is fetched by load() scoped to this
503
+ // task's own version, because the server refuses a cross-version move
504
+ // (goal_version_mismatch, 409) and there is no point offering one.
505
+ if (isSupervisor && t.version_id) {
506
+ const openGoals = (Array.isArray(goalsList) ? goalsList : []).filter((g) => g.status !== 'archived');
507
+ const goalOpts = openGoals.length
508
+ ? openGoals.map((g) => `<option value="${escapeHtml(String(g.id))}"${String(g.id) === String(t.goal_id) ? ' selected' : ''}>${escapeHtml(String(g.id))} — ${escapeHtml(g.title || '')}</option>`).join('')
509
+ : '<option value="">No open goals in this version</option>';
510
+ parts.push(`
511
+ <div class="td-form" id="move-goal-form">
512
+ <label class="td-field td-field--wide"><span>Move to goal</span>
513
+ <select id="move-goal-select">${goalOpts}</select>
514
+ </label>
515
+ <div class="td-actions">
516
+ <button type="button" class="td-btn" id="move-goal-btn">Move</button>
517
+ </div>
518
+ </div>
519
+ `);
520
+ }
521
+
476
522
  const creditVal = window.OTBBranding?.taskCreditValue?.(t) ?? t.credits_reward;
477
523
  parts.push(figureBand([
478
524
  [window.OTBBranding?.taskCreditLabel?.(t) ?? curLabel(), creditVal],
@@ -550,8 +596,6 @@
550
596
  // toast. `confirmed → shipped` is the any-builder-supervisor / own-claim ship;
551
597
  // `completed → confirmed` is the Archon-only grader-bypass confirm (others see
552
598
  // a read-only note — the grader/Archon still has to act).
553
- const rank = (viewer && viewer.rank) ? String(viewer.rank).toLowerCase() : '';
554
- const isSupervisor = rank === 'metic' || rank === 'archon';
555
599
  const ownsLastClaim = !!(viewer && lastClaim && lastClaim.github_login &&
556
600
  lastClaim.github_login === viewer.github_login);
557
601
  let lifecycle = '';
@@ -610,6 +654,33 @@
610
654
  runLifecycle(confirmBtn, `${API}/tasks/${encodeURIComponent(t.id)}/confirm`, body, `#${t.id} confirmed.`);
611
655
  });
612
656
  }
657
+
658
+ // task 1003063 (R10): PATCH goal_id, then a full reload so the goal line, the
659
+ // picker's preselected option and everything else on the record re-render
660
+ // against the server's own state — no local patchwork of the DOM.
661
+ const moveGoalBtn = $('#move-goal-btn');
662
+ if (moveGoalBtn) {
663
+ moveGoalBtn.addEventListener('click', async () => {
664
+ const sel = $('#move-goal-select');
665
+ const goalId = sel ? Number(sel.value) : NaN;
666
+ if (!Number.isInteger(goalId) || goalId <= 0) { toast('Choose a goal first.'); return; }
667
+ const orig = moveGoalBtn.textContent;
668
+ moveGoalBtn.disabled = true;
669
+ moveGoalBtn.textContent = '…';
670
+ try {
671
+ await patchJSON(`${API}/tasks/${encodeURIComponent(t.id)}`, { goal_id: goalId });
672
+ toast(`#${t.id} moved to goal ${goalId}.`);
673
+ await load();
674
+ } catch (err) {
675
+ if (err && err.message === 'redirecting to auth') return;
676
+ // The server's own sentence, verbatim (a rank_forbidden 403 or a
677
+ // goal_version_mismatch 409 both say exactly what went wrong).
678
+ toast(errMessage(err && err.data) || `Could not move that task (${(err && err.status) || '?'}). Try again.`);
679
+ moveGoalBtn.disabled = false;
680
+ moveGoalBtn.textContent = orig;
681
+ }
682
+ });
683
+ }
613
684
  }
614
685
 
615
686
  // The two honest failures. Both keep the <h1> — the empty and error renders are
@@ -664,6 +735,18 @@
664
735
  goal = (g && g.goal) ? g.goal : null;
665
736
  } catch (_) { /* cosmetic — the goal link still resolves without it */ }
666
737
  }
738
+ // task 1003063 (R10): the move-to-goal picker's options, fetched only for
739
+ // the rank that can act on it and scoped to this task's own version (the
740
+ // server refuses a cross-version move). Best-effort: a failed read just
741
+ // means an empty picker, never a blocked page.
742
+ let goalsList = null;
743
+ const viewerRank = (viewer && viewer.rank) ? String(viewer.rank).toLowerCase() : '';
744
+ if ((viewerRank === 'metic' || viewerRank === 'archon') && d.task.version_id) {
745
+ try {
746
+ const gl = await getJSON(`${API}/goals?version=${encodeURIComponent(d.task.version_id)}`, { softAuth: true });
747
+ goalsList = Array.isArray(gl && gl.goals) ? gl.goals : [];
748
+ } catch (_) { /* cosmetic — the picker just has nothing to offer */ }
749
+ }
667
750
  // The repo bases a description's relative paths resolve onto (the primer's
668
751
  // and drachmae's pattern). Best-effort: without them a path passes through.
669
752
  try {
@@ -675,6 +758,7 @@
675
758
  grade: d.grade || null,
676
759
  sessionLogs: Array.isArray(d.session_logs) ? d.session_logs : null,
677
760
  claims: Array.isArray(d.claims) ? d.claims : null,
761
+ goalsList,
678
762
  });
679
763
  } catch (err) {
680
764
  if (err && err.code === AUTH_NEEDED) { toAuth(); return; }
@@ -240,6 +240,13 @@ async function captureIdea({
240
240
  // (a hall filing surface, a Discord classifier) can, and when it does the
241
241
  // goal's scope wall is applied to it verbatim. See ./routing.js.
242
242
  touches = null,
243
+ // task 1003068 (BV1.R12): a HINT-ONLY goal guess — NEVER fed to decideLanding,
244
+ // only recorded as the advisory suggested_goal_id column. `goalId` is a
245
+ // caller's routing DECISION; this is a machine guess with no such authority
246
+ // (the Discord #ideas classifier has no human behind it to have decided
247
+ // anything). Ignored when `goalId` is already set, so every existing caller
248
+ // is untouched.
249
+ suggestedGoalId = null,
243
250
  }, deps = {}) {
244
251
  if (!title || typeof title !== 'string') {
245
252
  throw { code: 'TITLE_REQUIRED' };
@@ -277,7 +284,7 @@ async function captureIdea({
277
284
  $7)
278
285
  RETURNING ${IDEA_COLS}`,
279
286
  [title, bodyMd, resolvedKind, suggestedVersion, suggestedPriority, capturedBy,
280
- goalId ?? null]
287
+ goalId ?? suggestedGoalId ?? null]
281
288
  );
282
289
  // A brand-new idea has no ideas_grades row, so it is quick by definition —
283
290
  // no join needed to know that. Grading it (R143's Full Idea capture) is a