@bongos/core 1.17.0 → 1.17.1
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 +154 -83
- package/CLAUDE.md +11 -238
- package/bin/bongos.js +1 -0
- package/clients/bongos-client/README.md +4 -4
- package/clients/bongos-client/bongos-client.global.js +14 -1
- package/clients/bongos-client/index.cjs +14 -1
- package/clients/bongos-client/index.d.ts +19 -2
- package/clients/bongos-client/index.mjs +11 -1
- package/clients/bongos-client/package.json +1 -1
- package/docs/adr/0133-github-app-manifest-onboarding-signin.md +2 -0
- package/docs/adr/0134-private-first-npm-distribution.md +20 -6
- package/docs/adr/0135-module-upstream-submission-interim-queue.md +34 -0
- package/docs/adr/README.md +5 -4
- package/docs/api/openapi.json +315 -6
- package/docs/api-reference.md +10 -5
- package/docs/architecture.md +1 -1
- package/docs/file-map.md +2 -2
- package/docs/modules-contract.md +2 -0
- package/docs/recipes/instance-deploy.md +4 -4
- package/docs/recipes/instance-first-run.md +51 -0
- package/docs/recipes/packaging-the-core.md +5 -5
- package/docs/recipes/private-npm-distribution.md +22 -12
- package/docs/recipes/self-host.md +1 -1
- package/docs/recipes/upgrading-the-core.md +8 -8
- package/migrations/core_191_module_submissions.sql +45 -0
- package/modules/autonomy/CLAUDE.md +11 -45
- package/modules/builder-settings/CLAUDE.md +11 -54
- package/modules/dev-box/CLAUDE.md +11 -34
- package/modules/discord/CLAUDE.md +11 -46
- package/modules/economy/CLAUDE.md +11 -44
- package/modules/grading/CLAUDE.md +11 -38
- package/modules/hall-ui/CLAUDE.md +11 -57
- package/modules/hall-ui/public/goals-page.js +227 -22
- package/modules/hall-ui/public/goals.html +16 -0
- package/modules/hall-ui/public/new-project-wizard.js +5 -4
- package/modules/ideas/CLAUDE.md +11 -33
- package/modules/lifecycle/CLAUDE.md +11 -59
- package/modules/lifecycle/routes/goals.js +80 -0
- package/modules/memory/CLAUDE.md +11 -36
- package/modules/onboarding/CLAUDE.md +11 -51
- package/modules/security/CLAUDE.md +11 -40
- package/modules/sessions/CLAUDE.md +11 -35
- package/modules/status-ui/CLAUDE.md +11 -43
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/create.html +58 -12
- package/public-landing/index.html +40 -21
- package/scripts/gds/CLAUDE.md +11 -140
- package/scripts/gds/claude-materialize.js +151 -18
- package/scripts/gds/claude-md-template.js +84 -0
- package/scripts/gds/cli-lib.js +112 -2
- package/scripts/gds/gen-api-client.js +10 -10
- package/scripts/gds/init.js +53 -13
- package/scripts/gds/migration-namespace.js +1 -1
- package/scripts/gds/module.js +118 -4
- package/scripts/gds/npmrc.js +93 -0
- package/scripts/gds/onboard.js +13 -24
- package/scripts/gds/package-core.js +56 -14
- package/scripts/gds/provision.js +102 -17
- package/scripts/gds/seed-adopt-tasks.js +1 -1
- package/scripts/gds/status.js +6 -1
- package/scripts/gds/task.js +166 -0
- package/scripts/gds/upgrade.js +58 -18
- package/src/bongos/CLAUDE.md +11 -60
- package/src/bongos/auth.js +11 -0
- package/src/bongos/module-submissions.js +56 -0
- package/src/bongos/route-rank-check.js +6 -0
- package/src/bongos/routes/CLAUDE.md +11 -39
- package/src/bongos/routes/auth.js +6 -1
- package/src/bongos/routes/modules.js +123 -21
- package/src/instance-config.js +1 -1
- package/src/module-api.js +13 -1
- package/tests/claude_materialize.mjs +105 -0
- package/tests/device_flow_start.mjs +73 -0
- package/tests/first_run_engine_check.mjs +79 -0
- package/tests/goal_membership_requests.mjs +33 -3
- package/tests/hook_user_agents.mjs +62 -0
- package/tests/init.mjs +59 -6
- package/tests/instance_mismatch_guard.mjs +101 -0
- package/tests/module_cli.mjs +130 -2
- package/tests/module_submissions.mjs +135 -0
- package/tests/npmrc.mjs +105 -0
- package/tests/onboard.mjs +49 -0
- package/tests/package_core.mjs +68 -22
- package/tests/platform_boot.mjs +5 -2
- package/tests/provision.mjs +107 -8
- package/tests/provisioning_github_app_routes.mjs +29 -2
- package/tests/served_paths_shipped.mjs +2 -2
- package/tests/task_cli.mjs +99 -0
- package/tests/upgrade.mjs +89 -25
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
// carries a live credential verbatim, PRODUCE NOTHING — dist/ is never touched. The
|
|
39
39
|
// in-memory pass makes the abort atomic (same discipline as the mirror export).
|
|
40
40
|
// 5. Only when the whole set is clean: write the tree to
|
|
41
|
-
// dist/
|
|
41
|
+
// dist/bongos-core-<version>/, the .tgz, and the pin MANIFEST.
|
|
42
42
|
//
|
|
43
43
|
// THE VERSION STAMP is the platform version — src/module-api.js CORE_VERSION,
|
|
44
44
|
// read via the SAME fail-soft accessor scripts/gds/module.js + bongos upgrade use
|
|
@@ -55,9 +55,9 @@
|
|
|
55
55
|
// [--version <v>] [--droplet-ip <ip>] [--publish [--live]]
|
|
56
56
|
// Defaults: --ref HEAD, --out-dir dist, --version = platform CORE_VERSION.
|
|
57
57
|
// Outputs (nothing is committed — dist/ is gitignored):
|
|
58
|
-
// dist/
|
|
59
|
-
// dist/
|
|
60
|
-
// dist/
|
|
58
|
+
// dist/bongos-core-<version>/ the redacted core tree (inspectable)
|
|
59
|
+
// dist/bongos-core-<version>.tgz the installable unit
|
|
60
|
+
// dist/bongos-core-<version>.manifest.json the PIN (tree hash + per-file hashes)
|
|
61
61
|
// --publish after building, `npm publish` the .tgz as a PRIVATE scoped
|
|
62
62
|
// package (--access restricted). DRY-RUN unless --live is added
|
|
63
63
|
// (task 2089 / ADR 0134). Private publish needs the cloudbongos
|
|
@@ -81,7 +81,7 @@ const { collectEnvSecrets } = require(path.join(REPO_ROOT, 'modules/security/sec
|
|
|
81
81
|
const { redactBlob, parseLsTree } = require(path.join(REPO_ROOT, 'scripts/public-mirror-export.js'));
|
|
82
82
|
|
|
83
83
|
const MANIFEST_SCHEMA = 1;
|
|
84
|
-
const ARTIFACT_NAME = '
|
|
84
|
+
const ARTIFACT_NAME = 'bongos-core';
|
|
85
85
|
// The tar internal root MUST be `package/` — that is the one hardcoded npm
|
|
86
86
|
// convention for an installable tarball (`npm install ./x.tgz` / `npm pack`
|
|
87
87
|
// output both use it; npm does not read the directory name to find package.json,
|
|
@@ -92,7 +92,7 @@ const TAR_ROOT = 'package/';
|
|
|
92
92
|
// The self-describing manifest carried INSIDE the artifact (minus tarball.* — a
|
|
93
93
|
// file cannot contain its own hash). The authoritative PIN is the side-car next
|
|
94
94
|
// to the .tgz, which adds tarball.{sha256,bytes}.
|
|
95
|
-
const INNER_MANIFEST = `${TAR_ROOT}.
|
|
95
|
+
const INNER_MANIFEST = `${TAR_ROOT}.bongos-core.json`;
|
|
96
96
|
|
|
97
97
|
// ---------------------------------------------------------------------------
|
|
98
98
|
// The platform version — read CORE_VERSION via the doorway, lazily + fail-soft,
|
|
@@ -136,6 +136,38 @@ function sha256hex(buf) {
|
|
|
136
136
|
const DOC_RE = /\.(md|markdown)$/i;
|
|
137
137
|
function isDocPath(p) { return DOC_RE.test(String(p || '')); }
|
|
138
138
|
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// Agent-doc STUB (task 2121; surfaced by the 2026-07-08 demo task-creation audit).
|
|
141
|
+
// Claude Code AUTO-LOADS the nearest file named CLAUDE.md (or AGENTS.md) into the
|
|
142
|
+
// live session whenever an agent opens a sibling file. In a CONSUMER instance the
|
|
143
|
+
// core installs at node_modules/@bongos/core/, so the core repo's OWN CLAUDE.md tree
|
|
144
|
+
// (root + scripts/gds + src/bongos + every module) would silently inject the
|
|
145
|
+
// platform's project memory — wrong project title, hundreds of lines, dangling links
|
|
146
|
+
// — into the instance's context the moment an agent reads any core file.
|
|
147
|
+
// The PUBLIC MIRROR deliberately publishes CLAUDE.md as the portable methodology
|
|
148
|
+
// (ADR 0065 §4), so this cannot live in the shared publish-manifest — it is a
|
|
149
|
+
// package-core-only transform. We keep the files PRESENT (nothing that probes for
|
|
150
|
+
// them by path breaks) but replace their body with a tiny, brand-free stub, turning
|
|
151
|
+
// the auto-load into a harmless "this doesn't govern you" redirect.
|
|
152
|
+
const AGENT_DOC_RE = /(^|\/)(CLAUDE|AGENTS)\.md$/i;
|
|
153
|
+
function isAgentDoc(p) { return AGENT_DOC_RE.test(String(p || '')); }
|
|
154
|
+
const CORE_AGENT_DOC_STUB = `# @bongos/core — installed platform dependency (not your project's CLAUDE.md)
|
|
155
|
+
|
|
156
|
+
This file is a placeholder shipped inside the **@bongos/core** package. The core's own
|
|
157
|
+
internal agent notes are intentionally omitted from the published artifact so they cannot
|
|
158
|
+
load into your instance's context.
|
|
159
|
+
|
|
160
|
+
**It does not govern this repository.** Your own root \`CLAUDE.md\`, your \`.claude/\`
|
|
161
|
+
directory, and the live Bongos database are the source of truth for how work happens here
|
|
162
|
+
and who may do what.
|
|
163
|
+
|
|
164
|
+
You are seeing this only because an agent opened a file under \`node_modules/@bongos/core/\`.
|
|
165
|
+
Nothing in here needs to be read to use the platform — run \`bongos help\` for commands.
|
|
166
|
+
|
|
167
|
+
_To work ON the platform core itself, use the standalone @bongos/core source repository,
|
|
168
|
+
not this vendored copy._
|
|
169
|
+
`;
|
|
170
|
+
|
|
139
171
|
// Fail-loud gate for a FUNCTIONAL file: only an exact live-credential value (an env-secret
|
|
140
172
|
// VALUE present verbatim, >= 8 chars) is a real leak. The sample is withheld (it's a secret).
|
|
141
173
|
function functionalSecretLeaks(view, envSecrets) {
|
|
@@ -154,6 +186,12 @@ function functionalSecretLeaks(view, envSecrets) {
|
|
|
154
186
|
// `advisories` = brand-identity substrings + public IPs that ship as-is (non-fatal, for
|
|
155
187
|
// human awareness) — REUSES findLeaks so the detector is defined once.
|
|
156
188
|
function processCoreFile({ pathRel, raw, rules, sensitiveLiterals, envSecrets }) {
|
|
189
|
+
// An auto-loaded agent doc (CLAUDE.md/AGENTS.md) → ship a brand-free stub, never the
|
|
190
|
+
// core's own project memory (task 2121). The stub is brand-free by construction, so it
|
|
191
|
+
// needs no redaction and can never leak — it short-circuits ahead of the doc path.
|
|
192
|
+
if (isAgentDoc(pathRel)) {
|
|
193
|
+
return { buf: Buffer.from(CORE_AGENT_DOC_STUB, 'utf8'), leaks: [], advisories: [], kind: 'stub' };
|
|
194
|
+
}
|
|
157
195
|
if (isDocPath(pathRel)) {
|
|
158
196
|
const r = redactBlob({ buf: raw, rules, sensitiveLiterals, envSecrets });
|
|
159
197
|
const leaks = r.leaks.map((l) => ({ label: `doc:${l.label}`, sample: l.sample }));
|
|
@@ -174,7 +212,7 @@ function processCoreFile({ pathRel, raw, rules, sensitiveLiterals, envSecrets })
|
|
|
174
212
|
//
|
|
175
213
|
// The source repo's package.json/package-lock.json (isPublishable() includes
|
|
176
214
|
// both) are GAME-heavy — colyseus/phaser/sharp/discord.js plus the repo's own
|
|
177
|
-
// name/version — and are not what a downstream `npm install @
|
|
215
|
+
// name/version — and are not what a downstream `npm install @bongos/core`
|
|
178
216
|
// should see. These two files are SYNTHESIZED here (never copied verbatim from
|
|
179
217
|
// git) and staged in their place. Everything else still ships through the
|
|
180
218
|
// normal isPublishable()/processCoreFile path.
|
|
@@ -195,7 +233,7 @@ function buildPackageJson({ version, sourceDeps, sourceOverrides }) {
|
|
|
195
233
|
dependencies[name] = range;
|
|
196
234
|
}
|
|
197
235
|
const pkg = {
|
|
198
|
-
name: '@
|
|
236
|
+
name: '@bongos/core',
|
|
199
237
|
version,
|
|
200
238
|
description: 'Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).',
|
|
201
239
|
license: 'AGPL-3.0-or-later',
|
|
@@ -215,7 +253,7 @@ function buildPackageJson({ version, sourceDeps, sourceOverrides }) {
|
|
|
215
253
|
// default. `access:"restricted"` makes a scoped package private on publish, so a
|
|
216
254
|
// paid npm org can ship it where only members can install — no accidental public
|
|
217
255
|
// leak if someone forgets the CLI flag. Going public at launch is one step:
|
|
218
|
-
// `npm access public @
|
|
256
|
+
// `npm access public @bongos/core` (or flip this to "public"). The name must
|
|
219
257
|
// stay SCOPED to be private at all — unscoped npm names are always public.
|
|
220
258
|
publishConfig: { access: 'restricted' },
|
|
221
259
|
dependencies,
|
|
@@ -481,7 +519,7 @@ function publishTarball({ tgzPath, live }) {
|
|
|
481
519
|
if (!live) {
|
|
482
520
|
console.error(' Dry-run: nothing is uploaded. To publish for real: re-run with --publish --live.');
|
|
483
521
|
console.error(' That is PERMANENT and requires `npm login` + the cloudbongos npm org on a paid plan');
|
|
484
|
-
console.error(' (private scoped publish needs one). Going public later: `npm access public @
|
|
522
|
+
console.error(' (private scoped publish needs one). Going public later: `npm access public @bongos/core`.');
|
|
485
523
|
}
|
|
486
524
|
const r = spawnSync(NPM_BIN, args, { cwd: REPO_ROOT, stdio: ['inherit', process.stderr, 'inherit'] });
|
|
487
525
|
if (r.error || r.status !== 0) {
|
|
@@ -489,7 +527,7 @@ function publishTarball({ tgzPath, live }) {
|
|
|
489
527
|
if (live) console.error(' Common causes: not logged in (`npm login`), the org isn’t on a paid plan (private scoped publish requires one), or 2FA.');
|
|
490
528
|
process.exit(r.status || 1);
|
|
491
529
|
}
|
|
492
|
-
console.error(`[package-core] npm publish ${live ? 'succeeded — @
|
|
530
|
+
console.error(`[package-core] npm publish ${live ? 'succeeded — @bongos/core is live (private).' : 'dry-run OK — the tarball is publishable.'}`);
|
|
493
531
|
}
|
|
494
532
|
|
|
495
533
|
function main() {
|
|
@@ -520,7 +558,7 @@ function main() {
|
|
|
520
558
|
|
|
521
559
|
// 2b. Synthesize the core's own package.json + pruned package-lock.json from the
|
|
522
560
|
// SOURCE files at this ref (never the live working tree, so --ref stays
|
|
523
|
-
// reproducible) — express+pg only, name @
|
|
561
|
+
// reproducible) — express+pg only, name @bongos/core, version = the
|
|
524
562
|
// platform version being packaged.
|
|
525
563
|
const pkgJsonBlob = entries.find((e) => e.path === 'package.json');
|
|
526
564
|
const pkgLockBlob = entries.find((e) => e.path === 'package-lock.json');
|
|
@@ -557,6 +595,7 @@ function main() {
|
|
|
557
595
|
const advisories = []; // { path, label, sample } — non-fatal (brand/IP that ships verbatim)
|
|
558
596
|
let docCount = 0;
|
|
559
597
|
let funcCount = 0;
|
|
598
|
+
let stubCount = 0; // agent docs (CLAUDE.md/AGENTS.md) replaced with the brand-free stub (task 2121)
|
|
560
599
|
for (const e of publishable) {
|
|
561
600
|
let raw;
|
|
562
601
|
try { raw = gitBuf(['cat-file', 'blob', e.sha]); }
|
|
@@ -565,7 +604,7 @@ function main() {
|
|
|
565
604
|
processCoreFile({ pathRel: e.path, raw, rules, sensitiveLiterals, envSecrets });
|
|
566
605
|
for (const l of fileLeaks) leaks.push({ path: e.path, label: l.label, sample: l.sample });
|
|
567
606
|
for (const a of fileAdvisories) advisories.push({ path: e.path, label: a.label, sample: a.sample });
|
|
568
|
-
if (kind === 'doc') docCount++; else funcCount++;
|
|
607
|
+
if (kind === 'doc') docCount++; else if (kind === 'stub') stubCount++; else funcCount++;
|
|
569
608
|
filesMeta.push({ path: e.path, mode: e.mode, sha256: sha256hex(buf) });
|
|
570
609
|
staged.push({ name: TAR_ROOT + e.path, mode: parseInt(normalizeMode(e.mode), 8), buf });
|
|
571
610
|
}
|
|
@@ -593,7 +632,7 @@ function main() {
|
|
|
593
632
|
console.error(' for a live-credential leak, remove the secret at SOURCE (it should never be committed). Then re-run. dist/ was NOT touched.');
|
|
594
633
|
process.exit(1);
|
|
595
634
|
}
|
|
596
|
-
console.error(`[package-core] no-leak gate PASSED — ${staged.length} files clean (${docCount} docs redacted, ${funcCount} functional verbatim).`);
|
|
635
|
+
console.error(`[package-core] no-leak gate PASSED — ${staged.length} files clean (${docCount} docs redacted, ${stubCount} agent-docs stubbed, ${funcCount} functional verbatim).`);
|
|
597
636
|
|
|
598
637
|
// Advisory: brand-identity + public-IP literals that ship VERBATIM inside functional
|
|
599
638
|
// files. Not a leak (these are public, not secrets; a downstream rebrands via config) —
|
|
@@ -614,6 +653,7 @@ function main() {
|
|
|
614
653
|
redaction: {
|
|
615
654
|
model: 'docs-redacted+functional-verbatim', // task 1824
|
|
616
655
|
docs_redacted: docCount,
|
|
656
|
+
agent_docs_stubbed: stubCount, // CLAUDE.md/AGENTS.md → brand-free stub (task 2121)
|
|
617
657
|
functional_verbatim: funcCount,
|
|
618
658
|
rules: rules.length,
|
|
619
659
|
gate_literals: sensitiveLiterals.length,
|
|
@@ -675,6 +715,8 @@ module.exports = {
|
|
|
675
715
|
octal, tarChecksum, ustarHeader, splitUstarName, buildTar,
|
|
676
716
|
// task 1824 — the consumer-core file-classification layer (pure; unit-tested)
|
|
677
717
|
isDocPath, functionalSecretLeaks, processCoreFile,
|
|
718
|
+
// task 2121 — agent-doc (CLAUDE.md/AGENTS.md) stub so a consumer's node_modules can't auto-load core memory
|
|
719
|
+
isAgentDoc, CORE_AGENT_DOC_STUB,
|
|
678
720
|
// task 1882 (ADR 0108 §2) — the real installable npm package
|
|
679
721
|
CORE_DEP_NAMES, buildPackageJson, resolveLockDep, lockfileClosure, prunePackageLock,
|
|
680
722
|
// task 2089 (ADR 0134) — private-by-default npm publish
|
package/scripts/gds/provision.js
CHANGED
|
@@ -56,6 +56,12 @@ const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
|
56
56
|
// plain require (its require.main guard means importing it runs no CLI).
|
|
57
57
|
const oauthSecret = require('./oauth-secret');
|
|
58
58
|
|
|
59
|
+
// The registry-install gates + env-fed .npmrc (ADR 0134, task 2090). Lets the
|
|
60
|
+
// standalone scaffold OPT IN to pulling the PRIVATE @bongos/core from the npm
|
|
61
|
+
// registry (PROVISION_CORE_FROM_REGISTRY=1) instead of vendoring a tarball, with a
|
|
62
|
+
// fail-soft fallback to the vendored path when no NPM_TOKEN is present.
|
|
63
|
+
const { registryCoreRequested, hasNpmToken } = require('./npmrc');
|
|
64
|
+
|
|
59
65
|
// The HTTP User-Agent the manifest conversion stamps its GitHub call with (GitHub
|
|
60
66
|
// requires a UA). Overridable so a fork can identify itself; not secret.
|
|
61
67
|
const MANIFEST_UA = process.env.PROVISION_GITHUB_UA || 'cloudbongos-provisioner';
|
|
@@ -118,7 +124,7 @@ const CONFIG = {
|
|
|
118
124
|
// since the control plane and the run box are then the same machine.
|
|
119
125
|
controlPlaneIp: process.env.PROVISION_CONTROL_PLANE_IP || null,
|
|
120
126
|
// Parent dir for a STANDALONE instance's OWN repo checkout (hosting_shape='standalone',
|
|
121
|
-
// ADR 0108): each lives at <standaloneBase>/<slug> with @
|
|
127
|
+
// ADR 0108): each lives at <standaloneBase>/<slug> with @bongos/core installed as
|
|
122
128
|
// an npm dep. Distinct from co-tenant (which shares THIS control-plane checkout).
|
|
123
129
|
standaloneBase: process.env.PROVISION_STANDALONE_BASE || '/srv/cloudbongos',
|
|
124
130
|
// ── Privilege escalation (task 2066, ADR 0128) ───────────────────────────
|
|
@@ -207,13 +213,45 @@ function migrateCmd(inst, { privileged = false } = {}) {
|
|
|
207
213
|
: `PGDATABASE=${dbName(inst)} MEDUSA_GDS_ONLY=1 ./scripts/migrate.sh`;
|
|
208
214
|
}
|
|
209
215
|
|
|
216
|
+
// Seed the instance's FIRST version so it can take tasks from minute one (task 2119,
|
|
217
|
+
// goal 35). A freshly-migrated instance DB comes up with an EMPTY versions table —
|
|
218
|
+
// migrate runs instance-seeds-off (ADR 0105) and `bongos init` ran --no-seed — yet
|
|
219
|
+
// every task requires a version (tasks.version_id NOT NULL) and POST /versions is
|
|
220
|
+
// Archon-only (no Archon exists until first sign-in). Without a seed here, a brand-new
|
|
221
|
+
// project has NOWHERE to file a task. This runs on the box's DB path (no rank gate),
|
|
222
|
+
// ONLY IF the versions table is empty: idempotent, safe to re-run, and it never
|
|
223
|
+
// disturbs a project that already has versions. Mirrors seedViaApi's shape — V1
|
|
224
|
+
// (status 'building') + one done-when criterion so /status has a finish line from day
|
|
225
|
+
// one. `track` is set EXPLICITLY: versions.track is NOT NULL with no default (mig 112
|
|
226
|
+
// dropped the CHECK), and 'product' matches the canonical V1 seed in 003_pms.sql.
|
|
227
|
+
//
|
|
228
|
+
// The id/name/done_when come from instanceInitSpec so the starter version has ONE
|
|
229
|
+
// source of truth (the same spec the greenfield `bongos init` seed uses). SQL string
|
|
230
|
+
// literals are DOLLAR-QUOTED ($v$…$v$) so the whole -c argument is single-quoted —
|
|
231
|
+
// the one form that is safe UNCHANGED across all three executors: inline (/bin/sh),
|
|
232
|
+
// over SSH (bash -s), AND embedded in a dedicated droplet's cloud-init (bash -lc "…"),
|
|
233
|
+
// with no nested-quote or $-expansion hazard.
|
|
234
|
+
function seedFirstVersionCmd(inst, { privileged = false } = {}) {
|
|
235
|
+
const v = instanceInitSpec(inst).version;
|
|
236
|
+
const q = (s) => `$v$${s}$v$`; // dollar-quoted literal (no value contains a single quote or $)
|
|
237
|
+
const sql =
|
|
238
|
+
`INSERT INTO versions (id, name, track, status, done_when) ` +
|
|
239
|
+
`SELECT ${q(v.id)}, ${q(v.name)}, $v$product$v$, $v$building$v$, ${q(v.done_when)} ` +
|
|
240
|
+
`WHERE NOT EXISTS (SELECT 1 FROM versions); ` +
|
|
241
|
+
`INSERT INTO done_when_criteria (version_id, criterion_id, criterion_md, sort_order) ` +
|
|
242
|
+
`SELECT ${q(v.id)}, $v$first-goal$v$, ${q(v.done_when)}, 10 ` +
|
|
243
|
+
`WHERE EXISTS (SELECT 1 FROM versions WHERE id = ${q(v.id)}) ` +
|
|
244
|
+
`AND NOT EXISTS (SELECT 1 FROM done_when_criteria WHERE version_id = ${q(v.id)} AND criterion_id = $v$first-goal$v$);`;
|
|
245
|
+
return asPostgres(`psql -v ON_ERROR_STOP=1 -d ${dbName(inst)} -c '${sql}'`, privileged);
|
|
246
|
+
}
|
|
247
|
+
|
|
210
248
|
// ── STANDALONE shape (hosting_shape='standalone', ADR 0108, task 1978) ────────
|
|
211
249
|
// A standalone instance runs from its OWN repo checkout (host content: config/,
|
|
212
|
-
// migrations/instance/, the core PIN in package.json) with @
|
|
250
|
+
// migrations/instance/, the core PIN in package.json) with @bongos/core installed
|
|
213
251
|
// as an npm DEPENDENCY — NOT the shared control-plane checkout the co-tenant shape uses.
|
|
214
252
|
// It is the ADR 0108 DESTINATION model; these generators make provision.js drive the
|
|
215
253
|
// scripts/deploy/deploy-instance.sh runbook API-driven instead of by hand.
|
|
216
|
-
const CORE_PKG_DIR = 'node_modules/@
|
|
254
|
+
const CORE_PKG_DIR = 'node_modules/@bongos/core';
|
|
217
255
|
function standaloneRoot(inst, { base = CONFIG.standaloneBase } = {}) { return path.posix.join(base, inst.slug); }
|
|
218
256
|
// Sync the instance's own repo (host content + the core pin). Mirrors deploy-instance.sh
|
|
219
257
|
// step 1 — assumes the repo is already cloned at standaloneRoot (initial clone is a
|
|
@@ -281,7 +319,7 @@ function instanceInitSpec(inst, { ownerLogin } = {}) {
|
|
|
281
319
|
const INSTANCE_GITIGNORE = ['node_modules/', 'dist/', '.env', '.env.local', '*.log', ''].join('\n');
|
|
282
320
|
|
|
283
321
|
// Clone the (empty) target_ref → standaloneRoot → materialize config/ + .claude/ + a
|
|
284
|
-
// file:-pinned @
|
|
322
|
+
// file:-pinned @bongos/core + lockfile (via `bongos init --vendor-core`, task 2053)
|
|
285
323
|
// → commit + push. Idempotent (skips once the repo carries a package.json) and dry-run
|
|
286
324
|
// safe (every mutation goes through the injected exec/writeFile). ADR 0108 / 0125.
|
|
287
325
|
async function scaffoldStandaloneRepo(inst, deps) {
|
|
@@ -327,23 +365,44 @@ async function scaffoldStandaloneRepo(inst, deps) {
|
|
|
327
365
|
}
|
|
328
366
|
exec(`git -C ${root} checkout -B main`);
|
|
329
367
|
|
|
330
|
-
//
|
|
331
|
-
|
|
332
|
-
|
|
368
|
+
// Core-install path (ADR 0134, task 2090). DEFAULT = vendor a tarball built from THIS
|
|
369
|
+
// control-plane checkout (offline, proven — task 2053). OPT-IN = pull the PRIVATE
|
|
370
|
+
// @bongos/core from the npm registry (PROVISION_CORE_FROM_REGISTRY=1). The registry
|
|
371
|
+
// path needs NPM_TOKEN at `npm ci` time; if the flag is set but no token is present we
|
|
372
|
+
// FAIL SOFT to the vendored path so a tokenless standup still works (never a broken
|
|
373
|
+
// instance). The env is injectable for tests; the runner falls back to process.env.
|
|
374
|
+
const env = deps.env || process.env;
|
|
375
|
+
const wantRegistry = registryCoreRequested(env);
|
|
376
|
+
const useRegistry = wantRegistry && hasNpmToken(env);
|
|
377
|
+
if (wantRegistry && !useRegistry) {
|
|
378
|
+
log(' [scaffold] PROVISION_CORE_FROM_REGISTRY set but no NPM_TOKEN in env — falling back to the vendored-tarball core (ADR 0134 fail-soft).');
|
|
379
|
+
}
|
|
333
380
|
|
|
334
|
-
//
|
|
335
|
-
//
|
|
381
|
+
// materialize config/ + .claude/ + a pinned package.json + lockfile via `bongos init`.
|
|
382
|
+
// --no-seed: the instance's own DB is seeded by migrate, not here. Two mutually
|
|
383
|
+
// exclusive core-resolution shapes:
|
|
384
|
+
// registry (--registry-core): caret pin + env-fed .npmrc; `npm ci` pulls from the
|
|
385
|
+
// registry. init generates the lockfile (NPM_TOKEN is present — we gated on it).
|
|
386
|
+
// vendored (--vendor-core, default): a tarball built from this checkout, file:-pinned.
|
|
336
387
|
const spec = instanceInitSpec(inst, { ownerLogin });
|
|
337
388
|
const specPath = path.join(os.tmpdir(), `bongos-init-${inst.slug}.json`);
|
|
338
389
|
writeFile(specPath, JSON.stringify(spec, null, 2));
|
|
339
|
-
|
|
390
|
+
if (useRegistry) {
|
|
391
|
+
log(` [scaffold] core install: registry — PRIVATE @bongos/core@${coreV} via env-fed NPM_TOKEN (ADR 0134)`);
|
|
392
|
+
exec(`node scripts/gds/init.js --from ${specPath} --dir ${root} --no-seed --force --registry-core`);
|
|
393
|
+
} else {
|
|
394
|
+
log(` [scaffold] core install: vendored tarball (offline default)`);
|
|
395
|
+
exec(`node scripts/gds/package-core.js`); // cwd = REPO_ROOT → dist/bongos-core-<v>.tgz
|
|
396
|
+
const tgz = path.join(REPO_ROOT, 'dist', `bongos-core-${coreV}.tgz`);
|
|
397
|
+
exec(`node scripts/gds/init.js --from ${specPath} --dir ${root} --no-seed --force --vendor-core ${tgz}`);
|
|
398
|
+
}
|
|
340
399
|
|
|
341
400
|
// 4. commit + push the scaffold to the instance's own main (keep node_modules out).
|
|
342
401
|
writeFile(path.join(root, '.gitignore'), INSTANCE_GITIGNORE);
|
|
343
402
|
exec(`git -C ${root} config user.name "Cloud Bongos"`);
|
|
344
403
|
exec(`git -C ${root} config user.email "bot@cloudbongos.com"`);
|
|
345
404
|
exec(`git -C ${root} add -A`);
|
|
346
|
-
exec(`git -C ${root} commit -m "chore: scaffold ${inst.slug} on a pinned @
|
|
405
|
+
exec(`git -C ${root} commit -m "chore: scaffold ${inst.slug} on a pinned @bongos/core ${coreV} (provision.js, ADR 0108)"`, { allowFail: true });
|
|
347
406
|
exec(`git -C ${root} push -u origin main`);
|
|
348
407
|
log(` [scaffold] pushed initial scaffold to ${repo.owner}/${repo.name}@main`);
|
|
349
408
|
return { ok: true, scaffolded: true };
|
|
@@ -446,11 +505,25 @@ ${prefix}_OAUTH_ORIGIN=${origin}
|
|
|
446
505
|
# GitHub App Manifest flow (ADR 0133) — the owner clicked one link; no secret was pasted.`
|
|
447
506
|
: `# Provisioned by scripts/gds/provision.js (ADR 0111). Fill the OAuth app creds via the
|
|
448
507
|
# one-click GitHub App Manifest flow (ADR 0133), or manually (oauth-secret.js place):`;
|
|
508
|
+
// The founding owner (firstAdmin): the GitHub login src/bongos/auth.js seats as the
|
|
509
|
+
// instance's first Archon on first sign-in (maybeSeatFirstAdmin checks
|
|
510
|
+
// <prefix>_FIRST_ADMIN before branding.firstAdmin). WITHOUT it a provisioned instance
|
|
511
|
+
// has NO owner — first sign-in is refused / lands rankless and the owner can never
|
|
512
|
+
// claim their own instance (goal 35 / criterion 165 regression, caught live 2026-07-07:
|
|
513
|
+
// demo had every origin override but no FIRST_ADMIN, so lars589 could not be seated).
|
|
514
|
+
// Derived from the instance repo owner (target_ref = "<owner>/<repo>"), the SAME source
|
|
515
|
+
// instanceInitSpec uses for the scaffold's branding.firstAdmin — so this env override and
|
|
516
|
+
// the scaffolded config/branding.json agree. (Org-owned repo: the org login is not a
|
|
517
|
+
// sign-in identity, so it is left unset; the owner sets <prefix>_FIRST_ADMIN by hand.)
|
|
518
|
+
const ownerRepo = parseTargetRef(inst.target_ref);
|
|
519
|
+
const firstAdminLine = ownerRepo && ownerRepo.owner
|
|
520
|
+
? `${prefix}_FIRST_ADMIN=${ownerRepo.owner}\n`
|
|
521
|
+
: '';
|
|
449
522
|
return `# /etc/${inst.slug}/web.env — per-instance secrets (chmod 600, OUTSIDE the repo).
|
|
450
523
|
${credComment}
|
|
451
524
|
${prefix}_GITHUB_CLIENT_ID=${clientId || ''}
|
|
452
525
|
${prefix}_GITHUB_CLIENT_SECRET=${clientSecret || ''}
|
|
453
|
-
GDS_BRANDING_FILE=${PROVISIONED_BRANDING_FILE}
|
|
526
|
+
${firstAdminLine}GDS_BRANDING_FILE=${PROVISIONED_BRANDING_FILE}
|
|
454
527
|
${originLines}PORT=${inst.port}
|
|
455
528
|
PGDATABASE=${dbName(inst)}
|
|
456
529
|
`;
|
|
@@ -808,7 +881,7 @@ async function provisionInstance(inst, deps) {
|
|
|
808
881
|
// Co-tenant OR standalone: a process bound to an allocated port on the box it runs
|
|
809
882
|
// on. Co-tenant shares that box's checkout ($0, cloudbongos.com model — under
|
|
810
883
|
// remote-exec, the co-hosting box); standalone (ADR 0108) runs from its OWN repo
|
|
811
|
-
// dir with a pinned @
|
|
884
|
+
// dir with a pinned @bongos/core (synced in step 2) on the CONTROL PLANE.
|
|
812
885
|
// Allocate against the ports of whichever box it runs on (boxExec: remote for a
|
|
813
886
|
// co-tenant, local for a standalone — task 2074), then run the rest there.
|
|
814
887
|
if (!inst.port) {
|
|
@@ -838,11 +911,16 @@ async function provisionInstance(inst, deps) {
|
|
|
838
911
|
// ── step 2: code sync (standalone) + database ─────────────────────────────
|
|
839
912
|
if (local && standalone) {
|
|
840
913
|
// Standalone (ADR 0108 / deploy-instance.sh steps 1-2): sync the instance's own
|
|
841
|
-
// repo + install the pinned @
|
|
914
|
+
// repo + install the pinned @bongos/core, so the migrate + service below run
|
|
842
915
|
// from the INSTANCE root, not this control-plane checkout. cwd = standaloneRoot.
|
|
843
916
|
// boxExec = the control plane (where scaffoldStandaloneRepo just created the dir).
|
|
917
|
+
// REGISTRY CORE (ADR 0134): when the scaffold pinned the registry core, the committed
|
|
918
|
+
// .npmrc reads `_authToken=${NPM_TOKEN}` — so `npm ci` resolves the PRIVATE package
|
|
919
|
+
// only if NPM_TOKEN is in this runner's env (makeExec inherits process.env). The
|
|
920
|
+
// scaffold already gated on the token, so it is present; the VENDORED default needs
|
|
921
|
+
// no token (the tarball is committed). A tokenless registry `npm ci` 404s cleanly.
|
|
844
922
|
const root = standaloneRoot(inst);
|
|
845
|
-
log(' [code] standalone — pull instance repo + npm ci (pinned @
|
|
923
|
+
log(' [code] standalone — pull instance repo + npm ci (pinned @bongos/core)');
|
|
846
924
|
boxExec(standalonePullCmd(), { cwd: root });
|
|
847
925
|
boxExec(standaloneInstallCmd(), { cwd: root });
|
|
848
926
|
}
|
|
@@ -851,7 +929,13 @@ async function provisionInstance(inst, deps) {
|
|
|
851
929
|
boxExec(dbCreateCmd(inst, { privileged }), { allowFail: true }); // "already exists" is fine
|
|
852
930
|
if (standalone) boxExec(standaloneMigrateCmd(inst, { privileged }), { cwd: standaloneRoot(inst) }); // instance-root migrate
|
|
853
931
|
else boxExec(migrateCmd(inst, { privileged }));
|
|
854
|
-
|
|
932
|
+
// Seed the first version so the instance can take tasks from minute one (task 2119,
|
|
933
|
+
// goal 35). Only fires if the versions table is empty — idempotent, never disturbs an
|
|
934
|
+
// existing project. NOT allowFail: a re-run is a clean 0-row no-op, so a real failure
|
|
935
|
+
// here should surface. Dedicated bakes the same step into cloud-init (dedicatedUserData).
|
|
936
|
+
log(' [seed] first version (only if versions is empty)');
|
|
937
|
+
boxExec(seedFirstVersionCmd(inst, { privileged }));
|
|
938
|
+
} else { log(' (db create + migrate + first-version seed run via cloud-init on the droplet)'); }
|
|
855
939
|
|
|
856
940
|
// ── step 3: config + per-instance secrets ─────────────────────────────────
|
|
857
941
|
log(' [config] per-instance web.env');
|
|
@@ -929,6 +1013,7 @@ function dedicatedUserData(inst) {
|
|
|
929
1013
|
runcmd:
|
|
930
1014
|
- [ bash, -lc, "${dbCreateCmd(i)} || true" ]
|
|
931
1015
|
- [ bash, -lc, "cd ${REPO_ROOT} && ${migrateCmd(i)}" ]
|
|
1016
|
+
- [ bash, -lc, "${seedFirstVersionCmd(i)}" ]
|
|
932
1017
|
- [ bash, -lc, "systemctl daemon-reload && systemctl enable --now ${i.slug}" ]
|
|
933
1018
|
`;
|
|
934
1019
|
}
|
|
@@ -1310,7 +1395,7 @@ if (require.main === module) main();
|
|
|
1310
1395
|
module.exports = {
|
|
1311
1396
|
CONFIG, MAX_INTENT_ATTEMPTS,
|
|
1312
1397
|
// pure step generators (tested directly)
|
|
1313
|
-
dbName, dbCreateCmd, migrateCmd, serviceUnit, serviceUnitPath, webEnvBody, webEnvPath,
|
|
1398
|
+
dbName, dbCreateCmd, migrateCmd, seedFirstVersionCmd, serviceUnit, serviceUnitPath, webEnvBody, webEnvPath,
|
|
1314
1399
|
standaloneRoot, standalonePullCmd, standaloneInstallCmd, standaloneMigrateCmd,
|
|
1315
1400
|
// standalone scaffold (ADR 0108 / 0125, task 2055)
|
|
1316
1401
|
parseTargetRef, instanceRepoRemote, instanceInitSpec, scaffoldStandaloneRepo, coreVersionSafe,
|
|
@@ -19,7 +19,7 @@ const RUNBOOK_TASK = 1981; // goal-35 canonical runbook; A05 depends on it (exte
|
|
|
19
19
|
const TASKS = [
|
|
20
20
|
{ ref: 'A01', kind: 'feature', priority: 4, est: 180, crit: [CRIT], deps: [], extDeps: [],
|
|
21
21
|
title: 'bongos init --adopt: mode skeleton + additive config layering (no clobber)',
|
|
22
|
-
description: 'Add an `--adopt` mode to `scripts/gds/init.js` (ADR 0121 §Decision 1-2). Instead of the greenfield "stand up a NEW instance" path (which refuses where config/branding.json exists and whose --force OVERWRITES), --adopt LAYERS onto the existing tree: add the pinned @
|
|
22
|
+
description: 'Add an `--adopt` mode to `scripts/gds/init.js` (ADR 0121 §Decision 1-2). Instead of the greenfield "stand up a NEW instance" path (which refuses where config/branding.json exists and whose --force OVERWRITES), --adopt LAYERS onto the existing tree: add the pinned @bongos/core dependency + the lean config/ overrides + .claude/ ADDITIVELY. For any pre-existing file (package.json, an existing .claude/, a CI workflow) it MERGES or LEAVES-AND-REPORTS — it never overwrites, and it must NOT reuse the --force code path. Writes only the lean branding.json overrides (rest resolves from *.neutral.json). Serves criterion adopt-existing-repo. deps: none (the ADR-0108 configurable-root substrate is shipped).' },
|
|
23
23
|
{ ref: 'A02', kind: 'feature', priority: 4, est: 150, crit: [CRIT], deps: [], extDeps: [],
|
|
24
24
|
title: 'bongos init --adopt: detect the existing stack + layout',
|
|
25
25
|
description: 'Before writing anything, --adopt inspects the existing tree — language/runtime, package manager, test + CI setup, directory layout — so the enabled modules, the generated repo-map, and the core<->host fitness boundary (ADR 0062 §1) fit what is already there instead of assuming the reference layout (ADR 0121 §Decision 4). Output: a detected-stack summary consumed by A03 (real-state capture) and A04 (pre-flight). Serves criterion adopt-existing-repo. deps: none.' },
|
package/scripts/gds/status.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
//
|
|
20
20
|
// Exit codes: 0 ok · 1 network/api error · 2 bad invocation · 3 not found.
|
|
21
21
|
|
|
22
|
-
const { API_BASE, cliClient } = require('./cli-lib');
|
|
22
|
+
const { API_BASE, cliClient, assertInstanceMatch } = require('./cli-lib');
|
|
23
23
|
|
|
24
24
|
// The active internal version; override with GDS_STATUS_DEFAULT_VERSION (or pass
|
|
25
25
|
// --version). Bumped GDS-V4 → BONGOS-V1 (task 1709, ADR 0103): GDS-V4 was
|
|
@@ -319,6 +319,11 @@ async function main() {
|
|
|
319
319
|
console.log('Usage: node scripts/gds/status.js [--version ID] [Cn] [--json]');
|
|
320
320
|
process.exit(0);
|
|
321
321
|
}
|
|
322
|
+
// Fail closed if this repo's configured instance != the signed-in session's (task
|
|
323
|
+
// 2124 / demo-audit C2): fetchProgress uses a raw fetch(API_BASE/…) that bypasses the
|
|
324
|
+
// apiCall/cliClient guard, so without this `bongos status` would silently print another
|
|
325
|
+
// instance's progress. Skip only in --hook mode (the automated card path stays fail-open).
|
|
326
|
+
if (!opts.hook) await assertInstanceMatch();
|
|
322
327
|
const data = await fetchProgress(opts.version);
|
|
323
328
|
|
|
324
329
|
// Resolve the requested criterion (if any) once — every output mode needs it.
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/gds/task.js — `bongos task` : create + show a Bongos task from the terminal.
|
|
3
|
+
//
|
|
4
|
+
// task 2123 (demo-audit fix C1). The bongos CLI had start/claim/ship/status/… but no
|
|
5
|
+
// way to CREATE a task — so seeding the first task (a new owner's opening move) meant
|
|
6
|
+
// hand-driving POST /api/bongos/tasks through `bongos exec scripts/gds/api.js`. This is
|
|
7
|
+
// the first-class front door.
|
|
8
|
+
//
|
|
9
|
+
// Usage:
|
|
10
|
+
// bongos task new --title "…" --description "…" [--kind feature] [--discipline engineer]
|
|
11
|
+
// [--priority 3] [--version <id>] [--goal <id>] [--criteria C1,C2]
|
|
12
|
+
// [--touches a/,b/] [--est-minutes N] [--credits N] [--ready] [--json]
|
|
13
|
+
// bongos task show <id> [--json]
|
|
14
|
+
//
|
|
15
|
+
// Task CREATION is Archon-gated SERVER-SIDE (creating work shapes scope, ADR 0016). This
|
|
16
|
+
// CLI adds no authority — a lower rank gets a clear 403 message pointing at idea capture.
|
|
17
|
+
|
|
18
|
+
const { cliClient, requireSession, arg, hasFlag } = require('./cli-lib');
|
|
19
|
+
|
|
20
|
+
// Valid enums (mirrors the server's task-classifier vocab; a friendly client-side check
|
|
21
|
+
// so an obvious typo fails before the round-trip — the server stays authoritative).
|
|
22
|
+
const VALID_KINDS = ['feature', 'bug', 'infra', 'refactor', 'spike', 'learning-capture', 'cleanup', 'decision', 'blocker-resolution', 'verify'];
|
|
23
|
+
const VALID_DISCIPLINES = ['engineer', 'artist', 'ideator', 'ui'];
|
|
24
|
+
|
|
25
|
+
function csv(v) {
|
|
26
|
+
return String(v || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// PURE argv → { body, criteria, errors } — exported for the unit test (no network).
|
|
30
|
+
// `args` is the argv slice AFTER the `new` subcommand.
|
|
31
|
+
function buildTaskBody(args) {
|
|
32
|
+
const errors = [];
|
|
33
|
+
const title = arg('--title', args);
|
|
34
|
+
const description = arg('--description', args);
|
|
35
|
+
if (!title) errors.push('--title is required');
|
|
36
|
+
if (!description) errors.push('--description is required');
|
|
37
|
+
|
|
38
|
+
const kind = arg('--kind', args) || 'feature';
|
|
39
|
+
if (!VALID_KINDS.includes(kind)) errors.push(`--kind must be one of: ${VALID_KINDS.join(', ')}`);
|
|
40
|
+
const discipline = arg('--discipline', args) || 'engineer';
|
|
41
|
+
if (!VALID_DISCIPLINES.includes(discipline)) errors.push(`--discipline must be one of: ${VALID_DISCIPLINES.join(', ')}`);
|
|
42
|
+
|
|
43
|
+
const num = (flag) => { const v = arg(flag, args); return v == null ? undefined : Number(v); };
|
|
44
|
+
const priority = num('--priority');
|
|
45
|
+
if (priority !== undefined && (!Number.isInteger(priority) || priority < 1 || priority > 5)) {
|
|
46
|
+
errors.push('--priority must be an integer 1–5');
|
|
47
|
+
}
|
|
48
|
+
const goalRaw = arg('--goal', args);
|
|
49
|
+
const goalId = goalRaw == null ? undefined : Number(goalRaw);
|
|
50
|
+
if (goalRaw != null && (!Number.isInteger(goalId) || goalId <= 0)) errors.push('--goal must be a positive integer task-goal id');
|
|
51
|
+
|
|
52
|
+
const body = {
|
|
53
|
+
title,
|
|
54
|
+
description,
|
|
55
|
+
kind,
|
|
56
|
+
discipline,
|
|
57
|
+
status: hasFlag('--ready', args) ? 'ready' : 'backlog',
|
|
58
|
+
source: 'bongos-task-cli',
|
|
59
|
+
};
|
|
60
|
+
const version = arg('--version', args);
|
|
61
|
+
if (version) body.version_id = version;
|
|
62
|
+
if (goalId !== undefined) body.goal_id = goalId;
|
|
63
|
+
if (priority !== undefined) body.priority = priority;
|
|
64
|
+
const est = num('--est-minutes'); if (est !== undefined) body.est_minutes = est;
|
|
65
|
+
const md = num('--manual-degree'); if (md !== undefined) body.manual_degree = md;
|
|
66
|
+
const credits = num('--credits'); if (credits !== undefined) body.credits_reward = credits;
|
|
67
|
+
const touches = csv(arg('--touches', args)); if (touches.length) body.touches = touches;
|
|
68
|
+
const criteria = csv(arg('--criteria', args));
|
|
69
|
+
if (criteria.length) body.criterion_ids = criteria;
|
|
70
|
+
|
|
71
|
+
return { body, criteria, errors };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function usage() {
|
|
75
|
+
console.error('usage:');
|
|
76
|
+
console.error(' bongos task new --title "…" --description "…" [--kind feature] [--discipline engineer]');
|
|
77
|
+
console.error(' [--priority 3] [--version <id>] [--goal <id>] [--criteria C1,C2]');
|
|
78
|
+
console.error(' [--touches a/,b/] [--est-minutes N] [--credits N] [--ready] [--json]');
|
|
79
|
+
console.error(' bongos task show <id> [--json]');
|
|
80
|
+
console.error(`\n kinds: ${VALID_KINDS.join(' | ')}`);
|
|
81
|
+
console.error(` disciplines: ${VALID_DISCIPLINES.join(' | ')}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function hallLink(base, id) {
|
|
85
|
+
try { return `${new URL(base).origin}/builders#/task/${id}`; } catch { return `task #${id}`; }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// The version_id a task defaults to when --version is omitted: the most-recent
|
|
89
|
+
// BUILDING version (the capture.js precedent, so idea-capture and task-create agree).
|
|
90
|
+
// Returns null when none is building — the caller then requires an explicit --version.
|
|
91
|
+
async function resolveDefaultVersion(api) {
|
|
92
|
+
const r = await api.versions.getVersions();
|
|
93
|
+
if (!r.ok) return null;
|
|
94
|
+
const building = (r.data?.versions ?? []).filter((v) => v.status === 'building');
|
|
95
|
+
if (building.length === 0) return null;
|
|
96
|
+
building.sort((a, b) => {
|
|
97
|
+
const ta = a.started_at ? new Date(a.started_at).getTime() : 0;
|
|
98
|
+
const tb = b.started_at ? new Date(b.started_at).getTime() : 0;
|
|
99
|
+
return tb - ta;
|
|
100
|
+
});
|
|
101
|
+
return building[0].id;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function cmdNew(args) {
|
|
105
|
+
const { body, errors } = buildTaskBody(args);
|
|
106
|
+
if (errors.length) { console.error('task new: ' + errors.join('; ') + '\n'); usage(); process.exit(2); }
|
|
107
|
+
const api = await cliClient();
|
|
108
|
+
// version_id is required by the API; default to the active (building) version so a
|
|
109
|
+
// new owner never has to know the id, but keep --version as the explicit override.
|
|
110
|
+
if (!body.version_id) {
|
|
111
|
+
body.version_id = await resolveDefaultVersion(api);
|
|
112
|
+
if (!body.version_id) {
|
|
113
|
+
console.error('task new: could not resolve an active version — pass --version <id> (see `bongos status`).');
|
|
114
|
+
process.exit(2);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const r = await api.tasks.postTasks({ body });
|
|
118
|
+
if (r.status === 403) {
|
|
119
|
+
console.error('Creating a task is Archon-gated (creating work shapes scope). Your rank is lower, so the server declined.');
|
|
120
|
+
console.error('To propose it instead, capture an idea for triage:');
|
|
121
|
+
console.error(' bongos exec scripts/gds/capture.js "<your idea>"');
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
if (r.status !== 201 || !r.data?.task) {
|
|
125
|
+
console.error(`task new failed (${r.status}):`, r.data);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
const task = r.data.task;
|
|
129
|
+
if (hasFlag('--json', args)) { console.log(JSON.stringify(task, null, 2)); return; }
|
|
130
|
+
const { API_BASE } = require('./cli-lib');
|
|
131
|
+
console.log(`✓ Created task #${task.id} [${task.status}] — ${task.title}`);
|
|
132
|
+
console.log(` kind=${task.kind} · discipline=${task.discipline}${task.priority ? ` · P${task.priority}` : ''}${task.goal_id ? ` · goal ${task.goal_id}` : ''}`);
|
|
133
|
+
console.log(` ${hallLink(API_BASE, task.id)}`);
|
|
134
|
+
if (task.status !== 'ready') console.log(` (status '${task.status}' — pass --ready to make it claimable now, deps permitting)`);
|
|
135
|
+
console.log(` next: bongos claim ${task.id}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function cmdShow(args) {
|
|
139
|
+
const id = Number(args[0]);
|
|
140
|
+
if (!Number.isInteger(id) || id <= 0) { console.error('usage: bongos task show <id>'); process.exit(2); }
|
|
141
|
+
const api = await cliClient();
|
|
142
|
+
const r = await api.tasks.getTasksId({ id });
|
|
143
|
+
if (r.status === 404) { console.error(`task #${id} not found.`); process.exit(1); }
|
|
144
|
+
if (r.status !== 200 || !r.data?.task) { console.error(`task show failed (${r.status}):`, r.data); process.exit(1); }
|
|
145
|
+
const task = r.data.task;
|
|
146
|
+
if (hasFlag('--json', args)) { console.log(JSON.stringify(task, null, 2)); return; }
|
|
147
|
+
console.log(`#${task.id} [${task.status}] — ${task.title}`);
|
|
148
|
+
console.log(`kind=${task.kind} · discipline=${task.discipline}${task.priority ? ` · P${task.priority}` : ''}${task.goal_id ? ` · goal ${task.goal_id}` : ''}${task.version_id ? ` · ${task.version_id}` : ''}`);
|
|
149
|
+
if (task.description) console.log(`\n${task.description}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function main() {
|
|
153
|
+
const args = process.argv.slice(2);
|
|
154
|
+
const sub = args[0];
|
|
155
|
+
if (!sub || sub === 'help' || sub === '--help' || sub === '-h') { usage(); process.exit(sub ? 0 : 2); }
|
|
156
|
+
await requireSession();
|
|
157
|
+
if (sub === 'new') return cmdNew(args.slice(1));
|
|
158
|
+
if (sub === 'show') return cmdShow(args.slice(1));
|
|
159
|
+
console.error(`task: unknown subcommand "${sub}"\n`); usage(); process.exit(2);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (require.main === module) {
|
|
163
|
+
main().catch((err) => { console.error('fatal:', err); process.exit(1); });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = { buildTaskBody, VALID_KINDS, VALID_DISCIPLINES, csv, hallLink };
|