@bongos/core 1.19.390 → 1.19.391
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 +23 -13
- package/.claude/skills/builder-claim/SKILL.md +8 -2
- package/docs/file-map.md +2 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/claim.js +37 -4
- package/scripts/gds/worktree-claim-guard.js +104 -0
- package/src/module-api.js +3 -1
- package/tests/worktree_claim_guard.mjs +105 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
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.391",
|
|
6
|
+
"core_contract": "1.19.391",
|
|
7
|
+
"source_commit": "340dd8cf2dc62375b464f1deb56fa203cb37e47a",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-28T01:
|
|
9
|
+
"built_at": "2026-08-28T01:59:37.546Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 339,
|
|
13
13
|
"agent_docs_stubbed": 22,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1624,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1985,
|
|
20
|
+
"tree_sha256": "5e47c4db4acc79aeebd5d9582aa0d5bc78dfac50ac7fceab7fb64269a6c0f76b",
|
|
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": "248ba892c506cb2ba5787dd9bc11978623d7116e9eb911e79d9c6b33b2d7374e"
|
|
46
46
|
},
|
|
47
47
|
{
|
|
48
48
|
"path": ".claude/skills/builder-connect/SKILL.md",
|
|
@@ -1792,7 +1792,7 @@
|
|
|
1792
1792
|
{
|
|
1793
1793
|
"path": "docs/file-map.md",
|
|
1794
1794
|
"mode": "0000644",
|
|
1795
|
-
"sha256": "
|
|
1795
|
+
"sha256": "4da98da4c1d5bc5e2a1c1949775f362d8867faa5a2a0321ccd954dc0a5b71202"
|
|
1796
1796
|
},
|
|
1797
1797
|
{
|
|
1798
1798
|
"path": "docs/handoff-template.md",
|
|
@@ -5122,12 +5122,12 @@
|
|
|
5122
5122
|
{
|
|
5123
5123
|
"path": "package-lock.json",
|
|
5124
5124
|
"mode": "0000644",
|
|
5125
|
-
"sha256": "
|
|
5125
|
+
"sha256": "fa2614ac06762df93cebbd6d3b31ee9aa8610187156003199b894dedb875587c"
|
|
5126
5126
|
},
|
|
5127
5127
|
{
|
|
5128
5128
|
"path": "package.json",
|
|
5129
5129
|
"mode": "0000644",
|
|
5130
|
-
"sha256": "
|
|
5130
|
+
"sha256": "7ba34a8faeb6cf944e2603900caf3f203dc032a1a1eae04b87b93cf5a28cc293"
|
|
5131
5131
|
},
|
|
5132
5132
|
{
|
|
5133
5133
|
"path": "public-docs/index.html",
|
|
@@ -5707,7 +5707,7 @@
|
|
|
5707
5707
|
{
|
|
5708
5708
|
"path": "scripts/gds/claim.js",
|
|
5709
5709
|
"mode": "0000644",
|
|
5710
|
-
"sha256": "
|
|
5710
|
+
"sha256": "d2710556d771f5dcc7298806c91f3a33c7ad16b9f7738eef7edd14b7868d143b"
|
|
5711
5711
|
},
|
|
5712
5712
|
{
|
|
5713
5713
|
"path": "scripts/gds/classify-discipline.js",
|
|
@@ -6729,6 +6729,11 @@
|
|
|
6729
6729
|
"mode": "0000644",
|
|
6730
6730
|
"sha256": "be4fec619d5a0c76ba05bcc75042c1497f70da21b54a3d646884e2228f90b2ba"
|
|
6731
6731
|
},
|
|
6732
|
+
{
|
|
6733
|
+
"path": "scripts/gds/worktree-claim-guard.js",
|
|
6734
|
+
"mode": "0000644",
|
|
6735
|
+
"sha256": "1ad4dfc90a2c5cd8afe40cd43c6b3d119e9bb7710785e024b4dd8d45e7ab823f"
|
|
6736
|
+
},
|
|
6732
6737
|
{
|
|
6733
6738
|
"path": "scripts/gds/worktree.js",
|
|
6734
6739
|
"mode": "0000644",
|
|
@@ -7112,7 +7117,7 @@
|
|
|
7112
7117
|
{
|
|
7113
7118
|
"path": "src/module-api.js",
|
|
7114
7119
|
"mode": "0000644",
|
|
7115
|
-
"sha256": "
|
|
7120
|
+
"sha256": "b5e645e16766a357b39033daec49842b257e6ce4450718d2f4e76667e4c6f304"
|
|
7116
7121
|
},
|
|
7117
7122
|
{
|
|
7118
7123
|
"path": "src/module-loader/catalog.js",
|
|
@@ -9909,6 +9914,11 @@
|
|
|
9909
9914
|
"mode": "0000644",
|
|
9910
9915
|
"sha256": "43c7c42ffe92143443c45d5ba382d522efa0a16ef14f58675e6a86233e3f8e74"
|
|
9911
9916
|
},
|
|
9917
|
+
{
|
|
9918
|
+
"path": "tests/worktree_claim_guard.mjs",
|
|
9919
|
+
"mode": "0000644",
|
|
9920
|
+
"sha256": "28662ce24931e2623c8d54e6931d80f400715a6661a6e78d638f7e8cdaa6db87"
|
|
9921
|
+
},
|
|
9912
9922
|
{
|
|
9913
9923
|
"path": "tests/worktree_disambiguation.mjs",
|
|
9914
9924
|
"mode": "0000644",
|
|
@@ -9,7 +9,7 @@ You are claiming a task on behalf of the current builder. The claim is atomic
|
|
|
9
9
|
|
|
10
10
|
## What this skill does
|
|
11
11
|
|
|
12
|
-
Runs `node scripts/gds/claim.js <task-id> [--worktree NAME]`.
|
|
12
|
+
Runs `node scripts/gds/claim.js <task-id> [--worktree NAME]`. Before it hits the API at all, the CLI enforces **worktree-per-claim locally** (task 1002579 — see below). Then it hits `POST /api/gds/claims`, which transactionally:
|
|
13
13
|
|
|
14
14
|
1. Verifies the task exists and is `ready`.
|
|
15
15
|
2. Verifies all declared `dependencies[]` are in `shipped` status (V3.R02 / migration 020).
|
|
@@ -37,7 +37,13 @@ The server now **enforces** the one-claim half (task 1642): `POST /api/gds/claim
|
|
|
37
37
|
|
|
38
38
|
1. **Confirm the task id with the user** if they didn't give a number directly.
|
|
39
39
|
|
|
40
|
-
2. **
|
|
40
|
+
2. **Be in a per-claim worktree, not the main checkout.** `claim.js` **refuses** (exit 2) when it is run in the primary worktree — worktree-per-claim is the project default (task 1642) and task 1002579 is the check behind it. Branching in the main checkout parks it off `main`, and `ship.js`'s local auto-merge then has nowhere to merge INTO: the ship strands at `confirmed` until the server reconciler or `/merge-mode` catches it. The refusal names the exact fix; run it and claim from the new tree:
|
|
41
|
+
```
|
|
42
|
+
node scripts/gds/worktree.js add <task-id>
|
|
43
|
+
```
|
|
44
|
+
A linked worktree claims normally, and a tree git can't classify is allowed through (the guard never blocks on a guess). The escape hatch — for a builder who genuinely means to work in the main checkout — is `--here`, which downgrades the refusal to a loud warning and accepts the `confirmed`/`/merge-mode` finish.
|
|
45
|
+
|
|
46
|
+
**Determine the worktree name** (cross-platform). The worktree name is the leaf folder of the current path. Resolve it portably with node (works on Windows/macOS/Linux — no `basename`/`$PWD` shell-isms):
|
|
41
47
|
```
|
|
42
48
|
node -e "console.log(require('path').basename(process.cwd()))"
|
|
43
49
|
```
|
package/docs/file-map.md
CHANGED
|
@@ -152,7 +152,7 @@
|
|
|
152
152
|
│ ├── discord-channels.js ← Archon CLI: plan/apply the Discord channel layout via the API (ADR 0036, #727)
|
|
153
153
|
│ ├── setup.js ← one-time GitHub Device Flow → ~/.config/otb/gds-session.json
|
|
154
154
|
│ ├── start.js ← list claimable tasks + active claims (`/builder-start`)
|
|
155
|
-
│ ├── claim.js ← atomic POST /api/gds/claims (`/builder-claim`); prints the claim-time context pack incl. next-free ADR/migration numbers (V4.R26); printDisciplineMode() reads discipline-modes.json to route a claimed task to its playbook skill
|
|
155
|
+
│ ├── claim.js ← atomic POST /api/gds/claims (`/builder-claim`); runs the worktree-per-claim guard (worktree-claim-guard.js) BEFORE the session check and the POST; prints the claim-time context pack incl. next-free ADR/migration numbers (V4.R26); printDisciplineMode() reads discipline-modes.json to route a claimed task to its playbook skill
|
|
156
156
|
│ ├── discipline-modes.json ← discipline→operating-playbook map read by claim.js: ideator→/ideate, artist→/paint, engineer→/dev; absence of an entry = the default build-and-ship loop, fail-open ([#1267](https://example.com/builders#/task/1267))
|
|
157
157
|
│ ├── next-number.js ← deterministic next-free ADR + migration number from origin/main ∪ local working tree; supersedes the drift-prone claim-time DB counter (V4.R26, [#964](https://example.com/builders#/task/964)). `node scripts/gds/next-number.js [--json]`
|
|
158
158
|
│ ├── gen-repo-map.js ← Path B (ADR 0063, [#1224](https://example.com/builders#/task/1224)): zero-dep, deterministic symbol-skeleton generator → docs/repo-map.md + the generated factual block in each nested CLAUDE.md; ranks exported symbols by reference frequency. `--check` = CI gate; regenerated on deploy by ship.js regenerateRepoMap()
|
|
@@ -181,6 +181,7 @@
|
|
|
181
181
|
│ ├── recall.js ← `/recall` CLI: two-call progressive-disclosure search (summary → `--expand <id>`) over Bongos retrieval layer (ADR 0060); `node scripts/gds/recall.js <query>`
|
|
182
182
|
│ ├── 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
|
|
183
183
|
│ ├── 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'
|
|
184
|
+
│ ├── 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
|
|
184
185
|
│ ├── worktree.js ← (task 1002809) junction-safe worktree add/remove/list — outside-repo worktrees get a node_modules junction via node (no shell quoting), removed BEFORE `git worktree remove`
|
|
185
186
|
│ ├── learning-capture.js ← file a `learnings` row (DB-backed short-form rule capture, replacing hand-edited recipe files for a single gotcha)
|
|
186
187
|
│ ├── docs-entropy.js ← ([#…] docs-entropy) markdown-corpus linter: broken internal links + cross-file duplicate-prose detector across every tracked `.md`; `--verbose` / `--file-ideas`
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.391",
|
|
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.391",
|
|
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.391",
|
|
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] [--enforce-touches] [--join]
|
|
5
|
+
// node scripts/gds/claim.js <task-id> [--worktree NAME] [--enforce-touches] [--join] [--here]
|
|
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.
|
|
@@ -13,8 +13,9 @@ const path = require('node:path');
|
|
|
13
13
|
const { cliClient, requireSession, fmtTouches, printOnboardingNudge, rewardPolicy, costPlusOnlyNote } = require('./cli-lib');
|
|
14
14
|
const { nextNumbers, padAdr, padMig } = require('./next-number');
|
|
15
15
|
const { CORE_NS } = require('./migration-namespace');
|
|
16
|
-
const { contextPack, formatContextPack } = require('./context-pack');
|
|
16
|
+
const { contextPack, formatContextPack, resolveWorktreeRoot } = require('./context-pack');
|
|
17
17
|
const { printClaimRecall } = require('./claim-recall');
|
|
18
|
+
const { decideWorktreeClaim, ESCAPE_FLAG } = require('./worktree-claim-guard');
|
|
18
19
|
// task 1003153 — the DEAD dependency edge, one definition shared with the route
|
|
19
20
|
// and the detector. A lifecycle CLI may require the lifecycle module directly
|
|
20
21
|
// (the documented exception in modules/lifecycle/CLAUDE.md).
|
|
@@ -81,6 +82,34 @@ function printDisciplineMode(task) {
|
|
|
81
82
|
}
|
|
82
83
|
}
|
|
83
84
|
|
|
85
|
+
// task 1002579 — the I/O half of the worktree-per-claim guard. Probes the tree
|
|
86
|
+
// with resolveWorktreeRoot (the same signal the context pack and skill-preflight
|
|
87
|
+
// read, so there is one answer to "is this the main checkout") plus the current
|
|
88
|
+
// branch, hands both to the pure decision, prints it, and exits 2 on a refusal.
|
|
89
|
+
// Every probe failure degrades to isMainCheckout=null, which the decision treats
|
|
90
|
+
// as allow — a guard that cannot see the tree must never block a real claim.
|
|
91
|
+
function enforceWorktreeClaim(taskId, { allowMainCheckout }) {
|
|
92
|
+
let tree = { isMainCheckout: null };
|
|
93
|
+
let branch = null;
|
|
94
|
+
try {
|
|
95
|
+
const runGit = (a) => execSync(`git ${a.join(' ')}`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
96
|
+
tree = resolveWorktreeRoot({ runGit, cwd: process.cwd() });
|
|
97
|
+
branch = runGit(['rev-parse', '--abbrev-ref', 'HEAD']).trim() || null;
|
|
98
|
+
} catch (_) {
|
|
99
|
+
/* fail-open: an unclassifiable tree is allowed through */
|
|
100
|
+
}
|
|
101
|
+
const d = decideWorktreeClaim({
|
|
102
|
+
isMainCheckout: tree.isMainCheckout,
|
|
103
|
+
branch,
|
|
104
|
+
allowMainCheckout,
|
|
105
|
+
taskId,
|
|
106
|
+
});
|
|
107
|
+
if (d.verdict === 'allow') return;
|
|
108
|
+
for (const line of d.lines) console.error(line);
|
|
109
|
+
if (d.verdict === 'refuse') process.exit(2);
|
|
110
|
+
console.error('');
|
|
111
|
+
}
|
|
112
|
+
|
|
84
113
|
function captureHeadSha() {
|
|
85
114
|
try {
|
|
86
115
|
return execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim();
|
|
@@ -423,13 +452,17 @@ async function runGoalJoinFlow({ api, d, taskId, assumeYes }) {
|
|
|
423
452
|
}
|
|
424
453
|
|
|
425
454
|
async function main() {
|
|
426
|
-
await requireSession();
|
|
427
455
|
const args = process.argv.slice(2);
|
|
428
456
|
const taskId = Number(args[0]);
|
|
429
457
|
if (!Number.isFinite(taskId)) {
|
|
430
|
-
console.error('usage: node scripts/gds/claim.js <task-id> [--worktree NAME] [--enforce-touches] [--join]');
|
|
458
|
+
console.error('usage: node scripts/gds/claim.js <task-id> [--worktree NAME] [--enforce-touches] [--join] [--here]');
|
|
431
459
|
process.exit(2);
|
|
432
460
|
}
|
|
461
|
+
// task 1002579: worktree-per-claim, enforced BEFORE anything happens — before
|
|
462
|
+
// the session check, before the POST. The whole point is that the builder
|
|
463
|
+
// learns at claim time, not from a stranded ship hours later.
|
|
464
|
+
enforceWorktreeClaim(taskId, { allowMainCheckout: args.includes(ESCAPE_FLAG) });
|
|
465
|
+
await requireSession();
|
|
433
466
|
const wIdx = args.indexOf('--worktree');
|
|
434
467
|
// Explicit --worktree always wins; otherwise auto-detect a linked worktree so
|
|
435
468
|
// parallel worktrees off one login each hold their own claim (the server guard
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// scripts/gds/worktree-claim-guard.js — worktree-per-claim, enforced at CLAIM
|
|
2
|
+
// time instead of discovered at ship time (task 1002579).
|
|
3
|
+
//
|
|
4
|
+
// THE FAILURE THIS KILLS. A builder with direct repo access runs
|
|
5
|
+
// `git checkout -b task-N` in the PRIMARY worktree (the main checkout) rather
|
|
6
|
+
// than in a dedicated `.claude/worktrees/<name>`. Everything looks fine for the
|
|
7
|
+
// whole session — and then ship.js's local auto-merge finds the receiving tree
|
|
8
|
+
// is the very tree holding the feature branch, so there is nowhere to merge
|
|
9
|
+
// INTO. The ship bails with "the main checkout is on <branch>, not main"
|
|
10
|
+
// (ship-merge.js) and the task strands at `confirmed` until the server
|
|
11
|
+
// reconciler or /merge-mode catches it. The cause is at claim time; the symptom
|
|
12
|
+
// was hours away at ship time.
|
|
13
|
+
//
|
|
14
|
+
// Worktree-per-claim has been the documented default since task 1642 and the
|
|
15
|
+
// claim-time context pack has warned about it since task 1002573 — but both
|
|
16
|
+
// speak AFTER the claim lands, and neither stops anything. Owner directive
|
|
17
|
+
// (2026-08-05): make it the enforced project default for ALL builders.
|
|
18
|
+
//
|
|
19
|
+
// The whole decision is the pure function below so it can be unit-tested without
|
|
20
|
+
// a git repo; claim.js supplies the two facts (which kind of tree, which branch)
|
|
21
|
+
// from resolveWorktreeRoot() — the SAME probe the context pack and
|
|
22
|
+
// skill-preflight already use, so there is one answer to "is this the main
|
|
23
|
+
// checkout", not three that can disagree.
|
|
24
|
+
|
|
25
|
+
'use strict';
|
|
26
|
+
|
|
27
|
+
// The documented escape hatch. Named here so the refusal text and claim.js's
|
|
28
|
+
// argv parse can never drift apart.
|
|
29
|
+
const ESCAPE_FLAG = '--here';
|
|
30
|
+
|
|
31
|
+
// Decide whether a claim may proceed in the tree it is being run from.
|
|
32
|
+
//
|
|
33
|
+
// isMainCheckout true | false | null — null means "could not tell" (git
|
|
34
|
+
// unavailable / odd checkout)
|
|
35
|
+
// branch the current branch name, or null when unknown
|
|
36
|
+
// defaultBranch the branch the main checkout must stay parked on
|
|
37
|
+
// allowMainCheckout the builder passed the escape hatch
|
|
38
|
+
// taskId only used to spell the remedy commands concretely
|
|
39
|
+
//
|
|
40
|
+
// Returns { verdict: 'allow' | 'warn' | 'refuse', reason, lines }.
|
|
41
|
+
// - 'allow' — a linked worktree (the correct setup), or a tree we cannot
|
|
42
|
+
// classify. NEVER refuse on a guess: an unknown tree fails open,
|
|
43
|
+
// because a false refusal blocks real work and the ship-time bail
|
|
44
|
+
// still catches the real thing.
|
|
45
|
+
// - 'refuse' — the main checkout, no escape hatch. Exit before the claim lands.
|
|
46
|
+
// - 'warn' — the main checkout WITH the escape hatch: the builder has said
|
|
47
|
+
// they mean it, so say loudly what it costs and continue.
|
|
48
|
+
function decideWorktreeClaim({
|
|
49
|
+
isMainCheckout,
|
|
50
|
+
branch = null,
|
|
51
|
+
defaultBranch = 'main',
|
|
52
|
+
allowMainCheckout = false,
|
|
53
|
+
taskId = null,
|
|
54
|
+
} = {}) {
|
|
55
|
+
if (isMainCheckout !== true) {
|
|
56
|
+
return { verdict: 'allow', reason: isMainCheckout === false ? 'linked worktree' : 'tree kind unknown', lines: [] };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Two shapes of the same mistake, and the second is already broken: a main
|
|
60
|
+
// checkout parked OFF its default branch has ALREADY lost the merge target,
|
|
61
|
+
// so say that rather than warning about a future that has happened.
|
|
62
|
+
const parked = branch && branch !== defaultBranch && branch !== 'HEAD';
|
|
63
|
+
const headline = parked
|
|
64
|
+
? `the main checkout is already parked on \`${branch}\`, not \`${defaultBranch}\``
|
|
65
|
+
: `you are in the main checkout (on \`${branch || defaultBranch}\`), not a per-claim worktree`;
|
|
66
|
+
const cost = parked
|
|
67
|
+
? `The local auto-merge at ship time merges INTO this tree — while it holds \`${branch}\` there is nowhere to merge to, so the ship strands at \`confirmed\`.`
|
|
68
|
+
: `Branching here parks the main checkout off \`${defaultBranch}\`, and the local auto-merge at ship time then has nowhere to merge INTO: the ship strands at \`confirmed\` until the server reconciler (or /merge-mode) catches it.`;
|
|
69
|
+
|
|
70
|
+
const id = taskId == null ? '<task-id>' : String(taskId);
|
|
71
|
+
const remedy = [
|
|
72
|
+
' Work this claim in its own worktree instead:',
|
|
73
|
+
` node scripts/gds/worktree.js add ${id}`,
|
|
74
|
+
` cd .claude/worktrees/task-${id}`,
|
|
75
|
+
` node scripts/gds/claim.js ${id}`,
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
if (allowMainCheckout) {
|
|
79
|
+
return {
|
|
80
|
+
verdict: 'warn',
|
|
81
|
+
reason: headline,
|
|
82
|
+
lines: [
|
|
83
|
+
`⚠ Claiming in the MAIN CHECKOUT (${ESCAPE_FLAG} given) — ${headline}.`,
|
|
84
|
+
` ${cost}`,
|
|
85
|
+
' Automation that resets this tree mid-session can also take your uncommitted work with it.',
|
|
86
|
+
...remedy,
|
|
87
|
+
],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
verdict: 'refuse',
|
|
93
|
+
reason: headline,
|
|
94
|
+
lines: [
|
|
95
|
+
`✗ Refusing to claim here — ${headline}.`,
|
|
96
|
+
` ${cost}`,
|
|
97
|
+
' Worktree-per-claim is the project default (task 1642); this is the check behind it.',
|
|
98
|
+
...remedy,
|
|
99
|
+
` If you genuinely mean to work in the main checkout, re-run with ${ESCAPE_FLAG} (you will land at \`confirmed\` and finish through /merge-mode).`,
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = { decideWorktreeClaim, ESCAPE_FLAG };
|
package/src/module-api.js
CHANGED
|
@@ -1220,8 +1220,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
1220
1220
|
// landed since 1.19.388 with no explicit bump. run 33126279278. (task 1002620)
|
|
1221
1221
|
// 1.19.390 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1222
1222
|
// landed since 1.19.389 with no explicit bump. run 33133575931. (task 1002620)
|
|
1223
|
+
// 1.19.391 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1224
|
+
// landed since 1.19.390 with no explicit bump. run 33134514193. (task 1002620)
|
|
1223
1225
|
// ---------------------------------------------------------------------------
|
|
1224
|
-
const CORE_VERSION = '1.19.
|
|
1226
|
+
const CORE_VERSION = '1.19.391'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
1225
1227
|
|
|
1226
1228
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
1227
1229
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// tests/worktree_claim_guard.mjs
|
|
2
|
+
//
|
|
3
|
+
// task 1002579 — worktree-per-claim, enforced at CLAIM time. The failure it
|
|
4
|
+
// kills: branching in the PRIMARY worktree (the main checkout) instead of a
|
|
5
|
+
// dedicated `.claude/worktrees/<name>` leaves the main checkout parked off
|
|
6
|
+
// `main`, so ship.js's local auto-merge has nowhere to merge INTO and the task
|
|
7
|
+
// strands at `confirmed` — a symptom hours away from its cause.
|
|
8
|
+
//
|
|
9
|
+
// decideWorktreeClaim() is the pure core; these tests pin every branch with
|
|
10
|
+
// injected facts (no git, no repo, no claim).
|
|
11
|
+
//
|
|
12
|
+
// Run: node tests/worktree_claim_guard.mjs
|
|
13
|
+
|
|
14
|
+
import { strict as assert } from 'node:assert';
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
const { decideWorktreeClaim, ESCAPE_FLAG } = require('../scripts/gds/worktree-claim-guard.js');
|
|
19
|
+
|
|
20
|
+
let passed = 0;
|
|
21
|
+
let failed = 0;
|
|
22
|
+
function t(name, fn) {
|
|
23
|
+
try { fn(); passed++; console.log(` PASS ${name}`); }
|
|
24
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const text = (d) => d.lines.join('\n');
|
|
28
|
+
|
|
29
|
+
// ---- the core case: the primary checkout, on main -------------------------
|
|
30
|
+
t('REFUSE: the main checkout on `main` — the exact shape task 1642 documents', () => {
|
|
31
|
+
const d = decideWorktreeClaim({ isMainCheckout: true, branch: 'main', taskId: 42 });
|
|
32
|
+
assert.equal(d.verdict, 'refuse');
|
|
33
|
+
assert.match(text(d), /main checkout/i);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
t('REFUSE names the fix concretely, with the task id spelled into the commands', () => {
|
|
37
|
+
const d = decideWorktreeClaim({ isMainCheckout: true, branch: 'main', taskId: 42 });
|
|
38
|
+
const s = text(d);
|
|
39
|
+
assert.match(s, /worktree\.js add 42/, 'names the worktree-create command');
|
|
40
|
+
assert.match(s, /cd \.claude\/worktrees\/task-42/, 'names where to go');
|
|
41
|
+
assert.match(s, /claim\.js 42/, 'names the re-run');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
t('REFUSE names the escape hatch, so the refusal is never a dead end', () => {
|
|
45
|
+
const d = decideWorktreeClaim({ isMainCheckout: true, branch: 'main', taskId: 42 });
|
|
46
|
+
assert.match(text(d), new RegExp(ESCAPE_FLAG.replace(/-/g, '\-')));
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
t('a missing task id still yields runnable-shaped guidance, not "undefined"', () => {
|
|
50
|
+
const s = text(decideWorktreeClaim({ isMainCheckout: true, branch: 'main' }));
|
|
51
|
+
assert.match(s, /worktree\.js add <task-id>/);
|
|
52
|
+
assert.doesNotMatch(s, /undefined|null/);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// ---- the already-broken shape: primary checkout parked off main -----------
|
|
56
|
+
t('REFUSE: the main checkout ALREADY parked on a task branch reads as already-broken', () => {
|
|
57
|
+
const d = decideWorktreeClaim({ isMainCheckout: true, branch: 'task-99', taskId: 42 });
|
|
58
|
+
assert.equal(d.verdict, 'refuse');
|
|
59
|
+
const s = text(d);
|
|
60
|
+
assert.match(s, /already parked on `task-99`/, 'names the branch it is stuck on');
|
|
61
|
+
assert.match(s, /nowhere to merge/, 'names the ship-time consequence');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
t('a detached HEAD in the main checkout is not reported as "parked on HEAD"', () => {
|
|
65
|
+
const s = text(decideWorktreeClaim({ isMainCheckout: true, branch: 'HEAD', taskId: 42 }));
|
|
66
|
+
assert.doesNotMatch(s, /parked on `HEAD`/);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
t('defaultBranch is honoured — a `master` instance is not told main', () => {
|
|
70
|
+
const d = decideWorktreeClaim({ isMainCheckout: true, branch: 'master', defaultBranch: 'master', taskId: 7 });
|
|
71
|
+
assert.equal(d.verdict, 'refuse');
|
|
72
|
+
assert.doesNotMatch(text(d), /already parked/, 'master IS the default branch here');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// ---- the escape hatch: loud, but it proceeds ------------------------------
|
|
76
|
+
t('WARN, not refuse, when the escape hatch is given — and it still costs something out loud', () => {
|
|
77
|
+
const d = decideWorktreeClaim({ isMainCheckout: true, branch: 'main', allowMainCheckout: true, taskId: 42 });
|
|
78
|
+
assert.equal(d.verdict, 'warn');
|
|
79
|
+
assert.match(text(d), /confirmed/, 'says what it will cost at ship time');
|
|
80
|
+
assert.match(text(d), /worktree\.js add 42/, 'still names the better path');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// ---- no false positives ---------------------------------------------------
|
|
84
|
+
t('ALLOW: a real linked worktree is untouched — silently, no lines', () => {
|
|
85
|
+
const d = decideWorktreeClaim({ isMainCheckout: false, branch: 'claude/task-42', taskId: 42 });
|
|
86
|
+
assert.equal(d.verdict, 'allow');
|
|
87
|
+
assert.deepEqual(d.lines, []);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
t('ALLOW: a linked worktree that happens to be on `main` is still fine', () => {
|
|
91
|
+
assert.equal(decideWorktreeClaim({ isMainCheckout: false, branch: 'main' }).verdict, 'allow');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
t('ALLOW: tree kind UNKNOWN fails open — a guard that cannot see must not block', () => {
|
|
95
|
+
const d = decideWorktreeClaim({ isMainCheckout: null, branch: null, taskId: 42 });
|
|
96
|
+
assert.equal(d.verdict, 'allow');
|
|
97
|
+
assert.deepEqual(d.lines, []);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
t('ALLOW: called with no facts at all (every probe threw) still fails open', () => {
|
|
101
|
+
assert.equal(decideWorktreeClaim().verdict, 'allow');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
console.log(`\nworktree_claim_guard: ${passed} passed, ${failed} failed`);
|
|
105
|
+
process.exit(failed ? 1 : 0);
|