@bongos/core 1.21.70 → 1.21.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +114 -59
- package/.claude/skills/goal-create/SKILL.md +21 -3
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +5 -3
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/adr/0368-red-and-blue-teams-practice-on-a-range-never-on-live.md +99 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +46 -5
- package/docs/api-reference.md +4 -3
- package/docs/copy-inventory.md +25 -21
- package/docs/copy-registry.json +76 -30
- package/docs/file-map.md +1 -1
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-readings.json +8 -8
- package/docs/recipes/core-release-pipeline.md +6 -0
- package/docs/recipes/upgrading-the-core.md +2 -0
- package/modules/hall-ui/public/settings-software-update.js +77 -2
- package/modules/hall-ui/public/settings.html +1 -1
- package/modules/lifecycle/db-tasks.js +12 -0
- package/modules/lifecycle/goal-spec-task.js +129 -0
- package/modules/lifecycle/routes/goals.js +7 -9
- package/modules/lifecycle/routes/task-write-routes.js +11 -1
- package/modules/lifecycle/upkeep-home.js +55 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +37 -0
- package/scripts/gds/claude-materialize.js +125 -31
- package/scripts/gds/claude-md-template.js +47 -10
- package/scripts/gds/cli-lib.js +4 -4
- package/scripts/gds/feedback-send.js +3 -3
- package/scripts/gds/paste-token.js +1 -1
- package/scripts/gds/provision-net.js +1 -8
- package/scripts/gds/provision-units.js +28 -2
- package/scripts/gds/reauth.js +1 -1
- package/scripts/gds/run-unit-tests.js +3 -0
- package/scripts/gds/setup.js +1 -1
- package/scripts/gds/start.js +4 -4
- package/scripts/gds/surface.js +15 -3
- package/scripts/gds/task.js +32 -5
- package/scripts/gds/upgrade-operator.js +73 -0
- package/scripts/gds/upgrade.js +7 -2
- package/scripts/hall-preview/server.js +3 -0
- package/src/bongos/routes/core-update.js +98 -4
- package/src/bongos/routes/feedback.js +11 -1
- package/src/bongos/upgrade-request.js +158 -0
- package/src/module-api.js +1 -1
- package/tests/claude_materialize.mjs +195 -18
- package/tests/cli_allowlist_permissions.mjs +6 -2
- package/tests/cli_surface.mjs +2 -2
- package/tests/fixtures/page-ledger.mjs +5 -0
- package/tests/goal_admission_override.mjs +1 -1
- package/tests/goal_category.mjs +1 -1
- package/tests/goal_create_spec_entry.mjs +61 -0
- package/tests/goal_spec_task.mjs +197 -0
- package/tests/goal_spec_task_db.mjs +95 -0
- package/tests/provision_settings_apply.mjs +25 -1
- package/tests/software_update.mjs +18 -1
- package/tests/start_env_guard.mjs +2 -2
- package/tests/upgrade_operator.mjs +105 -0
- package/tests/upgrade_request.mjs +348 -0
- package/tests/upkeep_home.mjs +156 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/lifecycle/goal-spec-task.js — a goal made in the hall carries its own
|
|
4
|
+
// spec task, and the goal's other work waits behind it (task 1004585, ADR 0366 D3).
|
|
5
|
+
//
|
|
6
|
+
// WHY. The hall's goal form asks only what a person knows (a name, a description,
|
|
7
|
+
// a version, a category). The rest of a goal — its scope wall, its criteria, its
|
|
8
|
+
// first tasks — is inference, which /goal-create does in a session (ADR 0366 D2).
|
|
9
|
+
// So a hall-made goal arrives with a task that says exactly that, and nothing else
|
|
10
|
+
// in the goal can be claimed until it ships: an unspecced goal is visible as a
|
|
11
|
+
// task on the board, not a silence.
|
|
12
|
+
//
|
|
13
|
+
// HOW, with no new permission and no migration:
|
|
14
|
+
// - The spec task is an ordinary task, recognised by (source, source_ref) =
|
|
15
|
+
// ('goal-spec', 'goal-<id>-spec'). That pair is also what makes creating it
|
|
16
|
+
// idempotent (findTaskBySourceRef).
|
|
17
|
+
// - "Waits behind it" is an ordinary task→task dependency, so the claim gate's
|
|
18
|
+
// existing DEPS_NOT_SHIPPED refusal and the auto-promotion on ship do the rest.
|
|
19
|
+
// A goal-level edge would be simpler, but every task in the goal inherits it
|
|
20
|
+
// (unsatisfiedDeps), the spec task included, which would block it on itself.
|
|
21
|
+
// - The edge is added at the two places a task ENTERS a goal: db-tasks.createTask
|
|
22
|
+
// (every create vector passes through it) and db-tasks.updateTaskGoal (a move).
|
|
23
|
+
|
|
24
|
+
const SPEC_SOURCE = 'goal-spec';
|
|
25
|
+
|
|
26
|
+
function specSourceRef(goalId) {
|
|
27
|
+
return `goal-${goalId}-spec`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// The spec task's fields, for db.createTask. `ready` so it can be claimed at once;
|
|
31
|
+
// a spike, because its output is a decision (the scope and the criteria) rather
|
|
32
|
+
// than code; ideator, the craft whose pack covers "the scopes of work ideas become".
|
|
33
|
+
function specTaskFields(goal, { createdBy = null } = {}) {
|
|
34
|
+
const id = goal.id;
|
|
35
|
+
return {
|
|
36
|
+
versionId: goal.version_id,
|
|
37
|
+
goalId: id,
|
|
38
|
+
title: `Spec goal ${id}: its scope, criteria and first tasks`,
|
|
39
|
+
description: [
|
|
40
|
+
`Goal ${id} ("${goal.title}") was created from the hall, which asks only for a name and a description. This task settles the rest, and every other task in the goal waits on it (ADR 0366 D3).`,
|
|
41
|
+
'',
|
|
42
|
+
'Settle, in a session (run /goal-create against this goal):',
|
|
43
|
+
`1. The scope wall: which code areas the goal's tasks may touch. Set it with POST /goals/${id}/scope (the goal's owner or a manager; a protected area needs an Archon).`,
|
|
44
|
+
`2. Two to four done-when criteria, with POST /versions/${goal.version_id}/done-when under the goal (ADR 0154: the goal's owner or a manager).`,
|
|
45
|
+
'3. The first tasks, with their dependencies.',
|
|
46
|
+
'',
|
|
47
|
+
'If you do not own or manage this goal, draft the scope and criteria and hand them to its owner rather than skipping them.',
|
|
48
|
+
'',
|
|
49
|
+
'DONE WHEN: the goal has its scope (or a written reason it needs none), its criteria, and at least one claimable task.',
|
|
50
|
+
].join('\n'),
|
|
51
|
+
status: 'ready',
|
|
52
|
+
kind: 'spike',
|
|
53
|
+
discipline: 'ideator',
|
|
54
|
+
source: SPEC_SOURCE,
|
|
55
|
+
sourceRef: specSourceRef(id),
|
|
56
|
+
createdBy,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// The goal's spec task if it is still open, else null. An abandoned spec task
|
|
61
|
+
// gates nothing new (the owner decided the goal needs no spec); a shipped one is done.
|
|
62
|
+
async function openSpecTaskId(goalId, client) {
|
|
63
|
+
if (goalId == null) return null;
|
|
64
|
+
const { rows } = await client.query(
|
|
65
|
+
`SELECT id FROM tasks
|
|
66
|
+
WHERE goal_id = $1 AND source = $2 AND source_ref = $3
|
|
67
|
+
AND status NOT IN ('shipped', 'abandoned')
|
|
68
|
+
ORDER BY id ASC
|
|
69
|
+
LIMIT 1`,
|
|
70
|
+
[goalId, SPEC_SOURCE, specSourceRef(goalId)]
|
|
71
|
+
);
|
|
72
|
+
return rows[0] ? Number(rows[0].id) : null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Make a task that just entered a goal wait on that goal's open spec task.
|
|
76
|
+
// Returns the spec task id it now waits on, or null. A task that IS the spec task
|
|
77
|
+
// is left alone. Safe without a cycle check only for a brand-new task (nothing can
|
|
78
|
+
// depend on it yet) — a moved task goes through addTaskDependency, which checks.
|
|
79
|
+
async function gateOnSpecTask(task, client, { addTaskDependency = null } = {}) {
|
|
80
|
+
if (!task || task.goal_id == null) return null;
|
|
81
|
+
if (task.source === SPEC_SOURCE && task.source_ref === specSourceRef(task.goal_id)) return null;
|
|
82
|
+
const specId = await openSpecTaskId(task.goal_id, client);
|
|
83
|
+
if (specId == null || specId === Number(task.id)) return null;
|
|
84
|
+
if (addTaskDependency) {
|
|
85
|
+
try {
|
|
86
|
+
await addTaskDependency(task.id, specId);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
// A cycle means the spec task already waits on this task; gating it would
|
|
89
|
+
// deadlock both, so the move stands without the edge.
|
|
90
|
+
if (err && err.code === 'CYCLE') return null;
|
|
91
|
+
throw err;
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
await client.query(
|
|
95
|
+
`INSERT INTO dependencies (from_kind, from_id, to_kind, to_id)
|
|
96
|
+
VALUES ('task', $1, 'task', $2)
|
|
97
|
+
ON CONFLICT DO NOTHING`,
|
|
98
|
+
[task.id, specId]
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
return specId;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Give a goal its spec task, safely re-runnable: if the goal already has an OPEN
|
|
105
|
+
// spec task, that one is returned and nothing is written, so a second run (a retry,
|
|
106
|
+
// or a future "re-offer the spec" path) never files a duplicate. Run it inside the
|
|
107
|
+
// caller's transaction so the read and the insert see the same snapshot.
|
|
108
|
+
async function ensureSpecTask({ db, goal, client, createdBy = null }) {
|
|
109
|
+
const existing = await openSpecTaskId(goal.id, client);
|
|
110
|
+
if (existing != null) return { specTask: { id: existing }, created: false };
|
|
111
|
+
const specTask = await db.createTask(specTaskFields(goal, { createdBy }), { client });
|
|
112
|
+
return { specTask, created: true };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// POST /goals's create step. Without the opt-in it is exactly db.createGoal. With it,
|
|
116
|
+
// the goal and its spec task land in ONE transaction: a goal without its spec task
|
|
117
|
+
// would be ungated, which is the state D3 exists to end.
|
|
118
|
+
async function createGoalWithSpec({ db, withTx, goalFields, withSpecTask = false }) {
|
|
119
|
+
if (!withSpecTask) return { goal: await db.createGoal(goalFields), specTask: null };
|
|
120
|
+
let goal;
|
|
121
|
+
let specTask = null;
|
|
122
|
+
await withTx(async (client) => {
|
|
123
|
+
goal = await db.createGoal(goalFields, { client });
|
|
124
|
+
({ specTask } = await ensureSpecTask({ db, goal, client, createdBy: goalFields.createdBy }));
|
|
125
|
+
});
|
|
126
|
+
return { goal, specTask };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = { SPEC_SOURCE, specSourceRef, specTaskFields, openSpecTaskId, gateOnSpecTask, ensureSpecTask, createGoalWithSpec };
|
|
@@ -29,6 +29,7 @@ const hierarchy = require('../hierarchy');
|
|
|
29
29
|
const goalSuggest = require('../goal-suggest.js'); // task 1003062 — pre-creation goal suggester
|
|
30
30
|
const goalEdits = require('../goal-edits.js'); // task 1003102 — goal self-edit handlers
|
|
31
31
|
const goalCategory = require('../goal-category.js'); // task 1003275 — the ADVISORY human category (gates nothing)
|
|
32
|
+
const goalSpecTask = require('../goal-spec-task.js'); // task 1004585 — a hall-made goal carries its own spec task (ADR 0366 D3)
|
|
32
33
|
const goalRollup = require('../goal-rollup.js'); // task 1003278 — per-goal rollups computed in SQL, not in the browser
|
|
33
34
|
const goalGraph = require('../goal-graph.js'); // task 1003479 — the goal graph: nodes + cross-goal edges, aggregated in SQL
|
|
34
35
|
const goalForest = require('../goal-forest.js'); // task 1003480 — per-goal dependency forest
|
|
@@ -97,6 +98,7 @@ module.exports = function buildGoalsRouter() {
|
|
|
97
98
|
// requested without stating a cause. minLength 8 because 'ok' / '.' is not
|
|
98
99
|
// a reason, and the row is the only place the why survives.
|
|
99
100
|
admit_reason: { type: 'string', minLength: 8, maxLength: 500 },
|
|
101
|
+
spec_task: { type: 'boolean' }, // task 1004585 / ADR 0366 D3 — the hall form's opt-in (goal-spec-task.js)
|
|
100
102
|
})) return;
|
|
101
103
|
|
|
102
104
|
const scopeModules = req.body.scope_modules || [];
|
|
@@ -156,15 +158,11 @@ module.exports = function buildGoalsRouter() {
|
|
|
156
158
|
const admission = versionDecision.overrideApplied
|
|
157
159
|
? { admittedBy: req.builder.id, reason: admitReason }
|
|
158
160
|
: null;
|
|
159
|
-
const goal = await
|
|
160
|
-
versionId: req.body.version_id,
|
|
161
|
-
title: req.body.title,
|
|
161
|
+
const { goal, specTask } = await goalSpecTask.createGoalWithSpec({ db, withTx: api.withTx, withSpecTask: req.body.spec_task === true, goalFields: {
|
|
162
|
+
versionId: req.body.version_id, title: req.body.title,
|
|
162
163
|
subtitle: (req.body.subtitle || '').trim() || null, description: (req.body.description || '').trim() || null,
|
|
163
|
-
scopeModules,
|
|
164
|
-
|
|
165
|
-
categoryId,
|
|
166
|
-
admission,
|
|
167
|
-
});
|
|
164
|
+
scopeModules, createdBy: req.builder.id, categoryId, admission,
|
|
165
|
+
} });
|
|
168
166
|
// Stamp the predecessor's succeeded_by pointer at the new goal (the lineage).
|
|
169
167
|
if (succeedsId != null) {
|
|
170
168
|
await db.setGoalSuccessor({ goalId: succeedsId, successorId: goal.id });
|
|
@@ -182,7 +180,7 @@ module.exports = function buildGoalsRouter() {
|
|
|
182
180
|
// waved into a building version, by whom and why — the caller should not
|
|
183
181
|
// have to re-read the goal to find out whether their override took effect.
|
|
184
182
|
const admitted = admission ? await db.getGoalAdmission(goal.id) : null;
|
|
185
|
-
return res.status(201).json({ ok: true, goal, succeeds_goal_id: succeedsId, category_advisory, admitted });
|
|
183
|
+
return res.status(201).json({ ok: true, goal, succeeds_goal_id: succeedsId, category_advisory, admitted, spec_task: specTask });
|
|
186
184
|
} catch (err) {
|
|
187
185
|
log.error('[gds] POST /goals', err);
|
|
188
186
|
return res.fail('goal_create_failed', { status: 500, message: 'internal error' });
|
|
@@ -29,6 +29,7 @@ const { validateOrRespond, LIMITS, parseId } = api;
|
|
|
29
29
|
const { badKind, detectsMigration } = require('./task-route-helpers.js');
|
|
30
30
|
const { describeCheckViolation } = require('../pg-constraint-fail.js');
|
|
31
31
|
const { goalNotOpenRefusal } = require('./goal-route-authz.js');
|
|
32
|
+
const { resolveUpkeepHome, isUpkeepFiling, UPKEEP_HINT } = require('../upkeep-home.js'); // task 1004597
|
|
32
33
|
// task 1003208: structured logging (pino via the doorway) — was console.*.
|
|
33
34
|
const log = api.logger('lifecycle');
|
|
34
35
|
|
|
@@ -189,10 +190,18 @@ module.exports = function registerTaskWriteRoutes(router) {
|
|
|
189
190
|
// nobody reads. This is the human/agent door, and it always has a goal to
|
|
190
191
|
// name — /builder-start lists work by goal, and POST /goals/:id/tasks has
|
|
191
192
|
// required one since BV1.R60.
|
|
193
|
+
// task 1004597: the one exception is PLATFORM UPKEEP (a core upgrade, Bongos
|
|
194
|
+
// maintenance), which fits no product goal. An explicit kind 'infra' with no
|
|
195
|
+
// goal lands in the version's maintenance goal — see upkeep-home.js.
|
|
196
|
+
const upkeepHome = await resolveUpkeepHome(body, { ensureMaintenanceGoal: db.ensureMaintenanceGoal, withTx: api.withTx });
|
|
197
|
+
if (upkeepHome) body.goal_id = Number(upkeepHome.id);
|
|
192
198
|
if (body.goal_id == null) {
|
|
193
199
|
return res.fail('goal_id_required', {
|
|
194
200
|
status: 400,
|
|
195
|
-
message: 'goal_id is required — every task belongs to a goal. List them at GET /goals?version=<id>, or author under one directly with POST /goals/:id/tasks.'
|
|
201
|
+
message: 'goal_id is required — every task belongs to a goal. List them at GET /goals?version=<id>, or author under one directly with POST /goals/:id/tasks. ' +
|
|
202
|
+
(isUpkeepFiling(body)
|
|
203
|
+
? `Version '${body.version_id}' is not being built right now, so it has no maintenance goal for upkeep — file it against the version that is.`
|
|
204
|
+
: UPKEEP_HINT),
|
|
196
205
|
});
|
|
197
206
|
}
|
|
198
207
|
if (body.goal_id != null) {
|
|
@@ -297,6 +306,7 @@ module.exports = function registerTaskWriteRoutes(router) {
|
|
|
297
306
|
needs_migration: needsMigration,
|
|
298
307
|
criteria,
|
|
299
308
|
suggested_criteria,
|
|
309
|
+
...(upkeepHome ? { goal_home: 'maintenance' } : {}), // task 1004597
|
|
300
310
|
goal_advisory, dependency_advisory, est_advisory: await require('../est-advisory').buildEstAdvisory({ db, task }), // task 1003148
|
|
301
311
|
});
|
|
302
312
|
} catch (err) {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/lifecycle/upkeep-home.js — WHERE PLATFORM UPKEEP GOES WHEN NO PRODUCT
|
|
4
|
+
// GOAL FITS (task 1004597).
|
|
5
|
+
//
|
|
6
|
+
// THE CONTRADICTION THIS CLOSES. The ledger rule says record a change before you
|
|
7
|
+
// make it; the goal rule (ADR 0250 D4, BV1.R11) says every task belongs to a
|
|
8
|
+
// goal. A project's goals are product goals ("Marketing", "Live Floor"), so a
|
|
9
|
+
// builder about to upgrade the Bongos core had nowhere honest to file it: POST
|
|
10
|
+
// /tasks refused `goal_id_required`, and the only ways forward were to misfile
|
|
11
|
+
// the upgrade under a product goal or skip the record. One session (mercury,
|
|
12
|
+
// 2026-10-06) chose the latter.
|
|
13
|
+
//
|
|
14
|
+
// THE ANSWER REUSES THE HOMELESS-BUG HOME. Every building version already has a
|
|
15
|
+
// find-or-create MAINTENANCE goal (ADR 0262, `ensureMaintenanceGoal`): open, no
|
|
16
|
+
// scope wall, exempt from the version-close count, carried forward on close.
|
|
17
|
+
// Platform upkeep is maintenance in exactly that sense, so a task filed with
|
|
18
|
+
// NO goal and kind 'infra' lands there.
|
|
19
|
+
//
|
|
20
|
+
// ONLY AN EXPLICIT kind COUNTS — never the classifier's guess. The defect
|
|
21
|
+
// BV1.R11 fixed was SILENT bucketing (53 tasks filed out of sight by a default).
|
|
22
|
+
// A caller who writes kind 'infra' has said out loud what the work is; a feature
|
|
23
|
+
// whose text merely reads like infra must still name its goal.
|
|
24
|
+
//
|
|
25
|
+
// RANK AND APPROVAL ARE UNTOUCHED. This only answers "which goal"; the route's
|
|
26
|
+
// own permission check (task.create, Metic+) and status rules run exactly as for
|
|
27
|
+
// any other goal.
|
|
28
|
+
|
|
29
|
+
const UPKEEP_KIND = 'infra';
|
|
30
|
+
|
|
31
|
+
// isUpkeepFiling — true when a create names no goal and explicitly says it is
|
|
32
|
+
// platform upkeep. PURE; exported so the rule has one spelling.
|
|
33
|
+
function isUpkeepFiling(body) {
|
|
34
|
+
return !!body && body.goal_id == null && body.kind === UPKEEP_KIND;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// resolveUpkeepHome — the maintenance goal row an upkeep filing lands in, or
|
|
38
|
+
// null when it is not an upkeep filing or the version cannot take work (not
|
|
39
|
+
// `building`). The find-or-create runs inside its own transaction so the
|
|
40
|
+
// advisory lock in ensureMaintenanceGoal actually serialises concurrent filers
|
|
41
|
+
// (outside a transaction it would release at the end of its own statement).
|
|
42
|
+
async function resolveUpkeepHome(body, deps) {
|
|
43
|
+
if (!isUpkeepFiling(body)) return null;
|
|
44
|
+
const { ensureMaintenanceGoal, withTx } = deps;
|
|
45
|
+
return withTx((client) => ensureMaintenanceGoal(body.version_id, { client }));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// The plain-words half of the goal_id_required refusal: what to do when the work
|
|
49
|
+
// is upkeep rather than product work. Shared by the route and the CLI so the two
|
|
50
|
+
// never drift.
|
|
51
|
+
const UPKEEP_HINT =
|
|
52
|
+
'Platform upkeep — a Bongos upgrade or other maintenance — has a standing home: ' +
|
|
53
|
+
`file it with kind "${UPKEEP_KIND}" and no goal, and it goes into this version's maintenance goal.`;
|
|
54
|
+
|
|
55
|
+
module.exports = { UPKEEP_KIND, UPKEEP_HINT, isUpkeepFiling, resolveUpkeepHome };
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.71",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.71",
|
|
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.21.
|
|
3
|
+
"version": "1.21.71",
|
|
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/release-notes.json
CHANGED
|
@@ -9199,5 +9199,42 @@
|
|
|
9199
9199
|
{
|
|
9200
9200
|
"text": "Reporting a problem with Bongos now works from every project, including ones like Will's that could not send feedback before. It takes a second, needs no extra sign-in, and the Bongos team can see who really sent each report."
|
|
9201
9201
|
}
|
|
9202
|
+
],
|
|
9203
|
+
"1.21.71": [
|
|
9204
|
+
{
|
|
9205
|
+
"id": "1004595",
|
|
9206
|
+
"text": "A hosted project's Software update page no longer stops at \"Update ready\". When the people who run its server do the upgrade, the owner now sees who that is and an \"Ask for this upgrade\" button; one press reaches the Cloud Bon"
|
|
9207
|
+
},
|
|
9208
|
+
{
|
|
9209
|
+
"id": "1004621",
|
|
9210
|
+
"text": "The assistant in a Bongos project no longer starts every session with a failed command. Its instructions now say `npx bongos …`, which works as soon as the project is installed, instead of a bare `bongos` that the computer cou"
|
|
9211
|
+
},
|
|
9212
|
+
{
|
|
9213
|
+
"id": "1004586",
|
|
9214
|
+
"text": "The goal-planning assistant can now finish a goal that was started in the hall: it works out the goal's scope, its 'done when' checks and its first tasks, then releases the goal's work."
|
|
9215
|
+
},
|
|
9216
|
+
{
|
|
9217
|
+
"id": "1004622",
|
|
9218
|
+
"text": "Fixed a broken test that was stopping every new Bongos release from being published."
|
|
9219
|
+
},
|
|
9220
|
+
{
|
|
9221
|
+
"id": "1004596",
|
|
9222
|
+
"text": "When someone on a hosted project (like Will's) asks to upgrade from their own computer, Bongos now tells them plainly that the project's operator does upgrades, and how to ask for one — from the Software update page or with on"
|
|
9223
|
+
},
|
|
9224
|
+
{
|
|
9225
|
+
"id": "1004619",
|
|
9226
|
+
"text": "Bongos now has a written plan for red and blue security teams: attacks happen on a separate practice server, never on the live site, and every release gets checked there."
|
|
9227
|
+
},
|
|
9228
|
+
{
|
|
9229
|
+
"id": "1004597",
|
|
9230
|
+
"text": "Recording routine Bongos upkeep, such as a core upgrade, no longer gets refused just because none of a project's goals fit. Filed as upkeep with no goal, it now goes into the version's standing maintenance goal. Any other task"
|
|
9231
|
+
},
|
|
9232
|
+
{
|
|
9233
|
+
"id": "1004585",
|
|
9234
|
+
"text": "A goal created from the hall now arrives with a 'Spec goal N' task. Nothing else in that goal can be picked up until someone finishes it, so a goal never sits half-defined."
|
|
9235
|
+
},
|
|
9236
|
+
{
|
|
9237
|
+
"text": "Hosted projects like Will's now get a working Update button in Settings, instead of being told to run a command on a server they don't have access to."
|
|
9238
|
+
}
|
|
9202
9239
|
]
|
|
9203
9240
|
}
|
|
@@ -33,7 +33,7 @@ const { spawnSync } = require('node:child_process');
|
|
|
33
33
|
const { VERBS } = require('../../bin/bongos.js'); // side-effect-free require (guarded main)
|
|
34
34
|
const ic = require('../../src/instance-config'); // neutral configDir/envPrefix fallbacks (never a founder literal)
|
|
35
35
|
const { isCorePackageName, CORE_PACKAGE_NAMES } = require('../../src/module-loader/catalog'); // one definition of "this checkout IS the core"
|
|
36
|
-
const { renderInstanceClaudeMd } = require('./claude-md-template'); // task 2104 — the starter CLAUDE.md
|
|
36
|
+
const { renderInstanceClaudeMd, refreshStarterCliLines } = require('./claude-md-template'); // task 2104 — the starter CLAUDE.md
|
|
37
37
|
|
|
38
38
|
// script basename (e.g. "ship.js") → dedicated bongos verb, for verbs that map to a bare script with
|
|
39
39
|
// NO prefixArgs. Verbs whose script is driven by prefixArgs (box → api.js) are EXCLUDED so a generic
|
|
@@ -61,16 +61,40 @@ const GDS_INVOCATION_RE = new RegExp(LAUNCHER + String.raw`\s+"?(?:\$CLAUDE_PROJ
|
|
|
61
61
|
// A non-gds core script under scripts/ (rare in skills, but keep the transform total).
|
|
62
62
|
const OTHER_SCRIPT_RE = new RegExp(LAUNCHER + String.raw`\s+"?(?:\$CLAUDE_PROJECT_DIR\/|\.\/)?scripts\/((?!gds\/)[a-zA-Z0-9_./-]+\.js)"?`, 'g');
|
|
63
63
|
|
|
64
|
-
//
|
|
65
|
-
//
|
|
64
|
+
// How a project runs the CLI (task 1004621). The `bongos` bin ships inside the @bongos/core
|
|
65
|
+
// dependency (node_modules/.bin/bongos), which is NOT on PATH, so a bare `bongos ship` written
|
|
66
|
+
// into a project's skills failed with `command not found` on every session's first call and the
|
|
67
|
+
// assistant had to recover by retrying through npx. `npx` finds the project's own bin right after
|
|
68
|
+
// `npm install`, with no setup, on every OS and shell — so that is the spelling a project reads.
|
|
69
|
+
const PROJECT_CLI = 'npx bongos';
|
|
70
|
+
|
|
71
|
+
// A hand-written bare `bongos <verb>` in skill prose (e.g. "`bongos paste-token <token>`"), so it
|
|
72
|
+
// takes the npx spelling too. Case-sensitive on purpose ("Bongos status" is a product noun), only a
|
|
73
|
+
// KNOWN verb, and never when already prefixed by `npx ` or glued to a path/name (`@bongos/core`,
|
|
74
|
+
// `bongos-feedback`, `bongos: command not found` all stay). The `npx ` lookbehind is what keeps a
|
|
75
|
+
// second run from writing `npx npx bongos`.
|
|
76
|
+
function bareVerbRe(verbs) {
|
|
77
|
+
const names = Object.keys(verbs || {}).filter((v) => /^[a-z][a-z-]*$/.test(v));
|
|
78
|
+
if (!names.length) return null;
|
|
79
|
+
names.sort((a, b) => b.length - a.length); // `paste-token` before a shorter prefix of it
|
|
80
|
+
return new RegExp(String.raw`(?<![\w@/.\-])(?<!npx )bongos (${names.join('|')})(?![\w-])`, 'g');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Pure: rewrite core-script invocations in a skill/settings command string to the project's CLI
|
|
84
|
+
// spelling (`npx bongos <verb>`, else `npx bongos exec scripts/gds/<x>.js`), and give any bare
|
|
85
|
+
// `bongos <verb>` the same npx prefix. Idempotent: its own output contains neither a
|
|
86
|
+
// `node scripts/…` invocation nor an un-prefixed `bongos <verb>`, so a second run changes nothing.
|
|
66
87
|
function bongosify(text, verbs = VERBS) {
|
|
67
88
|
const map = scriptToVerb(verbs);
|
|
68
|
-
|
|
89
|
+
const bare = bareVerbRe(verbs);
|
|
90
|
+
let out = String(text == null ? '' : text);
|
|
91
|
+
if (bare) out = out.replace(bare, (_whole, verb) => `${PROJECT_CLI} ${verb}`);
|
|
92
|
+
return out
|
|
69
93
|
.replace(GDS_INVOCATION_RE, (_whole, script) => {
|
|
70
94
|
const verb = map[`${script}.js`];
|
|
71
|
-
return verb ?
|
|
95
|
+
return verb ? `${PROJECT_CLI} ${verb}` : `${PROJECT_CLI} exec scripts/gds/${script}.js`;
|
|
72
96
|
})
|
|
73
|
-
.replace(OTHER_SCRIPT_RE, (_whole, rel) =>
|
|
97
|
+
.replace(OTHER_SCRIPT_RE, (_whole, rel) => `${PROJECT_CLI} exec scripts/${rel}`);
|
|
74
98
|
}
|
|
75
99
|
|
|
76
100
|
// ---- instance rebrand (task 2103) -----------------------------------------
|
|
@@ -340,34 +364,38 @@ function copyTreeTransformed(srcDir, destDir, { dryRun, onFile, overwrite = true
|
|
|
340
364
|
// path and is the agent's escape hatch, so allow-listing it would be a blanket
|
|
341
365
|
// grant wearing a narrow rule's clothes. The one script the core's own list
|
|
342
366
|
// blesses (`api.js`) is named in full instead.
|
|
367
|
+
//
|
|
368
|
+
// EVERY RULE IS THE `npx bongos` SPELLING (task 1004621): a project has no bare
|
|
369
|
+
// `bongos` on PATH, so its skills, its CLAUDE.md and Settings → CLI access
|
|
370
|
+
// (task 1004575) all say `npx bongos …`, and a bare-`bongos` rule would allow a
|
|
371
|
+
// command nothing runs. An instance that took the older bare starter gains the
|
|
372
|
+
// npx twin of each bare rule it still holds on its next upgrade
|
|
373
|
+
// (topUpNpxAllowTwins below).
|
|
343
374
|
const STARTER_COMMANDS = [
|
|
344
|
-
'bongos claim:*',
|
|
345
|
-
'bongos ship:*',
|
|
346
|
-
'bongos release:*',
|
|
347
|
-
'bongos reauth:*',
|
|
348
|
-
'bongos paste-token:*',
|
|
349
|
-
// What Settings → CLI access hands out since task 1004575: a clone has no
|
|
350
|
-
// bare `bongos` on PATH, so the copied command runs through npx.
|
|
375
|
+
'npx bongos claim:*',
|
|
376
|
+
'npx bongos ship:*',
|
|
377
|
+
'npx bongos release:*',
|
|
378
|
+
'npx bongos reauth:*',
|
|
351
379
|
'npx bongos paste-token:*',
|
|
352
|
-
'bongos exec scripts/gds/api.js:*',
|
|
380
|
+
'npx bongos exec scripts/gds/api.js:*',
|
|
353
381
|
// The read-only subset (task 1003992, ADR 0304). `git branch` and `git fetch`
|
|
354
382
|
// are exact forms on purpose: a flag turns either into a write or an exec. No
|
|
355
383
|
// `git diff|log|show` rule, also on purpose: the harness already runs their
|
|
356
384
|
// read-only forms unprompted, and a prefix rule would switch off its flag check
|
|
357
385
|
// (`--output=<file>` writes anywhere, `diff --no-index` reads any file).
|
|
358
386
|
// tests/cli_allowlist_permissions.mjs pins this list to the core's, rule for rule.
|
|
359
|
-
'bongos start:*',
|
|
360
|
-
'bongos status:*',
|
|
361
|
-
'bongos recall:*',
|
|
362
|
-
'bongos exec scripts/gds/me.js:*',
|
|
363
|
-
'bongos doctor:*',
|
|
364
|
-
'bongos exec scripts/gds/preflight.js:*',
|
|
365
|
-
'bongos exec scripts/gds/skill-preflight.js:*',
|
|
366
|
-
'bongos exec scripts/gds/ship-check.js:*',
|
|
367
|
-
'bongos exec scripts/gds/fitness.js:*',
|
|
368
|
-
'bongos exec scripts/gds/strand-watch.js:*',
|
|
369
|
-
'bongos exec scripts/gds/stale-worktrees.js:*',
|
|
370
|
-
'bongos exec scripts/gds/read-session-export.js:*',
|
|
387
|
+
'npx bongos start:*',
|
|
388
|
+
'npx bongos status:*',
|
|
389
|
+
'npx bongos recall:*',
|
|
390
|
+
'npx bongos exec scripts/gds/me.js:*',
|
|
391
|
+
'npx bongos doctor:*',
|
|
392
|
+
'npx bongos exec scripts/gds/preflight.js:*',
|
|
393
|
+
'npx bongos exec scripts/gds/skill-preflight.js:*',
|
|
394
|
+
'npx bongos exec scripts/gds/ship-check.js:*',
|
|
395
|
+
'npx bongos exec scripts/gds/fitness.js:*',
|
|
396
|
+
'npx bongos exec scripts/gds/strand-watch.js:*',
|
|
397
|
+
'npx bongos exec scripts/gds/stale-worktrees.js:*',
|
|
398
|
+
'npx bongos exec scripts/gds/read-session-export.js:*',
|
|
371
399
|
'git status:*',
|
|
372
400
|
'git branch --list',
|
|
373
401
|
'git branch --show-current',
|
|
@@ -473,6 +501,37 @@ function topUpStarterDeny(existingText, wanted) {
|
|
|
473
501
|
return { text: `${JSON.stringify(next, null, 2)}\n`, added, reason: null };
|
|
474
502
|
}
|
|
475
503
|
|
|
504
|
+
// topUpNpxAllowTwins — the `npx bongos` twin of every bare-`bongos` allow rule an instance's OWN
|
|
505
|
+
// settings.json holds (task 1004621). The skills an upgrade lands now say `npx bongos ship`, and a
|
|
506
|
+
// `Bash(bongos ship:*)` rule does not match that, so without the twin the upgrade would trade a
|
|
507
|
+
// `command not found` for a permission prompt on every blessed command. This is NOT the shell-rule
|
|
508
|
+
// top-up task 1004158 ruled out: nothing new is offered. Each twin is a rule the owner already
|
|
509
|
+
// holds, respelled for the one launcher that reaches the same project-local bin — a rule they
|
|
510
|
+
// removed gets no twin, and nothing they wrote is removed or rewritten. Same { text, added, reason }
|
|
511
|
+
// contract as topUpStarterDeny; an unreadable shape is left alone and reported.
|
|
512
|
+
const BARE_BONGOS_RULE_RE = /^(\w+)\(bongos (.+)\)$/;
|
|
513
|
+
function topUpNpxAllowTwins(existingText) {
|
|
514
|
+
const isObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
515
|
+
const cannotCheck = (reason) => ({ text: null, added: [], reason });
|
|
516
|
+
let doc;
|
|
517
|
+
try { doc = JSON.parse(existingText); } catch (err) { return cannotCheck(`not valid JSON (${err.message})`); }
|
|
518
|
+
if (!isObject(doc)) return cannotCheck('its top level is not a JSON object');
|
|
519
|
+
const perms = doc.permissions === undefined ? {} : doc.permissions;
|
|
520
|
+
if (!isObject(perms)) return cannotCheck('its "permissions" is not an object');
|
|
521
|
+
const allow = perms.allow === undefined ? [] : perms.allow;
|
|
522
|
+
if (!Array.isArray(allow)) return cannotCheck('its "permissions.allow" is not an array');
|
|
523
|
+
const have = new Set(allow);
|
|
524
|
+
const added = [];
|
|
525
|
+
for (const rule of allow) {
|
|
526
|
+
const m = typeof rule === 'string' && rule.match(BARE_BONGOS_RULE_RE);
|
|
527
|
+
const twin = m && `${m[1]}(${PROJECT_CLI} ${m[2]})`;
|
|
528
|
+
if (twin && !have.has(twin)) { have.add(twin); added.push(twin); }
|
|
529
|
+
}
|
|
530
|
+
if (!added.length) return { text: null, added: [], reason: null };
|
|
531
|
+
const next = { ...doc, permissions: { ...perms, allow: [...allow, ...added] } };
|
|
532
|
+
return { text: `${JSON.stringify(next, null, 2)}\n`, added, reason: null };
|
|
533
|
+
}
|
|
534
|
+
|
|
476
535
|
// The starter an instance gets when the core root carries no settings.json — a
|
|
477
536
|
// package-only install. Permissions ONLY: no hooks (they are topology), no env.
|
|
478
537
|
function renderStarterSettings(branding = {}) {
|
|
@@ -778,9 +837,26 @@ function materializeClaude({ coreRoot, instanceDir, dryRun = false, overwrite =
|
|
|
778
837
|
// The read is guarded: existsSync passing does not mean readable (a directory at that path,
|
|
779
838
|
// a locked or root-owned file), and this runs AFTER the pin moved and the migrations ran, so
|
|
780
839
|
// a throw here would leave a half-applied upgrade. It fails soft, like its sibling above.
|
|
840
|
+
// The npx twins of the owner's own bare-`bongos` allow rules ride the same single read and
|
|
841
|
+
// write (task 1004621): applied first, so the deny top-up below works on — and writes — the
|
|
842
|
+
// text that already carries them. An unparseable file trips both the same way and is
|
|
843
|
+
// reported once, by the deny half.
|
|
781
844
|
let topUp;
|
|
782
|
-
|
|
783
|
-
|
|
845
|
+
let twins = { text: null, added: [], reason: null };
|
|
846
|
+
try {
|
|
847
|
+
const raw = fs.readFileSync(settingsDest, 'utf8');
|
|
848
|
+
twins = topUpNpxAllowTwins(raw);
|
|
849
|
+
topUp = topUpStarterDeny(twins.text || raw, credentialDeny(toBranding));
|
|
850
|
+
} catch (err) { topUp = { text: null, added: [], reason: `it could not be read (${err.code || err.message})` }; }
|
|
851
|
+
if (twins.added.length) {
|
|
852
|
+
summary.settingsAllowTwinsAdded = twins.added;
|
|
853
|
+
summary.notes.push(`.claude/settings.json kept every rule it already had and GAINED the \`npx bongos\` twin of ${twins.added.length} bare-\`bongos\` allow rule(s) it held — the skills now say \`npx bongos …\`, which a bare rule does not match: ${twins.added.join(', ')}`);
|
|
854
|
+
log(` ${dryRun ? '[dry-run] would add' : '✓ added'} the npx twin of ${twins.added.length} bare-bongos allow rule(s) to .claude/settings.json (skills now run \`npx bongos …\`; every rule you wrote is kept): ${twins.added.join(', ')}`);
|
|
855
|
+
if (!topUp.text && !topUp.reason) {
|
|
856
|
+
if (!dryRun) fs.writeFileSync(settingsDest, twins.text);
|
|
857
|
+
if (!dryRun) addWritten('.claude/settings.json'); // real run only — the same reasoning as the deny write below
|
|
858
|
+
}
|
|
859
|
+
}
|
|
784
860
|
if (topUp.text) {
|
|
785
861
|
if (!dryRun) fs.writeFileSync(settingsDest, topUp.text);
|
|
786
862
|
// Reported as written only on the REAL run, deliberately. writtenPaths doubles as the set
|
|
@@ -803,7 +879,7 @@ function materializeClaude({ coreRoot, instanceDir, dryRun = false, overwrite =
|
|
|
803
879
|
summary.settingsDenyUncheckable = topUp.reason;
|
|
804
880
|
summary.notes.push(`.claude/settings.json could not be checked for missing deny rules — ${topUp.reason}. It was left exactly as it is, so if it predates task 1003966 its local agent can still read keys and session tokens; compare its permissions.deny against the starter by hand, or fix the file and upgrade again.`);
|
|
805
881
|
log(` ! .claude/settings.json could NOT be checked for missing deny rules — ${topUp.reason}. Left untouched; its deny half may still be missing.`);
|
|
806
|
-
} else {
|
|
882
|
+
} else if (!twins.added.length) {
|
|
807
883
|
summary.skippedFiles.push('settings.json');
|
|
808
884
|
log(' • .claude/settings.json already present — left untouched (the instance owns it; it already carries every credential deny rule. The starter\'s shelf rules are not offered here — they guard a workflow, not a secret, and are starter-only: task 1004158).');
|
|
809
885
|
}
|
|
@@ -859,9 +935,27 @@ function materializeClaude({ coreRoot, instanceDir, dryRun = false, overwrite =
|
|
|
859
935
|
// branding. Written only when ABSENT — never clobber a CLAUDE.md the instance has
|
|
860
936
|
// grown (safe for init, adopt, and upgrade alike; the self-copy guard above already
|
|
861
937
|
// keeps this from ever touching the core/OTB checkout's own CLAUDE.md).
|
|
938
|
+
// One narrow exception (task 1004621): the starter's OWN sentences that told the assistant to
|
|
939
|
+
// type a bare `bongos …` — not on PATH in a project — are swapped for their `npx bongos …` form
|
|
940
|
+
// where they still read exactly as the starter wrote them (refreshStarterCliLines). Not in
|
|
941
|
+
// adopt mode, which never touches what the owner already had.
|
|
862
942
|
const claudeMdPath = path.join(instanceDir, 'CLAUDE.md');
|
|
863
943
|
if (fs.existsSync(claudeMdPath)) {
|
|
864
|
-
|
|
944
|
+
let refreshed = { changed: 0 };
|
|
945
|
+
if (overwrite) {
|
|
946
|
+
try { refreshed = refreshStarterCliLines(fs.readFileSync(claudeMdPath, 'utf8')); } catch { /* unreadable: leave it, as before */ }
|
|
947
|
+
}
|
|
948
|
+
if (refreshed.changed) {
|
|
949
|
+
if (!dryRun) fs.writeFileSync(claudeMdPath, refreshed.text);
|
|
950
|
+
// Real run only, like the settings top-up: it preserves content, so the upgrade pre-flight's
|
|
951
|
+
// "materialize overwrites this anyway" waiver must not cover an owner's uncommitted edit.
|
|
952
|
+
if (!dryRun) addWritten('CLAUDE.md');
|
|
953
|
+
summary.claudeMdCliLines = refreshed.changed;
|
|
954
|
+
summary.notes.push(`CLAUDE.md kept as the instance's own, except ${refreshed.changed} unedited starter sentence(s) now say \`npx bongos …\` — the CLI is not on PATH in a project, so a bare \`bongos\` failed with command not found.`);
|
|
955
|
+
log(` ${dryRun ? '[dry-run] would update' : '✓ updated'} ${refreshed.changed} unedited starter sentence(s) in CLAUDE.md to \`npx bongos …\` (everything else untouched)`);
|
|
956
|
+
} else {
|
|
957
|
+
summary.notes.push('CLAUDE.md already present — left untouched (the instance owns it).');
|
|
958
|
+
}
|
|
865
959
|
} else {
|
|
866
960
|
if (!dryRun) {
|
|
867
961
|
fs.mkdirSync(instanceDir, { recursive: true });
|
|
@@ -1018,4 +1112,4 @@ if (require.main === module) {
|
|
|
1018
1112
|
process.exit(s.refused.length ? 1 : 0);
|
|
1019
1113
|
}
|
|
1020
1114
|
|
|
1021
|
-
module.exports = { scriptToVerb, bongosify, transformSettings, readSettingsTransform, renderStarterSettings, STARTER_ALLOW, copyTreeTransformed, materializeClaude, materializeWillRewrite, materializeModuleSkillsIntoCore, isCoreCheckoutRoot, readModuleSkillsManifest, MODULE_SKILLS_MANIFEST, readInstanceSkillsManifest, INSTANCE_SKILLS_MANIFEST, GDS_INVOCATION_RE, buildRebrandRules, rebrandText, disabledModuleSkills, moduleSkillRoster, moduleSkillSources, vendoredSkillGate, SKILL_NAME_RE, VENDOR_DEFAULT_POLICY };
|
|
1115
|
+
module.exports = { scriptToVerb, bongosify, PROJECT_CLI, topUpNpxAllowTwins, transformSettings, readSettingsTransform, renderStarterSettings, STARTER_ALLOW, copyTreeTransformed, materializeClaude, materializeWillRewrite, materializeModuleSkillsIntoCore, isCoreCheckoutRoot, readModuleSkillsManifest, MODULE_SKILLS_MANIFEST, readInstanceSkillsManifest, INSTANCE_SKILLS_MANIFEST, GDS_INVOCATION_RE, buildRebrandRules, rebrandText, disabledModuleSkills, moduleSkillRoster, moduleSkillSources, vendoredSkillGate, SKILL_NAME_RE, VENDOR_DEFAULT_POLICY };
|