@bongos/core 1.19.279 → 1.19.280
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 +11 -11
- package/.claude/skills/builder-claim/SKILL.md +5 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/claim.js +173 -16
- package/src/module-api.js +3 -1
- package/tests/claim_error_surface.mjs +55 -2
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.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.280",
|
|
6
|
+
"core_contract": "1.19.280",
|
|
7
|
+
"source_commit": "60f4ffc6a9e2ee27fd0325d8f332658692b5d954",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-21T20:
|
|
9
|
+
"built_at": "2026-08-21T20:22:42.788Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 319,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
19
|
"file_count": 1718,
|
|
20
|
-
"tree_sha256": "
|
|
20
|
+
"tree_sha256": "81c6c6bebc79cb9374d40181db1cb84c1bd035f0bd2c1062f261078df64042e8",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
{
|
|
43
43
|
"path": ".claude/skills/builder-claim/SKILL.md",
|
|
44
44
|
"mode": "0000644",
|
|
45
|
-
"sha256": "
|
|
45
|
+
"sha256": "057a5bac75084ea29e5dc3da1cf457089d77c882bfb34936f3c0b37889edc1c3"
|
|
46
46
|
},
|
|
47
47
|
{
|
|
48
48
|
"path": ".claude/skills/builder-connect/SKILL.md",
|
|
@@ -4497,12 +4497,12 @@
|
|
|
4497
4497
|
{
|
|
4498
4498
|
"path": "package-lock.json",
|
|
4499
4499
|
"mode": "0000644",
|
|
4500
|
-
"sha256": "
|
|
4500
|
+
"sha256": "db9e4dfef2e02b0e40fa3535f1af51d53071128b960698cf48b04fd6995eb54c"
|
|
4501
4501
|
},
|
|
4502
4502
|
{
|
|
4503
4503
|
"path": "package.json",
|
|
4504
4504
|
"mode": "0000644",
|
|
4505
|
-
"sha256": "
|
|
4505
|
+
"sha256": "99c8deb7b7edff1c7a5560a59dd9887aefa080786d3f2c9acd6c87c5b495c114"
|
|
4506
4506
|
},
|
|
4507
4507
|
{
|
|
4508
4508
|
"path": "public-docs/index.html",
|
|
@@ -4977,7 +4977,7 @@
|
|
|
4977
4977
|
{
|
|
4978
4978
|
"path": "scripts/gds/claim.js",
|
|
4979
4979
|
"mode": "0000644",
|
|
4980
|
-
"sha256": "
|
|
4980
|
+
"sha256": "3e830a3e319c4b67d87547ae4ad2d647ee3ce74725c84d7f64788c094f0974c7"
|
|
4981
4981
|
},
|
|
4982
4982
|
{
|
|
4983
4983
|
"path": "scripts/gds/classify-discipline.js",
|
|
@@ -6147,7 +6147,7 @@
|
|
|
6147
6147
|
{
|
|
6148
6148
|
"path": "src/module-api.js",
|
|
6149
6149
|
"mode": "0000644",
|
|
6150
|
-
"sha256": "
|
|
6150
|
+
"sha256": "afc9e2e6432d8a8f2e6a9812e92d1673f637c18b93555313c4881c9c44839914"
|
|
6151
6151
|
},
|
|
6152
6152
|
{
|
|
6153
6153
|
"path": "src/module-loader/catalog.js",
|
|
@@ -6582,7 +6582,7 @@
|
|
|
6582
6582
|
{
|
|
6583
6583
|
"path": "tests/claim_error_surface.mjs",
|
|
6584
6584
|
"mode": "0000644",
|
|
6585
|
-
"sha256": "
|
|
6585
|
+
"sha256": "33b0bbbb4c1a1f8b44a71c692a40b8a7e9cdde2e32483226484427ccf24238b5"
|
|
6586
6586
|
},
|
|
6587
6587
|
{
|
|
6588
6588
|
"path": "tests/claim_gate_open_blocker.mjs",
|
|
@@ -66,6 +66,11 @@ The server now **enforces** the one-claim half (task 1642): `POST /api/gds/claim
|
|
|
66
66
|
|
|
67
67
|
11. **On `session_already_has_active_claim`** (task 1642 — applies to **all ranks**): this *session* already holds an active claim, and a session may hold only one (one worktree → one branch → no commingled ships). The body names `active_task_id` + `active_task_title`. Surface them with `what_to_do`: either `/builder-ship N` (or `/builder-release N`) the in-flight task first, **or** open a fresh worktree/session for this new task and claim there. Do not retry in the same worktree — the refusal is the guard working. (`session_claim_check_failed` is the rare fail-closed variant when the server couldn't verify the session state; same remedy — retry, or use a fresh session/worktree.)
|
|
68
68
|
|
|
69
|
+
12. **On `GOAL_MEMBERSHIP_REQUIRED`** (task 1003137/1003138): the task lives in a goal the builder hasn't joined. The refusal is resolvable **in-session — never send the builder to the hall UI**. The body carries `goal_id`, `visibility`, and the `action` (`join` | `apply`); the CLI already renders the offer and, on a TTY, prompts `[Y/n]` itself. In an agent session (no TTY) the CLI prints the one-command path instead — narrate the same offer to the user:
|
|
70
|
+
- **`action: join`** (public goal): offer *"Task N lives in goal G ("title", public) — you're not a member. Join and claim?"* On yes, run `node scripts/gds/claim.js <task-id> --join` — it joins the goal and lands the claim in one command. On no, stop: declining leaves no membership row.
|
|
71
|
+
- **`action: apply`** (private goal, or a public goal with Archon-mediated protected scope): offer to file the application. On yes, run the same `--join` command — it files exactly one pending request and states plainly that **nothing can be claimed until an owner approves** (do not retry the claim; check the goal inbox later). Re-running reports "already pending", not an error. A private goal's refusal deliberately carries no title or scope — don't guess at them.
|
|
72
|
+
- **Never pass `--join` without the user's yes.** The owner chose the explicit-join model: the offer is a prompt, not a silent side effect.
|
|
73
|
+
|
|
69
74
|
## Constraints
|
|
70
75
|
|
|
71
76
|
- **Don't retry on conflict.** Failures here are intentional — they protect us from race conditions.
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.280",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.280",
|
|
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.
|
|
3
|
+
"version": "1.19.280",
|
|
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",
|
package/scripts/gds/claim.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Builder claim — atomically claim a task.
|
|
3
3
|
//
|
|
4
4
|
// Usage:
|
|
5
|
-
// node scripts/gds/claim.js <task-id> [--worktree NAME]
|
|
5
|
+
// node scripts/gds/claim.js <task-id> [--worktree NAME] [--enforce-touches] [--join]
|
|
6
6
|
//
|
|
7
7
|
// On success: prints the claim id, the task title, and the touches[] so the
|
|
8
8
|
// builder knows the parallel-safety boundary they're working within.
|
|
@@ -165,22 +165,31 @@ function printClaimCard(task, widgetsEnabled = true) {
|
|
|
165
165
|
} catch (_) { /* never block a claim on the card */ }
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
+
// task 1003138: the envelope/flat normalization from formatClaimFailure, pulled
|
|
169
|
+
// out so the goal-join flow reads the same { code, message, d } regardless of
|
|
170
|
+
// response shape. Pure + exported (tested through both consumers).
|
|
171
|
+
function claimErrorInfo(r) {
|
|
172
|
+
const errRaw = r && r.data ? r.data.error : undefined;
|
|
173
|
+
const isEnvelope = errRaw !== null && typeof errRaw === 'object';
|
|
174
|
+
return {
|
|
175
|
+
code: isEnvelope ? errRaw.code : errRaw,
|
|
176
|
+
message: isEnvelope ? errRaw.message : undefined,
|
|
177
|
+
// Structured fields live under .details in the envelope; fall back to the
|
|
178
|
+
// top-level body for the legacy flat shape (so both keep working).
|
|
179
|
+
d: { ...(r && r.data ? r.data : {}), ...((isEnvelope && errRaw.details) || {}) },
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
168
183
|
// task 1476: render a failed claim as GUIDANCE, not a raw object dump. The API
|
|
169
184
|
// returns the R01 error envelope (ADR 0116): { error: { code, message, details } }.
|
|
170
|
-
// Older paths returned a flat { error: 'CODE', ...fields } shape.
|
|
171
|
-
// BOTH — without it `r.data.error` is now an object (never === a
|
|
172
|
-
// branch below silently missed and the refusal printed as
|
|
173
|
-
// (no console / no process.exit) + exported so it can be
|
|
174
|
-
// real envelope shapes. Returns { lines, exit }.
|
|
185
|
+
// Older paths returned a flat { error: 'CODE', ...fields } shape. claimErrorInfo
|
|
186
|
+
// normalizes BOTH — without it `r.data.error` is now an object (never === a
|
|
187
|
+
// string), so every branch below silently missed and the refusal printed as
|
|
188
|
+
// `[object Object]`. Pure (no console / no process.exit) + exported so it can be
|
|
189
|
+
// unit-tested against the real envelope shapes. Returns { lines, exit }.
|
|
175
190
|
function formatClaimFailure(r, taskId) {
|
|
176
191
|
const lines = [];
|
|
177
|
-
const
|
|
178
|
-
const isEnvelope = errRaw !== null && typeof errRaw === 'object';
|
|
179
|
-
const code = isEnvelope ? errRaw.code : errRaw;
|
|
180
|
-
const message = isEnvelope ? errRaw.message : undefined;
|
|
181
|
-
// Structured fields live under .details in the envelope; fall back to the
|
|
182
|
-
// top-level body for the legacy flat shape (so both keep working).
|
|
183
|
-
const d = { ...(r && r.data ? r.data : {}), ...((isEnvelope && errRaw.details) || {}) };
|
|
192
|
+
const { code, message, d } = claimErrorInfo(r);
|
|
184
193
|
|
|
185
194
|
// #766: a generic onboarding stage gate (kept rendering cleanly, not as raw JSON).
|
|
186
195
|
if (code === 'stage_required') {
|
|
@@ -263,17 +272,147 @@ function formatClaimFailure(r, taskId) {
|
|
|
263
272
|
lines.push('Resolve it first (/blocker-solve N drives one to done; resolving auto-promotes its linked tasks) — or pick another task from /builder-start.');
|
|
264
273
|
return { lines, exit: 1 };
|
|
265
274
|
}
|
|
275
|
+
if (code === 'GOAL_MEMBERSHIP_REQUIRED') {
|
|
276
|
+
// task 1003138: the non-interactive rendering of the membership refusal —
|
|
277
|
+
// the interactive offer (runGoalJoinFlow) normally intercepts this code
|
|
278
|
+
// before it gets here; this branch covers a retry that re-refused, an
|
|
279
|
+
// unexpected body, or a decline.
|
|
280
|
+
lines.push(`Task #${taskId} lives in goal ${d.goal_id}${d.goal_title ? ` ("${d.goal_title}")` : ''}${d.visibility ? ` (${d.visibility})` : ''} — you are not a member.`);
|
|
281
|
+
lines.push(d.action === 'apply'
|
|
282
|
+
? ` Apply to join (POST /api/bongos/goals/${d.goal_id}/requests) — nothing can be claimed until an owner approves. Re-run with --join to file the application from here.`
|
|
283
|
+
: ` Join and claim in one command: node scripts/gds/claim.js ${taskId} --join`);
|
|
284
|
+
return { lines, exit: 1 };
|
|
285
|
+
}
|
|
266
286
|
// Fallback — still readable: the message + code, never a raw [object Object].
|
|
267
287
|
lines.push(`Claim failed (${r ? r.status : '?'})${code ? ` [${code}]` : ''}${message ? `: ${message}` : ''}.`);
|
|
268
288
|
return { lines, exit: 1 };
|
|
269
289
|
}
|
|
270
290
|
|
|
291
|
+
// task 1003138 (owner ask, verbatim: 'claude should offer to join so they dont
|
|
292
|
+
// need to go through the ui') — the goal-membership refusal (403
|
|
293
|
+
// GOAL_MEMBERSHIP_REQUIRED, the task-1003137 gate) is resolvable in-session.
|
|
294
|
+
// goalMembershipOfferPlan is the PURE half: given the refusal details it names
|
|
295
|
+
// the offer to make. Exported for unit tests; the I/O lives in runGoalJoinFlow.
|
|
296
|
+
// Returns null when the body is missing the fields the flow needs (falls back
|
|
297
|
+
// to formatClaimFailure), else { action, headerLines, question }.
|
|
298
|
+
function goalMembershipOfferPlan(d, taskId) {
|
|
299
|
+
if (!d || d.goal_id == null || (d.action !== 'join' && d.action !== 'apply')) return null;
|
|
300
|
+
const g = d.goal_id;
|
|
301
|
+
if (d.action === 'join') {
|
|
302
|
+
return {
|
|
303
|
+
action: 'join',
|
|
304
|
+
headerLines: [
|
|
305
|
+
`✗ Task ${taskId} lives in goal ${g}`,
|
|
306
|
+
...(d.goal_title ? [` "${d.goal_title}" (${d.visibility || 'public'})`] : []),
|
|
307
|
+
' You are not a member.',
|
|
308
|
+
],
|
|
309
|
+
question: `Join goal ${g} and claim? [Y/n] `,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
// 'apply' — a private goal, or a public one whose scope is Archon-mediated
|
|
313
|
+
// (the server folds both to apply; a pending request grants nothing yet).
|
|
314
|
+
return {
|
|
315
|
+
action: 'apply',
|
|
316
|
+
headerLines: d.visibility === 'private'
|
|
317
|
+
? [`✗ Goal ${g} is private.`]
|
|
318
|
+
: [
|
|
319
|
+
`✗ Task ${taskId} lives in goal ${g}${d.goal_title ? ` ("${d.goal_title}")` : ''} — joining needs approval (Archon-mediated scope).`,
|
|
320
|
+
],
|
|
321
|
+
question: 'Apply to join? [Y/n] ',
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// One yes/no on the real terminal (default yes — the owner-approved [Y/n]
|
|
326
|
+
// shape). Returns null when there is no TTY to ask — the caller then prints
|
|
327
|
+
// the --join re-run hint instead of hanging a non-interactive session.
|
|
328
|
+
async function promptYesNo(question) {
|
|
329
|
+
if (!process.stdin.isTTY) return null;
|
|
330
|
+
const readline = require('node:readline/promises');
|
|
331
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
332
|
+
try {
|
|
333
|
+
const a = (await rl.question(question)).trim().toLowerCase();
|
|
334
|
+
return a === '' || a === 'y' || a === 'yes';
|
|
335
|
+
} finally {
|
|
336
|
+
rl.close();
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// The I/O half of the offer. Outcomes: 'retry' (joined — re-POST the claim),
|
|
341
|
+
// 'stop' (declined / filed / blocked — everything already printed, exit 1),
|
|
342
|
+
// 'unhandled' (fall through to formatClaimFailure). Never joins or files
|
|
343
|
+
// without a yes: --join is the standing yes for non-interactive sessions.
|
|
344
|
+
async function runGoalJoinFlow({ api, d, taskId, assumeYes }) {
|
|
345
|
+
const plan = goalMembershipOfferPlan(d, taskId);
|
|
346
|
+
if (!plan) return 'unhandled';
|
|
347
|
+
const goalId = d.goal_id;
|
|
348
|
+
for (const line of plan.headerLines) console.error(line);
|
|
349
|
+
|
|
350
|
+
const yes = assumeYes ? true : await promptYesNo(plan.question);
|
|
351
|
+
if (yes === null) {
|
|
352
|
+
// Non-interactive (an agent session): make the one-command path explicit.
|
|
353
|
+
console.error(plan.action === 'join'
|
|
354
|
+
? ` To join goal ${goalId} and claim in one command: node scripts/gds/claim.js ${taskId} --join`
|
|
355
|
+
: ` To file the application from here: node scripts/gds/claim.js ${taskId} --join (nothing claimable until approved)`);
|
|
356
|
+
return 'stop';
|
|
357
|
+
}
|
|
358
|
+
if (!yes) {
|
|
359
|
+
// The owner chose the explicit-join model — declining leaves NO membership
|
|
360
|
+
// row and NO request.
|
|
361
|
+
console.error(plan.action === 'join'
|
|
362
|
+
? ' Declined — nothing joined, nothing claimed.'
|
|
363
|
+
: ' Declined — no application filed, nothing claimed.');
|
|
364
|
+
return 'stop';
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (plan.action === 'join') {
|
|
368
|
+
const jr = await api.goals.postGoalsIdJoin({ id: goalId, body: {} });
|
|
369
|
+
if (jr.status === 200 || jr.status === 201) {
|
|
370
|
+
console.error(`+ joined goal ${goalId} (member)`);
|
|
371
|
+
return 'retry';
|
|
372
|
+
}
|
|
373
|
+
const j = claimErrorInfo(jr);
|
|
374
|
+
if (j.code === 'archon_approval_required') {
|
|
375
|
+
// The third branch, handled honestly (routes/goals.js:615): a PUBLIC goal
|
|
376
|
+
// with protected scope is not self-joinable — don't loop, name the path.
|
|
377
|
+
console.error(`✗ Goal ${goalId}'s scope is Archon-mediated — self-join is not allowed.`);
|
|
378
|
+
console.error(` Request to join instead: node scripts/gds/claim.js ${taskId} --join (after the server re-offers apply), or ask an Archon to add you.`);
|
|
379
|
+
return 'stop';
|
|
380
|
+
}
|
|
381
|
+
if (j.code === 'goal_private') {
|
|
382
|
+
// Visibility flipped between refusal and join — report, don't loop.
|
|
383
|
+
console.error(`✗ Goal ${goalId} is private now — request to join (POST /api/bongos/goals/${goalId}/requests) and wait for approval.`);
|
|
384
|
+
return 'stop';
|
|
385
|
+
}
|
|
386
|
+
console.error(`✗ join failed (${jr.status})${j.code ? ` [${j.code}]` : ''}${j.message ? `: ${j.message}` : ''}.`);
|
|
387
|
+
return 'stop';
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// 'apply' — file the request; a pending row grants nothing, so never retry
|
|
391
|
+
// the claim here.
|
|
392
|
+
const ar = await api.goals.postGoalsIdRequests({ id: goalId, body: {} });
|
|
393
|
+
if (ar.status === 201) {
|
|
394
|
+
console.error('+ request filed — pending owner approval');
|
|
395
|
+
console.error(' (nothing to claim until approved)');
|
|
396
|
+
return 'stop';
|
|
397
|
+
}
|
|
398
|
+
const a = claimErrorInfo(ar);
|
|
399
|
+
if (a.code === 'request_pending') {
|
|
400
|
+
// The core_188 partial-unique: one pending ask per goal per builder —
|
|
401
|
+
// re-running reports, it does not error.
|
|
402
|
+
console.error('• already pending — your earlier request for this goal awaits owner approval.');
|
|
403
|
+
console.error(' (nothing to claim until approved)');
|
|
404
|
+
return 'stop';
|
|
405
|
+
}
|
|
406
|
+
console.error(`✗ application failed (${ar.status})${a.code ? ` [${a.code}]` : ''}${a.message ? `: ${a.message}` : ''}.`);
|
|
407
|
+
return 'stop';
|
|
408
|
+
}
|
|
409
|
+
|
|
271
410
|
async function main() {
|
|
272
411
|
await requireSession();
|
|
273
412
|
const args = process.argv.slice(2);
|
|
274
413
|
const taskId = Number(args[0]);
|
|
275
414
|
if (!Number.isFinite(taskId)) {
|
|
276
|
-
console.error('usage: node scripts/gds/claim.js <task-id> [--worktree NAME] [--enforce-touches]');
|
|
415
|
+
console.error('usage: node scripts/gds/claim.js <task-id> [--worktree NAME] [--enforce-touches] [--join]');
|
|
277
416
|
process.exit(2);
|
|
278
417
|
}
|
|
279
418
|
const wIdx = args.indexOf('--worktree');
|
|
@@ -287,10 +426,15 @@ async function main() {
|
|
|
287
426
|
// --enforce-touches so adjacency becomes a HARD refusal (it fans concurrent
|
|
288
427
|
// unsupervised agents that could clobber each other on shared files).
|
|
289
428
|
const enforceTouches = args.includes('--enforce-touches');
|
|
429
|
+
// task 1003138: --join is the standing yes to the goal-membership offer —
|
|
430
|
+
// join (public) or file the application (private/protected) and, for a join,
|
|
431
|
+
// retry the claim, all in one command. Without it an interactive session is
|
|
432
|
+
// asked [Y/n]; a non-TTY session is shown the --join re-run line.
|
|
433
|
+
const assumeYes = args.includes('--join');
|
|
290
434
|
const headSha = captureHeadSha();
|
|
291
435
|
|
|
292
436
|
const api = await cliClient();
|
|
293
|
-
const
|
|
437
|
+
const postClaim = () => api.claims.postClaims({
|
|
294
438
|
body: {
|
|
295
439
|
task_id: taskId,
|
|
296
440
|
worktree_name: worktree,
|
|
@@ -298,6 +442,19 @@ async function main() {
|
|
|
298
442
|
enforce_touches: enforceTouches,
|
|
299
443
|
},
|
|
300
444
|
});
|
|
445
|
+
let r = await postClaim();
|
|
446
|
+
// task 1003138: a goal-membership refusal (the task-1003137 gate) is
|
|
447
|
+
// resolvable in-session — offer the join/application, and on a successful
|
|
448
|
+
// join retry the claim ONCE (a second refusal falls through to the plain
|
|
449
|
+
// rendering; never loop).
|
|
450
|
+
if (r.status !== 201 && claimErrorInfo(r).code === 'GOAL_MEMBERSHIP_REQUIRED') {
|
|
451
|
+
const outcome = await runGoalJoinFlow({ api, d: claimErrorInfo(r).d, taskId, assumeYes });
|
|
452
|
+
if (outcome === 'retry') {
|
|
453
|
+
r = await postClaim();
|
|
454
|
+
} else if (outcome === 'stop') {
|
|
455
|
+
process.exit(1); // everything already printed; no claim landed
|
|
456
|
+
}
|
|
457
|
+
}
|
|
301
458
|
if (r.status === 201) {
|
|
302
459
|
const { claim, task } = r.data;
|
|
303
460
|
console.log(`Claimed task #${task.id}: ${task.title}`);
|
|
@@ -433,4 +590,4 @@ if (require.main === module) {
|
|
|
433
590
|
});
|
|
434
591
|
}
|
|
435
592
|
|
|
436
|
-
module.exports = { formatClaimFailure };
|
|
593
|
+
module.exports = { formatClaimFailure, claimErrorInfo, goalMembershipOfferPlan };
|
package/src/module-api.js
CHANGED
|
@@ -985,8 +985,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
985
985
|
// landed since 1.19.277 with no explicit bump. run 32508270995. (task 1002620)
|
|
986
986
|
// 1.19.279 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
987
987
|
// landed since 1.19.278 with no explicit bump. run 32521664476. (task 1002620)
|
|
988
|
+
// 1.19.280 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
989
|
+
// landed since 1.19.279 with no explicit bump. run 32523185119. (task 1002620)
|
|
988
990
|
// ---------------------------------------------------------------------------
|
|
989
|
-
const CORE_VERSION = '1.19.
|
|
991
|
+
const CORE_VERSION = '1.19.280'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
990
992
|
|
|
991
993
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
992
994
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -14,7 +14,7 @@ import { strict as assert } from 'node:assert';
|
|
|
14
14
|
import { createRequire } from 'node:module';
|
|
15
15
|
|
|
16
16
|
const require = createRequire(import.meta.url);
|
|
17
|
-
const { formatClaimFailure } = require('../scripts/gds/claim.js');
|
|
17
|
+
const { formatClaimFailure, claimErrorInfo, goalMembershipOfferPlan } = require('../scripts/gds/claim.js');
|
|
18
18
|
|
|
19
19
|
let passed = 0;
|
|
20
20
|
let failed = 0;
|
|
@@ -36,7 +36,7 @@ t('never dumps [object Object] for any known code', () => {
|
|
|
36
36
|
'session_already_has_bound_claim',
|
|
37
37
|
'newcomer_must_ship_first', 'ALREADY_CLAIMED', 'MODULE_CONFLICT',
|
|
38
38
|
'TOUCHES_CONFLICT', 'TASK_NOT_READY', 'TASK_NOT_FOUND', 'DEPS_NOT_SHIPPED',
|
|
39
|
-
'OPEN_BLOCKER',
|
|
39
|
+
'OPEN_BLOCKER', 'GOAL_MEMBERSHIP_REQUIRED',
|
|
40
40
|
'some_unmapped_future_code',
|
|
41
41
|
];
|
|
42
42
|
for (const c of codes) {
|
|
@@ -138,5 +138,58 @@ t('unknown code: fallback carries code + message, exit 1', () => {
|
|
|
138
138
|
assert.match(text(r), /boom/);
|
|
139
139
|
});
|
|
140
140
|
|
|
141
|
+
// ---- task 1003138: the goal-membership refusal is resolvable in-session ----
|
|
142
|
+
// The task-1003137 gate returns 403 GOAL_MEMBERSHIP_REQUIRED with { goal_id,
|
|
143
|
+
// visibility, action: join|apply, goal_title? }. claim.js offers the join (or
|
|
144
|
+
// files the application) inline; these pin the pure halves — the offer plan and
|
|
145
|
+
// the non-interactive fallthrough rendering.
|
|
146
|
+
|
|
147
|
+
t('GOAL_MEMBERSHIP_REQUIRED fallthrough: join names the one-command path, apply names the approval wait', () => {
|
|
148
|
+
const join = text(formatClaimFailure(
|
|
149
|
+
envelope('GOAL_MEMBERSHIP_REQUIRED', { goal_id: 1000045, visibility: 'public', action: 'join', goal_title: 'Public builder profiles' }, 'not a member', 403), 1002784));
|
|
150
|
+
assert.match(join, /goal 1000045/);
|
|
151
|
+
assert.match(join, /Public builder profiles/);
|
|
152
|
+
assert.match(join, /--join/, 'join path advertises the one-command re-run');
|
|
153
|
+
const apply = text(formatClaimFailure(
|
|
154
|
+
envelope('GOAL_MEMBERSHIP_REQUIRED', { goal_id: 1000099, visibility: 'private', action: 'apply' }, 'not a member', 403), 5));
|
|
155
|
+
assert.match(apply, /goal 1000099/);
|
|
156
|
+
assert.match(apply, /approv/i, 'apply path says approval gates the claim');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
t('goalMembershipOfferPlan: join offer matches the owner-approved shape (goal named, title shown, [Y/n])', () => {
|
|
160
|
+
const p = goalMembershipOfferPlan({ goal_id: 1000045, visibility: 'public', action: 'join', goal_title: 'Public builder profiles' }, 1002784);
|
|
161
|
+
assert.equal(p.action, 'join');
|
|
162
|
+
assert.match(p.headerLines[0], /Task 1002784 lives in goal 1000045/);
|
|
163
|
+
assert.match(p.headerLines[1], /"Public builder profiles" \(public\)/);
|
|
164
|
+
assert.match(p.headerLines[2], /not a member/i);
|
|
165
|
+
assert.equal(p.question, 'Join goal 1000045 and claim? [Y/n] ');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
t('goalMembershipOfferPlan: private goal offers apply and shows no title (there is none to leak)', () => {
|
|
169
|
+
const p = goalMembershipOfferPlan({ goal_id: 1000099, visibility: 'private', action: 'apply' }, 5);
|
|
170
|
+
assert.equal(p.action, 'apply');
|
|
171
|
+
assert.deepEqual(p.headerLines, ['✗ Goal 1000099 is private.']);
|
|
172
|
+
assert.equal(p.question, 'Apply to join? [Y/n] ');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
t('goalMembershipOfferPlan: public protected-scope goal offers apply and says why', () => {
|
|
176
|
+
const p = goalMembershipOfferPlan({ goal_id: 7, visibility: 'public', action: 'apply', goal_title: 'Core wall' }, 5);
|
|
177
|
+
assert.equal(p.action, 'apply');
|
|
178
|
+
assert.match(p.headerLines[0], /Archon-mediated/i);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
t('goalMembershipOfferPlan: a malformed body plans nothing (falls back to formatClaimFailure)', () => {
|
|
182
|
+
assert.equal(goalMembershipOfferPlan(null, 5), null);
|
|
183
|
+
assert.equal(goalMembershipOfferPlan({ action: 'join' }, 5), null, 'no goal_id → no plan');
|
|
184
|
+
assert.equal(goalMembershipOfferPlan({ goal_id: 7, action: 'teleport' }, 5), null, 'unknown action → no plan');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
t('claimErrorInfo: reads envelope and flat shapes identically', () => {
|
|
188
|
+
const env = claimErrorInfo(envelope('X', { a: 1 }, 'msg'));
|
|
189
|
+
assert.equal(env.code, 'X'); assert.equal(env.message, 'msg'); assert.equal(env.d.a, 1);
|
|
190
|
+
const flat = claimErrorInfo({ status: 409, data: { error: 'X', a: 1 } });
|
|
191
|
+
assert.equal(flat.code, 'X'); assert.equal(flat.d.a, 1);
|
|
192
|
+
});
|
|
193
|
+
|
|
141
194
|
console.log(`\nclaim_error_surface: ${passed} passed, ${failed} failed`);
|
|
142
195
|
process.exit(failed === 0 ? 0 : 1);
|