@bongos/core 1.18.19 → 1.19.3
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 +547 -197
- package/.claude/skills/builder-box/SKILL.md +1 -1
- package/.claude/skills/builder-claim/SKILL.md +1 -1
- package/.claude/skills/goal-create/SKILL.md +1 -1
- package/.claude/skills/idea-triage/SKILL.md +1 -1
- package/.claude/skills/planning-session/SKILL.md +5 -5
- package/.claude/skills/scan-before-install/SKILL.md +165 -0
- package/bin/bongos.js +33 -7
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +63 -41
- package/clients/bongos-client/index.cjs +63 -41
- package/clients/bongos-client/index.d.ts +78 -44
- package/clients/bongos-client/index.mjs +63 -41
- package/docs/adr/0020-security-model-public-stub.md +1 -1
- package/docs/adr/0097-one-active-claim-per-session-and-worktree-binding.md +2 -0
- package/docs/adr/0137-upstream-publish-policy-manual-owner-gated.md +1 -1
- package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +1 -1
- package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +2 -1
- package/docs/adr/0155-adopt-private-repo-widen-oauth-scope.md +51 -0
- package/docs/adr/0156-user-set-display-name-wins.md +62 -0
- package/docs/adr/0157-archon-is-rank-and-identity-only.md +93 -0
- package/docs/adr/0158-choke-point-verdict-taxonomy.md +174 -0
- package/docs/adr/0159-merge-gate-reconciliation.md +88 -0
- package/docs/adr/0160-main-audit-repo-verifiable-caller.md +77 -0
- package/docs/adr/0161-publish-on-merge.md +34 -0
- package/docs/adr/README.md +9 -0
- package/docs/api/openapi.json +1209 -233
- package/docs/api-reference.md +61 -46
- package/docs/architecture.md +1 -1
- package/docs/canonical-permissions.md +61 -16
- package/docs/design/vanilla-hall-ui-redesign-scope.md +1 -1
- package/docs/file-map.md +4 -1
- package/docs/onboarding/diagrams/assertions.json +3 -3
- package/docs/recipes/core-release-pipeline.md +181 -0
- package/docs/recipes/packaging-the-core.md +2 -0
- package/docs/recipes/parallel-goal-waves.md +1 -3
- package/docs/recipes/private-npm-distribution.md +6 -2
- package/docs/recipes/upgrading-the-core.md +2 -0
- package/migrations/core_197_display_name_source.sql +56 -0
- package/migrations/core_204_rekey_active_claim_per_session_worktree.sql +77 -0
- package/migrations/core_205_versions_track_default.sql +28 -0
- package/modules/autonomy/routes/autonomy.js +1 -1
- package/modules/builder-settings/db.js +7 -1
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +63 -41
- package/modules/dev-box/routes/box.js +11 -9
- package/modules/discord/auth.js +3 -1
- package/modules/discord/routes/discord.js +8 -2
- package/modules/discord/ship-broadcast.js +48 -1
- package/modules/governance/catalog.js +70 -20
- package/modules/governance/db.js +102 -0
- package/modules/governance/migrations/governance_006_version_create.sql +38 -0
- package/modules/governance/migrations/governance_007_delegate_archon_ops_to_metic.sql +72 -0
- package/modules/governance/routes/governance.js +235 -6
- package/modules/grading/grader-workers/index.js +18 -9
- package/modules/hall-ui/public/gate.js +6 -2
- package/modules/hall-ui/public/goals-page.js +6 -2
- package/modules/hall-ui/public/governance.css +100 -0
- package/modules/hall-ui/public/governance.html +77 -0
- package/modules/hall-ui/public/governance.js +353 -0
- package/modules/hall-ui/public/harbor.js +14 -8
- package/modules/hall-ui/public/instances-panel.js +3 -1
- package/modules/hall-ui/public/new-project-wizard.js +6 -0
- package/modules/hall-ui/public/people.html +51 -0
- package/modules/hall-ui/public/people.js +100 -0
- package/modules/hall-ui/public/profile.html +2 -1
- package/modules/hall-ui/public/profile.js +9 -3
- package/modules/hall-ui/public/sessions.js +4 -2
- package/modules/hall-ui/public/shell.js +28 -7
- package/modules/hall-ui/public/task-detail.js +6 -3
- package/modules/hall-ui/public/watch.js +17 -9
- package/modules/ideas/blockers.js +44 -7
- package/modules/ideas/routes/blockers.js +12 -7
- package/modules/ideas/routes/inbox.js +2 -2
- package/modules/lifecycle/db.js +84 -17
- package/modules/lifecycle/github-push.js +37 -10
- package/modules/lifecycle/publish-reconciler.js +128 -31
- package/modules/lifecycle/routes/claims.js +10 -4
- package/modules/lifecycle/routes/dependencies.js +2 -2
- package/modules/lifecycle/routes/done-when.js +4 -4
- package/modules/lifecycle/routes/gate-approvals.js +9 -3
- package/modules/lifecycle/routes/goals.js +5 -5
- package/modules/lifecycle/routes/tasks.js +99 -69
- package/modules/lifecycle/routes/versions.js +13 -2
- package/modules/lifecycle/ship-card.js +38 -7
- package/modules/memory/routes/memory.js +2 -2
- package/modules/onboarding/routes/access-requests.js +3 -3
- package/modules/platform-identity/routes/projects.js +10 -6
- package/modules/platform-identity/routes/scouting.js +2 -2
- package/modules/provisioning/migrations/provisioning_004_onboard_mode.sql +30 -0
- package/modules/provisioning/provisioning.js +24 -6
- package/modules/provisioning/routes/provisioning.js +24 -6
- package/modules/security/artifact-scan/aggregate.js +133 -0
- package/modules/security/artifact-scan/detectors/capabilities.js +53 -0
- package/modules/security/artifact-scan/detectors/config-obfuscation.js +41 -0
- package/modules/security/artifact-scan/detectors/credentials.js +112 -0
- package/modules/security/artifact-scan/detectors/dependencies.js +115 -0
- package/modules/security/artifact-scan/detectors/hooks.js +116 -0
- package/modules/security/artifact-scan/detectors/injection.js +118 -0
- package/modules/security/artifact-scan/detectors/mcp.js +102 -0
- package/modules/security/artifact-scan/detectors/network.js +67 -0
- package/modules/security/artifact-scan/detectors/npm-lifecycle.js +67 -0
- package/modules/security/artifact-scan/detectors/permissions.js +91 -0
- package/modules/security/artifact-scan/detectors/shared.js +219 -0
- package/modules/security/artifact-scan/fence-local.js +99 -0
- package/modules/security/artifact-scan/fetch.js +330 -0
- package/modules/security/artifact-scan/floor.js +319 -0
- package/modules/security/artifact-scan/limits.js +66 -0
- package/modules/security/artifact-scan/manifest.js +203 -0
- package/modules/security/artifact-scan/mirror.js +158 -0
- package/modules/security/artifact-scan/quarantine.js +149 -0
- package/modules/security/artifact-scan/rebuild-spec.js +120 -0
- package/modules/security/artifact-scan/report.js +155 -0
- package/modules/security/artifact-scan/sanitize.js +317 -0
- package/modules/security/migrations/security_001_tool_scans.sql +145 -0
- package/modules/security/module.json +2 -2
- package/modules/security/routes/reports.js +11 -7
- package/modules/security/routes/scans.js +115 -0
- package/modules/security/scan-store.js +214 -0
- package/modules/security/tests/aggregate.mjs +283 -0
- package/modules/security/tests/artifact_scan.mjs +591 -0
- package/modules/security/tests/floor.mjs +549 -0
- package/modules/security/tests/mirror.mjs +350 -0
- package/modules/security/tests/sanitize_rebuild.mjs +499 -0
- package/modules/security/tests/scan_store.mjs +250 -0
- package/modules/security/tests/tool_scans_migration.mjs +116 -0
- package/modules/sessions/routes/sessions.js +6 -5
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/create.html +13 -3
- package/scripts/gds/abandon.js +1 -1
- package/scripts/gds/audit-authorship.sh +6 -0
- package/scripts/gds/box.js +1 -1
- package/scripts/gds/bump-version.js +76 -0
- package/scripts/gds/claim.js +15 -0
- package/scripts/gds/cli-lib.js +16 -4
- package/scripts/gds/gating.js +28 -13
- package/scripts/gds/gen-api-docs.js +1 -1
- package/scripts/gds/goal.js +234 -0
- package/scripts/gds/init.js +77 -15
- package/scripts/gds/main-audit.js +80 -11
- package/scripts/gds/mirror-redact.js +6 -0
- package/scripts/gds/newcomer-floor.js +2 -2
- package/scripts/gds/provision.js +230 -94
- package/scripts/gds/run-unit-tests.js +9 -1
- package/scripts/gds/sandbox-stage.js +66 -9
- package/scripts/gds/search-ingest-md.js +1 -0
- package/scripts/gds/sequence.js +67 -8
- package/scripts/gds/ship.js +183 -50
- package/scripts/hall-preview/server.js +1 -1
- package/scripts/migrate.sh +5 -5
- package/src/bongos/auth.js +129 -27
- package/src/bongos/db-kernel.js +66 -5
- package/src/bongos/db.js +6 -7
- package/src/bongos/github-repos.js +19 -7
- package/src/bongos/goal-scope-check.js +1 -1
- package/src/bongos/module-scope-map.js +3 -1
- package/src/bongos/permission-path-check.js +1 -1
- package/src/bongos/route-rank-check.js +73 -30
- package/src/bongos/routes/audit-log.js +4 -2
- package/src/bongos/routes/auth.js +8 -4
- package/src/bongos/routes/backup.js +7 -4
- package/src/bongos/routes/builders.js +23 -11
- package/src/bongos/routes/github.js +15 -4
- package/src/bongos/routes/llm-cache.js +5 -2
- package/src/bongos/routes/modules.js +9 -5
- package/src/bongos/routes/security.js +21 -12
- package/src/bongos/serve-internal.js +22 -7
- package/src/module-api.js +96 -13
- package/src/platform-server.js +14 -0
- package/tests/api_client.mjs +18 -2
- package/tests/api_path_404.mjs +3 -0
- package/tests/art_stage.mjs +2 -1
- package/tests/atlas.mjs +32 -4
- package/tests/auth_admission_gate.mjs +17 -5
- package/tests/auth_error_brand.mjs +41 -21
- package/tests/auth_resolve_failure.mjs +155 -0
- package/tests/blocker_link.mjs +88 -0
- package/tests/bongos_cli.mjs +91 -0
- package/tests/bongos_downloads.mjs +2 -1
- package/tests/box_dns_repoint.mjs +2 -0
- package/tests/box_ship_permissions.mjs +2 -0
- package/tests/box_terminal_ssrf.mjs +23 -17
- package/tests/bump_version.mjs +71 -0
- package/tests/claim_error_surface.mjs +23 -0
- package/tests/claude_materialize.mjs +4 -0
- package/tests/cli_box_scope.mjs +40 -0
- package/tests/cloud_bongos_pack.mjs +34 -4
- package/tests/conductor.mjs +2 -1
- package/tests/criterion_authoring_gate.mjs +28 -5
- package/tests/currency_label.mjs +10 -1
- package/tests/deploy_instance.mjs +2 -0
- package/tests/devbox_downloads.mjs +2 -1
- package/tests/display_name.mjs +95 -6
- package/tests/eviction_policy.mjs +2 -1
- package/tests/file_map.mjs +10 -0
- package/tests/first_admin_bootstrap.mjs +10 -2
- package/tests/first_run_engine_check.mjs +2 -1
- package/tests/fitness.mjs +26 -6
- package/tests/gate_approvals.mjs +15 -2
- package/tests/github_push_land_proof.mjs +50 -4
- package/tests/github_repos_helpers.mjs +30 -5
- package/tests/github_token_store.mjs +23 -0
- package/tests/goal_abuse_matrix.mjs +13 -3
- package/tests/goal_cli.mjs +89 -0
- package/tests/governance_abuse_matrix.mjs +386 -0
- package/tests/governance_fold.mjs +156 -0
- package/tests/governance_fold_bench.mjs +50 -0
- package/tests/governance_live_proof.mjs +201 -0
- package/tests/governance_page_gates.mjs +21 -9
- package/tests/governance_parity.mjs +11 -2
- package/tests/governance_require_permission.mjs +125 -1
- package/tests/governance_routes.mjs +334 -0
- package/tests/grader.mjs +7 -3
- package/tests/hall_nav.mjs +14 -4
- package/tests/helpers.mjs +73 -0
- package/tests/hook_core_resolve.mjs +2 -1
- package/tests/host_topology_skips.mjs +97 -0
- package/tests/infra_exec_paths.mjs +14 -4
- package/tests/init.mjs +147 -0
- package/tests/known_failing_ratchet.mjs +89 -0
- package/tests/lifecycle-card.mjs +2 -1
- package/tests/main_audit.mjs +72 -1
- package/tests/memory_isolation.mjs +56 -9
- package/tests/migrate_root_resolution.mjs +1 -1
- package/tests/mirror_redact.mjs +19 -0
- package/tests/module_api.mjs +3 -1
- package/tests/module_loader.mjs +4 -0
- package/tests/modules.mjs +35 -7
- package/tests/modules_route.mjs +29 -5
- package/tests/onboarding_observability.mjs +49 -11
- package/tests/onboarding_route_signals.mjs +1 -0
- package/tests/one_active_claim_per_session.mjs +80 -0
- package/tests/patch_gate_ordering.mjs +102 -0
- package/tests/pre_compact.mjs +2 -1
- package/tests/preview_server.mjs +2 -1
- package/tests/profile_route.mjs +1 -0
- package/tests/provision.mjs +170 -0
- package/tests/provisioning_onboard_mode.mjs +180 -0
- package/tests/publish_manifest.mjs +16 -3
- package/tests/publish_reconciler.mjs +181 -10
- package/tests/rank_tier_single_source.mjs +205 -0
- package/tests/recall_hints.mjs +2 -1
- package/tests/repo_map.mjs +4 -0
- package/tests/route_permission_check.mjs +14 -5
- package/tests/rubric_fixtures.mjs +3 -0
- package/tests/run_routine_two_root.mjs +2 -0
- package/tests/sandbox_stage.mjs +69 -7
- package/tests/search_ingest.mjs +7 -0
- package/tests/security_docs.mjs +28 -6
- package/tests/sequence.mjs +64 -0
- package/tests/session-cards.mjs +2 -1
- package/tests/session_start_freshness.mjs +2 -1
- package/tests/ship_broadcast_server.mjs +63 -1
- package/tests/ship_card.mjs +66 -0
- package/tests/ship_ci_deploy.mjs +66 -1
- package/tests/ship_smoke_gate.mjs +76 -0
- package/tests/versions_route.mjs +15 -0
- package/tests/workflow_exec_paths.mjs +16 -4
- package/tests/worktree_guard.mjs +2 -1
package/scripts/gds/gating.js
CHANGED
|
@@ -2,18 +2,25 @@
|
|
|
2
2
|
// scripts/gds/gating.js — owner kill-switch for main-branch merge gating.
|
|
3
3
|
//
|
|
4
4
|
// What "gating" means here — TWO independent levers, both toggled together:
|
|
5
|
-
// 1. GitHub branch protection on `main`: required status checks
|
|
6
|
-
//
|
|
5
|
+
// 1. GitHub branch protection on `main`: required status checks + required
|
|
6
|
+
// code-owner review (CODEOWNERS). The required contexts are ONLY checks
|
|
7
|
+
// that exist and can pass today — currently `unit` (the suite + fitness
|
|
8
|
+
// lane, task 1002610). The planned `secret-scan` and `gate-review`
|
|
9
|
+
// contexts (ADR 0058 phase 3; CORE_EXPECTED_CHECKS in adopt-preflight.js)
|
|
10
|
+
// join DEFAULT_PROTECTION when their workflows ship — requiring a context
|
|
11
|
+
// nothing reports blocks every merge indefinitely (D-12, ADR 0159).
|
|
7
12
|
// 2. The application-layer trust gates that run on every PR — the gate-review
|
|
8
|
-
// and grade-gate CI workflows
|
|
9
|
-
//
|
|
13
|
+
// and grade-gate CI workflows (ADR 0058 phases 1-3; NOT YET IN THE TREE —
|
|
14
|
+
// workflowState() reports them 'absent' and the toggles below no-op until
|
|
15
|
+
// they ship). Once live they post their own status checks and the ship /
|
|
16
|
+
// auto-merge pipeline refuses to land a PR while gate-review is red
|
|
10
17
|
// (it escalates gate-surface changes — infra/, migrations/, CODEOWNERS,
|
|
11
18
|
// scripts/gds plumbing — to owner approval). Branch protection alone does
|
|
12
19
|
// NOT neutralize these, which is why `off` also `gh workflow disable`s them.
|
|
13
20
|
// With gating ON, a gate-surface PR needs owner approval / --admin to merge.
|
|
14
21
|
// With gating OFF, ordinary `gh pr merge N` (and every builder / autonomous
|
|
15
|
-
// session) merges with no checks and no approval. `unit`
|
|
16
|
-
//
|
|
22
|
+
// session) merges with no checks and no approval. `unit` keeps running either
|
|
23
|
+
// way — a real safety net that forces no approval.
|
|
17
24
|
//
|
|
18
25
|
// It is REVERSIBLE by construction: `off` snapshots the live protection to a
|
|
19
26
|
// replay-ready file, then deletes protection; `on` PUTs the snapshot back,
|
|
@@ -47,19 +54,27 @@ const SNAPSHOT = require('../../src/instance-config').configPath('gating-snapsho
|
|
|
47
54
|
// approval / --admin on gate-surface changes. Branch protection alone does NOT
|
|
48
55
|
// neutralize these — they post their own status checks and the auto-merge /
|
|
49
56
|
// ship pipeline refuses to land a PR while gate-review is red. So the kill-switch
|
|
50
|
-
// also disables/enables these workflows.
|
|
51
|
-
//
|
|
57
|
+
// also disables/enables these workflows. PLANNED, not yet in the tree (ADR 0058
|
|
58
|
+
// phases 1-3): while absent, workflowState() reports 'absent' and both toggles
|
|
59
|
+
// no-op — the names stay here so the switch covers them the moment they ship.
|
|
60
|
+
// `unit` is deliberately left running — a real safety net that forces no approval.
|
|
52
61
|
const GATE_WORKFLOWS = ['gate-review.yml', 'grade-gate.yml'];
|
|
53
62
|
|
|
54
|
-
// Baked-in known-good config
|
|
55
|
-
//
|
|
63
|
+
// Baked-in known-good config. Used only as a restore fallback when no snapshot
|
|
64
|
+
// exists, so `on` can never be stuck. Requires ONLY contexts that exist and can
|
|
65
|
+
// pass today (ADR 0159 — D-12: a required context nothing reports blocks every
|
|
66
|
+
// merge indefinitely, and a required context that is red on every PR does the
|
|
67
|
+
// same). When the secret-scan / gate-review workflows ship (ADR 0058), add
|
|
68
|
+
// their contexts back here — those names are the contract (adopt-preflight.js
|
|
69
|
+
// CORE_EXPECTED_CHECKS, init.js), tool-agnostic on purpose.
|
|
56
70
|
const DEFAULT_PROTECTION = {
|
|
57
71
|
required_status_checks: {
|
|
58
72
|
strict: false,
|
|
59
73
|
checks: [
|
|
60
74
|
{ context: 'unit', app_id: 15368 },
|
|
61
|
-
|
|
62
|
-
{ context: '
|
|
75
|
+
// Stated intent, not yet requirable — uncomment as each workflow ships:
|
|
76
|
+
// { context: 'secret-scan', app_id: 15368 }, // secret scanning (ADR 0058)
|
|
77
|
+
// { context: 'gate-review', app_id: 15368 }, // gate-surface review (ADR 0058 phase 3)
|
|
63
78
|
],
|
|
64
79
|
},
|
|
65
80
|
enforce_admins: false,
|
|
@@ -270,7 +285,7 @@ function cmdOff(repo) {
|
|
|
270
285
|
|
|
271
286
|
console.log('\n🔴 gating OFF. `main` is UNPROTECTED and the gate-review/grade CI gates no longer');
|
|
272
287
|
console.log(' run — ordinary `gh pr merge N` (and every builder / autonomous session) merges');
|
|
273
|
-
console.log(' with no checks and no approval. (unit
|
|
288
|
+
console.log(' with no checks and no approval. (unit still runs.)');
|
|
274
289
|
console.log(' ↩ restore everything: node scripts/gds/gating.js on');
|
|
275
290
|
}
|
|
276
291
|
|
|
@@ -81,7 +81,7 @@ const RANK_BLURB = {
|
|
|
81
81
|
public: 'No authentication — any caller.',
|
|
82
82
|
'any-builder': 'Any authenticated builder (row-level ownership enforced in-handler).',
|
|
83
83
|
'metic+archon': 'Metic or Archon rank (review/triage powers).',
|
|
84
|
-
archon: 'Archon only (the
|
|
84
|
+
archon: 'Archon only (rank and identity management + the escalation keys — the trust boundary).',
|
|
85
85
|
'bfg-principal': 'The BFG cross-builder memory service principal (not a human rank).',
|
|
86
86
|
unknown: 'Gate could not be classified from source — treat as privileged.',
|
|
87
87
|
};
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Goal board CLI — list goals, or show one goal's criteria + tasks + what to claim next.
|
|
3
|
+
//
|
|
4
|
+
// Usage:
|
|
5
|
+
// node scripts/gds/goal.js list [--version V] [--status open|achieved|archived|all]
|
|
6
|
+
// node scripts/gds/goal.js show <id>
|
|
7
|
+
//
|
|
8
|
+
// Read-only. Default list filter is --status open.
|
|
9
|
+
|
|
10
|
+
const { cliClient, requireSession, arg } = require('./cli-lib');
|
|
11
|
+
|
|
12
|
+
const GOAL_GLYPH = { open: '○', achieved: '✓', archived: '·' };
|
|
13
|
+
const IN_FLIGHT = new Set(['active', 'completed', 'confirmed']);
|
|
14
|
+
const LIST_STATUSES = ['open', 'achieved', 'archived', 'all'];
|
|
15
|
+
|
|
16
|
+
function usageExit() {
|
|
17
|
+
console.error('usage: node scripts/gds/goal.js list [--version V] [--status open|achieved|archived|all]');
|
|
18
|
+
console.error(' node scripts/gds/goal.js show <id>');
|
|
19
|
+
process.exit(2);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// abandoned is a terminal state (deliberately cut), not pending work — excluded
|
|
23
|
+
// from every denominator, the same rule migration 017 applies to version_progress.
|
|
24
|
+
function liveTasks(tasks) {
|
|
25
|
+
return tasks.filter((t) => t.status !== 'abandoned');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function rollup(tasks) {
|
|
29
|
+
const live = liveTasks(tasks);
|
|
30
|
+
const shipped = live.filter((t) => t.status === 'shipped').length;
|
|
31
|
+
const ready = live.filter((t) => t.status === 'ready').length;
|
|
32
|
+
const inFlight = live.filter((t) => IN_FLIGHT.has(t.status)).length;
|
|
33
|
+
const parts = [`${shipped}/${live.length} shipped`];
|
|
34
|
+
if (inFlight) parts.push(`${inFlight} in flight`);
|
|
35
|
+
if (ready) parts.push(`${ready} ready`);
|
|
36
|
+
return parts.join(' · ');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function list(api, args) {
|
|
40
|
+
const version = arg('--version', args) || null;
|
|
41
|
+
if (args.includes('--version') && (!version || !/^[\w.-]+$/.test(version))) usageExit();
|
|
42
|
+
const status = arg('--status', args) || 'open';
|
|
43
|
+
if (!LIST_STATUSES.includes(status)) usageExit();
|
|
44
|
+
const query = {};
|
|
45
|
+
if (version) query.version = version;
|
|
46
|
+
if (status !== 'all') query.status = status;
|
|
47
|
+
|
|
48
|
+
const g = await api.goals.getGoals({ query });
|
|
49
|
+
if (!g.ok) {
|
|
50
|
+
console.error('goals list failed:', JSON.stringify(g.data));
|
|
51
|
+
// process.exitCode (not process.exit): hard-exit while undici keep-alive
|
|
52
|
+
// sockets tear down trips a libuv assertion on Windows.
|
|
53
|
+
process.exitCode = 1;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const goals = g.data.goals || [];
|
|
57
|
+
if (goals.length === 0) {
|
|
58
|
+
console.log('No goals match.');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// One goal_id-filtered fetch per listed goal, in parallel — returns exactly
|
|
63
|
+
// the rows the rollups need, never a version-wide or table-wide over-fetch.
|
|
64
|
+
const results = await Promise.all(goals.map((goal) => api.tasks.getTasks({ query: { goal_id: goal.id } })));
|
|
65
|
+
const failed = results.find((r) => !r.ok);
|
|
66
|
+
if (failed) {
|
|
67
|
+
console.error('tasks fetch failed (rollups would be wrong):', JSON.stringify(failed.data));
|
|
68
|
+
process.exitCode = 1;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const byGoal = new Map(goals.map((goal, i) => [String(goal.id), results[i].data.tasks || []]));
|
|
72
|
+
|
|
73
|
+
for (const goal of goals) {
|
|
74
|
+
const glyph = GOAL_GLYPH[goal.status] || '?';
|
|
75
|
+
const tasks = byGoal.get(String(goal.id)) || [];
|
|
76
|
+
const tail = liveTasks(tasks).length ? ` [${rollup(tasks)}]` : '';
|
|
77
|
+
console.log(`${glyph} #${goal.id} ${goal.version_id} ${goal.title}${tail}`);
|
|
78
|
+
}
|
|
79
|
+
console.log('');
|
|
80
|
+
console.log(`${goals.length} goal${goals.length === 1 ? '' : 's'} · node scripts/gds/goal.js show <id> for detail`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function criterionGlyph(c) {
|
|
84
|
+
if (c.satisfied) return '✓';
|
|
85
|
+
if (c.pending_review) return '◇';
|
|
86
|
+
return '○';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function criterionState(c) {
|
|
90
|
+
if (c.satisfied) return 'satisfied';
|
|
91
|
+
if (c.pending_review) return 'met — pending review (/goal-review)';
|
|
92
|
+
const linked = Number(c.linked_tasks) || 0;
|
|
93
|
+
if (linked === 0) return 'no linked tasks yet';
|
|
94
|
+
const n = Number(c.unshipped_tasks) || 0;
|
|
95
|
+
return `${n} of ${linked} linked task${linked === 1 ? '' : 's'} unshipped`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function show(api, id) {
|
|
99
|
+
const [g, t] = await Promise.all([
|
|
100
|
+
api.goals.getGoalsId({ id }),
|
|
101
|
+
api.tasks.getTasks({ query: { goal_id: id } }),
|
|
102
|
+
]);
|
|
103
|
+
if (!g.ok) {
|
|
104
|
+
console.error(`goal ${id} fetch failed:`, JSON.stringify(g.data));
|
|
105
|
+
process.exitCode = 1;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (!t.ok) {
|
|
109
|
+
console.error(`tasks fetch for goal ${id} failed:`, JSON.stringify(t.data));
|
|
110
|
+
process.exitCode = 1;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const { goal, members, criteria, dependencies } = g.data;
|
|
114
|
+
const tasks = t.data.tasks || [];
|
|
115
|
+
|
|
116
|
+
console.log(`#${goal.id} ${goal.title}`);
|
|
117
|
+
console.log(`${goal.version_id} · ${goal.status} · modules: ${(goal.scope_modules || []).join(', ') || '—'}`);
|
|
118
|
+
const allMembers = members || [];
|
|
119
|
+
const lead = allMembers.filter((m) => m.role === 'lead').map((m) => `@${m.github_login}`);
|
|
120
|
+
if (lead.length) {
|
|
121
|
+
console.log(`lead: ${lead.join(', ')}${allMembers.length > lead.length ? ` (+${allMembers.length - lead.length} members)` : ''}`);
|
|
122
|
+
} else if (allMembers.length) {
|
|
123
|
+
console.log(`members: ${allMembers.length} (no lead)`);
|
|
124
|
+
}
|
|
125
|
+
for (const d of dependencies || []) {
|
|
126
|
+
console.log(`prerequisite goal: #${d.id ?? d.goal_id} ${d.title || ''} ${d.satisfied ? '(satisfied)' : '(NOT satisfied)'}`);
|
|
127
|
+
}
|
|
128
|
+
if (goal.description) {
|
|
129
|
+
console.log('');
|
|
130
|
+
console.log(goal.description);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if ((criteria || []).length) {
|
|
134
|
+
console.log('');
|
|
135
|
+
console.log('Done-when criteria:');
|
|
136
|
+
for (const c of criteria) {
|
|
137
|
+
console.log(` ${criterionGlyph(c)} ${c.criterion_id} — ${criterionState(c)}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (tasks.length === 0) {
|
|
142
|
+
console.log('');
|
|
143
|
+
console.log('No tasks filed under this goal yet.');
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
await printTaskBuckets(api, tasks);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Pure bucketing, split from printing so it unit-tests without a live API.
|
|
150
|
+
function bucketTasks(tasks) {
|
|
151
|
+
return {
|
|
152
|
+
shipped: tasks.filter((x) => x.status === 'shipped'),
|
|
153
|
+
inFlight: tasks.filter((x) => IN_FLIGHT.has(x.status)),
|
|
154
|
+
ready: tasks.filter((x) => x.status === 'ready'),
|
|
155
|
+
gated: tasks.filter((x) => x.status === 'backlog' || x.status === 'blocked'),
|
|
156
|
+
abandoned: tasks.filter((x) => x.status === 'abandoned'),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function printTaskBuckets(api, tasks) {
|
|
161
|
+
const buckets = bucketTasks(tasks);
|
|
162
|
+
const live = liveTasks(tasks);
|
|
163
|
+
|
|
164
|
+
// Explain WHY each gated task waits — parallel, with per-task catch: one
|
|
165
|
+
// failed dependency fetch degrades to "dependencies unknown" for that task
|
|
166
|
+
// instead of aborting the listing.
|
|
167
|
+
const waits = new Map();
|
|
168
|
+
const waitsUnknown = new Set();
|
|
169
|
+
await Promise.all(buckets.gated.map(async (task) => {
|
|
170
|
+
try {
|
|
171
|
+
const d = await api.tasks.getTasksIdDependencies({ id: task.id });
|
|
172
|
+
if (!d.ok) { waitsUnknown.add(String(task.id)); return; }
|
|
173
|
+
const unshipped = (d.data.dependencies || []).filter((dep) => dep.status !== 'shipped');
|
|
174
|
+
if (unshipped.length) waits.set(String(task.id), unshipped.map((dep) => `task ${dep.id} (${dep.status})`).join(', '));
|
|
175
|
+
} catch {
|
|
176
|
+
waitsUnknown.add(String(task.id));
|
|
177
|
+
}
|
|
178
|
+
}));
|
|
179
|
+
|
|
180
|
+
console.log('');
|
|
181
|
+
const abandonedNote = buckets.abandoned.length ? `, ${buckets.abandoned.length} abandoned` : '';
|
|
182
|
+
console.log(`Tasks (${buckets.shipped.length}/${live.length} shipped${abandonedNote}):`);
|
|
183
|
+
for (const x of buckets.shipped) console.log(` ✓ ${x.id} ${x.title}`);
|
|
184
|
+
for (const x of buckets.inFlight) console.log(` ▶ ${x.id} ${x.title} [${x.status}]`);
|
|
185
|
+
for (const x of buckets.ready) console.log(` ● ${x.id} ${x.title} [ready — claimable now]`);
|
|
186
|
+
for (const x of buckets.gated) {
|
|
187
|
+
const key = String(x.id);
|
|
188
|
+
const why = waits.has(key) ? ` — waits on ${waits.get(key)}`
|
|
189
|
+
: waitsUnknown.has(key) ? ' — dependencies unknown (fetch failed)' : '';
|
|
190
|
+
console.log(` ○ ${x.id} ${x.title} [${x.status}${why}]`);
|
|
191
|
+
}
|
|
192
|
+
for (const x of buckets.abandoned) console.log(` ✕ ${x.id} ${x.title} [abandoned — excluded from progress]`);
|
|
193
|
+
|
|
194
|
+
console.log('');
|
|
195
|
+
if (buckets.ready.length) {
|
|
196
|
+
console.log(`Next: node scripts/gds/claim.js ${buckets.ready[0].id}`);
|
|
197
|
+
} else if (buckets.inFlight.length) {
|
|
198
|
+
console.log('Nothing claimable — work in flight must land first.');
|
|
199
|
+
} else if (buckets.gated.length) {
|
|
200
|
+
const allWait = buckets.gated.every((x) => waits.has(String(x.id)));
|
|
201
|
+
console.log(allWait
|
|
202
|
+
? 'Nothing claimable — every remaining task waits on an unshipped dependency.'
|
|
203
|
+
: 'Nothing claimable — remaining tasks are backlog/blocked and not yet promoted to ready.');
|
|
204
|
+
} else if (live.length === 0) {
|
|
205
|
+
console.log('No live tasks — everything filed here was abandoned.');
|
|
206
|
+
} else {
|
|
207
|
+
console.log('All tasks shipped — run /goal-review to confirm criteria.');
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function main() {
|
|
212
|
+
await requireSession();
|
|
213
|
+
const args = process.argv.slice(2);
|
|
214
|
+
const cmd = args[0];
|
|
215
|
+
const api = await cliClient();
|
|
216
|
+
|
|
217
|
+
if (cmd === 'list') {
|
|
218
|
+
await list(api, args.slice(1));
|
|
219
|
+
} else if (cmd === 'show') {
|
|
220
|
+
if (!/^\d+$/.test(args[1] || '')) usageExit();
|
|
221
|
+
await show(api, Number(args[1]));
|
|
222
|
+
} else {
|
|
223
|
+
usageExit();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (require.main === module) {
|
|
228
|
+
main().catch((err) => {
|
|
229
|
+
console.error('goal.js failed:', err && err.message ? err.message : err);
|
|
230
|
+
process.exitCode = 1;
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
module.exports = { rollup, criterionGlyph, criterionState, bucketTasks, liveTasks };
|
package/scripts/gds/init.js
CHANGED
|
@@ -258,6 +258,13 @@ function buildBrandingConfig(spec) {
|
|
|
258
258
|
firstAdmin: spec.firstAdmin,
|
|
259
259
|
currency: { label: (spec.currency && spec.currency.label) || 'credits', symbol: (spec.currency && spec.currency.symbol) || '' },
|
|
260
260
|
envPrefix: spec.envPrefix || 'CLOUDBONGOS',
|
|
261
|
+
// Every instance gets its OWN session/config dir (task 1002626). Without
|
|
262
|
+
// this, instance-config.js falls back to the shared 'cloudbongos' dir, so
|
|
263
|
+
// every un-branded instance on a machine reads/writes the SAME
|
|
264
|
+
// ~/.config/cloudbongos/gds-session.json — signing in to one silently
|
|
265
|
+
// evicts the CLI session of every other, including the hub. Same slug
|
|
266
|
+
// family as the package name + provisioning slug.
|
|
267
|
+
configDir: spec.configDir ? String(spec.configDir).trim() : slugify(productName, 'instance'),
|
|
261
268
|
};
|
|
262
269
|
if (spec.tagline) branding.copy = { tagline: spec.tagline };
|
|
263
270
|
return branding;
|
|
@@ -364,7 +371,11 @@ function kickoffTasks(spec) {
|
|
|
364
371
|
if (!provisioningRequest(spec)) tasks.push(productionTopologyKickoff());
|
|
365
372
|
tasks.push({
|
|
366
373
|
title: 'Fill the instance look with your own LLM',
|
|
367
|
-
|
|
374
|
+
// 'ideator' is a DISCIPLINE, not a kind — tasks_kind_check rejects it as a
|
|
375
|
+
// kind and the task silently vanished from the checklist (the one non-infra
|
|
376
|
+
// kickoff step). Deciding the instance look is a decision by an ideator.
|
|
377
|
+
kind: 'decision',
|
|
378
|
+
discipline: 'ideator',
|
|
368
379
|
description: 'init wrote a lean config/branding.json and left the LOOK to you (a locked project rule: brand with your ' +
|
|
369
380
|
'own LLM, not a generator). Hand docs/branding-fill-prompt.md to your AI to design a palette/fonts/wordmark, or keep the vanilla look.',
|
|
370
381
|
});
|
|
@@ -451,6 +462,9 @@ function versionPayload(spec) {
|
|
|
451
462
|
id: v.id,
|
|
452
463
|
name: v.name,
|
|
453
464
|
status: v.status || 'building',
|
|
465
|
+
// versions.track is NOT NULL (no DB default) — always send it explicitly.
|
|
466
|
+
// An instance's first version is its product track unless the spec says otherwise.
|
|
467
|
+
track: v.track === 'internal' ? 'internal' : 'product',
|
|
454
468
|
done_when: v.done_when,
|
|
455
469
|
criteria: [{ criterion_id: v.criterion_id ? slugify(v.criterion_id) : 'first-goal', criterion_md: v.done_when }],
|
|
456
470
|
};
|
|
@@ -728,12 +742,19 @@ async function seedViaApi(spec, { dryRun, log = console.log, api = apiCall } = {
|
|
|
728
742
|
else log(` ✓ created version ${vp.id} + 1 done-when goal`);
|
|
729
743
|
const criterionId = vp.criteria[0].criterion_id;
|
|
730
744
|
|
|
731
|
-
// A tracked kickoff/starter task (status ready
|
|
745
|
+
// A tracked kickoff/starter task (status ready). Returns true on success.
|
|
746
|
+
// The whole point of filing these is "nothing is silently skipped" — so a
|
|
747
|
+
// rejected POST must be LOUD: every failure is collected with its reason and
|
|
748
|
+
// reported against the expected count, never absorbed into a smaller total.
|
|
749
|
+
let attempted = 0;
|
|
750
|
+
const failures = [];
|
|
732
751
|
const postTask = async (t, extra = {}) => {
|
|
752
|
+
attempted++;
|
|
733
753
|
const res = await api('POST', '/api/gds/tasks', {
|
|
734
754
|
version_id: vp.id, title: t.title, description: t.description || '', status: 'ready',
|
|
735
|
-
kind: t.kind || 'infra', discipline: 'engineer', source: 'bongos-init', ...extra,
|
|
755
|
+
kind: t.kind || 'infra', discipline: t.discipline || 'engineer', source: 'bongos-init', ...extra,
|
|
736
756
|
});
|
|
757
|
+
if (!res.ok) failures.push({ title: t.title, status: res.status, detail: JSON.stringify(res.data) });
|
|
737
758
|
return res.ok;
|
|
738
759
|
};
|
|
739
760
|
|
|
@@ -771,8 +792,12 @@ async function seedViaApi(spec, { dryRun, log = console.log, api = apiCall } = {
|
|
|
771
792
|
}
|
|
772
793
|
|
|
773
794
|
const kickoffLabel = provisioned ? `${kickoff.length} kickoff + provisioning request` : `${kickoff.length} kickoff`;
|
|
774
|
-
|
|
775
|
-
|
|
795
|
+
if (failures.length) {
|
|
796
|
+
log(` ✗ ${failures.length} of ${attempted} task(s) FAILED to file — the checklist below is INCOMPLETE:`);
|
|
797
|
+
for (const f of failures) log(` ✗ "${f.title}" (${f.status}): ${f.detail}`);
|
|
798
|
+
}
|
|
799
|
+
log(` ${failures.length ? '⚠' : '✓'} filed ${created} of ${attempted} task(s) (${starters.length} starter + ${kickoffLabel})`);
|
|
800
|
+
return { seeded: true, created, attempted, failures, provisioned };
|
|
776
801
|
}
|
|
777
802
|
|
|
778
803
|
// ---------------------------------------------------------------------------
|
|
@@ -795,7 +820,12 @@ function confirmProceed({ input = process.stdin, output = process.stdout } = {})
|
|
|
795
820
|
// seeds the repo's REAL version/goal + IMPORTS its existing backlog (task 2044), NOT a
|
|
796
821
|
// synthetic "V1 — Prove the loop". `--dry-run` previews everything and writes nothing;
|
|
797
822
|
// `--no-seed` layers without seeding. Returns a process exit code.
|
|
798
|
-
|
|
823
|
+
// `coreDep`/`vendorPlan` (task 1002562): adopt used to ALWAYS write the caret range,
|
|
824
|
+
// which resolves NOWHERE on npm — so an adopted repo could not `npm ci --omit=dev`, the
|
|
825
|
+
// exact command the provisioner runs on the box. Greenfield already had --vendor-core for
|
|
826
|
+
// this; passing the same resolved pin through here makes an adopted repo equally
|
|
827
|
+
// installable. Omitted (a plain `bongos init --adopt` on a laptop) → the caret, unchanged.
|
|
828
|
+
async function runAdopt(spec, { dir, dryRun, noSeed = false, accept = false, coreDep = null, vendorPlan = null, log = console.log }) {
|
|
799
829
|
log(`\nAdopting Cloud Bongos onto the existing repo at "${dir}"${dryRun ? ' (dry run — nothing will be written or created)' : ''}:`);
|
|
800
830
|
log(' Layering — pre-existing files are left untouched; nothing is overwritten.');
|
|
801
831
|
|
|
@@ -827,7 +857,30 @@ async function runAdopt(spec, { dir, dryRun, noSeed = false, accept = false, log
|
|
|
827
857
|
}
|
|
828
858
|
|
|
829
859
|
const cfg = layerConfigs(spec, { dir, dryRun, log });
|
|
830
|
-
const pkg = layerPackageJson(spec, { dir, dryRun, log });
|
|
860
|
+
const pkg = layerPackageJson(spec, { dir, dryRun, log, ...(coreDep ? { coreRange: coreDep } : {}) });
|
|
861
|
+
|
|
862
|
+
// Vendor the pinned core tarball + generate the lockfile, mirroring the greenfield
|
|
863
|
+
// --vendor-core steps, so the adopted repo is `npm ci --omit=dev`-installable (ADR 0108).
|
|
864
|
+
// Skipped when layerPackageJson found the dep ALREADY present — re-pinning someone's
|
|
865
|
+
// existing declaration is an overwrite, and adopt never overwrites.
|
|
866
|
+
if (vendorPlan && pkg.action !== 'present' && pkg.action !== 'unparseable') {
|
|
867
|
+
const upgrade = require('./upgrade');
|
|
868
|
+
if (dryRun) {
|
|
869
|
+
log(`\n [dry-run] would vendor ${vendorPlan.tgz} → ${vendorPlan.vendorRel} and run \`npm install\` to generate package-lock.json`);
|
|
870
|
+
} else {
|
|
871
|
+
log('\nVendoring the pinned @bongos/core + generating the lockfile:');
|
|
872
|
+
upgrade.vendorTarball({ instanceDir: dir, fromTgz: vendorPlan.tgz, targetVersion: vendorPlan.version });
|
|
873
|
+
log(` ✓ vendored ${vendorPlan.vendorRel}`);
|
|
874
|
+
const inst = upgrade.npmInstall(dir);
|
|
875
|
+
if (!inst.ok) {
|
|
876
|
+
log(` ! npm install failed (${inst.reason || 'exit ' + inst.code}) — package-lock.json not generated; run \`npm install\` in ${dir} by hand before provisioning.`);
|
|
877
|
+
} else {
|
|
878
|
+
log(' ✓ npm install → package-lock.json generated (node_modules/@bongos/core installed)');
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
} else if (vendorPlan && pkg.action === 'present') {
|
|
882
|
+
log(` • ${CORE_PKG} was already declared — left as-is; the vendored tarball was NOT pinned over it.`);
|
|
883
|
+
}
|
|
831
884
|
|
|
832
885
|
log('\nMaterializing the .claude/ methodology surface (additive — existing files left untouched):');
|
|
833
886
|
const claude = materializeClaude({ coreRoot: resolveCoreRoot(), instanceDir: dir, dryRun, overwrite: false, log });
|
|
@@ -846,7 +899,10 @@ async function runAdopt(spec, { dir, dryRun, noSeed = false, accept = false, log
|
|
|
846
899
|
? `${backlog.tasks.length} imported task(s) from ${backlog.source}${backlog.truncated ? ` (capped at ${BACKLOG_MAX})` : ''}`
|
|
847
900
|
: 'no backlog found (capture tasks later)';
|
|
848
901
|
log(`\nReflecting real state — seeding version "${spec.version && spec.version.id}" + goal + ${importNote}:`);
|
|
849
|
-
await seedViaApi(spec, { dryRun, log });
|
|
902
|
+
const seedResult = (await seedViaApi(spec, { dryRun, log })) || {};
|
|
903
|
+
if (Array.isArray(seedResult.failures) && seedResult.failures.length) {
|
|
904
|
+
log(`\n ⚠ ${seedResult.failures.length} seed task(s) failed to file (details above) — re-file them by hand or re-run the seed; the tracked checklist is incomplete until they exist.`);
|
|
905
|
+
}
|
|
850
906
|
} else {
|
|
851
907
|
log('\n (--no-seed: layered config only; skipped real-state seeding)');
|
|
852
908
|
}
|
|
@@ -907,17 +963,16 @@ async function main(argv) {
|
|
|
907
963
|
return 1;
|
|
908
964
|
}
|
|
909
965
|
|
|
910
|
-
//
|
|
911
|
-
// scaffold/overwrite below. Its own report + next-steps live in runAdopt.
|
|
912
|
-
if (adopt) return await runAdopt(spec, { dir, dryRun, noSeed, accept });
|
|
913
|
-
|
|
914
|
-
// Greenfield core-dep resolution (ADR 0108 gap 1/4, task 2053). With --vendor-core, pin
|
|
966
|
+
// Core-dep resolution (ADR 0108 gap 1/4, task 2053). With --vendor-core, pin
|
|
915
967
|
// package.json to the vendored `file:` tarball (the caret range resolves NOWHERE on npm,
|
|
916
968
|
// so a caret-only scaffold can't `npm ci`); the tarball is copied + the lockfile generated
|
|
917
|
-
//
|
|
969
|
+
// below. Without it, the scaffold carries the declared caret (a local/dev scaffold the
|
|
918
970
|
// owner resolves another way). Reuses the bongos-upgrade vendoring + install steps.
|
|
971
|
+
//
|
|
972
|
+
// Resolved BEFORE the adopt branch (task 1002562) because BOTH paths need it: an adopted
|
|
973
|
+
// repo has to be `npm ci`-installable by the provisioner exactly like a scaffolded one.
|
|
919
974
|
const upgrade = require('./upgrade');
|
|
920
|
-
let coreDep = null; // null → writeConfigs
|
|
975
|
+
let coreDep = null; // null → writeConfigs/layerPackageJson write the caret range
|
|
921
976
|
let vendorPlan = null;
|
|
922
977
|
if (vendorCore) {
|
|
923
978
|
const tgz = path.resolve(vendorCore);
|
|
@@ -935,6 +990,10 @@ async function main(argv) {
|
|
|
935
990
|
vendorPlan = { tgz, version, vendorRel };
|
|
936
991
|
}
|
|
937
992
|
|
|
993
|
+
// Brownfield (ADR 0121): a distinct LAYER + real-state path — never the greenfield
|
|
994
|
+
// scaffold/overwrite below. Its own report + next-steps live in runAdopt.
|
|
995
|
+
if (adopt) return await runAdopt(spec, { dir, dryRun, noSeed, accept, coreDep, vendorPlan });
|
|
996
|
+
|
|
938
997
|
console.log(`\nScaffolding "${spec.identity.productName}"${dryRun ? ' (dry run — nothing will be written or created)' : ''}:`);
|
|
939
998
|
writeConfigs(spec, { dir, force, dryRun, coreDep });
|
|
940
999
|
|
|
@@ -1024,6 +1083,9 @@ async function main(argv) {
|
|
|
1024
1083
|
else console.log(' (--no-seed: skipped version/task seeding)');
|
|
1025
1084
|
|
|
1026
1085
|
console.log(`\n${dryRun ? 'Dry run complete.' : 'Done.'} Next:`);
|
|
1086
|
+
if (Array.isArray(seedResult.failures) && seedResult.failures.length) {
|
|
1087
|
+
console.log(` ⚠ ${seedResult.failures.length} seed task(s) failed to file (details above) — re-file them by hand or re-run the seed; the tracked checklist is incomplete until they exist.`);
|
|
1088
|
+
}
|
|
1027
1089
|
console.log(' • fill the look: hand docs/branding-fill-prompt.md to your own LLM');
|
|
1028
1090
|
console.log(` • sign in as ${spec.firstAdmin} to become the owner, then run \`bongos start\` to see the kickoff tasks`);
|
|
1029
1091
|
console.log(' • work through the kickoff tasks the way you would any task: claim → ship');
|
|
@@ -34,12 +34,13 @@
|
|
|
34
34
|
// node scripts/gds/main-audit.js --revert # also print `git revert` plan
|
|
35
35
|
// node scripts/gds/main-audit.js --strict # also block on unresolved authors
|
|
36
36
|
// node scripts/gds/main-audit.js --db # resolve ranks from Postgres instead of the API
|
|
37
|
+
// node scripts/gds/main-audit.js --require-roster # CI: an empty roster is a loud DID-NOT-RUN, not a vacuous green
|
|
37
38
|
|
|
38
39
|
'use strict';
|
|
39
40
|
|
|
40
41
|
const { execSync } = require('node:child_process');
|
|
41
42
|
const ppc = require(require('node:path').join(__dirname, '..', '..', 'src', 'bongos', 'permission-path-check.js'));
|
|
42
|
-
const { cliClient } = require('./cli-lib');
|
|
43
|
+
const { cliClient, API_BASE } = require('./cli-lib');
|
|
43
44
|
|
|
44
45
|
// ---------- pure analyzer (tested in tests/main_audit.mjs) ----------
|
|
45
46
|
|
|
@@ -87,7 +88,7 @@ function analyzeCommits(commits, rankByIdentity, { strict = false } = {}) {
|
|
|
87
88
|
author: c.authorName,
|
|
88
89
|
rank,
|
|
89
90
|
files: hits.map((h) => h.file),
|
|
90
|
-
summary: `commit ${short(c.sha)} by '${c.authorName}' (rank '${rank}', below Metic) modified permission-sensitive path(s) [${fileList}] — this is the SEC #863 violation class. It must be reverted and re-landed by
|
|
91
|
+
summary: `commit ${short(c.sha)} by '${c.authorName}' (rank '${rank}', below Metic) modified permission-sensitive path(s) [${fileList}] — this is the SEC #863 violation class. It must be reverted and re-landed by a Metic+ builder if genuinely needed.`,
|
|
91
92
|
});
|
|
92
93
|
}
|
|
93
94
|
}
|
|
@@ -120,6 +121,34 @@ function git(args) {
|
|
|
120
121
|
return execSync(`git ${args}`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
121
122
|
}
|
|
122
123
|
|
|
124
|
+
// How far back the default scan reaches, in first-parent steps.
|
|
125
|
+
const DEFAULT_WINDOW = 200;
|
|
126
|
+
|
|
127
|
+
// First-parent depth of a ref. `<ref>~N` walks FIRST parents N times, so this —
|
|
128
|
+
// not the total commit count — is what bounds the window. On a repo where every
|
|
129
|
+
// land is a --no-ff merge the two diverge sharply: this one has 526 commits
|
|
130
|
+
// reachable from origin/main but a first-parent chain only 186 deep.
|
|
131
|
+
function firstParentDepth(ref) {
|
|
132
|
+
try {
|
|
133
|
+
return Number(git(`rev-list --count --first-parent ${ref}`).trim());
|
|
134
|
+
} catch (_) {
|
|
135
|
+
return NaN;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Clamp the window to what the history actually holds. Returns null when there
|
|
140
|
+
// is nothing to audit, so the caller reports that plainly instead of emitting a
|
|
141
|
+
// `<ref>~200..<ref>` that git rejects as an unknown revision (task 1002566).
|
|
142
|
+
function clampWindow(depth, want = DEFAULT_WINDOW) {
|
|
143
|
+
if (typeof depth !== 'number' || !Number.isFinite(depth) || depth < 2) return null;
|
|
144
|
+
return Math.min(want, depth - 1);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function defaultRange(ref, depth, want = DEFAULT_WINDOW) {
|
|
148
|
+
const n = clampWindow(depth, want);
|
|
149
|
+
return n === null ? null : `${ref}~${n}..${ref}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
123
152
|
// Parse `git log --name-only` with a NUL-delimited custom format into commits.
|
|
124
153
|
function parseCommits(range) {
|
|
125
154
|
// %x00 = NUL. Format: NUL sha NUL parents NUL authorName NUL authorEmail NUL subject \n files...
|
|
@@ -128,7 +157,9 @@ function parseCommits(range) {
|
|
|
128
157
|
try {
|
|
129
158
|
out = git(`log ${range} --name-only --format=${JSON.stringify(fmt)}`);
|
|
130
159
|
} catch (e) {
|
|
131
|
-
|
|
160
|
+
// Say plainly that nothing was audited. A bare git error here used to read
|
|
161
|
+
// as a transient glitch, which is how a dead check passed for a clean one.
|
|
162
|
+
return { error: `DID NOT RUN — git log could not resolve range ${range}, so no commits were audited: ${e.message}` };
|
|
132
163
|
}
|
|
133
164
|
const records = out.split('\x00').slice(1); // first chunk is empty / pre-first-NUL
|
|
134
165
|
const commits = [];
|
|
@@ -194,8 +225,25 @@ async function fetchRankMap({ useDb = false } = {}) {
|
|
|
194
225
|
try {
|
|
195
226
|
const api = await cliClient({ allowUnauthenticated: true });
|
|
196
227
|
const res = await api.builders.getBuildersRoster();
|
|
197
|
-
if (res && res.ok)
|
|
198
|
-
|
|
228
|
+
if (res && res.ok) {
|
|
229
|
+
const map = rowsToMap(res.data?.builders || []);
|
|
230
|
+
if (Object.keys(map).length) return map;
|
|
231
|
+
}
|
|
232
|
+
} catch (_) { /* offline / no session — fall through to the public surface */ }
|
|
233
|
+
// Public fallback (task 1002612): CI has no GDS session, and with an empty
|
|
234
|
+
// rank map every author resolves to null so a blocker can NEVER fire — a
|
|
235
|
+
// vacuous green. github_login + rank are public facts on this platform (the
|
|
236
|
+
// status dashboard serves them), and the public leaderboard is server-
|
|
237
|
+
// authoritative, so resolving from it is as trustworthy as the roster route.
|
|
238
|
+
// Caveat: a zero-activity builder may be absent → resolves as UNRESOLVED
|
|
239
|
+
// ('review' finding), never as falsely-trusted.
|
|
240
|
+
try {
|
|
241
|
+
const res = await fetch(`${API_BASE}/api/gds/public/leaderboard`);
|
|
242
|
+
if (res.ok) {
|
|
243
|
+
const data = await res.json();
|
|
244
|
+
return rowsToMap(data?.leaderboard || []);
|
|
245
|
+
}
|
|
246
|
+
} catch (_) { /* offline — map stays empty → unresolved authors */ }
|
|
199
247
|
return {};
|
|
200
248
|
}
|
|
201
249
|
|
|
@@ -221,12 +269,22 @@ async function main() {
|
|
|
221
269
|
range = process.env.OTB_AUDIT_RANGE;
|
|
222
270
|
}
|
|
223
271
|
if (!range || range === true) {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
} else {
|
|
272
|
+
const ref = verifyHead ? 'HEAD' : 'origin/main';
|
|
273
|
+
if (!verifyHead) {
|
|
227
274
|
// Default: recent origin/main. Fetch first so we're auditing the truth.
|
|
228
275
|
try { git('fetch origin main'); } catch (_) { /* offline — audit local */ }
|
|
229
|
-
|
|
276
|
+
}
|
|
277
|
+
// Clamp to the ref's real first-parent depth. A fixed ~200 silently made
|
|
278
|
+
// this audit unrunnable on any repo with a shorter chain (task 1002566).
|
|
279
|
+
const depth = firstParentDepth(ref);
|
|
280
|
+
range = defaultRange(ref, depth);
|
|
281
|
+
if (!range) {
|
|
282
|
+
console.error(
|
|
283
|
+
`main-audit: DID NOT RUN — cannot build a default range for ${ref}: its first-parent history is `
|
|
284
|
+
+ `${Number.isFinite(depth) ? `${depth} commit(s)` : 'unreadable'} deep. `
|
|
285
|
+
+ 'Pass --range <A..B> or set OTB_AUDIT_RANGE. This is a check that did not execute, not a clean audit.'
|
|
286
|
+
);
|
|
287
|
+
process.exit(2);
|
|
230
288
|
}
|
|
231
289
|
}
|
|
232
290
|
|
|
@@ -237,7 +295,15 @@ async function main() {
|
|
|
237
295
|
}
|
|
238
296
|
const rankMap = await fetchRankMap({ useDb });
|
|
239
297
|
if (Object.keys(rankMap).length === 0) {
|
|
240
|
-
|
|
298
|
+
// --require-roster (task 1002612): the CI caller refuses to "pass" an audit
|
|
299
|
+
// that could not have failed. With no ranks, every author is unresolved and
|
|
300
|
+
// a blocker can never fire — that is a check that did not execute, not a
|
|
301
|
+
// clean audit (the same false-green shape parseCommits guards against).
|
|
302
|
+
if (process.argv.includes('--require-roster')) {
|
|
303
|
+
console.error('main-audit: DID NOT RUN — the roster is unavailable (offline / no session / public leaderboard unreachable). An empty rank map can never produce a blocker, so exiting green would be a vacuous pass. Fix connectivity or drop --require-roster for a best-effort local read.');
|
|
304
|
+
process.exit(2);
|
|
305
|
+
}
|
|
306
|
+
console.error('main-audit: WARNING — could not load the GDS roster (offline / insufficient rank / no DB). Authors will be unresolved; run as a Metic+ builder or with --db (on the droplet) for a definitive audit.');
|
|
241
307
|
}
|
|
242
308
|
|
|
243
309
|
const result = analyzeCommits(parsed.commits, rankMap, { strict });
|
|
@@ -276,4 +342,7 @@ if (require.main === module) {
|
|
|
276
342
|
});
|
|
277
343
|
}
|
|
278
344
|
|
|
279
|
-
module.exports = {
|
|
345
|
+
module.exports = {
|
|
346
|
+
analyzeCommits, resolveRank, parseCommits, rowsToMap,
|
|
347
|
+
clampWindow, defaultRange, firstParentDepth, DEFAULT_WINDOW,
|
|
348
|
+
};
|