@bongos/core 1.19.680 → 1.19.681

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.bongos-core.json CHANGED
@@ -2,11 +2,11 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.680",
6
- "core_contract": "1.19.680",
7
- "source_commit": "e022c08bc3285327348cd482d38e0659e2ae8b3d",
5
+ "core_version": "1.19.681",
6
+ "core_contract": "1.19.681",
7
+ "source_commit": "e31ae913044e78d0b2b8202995eb551dad418a9e",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-12T01:35:27.332Z",
9
+ "built_at": "2026-09-12T02:05:45.659Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 476,
@@ -17,7 +17,7 @@
17
17
  "gate": "passed"
18
18
  },
19
19
  "file_count": 2624,
20
- "tree_sha256": "912d1481802f12085195156ce06ab53a39ca1e159441ebee3efe401ddbedf56d",
20
+ "tree_sha256": "f249c4df05688bdb744709b348940982241ccc686471029262564d9b79bdd140",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -322,7 +322,7 @@
322
322
  {
323
323
  "path": ".gitattributes",
324
324
  "mode": "0000644",
325
- "sha256": "83c94b055dc619fdbf3bc1142fc5357810d901e4200644f1fb399e6939571c5f"
325
+ "sha256": "72bc38b553e34bc652f0b5b5b2ab24999b4b11c2d6fc3699489e157fd0127a11"
326
326
  },
327
327
  {
328
328
  "path": "CLAUDE.md",
@@ -387,7 +387,7 @@
387
387
  {
388
388
  "path": "clients/bongos-client/index.d.ts",
389
389
  "mode": "0000644",
390
- "sha256": "ba2f6ea9be911785f06761110363357fc1257fb53dadcb51def6194d1f60c9e7"
390
+ "sha256": "101095af6616b30d3338ec12394343f360d8a583ef3da84f0dce49edc80630cb"
391
391
  },
392
392
  {
393
393
  "path": "clients/bongos-client/index.mjs",
@@ -1922,7 +1922,7 @@
1922
1922
  {
1923
1923
  "path": "docs/api/openapi.json",
1924
1924
  "mode": "0000644",
1925
- "sha256": "dbe27d28100bdbf007baefc90bfc93692b04fc8772019976292260617af175ac"
1925
+ "sha256": "b09110e4eec5225c0594e4f7a3bd3e18878f751ee4697af87209568d69483c3a"
1926
1926
  },
1927
1927
  {
1928
1928
  "path": "docs/architecture.md",
@@ -2797,7 +2797,7 @@
2797
2797
  {
2798
2798
  "path": "docs/module-api-changelog.md",
2799
2799
  "mode": "0000644",
2800
- "sha256": "592be55bbe17442266b4478ca962fcc0bdc4cbd66718c6b4af13a1cca911cc3e"
2800
+ "sha256": "244cffb195592c644ad06262ed1a0f610e7744fd62e634a35c5cddd954c9def6"
2801
2801
  },
2802
2802
  {
2803
2803
  "path": "docs/modules-contract.md",
@@ -5732,7 +5732,7 @@
5732
5732
  {
5733
5733
  "path": "modules/lifecycle/github-push.js",
5734
5734
  "mode": "0000644",
5735
- "sha256": "d50b04376a9725bae0f8fe9c81166224eb47242655fec49f33f89b0f87b555f0"
5735
+ "sha256": "1a84e2225c58969b7bc6c43643921278966c78a6049488deb6c39048982ae005"
5736
5736
  },
5737
5737
  {
5738
5738
  "path": "modules/lifecycle/goal-advisory.js",
@@ -5947,7 +5947,7 @@
5947
5947
  {
5948
5948
  "path": "modules/lifecycle/routes/tasks.js",
5949
5949
  "mode": "0000644",
5950
- "sha256": "366cb9c60952539ab8a946f8cf757754163b6cd6764a6430777b4529c86c6a75"
5950
+ "sha256": "456b1d53d2349a499e1b07897dc6e16c72d26156a71347382c5a2508dc94d28d"
5951
5951
  },
5952
5952
  {
5953
5953
  "path": "modules/lifecycle/routes/version-route-authz.js",
@@ -7777,12 +7777,12 @@
7777
7777
  {
7778
7778
  "path": "package-lock.json",
7779
7779
  "mode": "0000644",
7780
- "sha256": "f0fd6366abe9523c64332de417406bfe0ab6f6872abc6e651debbe5f1941cad0"
7780
+ "sha256": "fd92e4204b64289d8404adb924c9142b47c10b0a6c73d709583cbbad01cd6e0a"
7781
7781
  },
7782
7782
  {
7783
7783
  "path": "package.json",
7784
7784
  "mode": "0000644",
7785
- "sha256": "60cff4653b02fd38c3c7fb8673cb423a57f8360955578a6a345ccaef9bf1ac9a"
7785
+ "sha256": "242ae038fbdb23f8d375f04e794c87100df9df4c250b93942ca4eaed82017ca3"
7786
7786
  },
7787
7787
  {
7788
7788
  "path": "public-docs/index.html",
@@ -8332,7 +8332,7 @@
8332
8332
  {
8333
8333
  "path": "scripts/gds/git-merge-regen.js",
8334
8334
  "mode": "0000644",
8335
- "sha256": "f6f9c93faeb589d07105027655e42bd842d8d766119812b16bd534d58aa50816"
8335
+ "sha256": "9896dc201aaa7186838fe4dcecf5226fdb0736b1b7094a98649e2f8fcbfc4d1a"
8336
8336
  },
8337
8337
  {
8338
8338
  "path": "scripts/gds/go-live.js",
@@ -9542,7 +9542,7 @@
9542
9542
  {
9543
9543
  "path": "src/module-api.js",
9544
9544
  "mode": "0000644",
9545
- "sha256": "19014335bfaede6fba2e6ff79ba1fe746a2149345e25448b2ed0176bdb8a6649"
9545
+ "sha256": "b06c931aa58a7997f7733e13d2850d290540647c4a7d243cc07d27c20d5b812e"
9546
9546
  },
9547
9547
  {
9548
9548
  "path": "src/module-loader/catalog.js",
@@ -10212,7 +10212,7 @@
10212
10212
  {
10213
10213
  "path": "tests/conflict_resolve.mjs",
10214
10214
  "mode": "0000644",
10215
- "sha256": "7ad7be450b9350e7888f075944888ddf2a0e2d57a23568f410a9ff17bd340dec"
10215
+ "sha256": "e36410c3288ec8c38c22bd5fd285d7abc6e0cadc4433ce87028bb7f9c7d54806"
10216
10216
  },
10217
10217
  {
10218
10218
  "path": "tests/connections_api.mjs",
@@ -10642,7 +10642,7 @@
10642
10642
  {
10643
10643
  "path": "tests/git_merge_regen.mjs",
10644
10644
  "mode": "0000644",
10645
- "sha256": "c6cb1bda6809d8d384a9c35b376995af038d18c26b1b935cb5c0faebb163b72e"
10645
+ "sha256": "52f02bc7f0da86b29d4b39da78a34f9677f7bdd93e24a4e5365ffb1af64fc413"
10646
10646
  },
10647
10647
  {
10648
10648
  "path": "tests/github_push_ancestry.mjs",
@@ -12192,7 +12192,7 @@
12192
12192
  {
12193
12193
  "path": "tests/publish_branch_route.mjs",
12194
12194
  "mode": "0000644",
12195
- "sha256": "485b2b965bcf44a515571de43851c42a32f278dcdd02510f0d3cd9f6ac53e61c"
12195
+ "sha256": "f688f5da9bfc392ce203ee3b89dfa9ba23fd66ca4ebac603f6fb6faedb83b5c7"
12196
12196
  },
12197
12197
  {
12198
12198
  "path": "tests/publish_gate.mjs",
package/.gitattributes CHANGED
@@ -18,6 +18,7 @@
18
18
  *.bash text eol=lf
19
19
  .husky/pre-commit text eol=lf
20
20
  .husky/pre-push text eol=lf
21
+ .husky/post-merge text eol=lf
21
22
 
22
23
  # Node / Python sources: keep LF everywhere too. Node tolerates CRLF, but LF
23
24
  # keeps diffs clean and avoids surprises when these are sourced or piped.
@@ -57,8 +58,13 @@
57
58
  # publish-reconciler runs a temp-clone merge with this driver registered, so a PR
58
59
  # that conflicts only on these files lands without a human (GitHub's own auto-merge
59
60
  # can't run a custom driver, hence the server does the merge locally then pushes).
60
- # NB: the TOP-LEVEL CLAUDE.md is hand-written and deliberately NOT listed here
61
- # only the nested CLAUDE.md docs that carry a generated block are. scripts/gds/CLAUDE.md
61
+ # NB (task 1001431): the TOP-LEVEL CLAUDE.md IS listed now. It used to be excluded as
62
+ # "hand-written", and that was true until ADR 0062 §8 gave its §13 a generated
63
+ # BEGIN/END SESSION-LOG SNIPPET block — after which two sessions each adding a session-log
64
+ # file conflicted there on every merge with nothing to heal it. It is the same shape as
65
+ # docs/file-map.md (hand-written prose + a generated block), not the whole-file shape, so
66
+ # the driver 3-way-merges the prose and re-renders only the snippet; a genuine conflict in
67
+ # the prose still falls through to a normal git conflict. scripts/gds/CLAUDE.md
62
68
  # LEFT that set in task 1003548 (its ~40k-char block was 76% of a file loaded on every
63
69
  # visit to that directory; docs/repo-map.md still carries the full index), so its entry
64
70
  # came out with it — a path listed here whose generator no longer owns it would route a
@@ -72,6 +78,14 @@
72
78
  # appear in a merge and these two driver entries never fire. They are KEPT — harmless —
73
79
  # as a transition backstop for any in-flight branch that still carries a committed copy;
74
80
  # once none remain they can be dropped. The other four entries stay load-bearing.
81
+ # The LEADING SLASH is load-bearing: a gitattributes pattern with no slash matches at ANY
82
+ # depth, so a bare `CLAUDE.md` would also capture all 22 modules/*/CLAUDE.md and
83
+ # scripts/gds/CLAUDE.md — files the driver does NOT know (they are not in NESTED_DIRS, so
84
+ # REGENERABLE has no rule for them). The driver would then exit 1 on each and FORCE a
85
+ # conflict git would otherwise have merged cleanly — the exact 'routed through a driver
86
+ # that cannot regenerate it' failure this file warns about above. `/CLAUDE.md` anchors to
87
+ # the repo root. Verified with `git check-attr merge -- modules/economy/CLAUDE.md`.
88
+ /CLAUDE.md merge=otb-regen
75
89
  docs/repo-map.md merge=otb-regen
76
90
  docs/file-map.md merge=otb-regen
77
91
  docs/session-log-index.md merge=otb-regen
@@ -384,7 +384,6 @@ export interface PostTasksIdMergeRequest { into_task_id: number; reason?: string
384
384
  export interface PostTasksIdOverrideRequestRequest { rationale: string }
385
385
  export interface PostTasksIdOverrideRequestResponse { ok: boolean; request: unknown }
386
386
  export interface PostTasksIdPublishBranchRequest { branch: string; head_sha: string; bundle_b64: string; value_summary?: string }
387
- export interface PostTasksIdPublishBranchResponse { ok: boolean; pr_url: unknown; pr_number: unknown; head_sha: unknown; auto_merge: unknown }
388
387
  export interface PostTasksIdShipResponse { ok: boolean; task: unknown }
389
388
  export interface PostTasksIdVisualRequest { image_b64: string; content_type: string; alt?: string }
390
389
  export interface PostTasksIdVisualResponse { ok: boolean; visual_url: unknown; visual_alt: unknown; bytes: unknown }
@@ -1203,7 +1202,7 @@ export interface BongosClient {
1203
1202
  /** POST /tasks/{id}/promote — rank: metic+archon */
1204
1203
  postTasksIdPromote(args?: RequestArgs): Promise<ApiResponse>;
1205
1204
  /** POST /tasks/{id}/publish-branch — rank: any-builder */
1206
- postTasksIdPublishBranch(args: RequestArgs & { body: PostTasksIdPublishBranchRequest }): Promise<PostTasksIdPublishBranchResponse>;
1205
+ postTasksIdPublishBranch(args: RequestArgs & { body: PostTasksIdPublishBranchRequest }): Promise<ApiResponse>;
1207
1206
  /** GET /tasks/{id}/publish-status — rank: any-builder */
1208
1207
  getTasksIdPublishStatus(args?: RequestArgs): Promise<ApiResponse>;
1209
1208
  /** GET /tasks/{id}/recommendations — rank: any-builder */
@@ -15704,14 +15704,7 @@
15704
15704
  },
15705
15705
  "responses": {
15706
15706
  "200": {
15707
- "description": "Success.",
15708
- "content": {
15709
- "application/json": {
15710
- "schema": {
15711
- "$ref": "#/components/schemas/PostTasksIdPublishBranchResponse"
15712
- }
15713
- }
15714
- }
15707
+ "description": "Success."
15715
15708
  },
15716
15709
  "400": {
15717
15710
  "$ref": "#/components/responses/ValidationFailed"
@@ -22442,25 +22435,6 @@
22442
22435
  ],
22443
22436
  "additionalProperties": false
22444
22437
  },
22445
- "PostTasksIdPublishBranchResponse": {
22446
- "type": "object",
22447
- "properties": {
22448
- "ok": {
22449
- "type": "boolean"
22450
- },
22451
- "pr_url": {},
22452
- "pr_number": {},
22453
- "head_sha": {},
22454
- "auto_merge": {}
22455
- },
22456
- "required": [
22457
- "ok",
22458
- "pr_url",
22459
- "pr_number",
22460
- "head_sha",
22461
- "auto_merge"
22462
- ]
22463
- },
22464
22438
  "PostTasksIdShipResponse": {
22465
22439
  "type": "object",
22466
22440
  "properties": {
@@ -23019,8 +22993,8 @@
23019
22993
  }
23020
22994
  },
23021
22995
  "x-endpoint-count": 363,
23022
- "x-schema-count": 402,
22996
+ "x-schema-count": 401,
23023
22997
  "x-undocumented-bodies": 9,
23024
- "x-response-schemas": 271,
22998
+ "x-response-schemas": 270,
23025
22999
  "x-generated-by": "scripts/gds/gen-api-docs.js"
23026
23000
  }
@@ -1819,5 +1819,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
1819
1819
  landed since 1.19.678 with no explicit bump. run 34657605820. (task 1002620)
1820
1820
  1.19.680 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1821
1821
  landed since 1.19.679 with no explicit bump. run 34665216511. (task 1002620)
1822
+ 1.19.681 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1823
+ landed since 1.19.680 with no explicit bump. run 34666648354. (task 1002620)
1822
1824
  ---------------------------------------------------------------------------
1823
1825
  ```
@@ -513,8 +513,21 @@ async function openOrFindPullRequest({ branch, title, body }, deps = {}) {
513
513
  // without auto-merge enabled, or a PR already mergeable, returns false and the
514
514
  // caller surfaces the open PR (exactly like ciLand on a failed enable).
515
515
  async function enableAutoMerge({ nodeId }, deps = {}) {
516
+ return (await enableAutoMergeDetailed({ nodeId }, deps)).ok;
517
+ }
518
+
519
+ // enableAutoMergeDetailed — the same arming call, but it says WHY it failed
520
+ // (task 1001431). enableAutoMerge collapses every failure to `false`, so the route
521
+ // answered a builder with `auto_merge: false` and no cause, and the single most
522
+ // common cause is not an outage at all: a repository with `allow_auto_merge` OFF
523
+ // can never be armed, so every publish reports the same silent false forever and
524
+ // the PR waits for a human nobody told. Returns { ok, reason } — reason is null on
525
+ // success, else a short scrubbed string safe to hand the (ownership-gated) builder.
526
+ // enableAutoMerge stays the boolean façade so existing callers are untouched.
527
+ async function enableAutoMergeDetailed({ nodeId }, deps = {}) {
516
528
  const token = await resolveToken(deps);
517
- if (!token || !nodeId) return false;
529
+ if (!token) return { ok: false, reason: 'no GitHub token could be resolved' };
530
+ if (!nodeId) return { ok: false, reason: 'the pull request has no node id to arm' };
518
531
  const doFetch = deps.fetchImpl || fetch;
519
532
  const query = `mutation($id:ID!){enablePullRequestAutoMerge(input:{pullRequestId:$id,mergeMethod:MERGE}){clientMutationId}}`;
520
533
  try {
@@ -523,11 +536,15 @@ async function enableAutoMerge({ nodeId }, deps = {}) {
523
536
  headers: { ...ghHeaders(token), 'Content-Type': 'application/json' },
524
537
  body: JSON.stringify({ query, variables: { id: nodeId } }),
525
538
  });
526
- if (!res.ok) return false;
539
+ if (!res.ok) return { ok: false, reason: `GitHub GraphQL returned HTTP ${res.status}` };
527
540
  const out = await res.json();
528
- return !out.errors;
529
- } catch (_) {
530
- return false;
541
+ if (!out || !out.errors) return { ok: true, reason: null };
542
+ // GitHub answers a repo with auto-merge disabled with a plain GraphQL error;
543
+ // its own message is the actionable one, so pass it through (truncated).
544
+ const msg = out.errors.map((e) => e && e.message).filter(Boolean).join('; ');
545
+ return { ok: false, reason: scrubCredential(msg || 'GitHub GraphQL rejected the mutation').slice(0, 300) };
546
+ } catch (e) {
547
+ return { ok: false, reason: scrubCredential((e && e.message) || 'the GraphQL request threw').slice(0, 300) };
531
548
  }
532
549
  }
533
550
 
@@ -1388,6 +1405,7 @@ module.exports = {
1388
1405
  pushBundle,
1389
1406
  openOrFindPullRequest,
1390
1407
  enableAutoMerge,
1408
+ enableAutoMergeDetailed,
1391
1409
  getPublishStatus,
1392
1410
  getMergeState,
1393
1411
  mergeIfGreen,
@@ -67,6 +67,33 @@ const { MIGRATION_KEYWORD_RE, VALID_FITS, badKind, deHashRefs, detectsMigration,
67
67
  // task 1003208: structured logging (pino via the doorway) — was console.*.
68
68
  const log = api.logger('lifecycle');
69
69
  const registerTaskWriteRoutes = require('./task-write-routes.js'); // task 1003206 — POST /tasks + PATCH /tasks/:id
70
+ // The body of the generic `publish_failed` 500 from POST /tasks/:id/publish-branch.
71
+ // Pure and exported so the response a builder actually receives is testable without
72
+ // standing up the route (the file's existing convention — see rankAttestsGateTrust).
73
+ //
74
+ // task 1001431 added `code` + `detail`. Seven error codes get their own mapped response
75
+ // above the fallthrough; every OTHER code github-push can throw — APP_TOKEN_FAILED,
76
+ // GRAPHQL_FAILED, BAD_PR, PR_PATCH_FAILED, STATUS_FETCH_FAILED — arrived here and was
77
+ // dropped on the floor, so the builder saw a bare `publish_failed` with nothing to act
78
+ // on and no way to tell an expired App token from a GraphQL outage. The code is an
79
+ // internal constant rather than user data, and this route is ownership-gated (claim
80
+ // holder or Archon) and already returns scrubbed git stderr — a short code name is
81
+ // strictly less revealing than what it ships beside.
82
+ //
83
+ // Every field stays OPTIONAL and keeps its existing name and meaning: ship.js prints
84
+ // `reason`, and older readers must not start seeing a key change shape under them.
85
+ function publishFailedDetail(e = {}) {
86
+ return {
87
+ ...(e.code ? { code: e.code } : {}),
88
+ ...(e.detail ? { detail: e.detail } : {}),
89
+ ...(e.git_step ? { step: e.git_step } : {}),
90
+ ...(e.stderr ? { reason: e.stderr } : {}),
91
+ ...(e.stderr_bytes != null ? { reason_bytes: e.stderr_bytes } : {}),
92
+ ...(e.exit_code != null ? { git_exit_code: e.exit_code } : {}),
93
+ ...(e.timed_out ? { timed_out: true } : {}),
94
+ };
95
+ }
96
+
70
97
  module.exports = function buildTasksRouter() {
71
98
  const router = express.Router();
72
99
 
@@ -1076,7 +1103,11 @@ module.exports = function buildTasksRouter() {
1076
1103
  `Merging triggers .github/workflows/deploy-prod.yml -> ${branding().domains.publicOrigin}. ` +
1077
1104
  `Auto-merge is enabled — GitHub lands it once the required checks pass.`;
1078
1105
  const pr = await githubPush.openOrFindPullRequest({ branch, title, body });
1079
- const autoMerge = await githubPush.enableAutoMerge({ nodeId: pr.node_id });
1106
+ // task 1001431: ask WHY when arming fails. `auto_merge: false` on its own told the
1107
+ // builder nothing, and the commonest cause is a repo with allow_auto_merge OFF —
1108
+ // a permanent condition that reads exactly like a transient blip.
1109
+ const autoMergeResult = await githubPush.enableAutoMergeDetailed({ nodeId: pr.node_id });
1110
+ const autoMerge = autoMergeResult.ok;
1080
1111
  // task 1139 (ADR 0058): attest the author's trust tier on the pushed sha so the
1081
1112
  // gate-review check can auto-approve a Metic+ author's gate-surface PR with no
1082
1113
  // owner click. Only the SERVER can set this (it holds req.builder's server-
@@ -1098,6 +1129,8 @@ module.exports = function buildTasksRouter() {
1098
1129
  }
1099
1130
  return res.status(201).json({
1100
1131
  ok: true, pr_url: pr.url, pr_number: pr.number, head_sha: pushedSha, auto_merge: autoMerge,
1132
+ // Only present when arming failed — the publish itself still succeeded (201).
1133
+ ...(autoMerge ? {} : { auto_merge_reason: autoMergeResult.reason }),
1101
1134
  });
1102
1135
  } catch (e) {
1103
1136
  if (e.code === 'TIP_MISMATCH') {
@@ -1134,13 +1167,7 @@ module.exports = function buildTasksRouter() {
1134
1167
  // `reason` keeps its name and position so existing readers (ship.js prints it)
1135
1168
  // keep working; it is simply no longer amputated. Everything here is already
1136
1169
  // credential-scrubbed at the throw site by github-push.scrubCredential.
1137
- return res.fail('publish_failed', 500, {
1138
- ...(e.git_step ? { step: e.git_step } : {}),
1139
- ...(e.stderr ? { reason: e.stderr } : {}),
1140
- ...(e.stderr_bytes != null ? { reason_bytes: e.stderr_bytes } : {}),
1141
- ...(e.exit_code != null ? { git_exit_code: e.exit_code } : {}),
1142
- ...(e.timed_out ? { timed_out: true } : {}),
1143
- });
1170
+ return res.fail('publish_failed', 500, publishFailedDetail(e));
1144
1171
  } finally {
1145
1172
  try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) { /* best-effort */ }
1146
1173
  }
@@ -1336,6 +1363,7 @@ module.exports.MIGRATION_KEYWORD_RE = MIGRATION_KEYWORD_RE;
1336
1363
  // #871: exported so tests/task_ownership_gate.mjs can drive the claim-ownership
1337
1364
  // gate directly (with a stubbed db.getLastClaimHolder) — no server/DB needed.
1338
1365
  module.exports.gateTaskOwnership = gateTaskOwnership;
1366
+ module.exports.publishFailedDetail = publishFailedDetail;
1339
1367
  // task 1053 (F): exported so the verify-attestation guard is unit-tested.
1340
1368
  module.exports.isVerifyTask = isVerifyTask;
1341
1369
  // task 1139 / 1169 (ADR 0058): exported so the pure rank-attestation decision
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.680",
3
+ "version": "1.19.681",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.680",
9
+ "version": "1.19.681",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.680",
3
+ "version": "1.19.681",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -16,6 +16,13 @@
16
16
  // mergeMainIntoBranch self-heal so the heal runs even where the driver isn't
17
17
  // installed per-clone.
18
18
  //
19
+ // THE DRIVER ONLY FIRES ON A CONFLICT. A merge that lands CLEAN still leaves the
20
+ // generated docs describing the pre-merge tree — new symbols, a new session log and a
21
+ // new skill all arrive without conflicting. Three things close that: ship.js (at ship
22
+ // time), the server's conflict-resolve.js step 7b (for a PR it resolves), and
23
+ // `.husky/post-merge` (task 1001431) for the bare local `git merge` / `git pull` that
24
+ // neither of those covers.
25
+ //
19
26
  // SAFETY: it regenerates the GENERATED content and 3-way-merges the hand-written
20
27
  // parts of nested docs; it succeeds ONLY if no conflict markers remain. A genuine
21
28
  // hand-written conflict falls through to a normal git conflict — it never invents a
@@ -61,7 +68,13 @@ const REPO_ROOT = path.resolve(__dirname, '..', '..');
61
68
  // branches that both touch hall/status/landing copy regenerate them differently
62
69
  // and conflict on ordinary text; they join the whole-file group for the same
63
70
  // reason session-log-index.md did.
71
+ // task 1001431: the ROOT CLAUDE.md joined too. It is mostly hand-written, but §13 carries
72
+ // the bounded BEGIN/END GENERATED SESSION-LOG SNIPPET block (ADR 0062 §8) — so two sessions
73
+ // that each add a session-log file regenerate that block differently and ALWAYS conflict
74
+ // there, and nothing healed it: the driver did not know the file, and `merge=union` would be
75
+ // wrong for prose. Same shape as docs/file-map.md (3-way-merge the prose, re-render the block).
64
76
  const REGENERABLE = new Set([
77
+ 'CLAUDE.md',
65
78
  'docs/repo-map.md',
66
79
  'docs/file-map.md',
67
80
  'docs/session-log-index.md',
@@ -124,6 +137,22 @@ function regenerateResolved(relpath, { base = '', ours = '', theirs = '' } = {})
124
137
  return { content: CI.renderReport(CI.buildRegistry()), ok: true };
125
138
  }
126
139
 
140
+ // The ROOT CLAUDE.md (task 1001431): hand-written prose carrying ONE generated block, the
141
+ // §13 session-log snippet. 3-way-merge the prose, then re-render the snippet from the
142
+ // session-log files on disk. Must be handled BEFORE the nested-CLAUDE.md fallthrough below,
143
+ // which derives a directory by stripping `/CLAUDE.md` — for the root path that strips
144
+ // nothing and would ask gen-repo-map for a symbol block for a directory named "CLAUDE.md".
145
+ // It also needs none of the code-symbol model, so it sits with the other pre-buildModel rules.
146
+ // Same provisionality caveat as the whole-file rules above: collectEntries() scans the
147
+ // working tree, which during a merge is still ours, so the block this writes describes our
148
+ // side. That is what makes the merge COMPLETE; the post-merge regeneration (conflict-resolve
149
+ // step 7b / ship-preflight-steps' resolveInWorktree) is what makes it CORRECT.
150
+ if (relpath === 'CLAUDE.md') {
151
+ const merged = threeWayMerge(base, ours, theirs);
152
+ const out = SI.injectSnippet(merged, SI.renderSnippet(SI.collectEntries()));
153
+ return { content: out, ok: !hasMarkers(out) };
154
+ }
155
+
127
156
  const model = G.buildModel();
128
157
 
129
158
  // docs/repo-map.md is fully generated — ignore all three sides, rebuild it.
package/src/module-api.js CHANGED
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
71
71
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
72
72
  // the entry to that file. Look for a version's history there, not here.
73
73
  // ---------------------------------------------------------------------------
74
- const CORE_VERSION = '1.19.680'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.681'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
75
75
 
76
76
  // A namespaced logger so a module's log lines are attributable + consistent.
77
77
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -554,6 +554,11 @@ test('1003546 every REGENERABLE artifact has a producer, and every producer is i
554
554
  ['docs/repo-map.md', 'scripts/gds/gen-repo-map.js'],
555
555
  ['docs/file-map.md', 'scripts/gds/gen-file-map.js'],
556
556
  ['docs/session-log-index.md', 'scripts/gds/gen-session-index.js'],
557
+ // task 1001431 — the ROOT CLAUDE.md joined REGENERABLE for its §13 snippet block.
558
+ // gen-session-index.js writes BOTH the (gitignored) whole index and that tracked
559
+ // snippet, and it is already in GENERATOR_SCRIPTS, so the server's post-merge
560
+ // regeneration covers the new entry with no change to the generator list.
561
+ ['CLAUDE.md', 'scripts/gds/gen-session-index.js'],
557
562
  [CI.REGISTRY_REL, 'scripts/gds/copy-inventory.js'],
558
563
  [CI.REPORT_REL, 'scripts/gds/copy-inventory.js'],
559
564
  ...G.NESTED_DIRS.map((d) => [`${d}/CLAUDE.md`, 'scripts/gds/gen-repo-map.js']),
@@ -45,6 +45,7 @@ const repo = path.resolve(here, '..');
45
45
  const regen = require('../scripts/gds/git-merge-regen.js');
46
46
  const G = require('../scripts/gds/gen-repo-map.js');
47
47
  const CI = require('../scripts/gds/copy-inventory.js');
48
+ const SI = require('../scripts/gds/gen-session-index.js'); // task 1001431 — the root CLAUDE.md's §13 snippet
48
49
 
49
50
  const MARKERS = /^(<{7}|={7}|>{7})/m;
50
51
 
@@ -120,10 +121,67 @@ function withBlockBody(src, body) {
120
121
 
121
122
  // ── 5) a non-generated path is never in scope ───────────────────────────────────
122
123
  {
123
- assert.equal(regen.REGENERABLE.has('CLAUDE.md'), false, 'top-level CLAUDE.md is hand-written — never auto-healed');
124
124
  assert.equal(regen.REGENERABLE.has('scripts/gds/box.js'), false, 'source files are never auto-healed');
125
125
  const r = regen.regenerateResolved('scripts/gds/box.js', { base: 'a', ours: 'b', theirs: 'c' });
126
126
  assert.equal(r.content, null, 'a non-generated path resolves to null content');
127
+
128
+ // REVERSAL (task 1001431). This block used to assert the opposite —
129
+ // REGENERABLE.has('CLAUDE.md') === false, 'top-level CLAUDE.md is hand-written'
130
+ // — and that was correct when it was written. ADR 0062 §8 then gave the root
131
+ // CLAUDE.md a generated §13 BEGIN/END SESSION-LOG SNIPPET block, which made the
132
+ // premise false without anything noticing: two sessions each adding a session-log
133
+ // file regenerate that block differently and conflict there on EVERY merge, and no
134
+ // layer healed it (not the driver, not ship.js, not the server resolver).
135
+ assert.equal(regen.REGENERABLE.has('CLAUDE.md'), true,
136
+ 'the root CLAUDE.md carries a generated §13 snippet block and IS healed (task 1001431)');
137
+ }
138
+
139
+ // ── 5b) the root CLAUDE.md heals: prose 3-way-merged, snippet re-rendered ────────
140
+ // It is the docs/file-map.md shape (hand-written prose + a generated block), NOT the
141
+ // whole-file shape — so a conflict in the PROSE must still fall through to a human.
142
+ {
143
+ const realRoot = fs.readFileSync(path.join(repo, 'CLAUDE.md'), 'utf8');
144
+ const freshSnippet = SI.renderSnippet(SI.collectEntries());
145
+
146
+ // A conflict confined to the generated block heals, and the block comes out equal to
147
+ // a fresh render regardless of what either side had in it.
148
+ const mangle = (tag) => SI.injectSnippet(realRoot, `${SI.BEGIN}\n- ${tag}\n${SI.END}`);
149
+ const healed = regen.regenerateResolved('CLAUDE.md', {
150
+ base: realRoot, ours: mangle('OURS entry'), theirs: mangle('THEIRS entry'),
151
+ });
152
+ assert.equal(healed.ok, true, 'a conflict inside the generated snippet must heal');
153
+ assert.ok(!MARKERS.test(healed.content), 'no conflict markers survive the heal');
154
+ assert.ok(healed.content.includes(freshSnippet), 'the snippet is re-rendered from the session-log files');
155
+ assert.ok(!healed.content.includes('OURS entry') && !healed.content.includes('THEIRS entry'),
156
+ 'neither side\'s stale snippet content survives');
157
+
158
+ // Hand-written PROSE that genuinely diverges is NOT auto-resolved.
159
+ const firstNL = realRoot.indexOf('\n');
160
+ const proseConflict = regen.regenerateResolved('CLAUDE.md', {
161
+ base: realRoot,
162
+ ours: '# OURS heading' + realRoot.slice(firstNL),
163
+ theirs: '# THEIRS heading' + realRoot.slice(firstNL),
164
+ });
165
+ assert.equal(proseConflict.ok, false, 'a real prose conflict must fall through to a human');
166
+ }
167
+
168
+ // ── 5c) the .gitattributes pattern for the root CLAUDE.md is ROOT-ANCHORED ───────
169
+ // `/CLAUDE.md` vs `CLAUDE.md` is the whole ballgame: a gitattributes pattern with no
170
+ // slash matches at ANY depth, so the bare spelling would also capture the 22
171
+ // modules/*/CLAUDE.md files and scripts/gds/CLAUDE.md — none of which are in
172
+ // NESTED_DIRS, so the driver has no rule for them, exits 1, and FORCES a conflict git
173
+ // would otherwise merge cleanly. Asserted through git itself, not by reading the file,
174
+ // because the pattern semantics are git's and not ours to restate.
175
+ {
176
+ const attr = (p) => {
177
+ const r = spawnSync('git', ['check-attr', 'merge', '--', p], { cwd: repo, encoding: 'utf8' });
178
+ return (r.stdout || '').trim();
179
+ };
180
+ assert.match(attr('CLAUDE.md'), /otb-regen$/, 'the root CLAUDE.md must get the driver');
181
+ for (const p of ['modules/economy/CLAUDE.md', 'scripts/gds/CLAUDE.md']) {
182
+ assert.doesNotMatch(attr(p), /otb-regen/,
183
+ `${p} is not regenerable — the root pattern must be anchored so it is not captured`);
184
+ }
127
185
  }
128
186
 
129
187
  // ── 6) END-TO-END: a real `git merge` invokes the driver and auto-resolves ──────
@@ -193,7 +251,11 @@ function withBlockBody(src, body) {
193
251
  .filter((line) => line && !line.startsWith('#'))
194
252
  .map((line) => line.split(/\s+/))
195
253
  .filter((tokens) => tokens.slice(1).includes('merge=otb-regen'))
196
- .map((tokens) => tokens[0]);
254
+ // Strip a leading `/`: in a gitattributes pattern that is a ROOT ANCHOR, not part of
255
+ // the path (task 1001431 — `/CLAUDE.md` must not also match modules/*/CLAUDE.md). The
256
+ // driver receives git's %P, which never carries it, so REGENERABLE keys are unanchored
257
+ // and the two lists are compared as the PATH sets they are.
258
+ .map((tokens) => tokens[0].replace(/^\//, ''));
197
259
  assert.ok(otbRegenPaths.length > 0, 'expected at least one merge=otb-regen line in .gitattributes');
198
260
  for (const p of otbRegenPaths) {
199
261
  assert.ok(regen.REGENERABLE.has(p), `.gitattributes declares merge=otb-regen for ${p} but REGENERABLE (git-merge-regen.js) does not know it`);
@@ -194,6 +194,63 @@ await t('enableAutoMerge: posts the graphql mutation; false on a graphql error',
194
194
  assert.equal(await gp.enableAutoMerge({ nodeId: null }, { token: 'TT', fetchImpl: okFetch }), false, 'no node id → false');
195
195
  });
196
196
 
197
+ // ---- publishFailedDetail: the generic 500 names its cause (task 1001431) --
198
+ await t('publishFailedDetail: threads the error code so publish_failed is never bare', async () => {
199
+ const D = buildTasksRouter.publishFailedDetail;
200
+
201
+ // The regression this closes: a code with no git_step/stderr used to produce {}, so the
202
+ // builder received `publish_failed` and nothing else — an expired App token and a
203
+ // GraphQL outage were indistinguishable.
204
+ assert.deepEqual(D({ code: 'APP_TOKEN_FAILED' }), { code: 'APP_TOKEN_FAILED' });
205
+ assert.deepEqual(D({ code: 'GRAPHQL_FAILED', detail: '[{"type":"FORBIDDEN"}]' }),
206
+ { code: 'GRAPHQL_FAILED', detail: '[{"type":"FORBIDDEN"}]' });
207
+
208
+ // Existing field names + meanings are unchanged — ship.js prints `reason`.
209
+ assert.deepEqual(
210
+ D({ code: 'GIT_FAILED', git_step: 'push', stderr: 'Permission denied', stderr_bytes: 17, exit_code: 128, timed_out: true }),
211
+ { code: 'GIT_FAILED', step: 'push', reason: 'Permission denied', reason_bytes: 17, git_exit_code: 128, timed_out: true }
212
+ );
213
+
214
+ // Absent fields stay absent rather than serializing as null/undefined keys.
215
+ assert.deepEqual(D({}), {});
216
+ assert.deepEqual(D(), {});
217
+ assert.equal('timed_out' in D({ timed_out: false }), false, 'a falsy timed_out is omitted, not sent as false');
218
+ assert.equal('git_exit_code' in D({ exit_code: 0 }), true, 'exit_code 0 is real and must survive');
219
+ });
220
+
221
+ // ---- enableAutoMergeDetailed: says WHY it could not arm (task 1001431) ----
222
+ await t('enableAutoMergeDetailed: reports the GitHub message instead of a bare false', async () => {
223
+ const okFetch = async () => jsonRes(200, { data: { enablePullRequestAutoMerge: {} } });
224
+ assert.deepEqual(await gp.enableAutoMergeDetailed({ nodeId: 'N1' }, { token: 'TT', fetchImpl: okFetch }),
225
+ { ok: true, reason: null });
226
+
227
+ // The real-world case: a repository with allow_auto_merge OFF. GitHub answers with a
228
+ // plain GraphQL error, and its own message is the actionable one.
229
+ const offFetch = async () => jsonRes(200, { errors: [{ message: 'Auto-merge is not allowed for this repository' }] });
230
+ const off = await gp.enableAutoMergeDetailed({ nodeId: 'N1' }, { token: 'TT', fetchImpl: offFetch });
231
+ assert.equal(off.ok, false);
232
+ assert.match(off.reason, /Auto-merge is not allowed/, 'the repo-level cause reaches the caller');
233
+
234
+ const httpFetch = async () => jsonRes(502, {});
235
+ const http = await gp.enableAutoMergeDetailed({ nodeId: 'N1' }, { token: 'TT', fetchImpl: httpFetch });
236
+ assert.equal(http.ok, false);
237
+ assert.match(http.reason, /HTTP 502/, 'an HTTP failure is distinguishable from a rejection');
238
+
239
+ const thrown = await gp.enableAutoMergeDetailed({ nodeId: 'N1' }, {
240
+ token: 'TT', fetchImpl: async () => { throw new Error('socket hang up'); },
241
+ });
242
+ assert.equal(thrown.ok, false);
243
+ assert.match(thrown.reason, /socket hang up/);
244
+
245
+ assert.equal((await gp.enableAutoMergeDetailed({ nodeId: null }, { token: 'TT', fetchImpl: okFetch })).ok, false);
246
+ assert.match((await gp.enableAutoMergeDetailed({ nodeId: null }, { token: 'TT', fetchImpl: okFetch })).reason,
247
+ /no node id/, 'a missing node id is named, not silently false');
248
+
249
+ // The boolean façade keeps its exact contract for every existing caller.
250
+ assert.equal(await gp.enableAutoMerge({ nodeId: 'N1' }, { token: 'TT', fetchImpl: okFetch }), true);
251
+ assert.equal(await gp.enableAutoMerge({ nodeId: 'N1' }, { token: 'TT', fetchImpl: offFetch }), false);
252
+ });
253
+
197
254
  // ---- getPublishStatus: the three states -----------------------------------
198
255
  await t('getPublishStatus: open PR → pending; merged + deploy success → success', async () => {
199
256
  const mergeSha = 'c'.repeat(40);