@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
|
@@ -309,7 +309,7 @@ module.exports = function buildTasksRouter() {
|
|
|
309
309
|
// BACKSTOP for the on-claim restock (the live mechanism is the post-claim hook
|
|
310
310
|
// in routes/claims.js). Archon-gated like all task creation (ADR 0016). Must
|
|
311
311
|
// be registered BEFORE GET /tasks/:id so ':id' doesn't swallow the literal.
|
|
312
|
-
router.post('/tasks/newcomer-restock', auth.requireBuilder, auth.
|
|
312
|
+
router.post('/tasks/newcomer-restock', auth.requireBuilder, auth.requirePermission('task.newcomer_restock'), async (_req, res) => {
|
|
313
313
|
try {
|
|
314
314
|
// The newcomer-chore restock carved into modules/onboarding/ (BV1.R81 / ADR
|
|
315
315
|
// 0093 §1): resolve the `onboarding` kernel port — never a direct require.
|
|
@@ -368,7 +368,7 @@ module.exports = function buildTasksRouter() {
|
|
|
368
368
|
// { depends_on_task_ids: [N, ...] }
|
|
369
369
|
// Returns 409 with code='cycle' if accepting would create a cycle.
|
|
370
370
|
// rank: metic+archon — task-shaping; Metic may wire deps of a task it can author (ADR 0090).
|
|
371
|
-
router.post('/tasks/:id/dependencies', auth.requireBuilder, auth.
|
|
371
|
+
router.post('/tasks/:id/dependencies', auth.requireBuilder, auth.requirePermission('task.dependency.manage'), async (req, res) => {
|
|
372
372
|
if (validateOrRespond(req, res, {
|
|
373
373
|
depends_on_task_ids: { type: 'array', maxItems: 200 },
|
|
374
374
|
// depends_on_task_id can be a number or a string-coerced number — leave
|
|
@@ -412,7 +412,7 @@ module.exports = function buildTasksRouter() {
|
|
|
412
412
|
|
|
413
413
|
// DELETE /tasks/:id/dependencies/:depId — removes the edge.
|
|
414
414
|
// rank: metic+archon — removes a dep edge; task-shaping, Metic+ (ADR 0090).
|
|
415
|
-
router.delete('/tasks/:id/dependencies/:depId', auth.requireBuilder, auth.
|
|
415
|
+
router.delete('/tasks/:id/dependencies/:depId', auth.requireBuilder, auth.requirePermission('task.dependency.manage'), async (req, res) => {
|
|
416
416
|
const id = Number(req.params.id);
|
|
417
417
|
const depId = Number(req.params.depId);
|
|
418
418
|
if (!Number.isFinite(id) || !Number.isFinite(depId)) {
|
|
@@ -425,7 +425,7 @@ module.exports = function buildTasksRouter() {
|
|
|
425
425
|
// PUT /tasks/:id/dependencies — replace the full dep set.
|
|
426
426
|
// body: { depends_on_task_ids: [N, ...] }
|
|
427
427
|
// rank: metic+archon — replaces the full dep set; task-shaping, Metic+ (ADR 0090).
|
|
428
|
-
router.put('/tasks/:id/dependencies', auth.requireBuilder, auth.
|
|
428
|
+
router.put('/tasks/:id/dependencies', auth.requireBuilder, auth.requirePermission('task.dependency.manage'), async (req, res) => {
|
|
429
429
|
if (validateOrRespond(req, res, {
|
|
430
430
|
depends_on_task_ids: { required: true, type: 'array', maxItems: 200 },
|
|
431
431
|
})) return;
|
|
@@ -467,7 +467,7 @@ module.exports = function buildTasksRouter() {
|
|
|
467
467
|
// POST /tasks/:id/criteria — body: { criterion_ids: [ref, ...] } OR
|
|
468
468
|
// { criterion_id: ref }. Metic+.
|
|
469
469
|
// rank: metic+archon — links a task it can author to a criterion; mirrors POST deps (ADR 0090).
|
|
470
|
-
router.post('/tasks/:id/criteria', auth.requireBuilder, auth.
|
|
470
|
+
router.post('/tasks/:id/criteria', auth.requireBuilder, auth.requirePermission('task.criterion.manage'), async (req, res) => {
|
|
471
471
|
if (validateOrRespond(req, res, {
|
|
472
472
|
criterion_ids: { type: 'array', maxItems: 100 },
|
|
473
473
|
// criterion_id is the singular alt-form (a numeric id, "Cn" token, or slug)
|
|
@@ -509,7 +509,7 @@ module.exports = function buildTasksRouter() {
|
|
|
509
509
|
// DELETE /tasks/:id/criteria/:criterionId — removes one link (criterionId is
|
|
510
510
|
// the numeric done_when_criteria.id, mirroring DELETE deps/:depId).
|
|
511
511
|
// rank: metic+archon — removes a criterion↔task edge; task-shaping, Metic+ (ADR 0090).
|
|
512
|
-
router.delete('/tasks/:id/criteria/:criterionId', auth.requireBuilder, auth.
|
|
512
|
+
router.delete('/tasks/:id/criteria/:criterionId', auth.requireBuilder, auth.requirePermission('task.criterion.manage'), async (req, res) => {
|
|
513
513
|
const id = Number(req.params.id);
|
|
514
514
|
const criterionId = Number(req.params.criterionId);
|
|
515
515
|
if (!Number.isFinite(id) || !Number.isFinite(criterionId)) {
|
|
@@ -525,7 +525,7 @@ module.exports = function buildTasksRouter() {
|
|
|
525
525
|
// ADR 0018). A Xenos/Thetes still cannot author — they file ideas. Setting rank
|
|
526
526
|
// and closing versions stay Archon. The every-route audit is task #231 (V3.R06).
|
|
527
527
|
// rank: metic+archon — create work (plan/scope mutation); ADR 0090 (was ADR 0016/0018 archon).
|
|
528
|
-
router.post('/tasks', auth.requireBuilder, auth.
|
|
528
|
+
router.post('/tasks', auth.requireBuilder, auth.requirePermission('task.create'), async (req, res) => {
|
|
529
529
|
if (validateOrRespond(req, res, {
|
|
530
530
|
version_id: { required: true, type: 'string', maxLength: 64 },
|
|
531
531
|
title: { required: true, type: 'string', maxLength: LIMITS.TITLE, minLength: 1 },
|
|
@@ -747,7 +747,7 @@ module.exports = function buildTasksRouter() {
|
|
|
747
747
|
// accepts true|false|null; newcomer_friendly is a boolean. Validation lives
|
|
748
748
|
// in hierarchy.js for parent; the rest are validated inline.
|
|
749
749
|
// rank: metic+archon — edit work (incl. newcomer_friendly flag); ADR 0090 (was ADR 0016 archon).
|
|
750
|
-
router.patch('/tasks/:id', auth.requireBuilder, auth.
|
|
750
|
+
router.patch('/tasks/:id', auth.requireBuilder, auth.requirePermission('task.edit'), async (req, res) => {
|
|
751
751
|
if (validateOrRespond(req, res, {
|
|
752
752
|
kind: { type: 'string', maxLength: 64 },
|
|
753
753
|
discipline: { type: 'string', maxLength: 64 },
|
|
@@ -797,18 +797,20 @@ module.exports = function buildTasksRouter() {
|
|
|
797
797
|
if (!hasParent && !hasKind && !hasDiscipline && !hasNewcomerOrXenos && !hasParallelSafe && !hasSecuritySensitive && !hasAutomationTag && !hasRequiresRank && !hasModuleKey && !hasGoalId) {
|
|
798
798
|
return res.fail('no_supported_fields', { status: 400, message: 'PATCH /tasks/:id supports parent_task_id, kind, discipline, parallel_safe, security_sensitive, automation_tag, requires_rank, module_key, goal_id, and newcomer_friendly (alias: xenos_claimable). ' + STATUS_HINT });
|
|
799
799
|
}
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
800
|
+
// ---- task 1002609 (G-22): settle EVERY gate and validation before the ----
|
|
801
|
+
// ---- FIRST write. The previous shape validated-and-wrote per field in ----
|
|
802
|
+
// sequence, so a body mixing a valid early field with a refused later one
|
|
803
|
+
// committed the early write and then 403/400'd — state changed on a request
|
|
804
|
+
// the caller was told was refused (a Metic sending { parallel_safe,
|
|
805
|
+
// security_sensitive } had parallel_safe committed before rank_forbidden).
|
|
806
|
+
// Authorization, type/enum checks, and the goal existence/version reads all
|
|
807
|
+
// run here; writes begin only after the last validation `return`.
|
|
808
|
+
// (Documented residual: parent_task_id validates inside hierarchy.attachParent
|
|
809
|
+
// at the end — its checks are atomic with its OWN write, but a refusal there
|
|
810
|
+
// still lands after the field writes below. No authorization gate is
|
|
811
|
+
// involved in that path; the handler's only rank gate is hoisted here.)
|
|
812
|
+
if (hasParallelSafe && body.parallel_safe !== null && typeof body.parallel_safe !== 'boolean') {
|
|
813
|
+
return res.fail('bad_parallel_safe', { status: 400, message: 'parallel_safe must be true, false, or null' });
|
|
812
814
|
}
|
|
813
815
|
if (hasSecuritySensitive) {
|
|
814
816
|
// task 990: explicit Archon-only-broadcast flag. Boolean only (no null) —
|
|
@@ -826,41 +828,19 @@ module.exports = function buildTasksRouter() {
|
|
|
826
828
|
if (typeof body.security_sensitive !== 'boolean') {
|
|
827
829
|
return res.fail('bad_security_sensitive', { status: 400, message: 'security_sensitive must be a boolean' });
|
|
828
830
|
}
|
|
829
|
-
try {
|
|
830
|
-
updated = await db.updateTaskSecuritySensitive(id, body.security_sensitive);
|
|
831
|
-
if (!updated) return res.fail('task_not_found', 404);
|
|
832
|
-
} catch (err) {
|
|
833
|
-
console.error('[gds] PATCH /tasks/:id security_sensitive', err);
|
|
834
|
-
return res.fail('patch_failed', { status: 500, message: 'internal error' });
|
|
835
|
-
}
|
|
836
831
|
}
|
|
832
|
+
const VALID_AUTOMATION_TAGS = new Set(['overnight-safe', 'live-only', 'needs-input']);
|
|
837
833
|
if (hasAutomationTag) {
|
|
838
834
|
const tag = body.automation_tag;
|
|
839
|
-
const VALID_AUTOMATION_TAGS = new Set(['overnight-safe', 'live-only', 'needs-input']);
|
|
840
835
|
if (tag !== null && (typeof tag !== 'string' || !VALID_AUTOMATION_TAGS.has(tag))) {
|
|
841
836
|
return res.fail('bad_automation_tag', { status: 400, message: 'automation_tag must be one of overnight-safe | live-only | needs-input, or null to clear', details: { valid: Array.from(VALID_AUTOMATION_TAGS) } });
|
|
842
837
|
}
|
|
843
|
-
try {
|
|
844
|
-
updated = await db.updateTaskAutomationTag(id, tag);
|
|
845
|
-
if (!updated) return res.fail('task_not_found', 404);
|
|
846
|
-
} catch (err) {
|
|
847
|
-
console.error('[gds] PATCH /tasks/:id automation_tag', err);
|
|
848
|
-
return res.fail('patch_failed', { status: 500, message: 'internal error' });
|
|
849
|
-
}
|
|
850
838
|
}
|
|
851
839
|
if (hasModuleKey) {
|
|
852
840
|
// BV1.R53 (task 1422): assign or clear the module affinity label. null clears.
|
|
853
|
-
|
|
854
|
-
if (key !== null && !isModuleKey(key)) {
|
|
841
|
+
if (body.module_key !== null && !isModuleKey(body.module_key)) {
|
|
855
842
|
return res.fail('bad_module_key', { status: 400, message: `module_key must be one of the known module keys, or null to clear. Valid keys: ${moduleKeys().join(', ')}.`, details: { valid: moduleKeys() } });
|
|
856
843
|
}
|
|
857
|
-
try {
|
|
858
|
-
updated = await db.updateTaskModuleKey(id, key ?? null);
|
|
859
|
-
if (!updated) return res.fail('task_not_found', 404);
|
|
860
|
-
} catch (err) {
|
|
861
|
-
console.error('[gds] PATCH /tasks/:id module_key', err);
|
|
862
|
-
return res.fail('patch_failed', { status: 500, message: 'internal error' });
|
|
863
|
-
}
|
|
864
844
|
}
|
|
865
845
|
if (hasGoalId) {
|
|
866
846
|
// task 1763: (re)assign the task's goal — the documented "Archon/Metic
|
|
@@ -882,8 +862,6 @@ module.exports = function buildTasksRouter() {
|
|
|
882
862
|
if (String(goal.version_id) !== String(task.version_id)) {
|
|
883
863
|
return res.fail('goal_version_mismatch', { status: 409, message: `Goal #${goalId} is in version '${goal.version_id}' but task #${id} is in '${task.version_id}'. A task can only join a goal in its own version.` });
|
|
884
864
|
}
|
|
885
|
-
updated = await db.updateTaskGoal(id, goalId);
|
|
886
|
-
if (!updated) return res.fail('task_not_found', 404);
|
|
887
865
|
} catch (err) {
|
|
888
866
|
console.error('[gds] PATCH /tasks/:id goal_id', err);
|
|
889
867
|
return res.fail('patch_failed', { status: 500, message: 'internal error' });
|
|
@@ -893,13 +871,77 @@ module.exports = function buildTasksRouter() {
|
|
|
893
871
|
// task 1498 / ADR 0084: the Archon override of the per-task rank floor.
|
|
894
872
|
// Validate the enum here; db.updateTaskRequiresRank clamps the value UP to
|
|
895
873
|
// the auto-derived floor (touches + security_sensitive), so a PATCH can
|
|
896
|
-
// only RAISE the gate — never open a sensitive task to a lower rank.
|
|
897
|
-
// AFTER the security_sensitive block above, so a same-request flag flip is
|
|
898
|
-
// already reflected in the derived floor.
|
|
874
|
+
// only RAISE the gate — never open a sensitive task to a lower rank.
|
|
899
875
|
const LIVE_RANKS = new Set(db.LIVE_RANK_LADDER);
|
|
900
876
|
if (typeof body.requires_rank !== 'string' || !LIVE_RANKS.has(body.requires_rank)) {
|
|
901
877
|
return res.fail('bad_requires_rank', { status: 400, message: `requires_rank must be one of ${db.LIVE_RANK_LADDER.join(' | ')}. An override can only raise the floor; it is clamped up to the auto-derived minimum.`, details: { valid: db.LIVE_RANK_LADDER } });
|
|
902
878
|
}
|
|
879
|
+
}
|
|
880
|
+
if (hasKind && !VALID_KINDS.has(body.kind)) {
|
|
881
|
+
return badKind(res);
|
|
882
|
+
}
|
|
883
|
+
if (hasDiscipline && !VALID_DISCIPLINES.has(body.discipline)) {
|
|
884
|
+
return res.fail('bad_discipline', 400, { valid: Array.from(VALID_DISCIPLINES) });
|
|
885
|
+
}
|
|
886
|
+
if (hasNewcomerOrXenos) {
|
|
887
|
+
// V3.R15 #239: prefer newcomer_friendly (canonical); fall back to
|
|
888
|
+
// xenos_claimable (pre-rename alias). Both map to the same DB column.
|
|
889
|
+
const incoming = hasNewcomerFriendly ? body.newcomer_friendly : body.xenos_claimable;
|
|
890
|
+
if (typeof incoming !== 'boolean') {
|
|
891
|
+
const field = hasNewcomerFriendly ? 'newcomer_friendly' : 'xenos_claimable';
|
|
892
|
+
return res.fail(`bad_${field}`, { status: 400, message: `${field} must be a boolean` });
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
// ---- all gates passed: apply the writes. Order preserved from the old ----
|
|
896
|
+
// shape — requires_rank still runs AFTER security_sensitive so a
|
|
897
|
+
// same-request flag flip is already reflected in the derived floor.
|
|
898
|
+
let updated = null;
|
|
899
|
+
if (hasParallelSafe) {
|
|
900
|
+
try {
|
|
901
|
+
updated = await db.updateTaskParallelSafe(id, body.parallel_safe);
|
|
902
|
+
if (!updated) return res.fail('task_not_found', 404);
|
|
903
|
+
} catch (err) {
|
|
904
|
+
console.error('[gds] PATCH /tasks/:id parallel_safe', err);
|
|
905
|
+
return res.fail('patch_failed', { status: 500, message: 'internal error' });
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
if (hasSecuritySensitive) {
|
|
909
|
+
try {
|
|
910
|
+
updated = await db.updateTaskSecuritySensitive(id, body.security_sensitive);
|
|
911
|
+
if (!updated) return res.fail('task_not_found', 404);
|
|
912
|
+
} catch (err) {
|
|
913
|
+
console.error('[gds] PATCH /tasks/:id security_sensitive', err);
|
|
914
|
+
return res.fail('patch_failed', { status: 500, message: 'internal error' });
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
if (hasAutomationTag) {
|
|
918
|
+
try {
|
|
919
|
+
updated = await db.updateTaskAutomationTag(id, body.automation_tag);
|
|
920
|
+
if (!updated) return res.fail('task_not_found', 404);
|
|
921
|
+
} catch (err) {
|
|
922
|
+
console.error('[gds] PATCH /tasks/:id automation_tag', err);
|
|
923
|
+
return res.fail('patch_failed', { status: 500, message: 'internal error' });
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
if (hasModuleKey) {
|
|
927
|
+
try {
|
|
928
|
+
updated = await db.updateTaskModuleKey(id, body.module_key ?? null);
|
|
929
|
+
if (!updated) return res.fail('task_not_found', 404);
|
|
930
|
+
} catch (err) {
|
|
931
|
+
console.error('[gds] PATCH /tasks/:id module_key', err);
|
|
932
|
+
return res.fail('patch_failed', { status: 500, message: 'internal error' });
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
if (hasGoalId) {
|
|
936
|
+
try {
|
|
937
|
+
updated = await db.updateTaskGoal(id, body.goal_id);
|
|
938
|
+
if (!updated) return res.fail('task_not_found', 404);
|
|
939
|
+
} catch (err) {
|
|
940
|
+
console.error('[gds] PATCH /tasks/:id goal_id', err);
|
|
941
|
+
return res.fail('patch_failed', { status: 500, message: 'internal error' });
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
if (hasRequiresRank) {
|
|
903
945
|
try {
|
|
904
946
|
updated = await db.updateTaskRequiresRank(id, body.requires_rank);
|
|
905
947
|
if (!updated) return res.fail('task_not_found', 404);
|
|
@@ -909,9 +951,6 @@ module.exports = function buildTasksRouter() {
|
|
|
909
951
|
}
|
|
910
952
|
}
|
|
911
953
|
if (hasKind) {
|
|
912
|
-
if (!VALID_KINDS.has(body.kind)) {
|
|
913
|
-
return badKind(res);
|
|
914
|
-
}
|
|
915
954
|
try {
|
|
916
955
|
updated = await db.updateTaskKind(id, body.kind);
|
|
917
956
|
if (!updated) return res.fail('task_not_found', 404);
|
|
@@ -921,9 +960,6 @@ module.exports = function buildTasksRouter() {
|
|
|
921
960
|
}
|
|
922
961
|
}
|
|
923
962
|
if (hasDiscipline) {
|
|
924
|
-
if (!VALID_DISCIPLINES.has(body.discipline)) {
|
|
925
|
-
return res.fail('bad_discipline', 400, { valid: Array.from(VALID_DISCIPLINES) });
|
|
926
|
-
}
|
|
927
963
|
try {
|
|
928
964
|
updated = await db.updateTaskDiscipline(id, body.discipline);
|
|
929
965
|
if (!updated) return res.fail('task_not_found', 404);
|
|
@@ -933,13 +969,7 @@ module.exports = function buildTasksRouter() {
|
|
|
933
969
|
}
|
|
934
970
|
}
|
|
935
971
|
if (hasNewcomerOrXenos) {
|
|
936
|
-
// V3.R15 #239: prefer newcomer_friendly (canonical); fall back to
|
|
937
|
-
// xenos_claimable (pre-rename alias). Both map to the same DB column.
|
|
938
972
|
const incoming = hasNewcomerFriendly ? body.newcomer_friendly : body.xenos_claimable;
|
|
939
|
-
if (typeof incoming !== 'boolean') {
|
|
940
|
-
const field = hasNewcomerFriendly ? 'newcomer_friendly' : 'xenos_claimable';
|
|
941
|
-
return res.fail(`bad_${field}`, { status: 400, message: `${field} must be a boolean` });
|
|
942
|
-
}
|
|
943
973
|
try {
|
|
944
974
|
updated = await db.updateTaskNewcomerFriendly(id, incoming);
|
|
945
975
|
if (!updated) return res.fail('task_not_found', 404);
|
|
@@ -977,7 +1007,7 @@ module.exports = function buildTasksRouter() {
|
|
|
977
1007
|
// PATCH refuses `status` outright). A restore lands in **backlog**, never straight
|
|
978
1008
|
// to ready: the task was retired for a reason, so it re-enters as un-workable and
|
|
979
1009
|
// takes a second, deliberate promote to become claimable again.
|
|
980
|
-
router.post('/tasks/:id/promote', auth.requireBuilder, auth.
|
|
1010
|
+
router.post('/tasks/:id/promote', auth.requireBuilder, auth.requirePermission('task.promote'), async (req, res) => {
|
|
981
1011
|
const id = Number(req.params.id);
|
|
982
1012
|
const task = await db.getTask(id);
|
|
983
1013
|
if (!task) return res.fail('task_not_found', 404);
|
|
@@ -1020,7 +1050,7 @@ module.exports = function buildTasksRouter() {
|
|
|
1020
1050
|
// task out from under its claim holder. No reward side-effects (the reward gate
|
|
1021
1051
|
// is a promote-only concern; backlog is the pre-workable state).
|
|
1022
1052
|
// rank: metic+archon — ready→backlog demotion; scope mutation; mirrors /promote (ADR 0090).
|
|
1023
|
-
router.post('/tasks/:id/demote', auth.requireBuilder, auth.
|
|
1053
|
+
router.post('/tasks/:id/demote', auth.requireBuilder, auth.requirePermission('task.demote'), async (req, res) => {
|
|
1024
1054
|
const id = Number(req.params.id);
|
|
1025
1055
|
const task = await db.getTask(id);
|
|
1026
1056
|
if (!task) return res.fail('task_not_found', 404);
|
|
@@ -1091,7 +1121,7 @@ module.exports = function buildTasksRouter() {
|
|
|
1091
1121
|
// GRADER_BYPASS_ENABLED==='1'; archon-only otherwise).
|
|
1092
1122
|
const confirmRankGate = (req, res, next) => {
|
|
1093
1123
|
if (graderBypassEnabled()) return next();
|
|
1094
|
-
return auth.
|
|
1124
|
+
return auth.requirePermission('task.confirm.grade_bypass')(req, res, next);
|
|
1095
1125
|
};
|
|
1096
1126
|
router.post('/tasks/:id/confirm', auth.requireBuilder, confirmRankGate, async (req, res) => {
|
|
1097
1127
|
if (validateOrRespond(req, res, {
|
|
@@ -1148,7 +1178,7 @@ module.exports = function buildTasksRouter() {
|
|
|
1148
1178
|
// triage they already delegated. Not one-way any more — /promote restores an
|
|
1149
1179
|
// abandoned task to backlog at the same rank — and the guards below (shipped
|
|
1150
1180
|
// refused, active claim refused) plus the audit_log entry are unchanged.
|
|
1151
|
-
router.post('/tasks/:id/abandon', auth.requireBuilder, auth.
|
|
1181
|
+
router.post('/tasks/:id/abandon', auth.requireBuilder, auth.requirePermission('task.abandon'), async (req, res) => {
|
|
1152
1182
|
if (validateOrRespond(req, res, {
|
|
1153
1183
|
reason: { type: 'string' }, // free-text retire reason (sliced to 2000 below)
|
|
1154
1184
|
})) return;
|
|
@@ -1228,7 +1258,7 @@ module.exports = function buildTasksRouter() {
|
|
|
1228
1258
|
// reaches it via the doorway api.tallyPeerVotes — BV1.R83) share one code path
|
|
1229
1259
|
// and can never diverge. This route keeps the Archon gate (the HTTP surface
|
|
1230
1260
|
// stays Archon-only); the in-process timer has no requester to authorize.
|
|
1231
|
-
router.post('/tasks/peer-votes/tally', auth.requireBuilder, auth.
|
|
1261
|
+
router.post('/tasks/peer-votes/tally', auth.requireBuilder, auth.requirePermission('task.peer_votes.tally'), async (req, res) => {
|
|
1232
1262
|
try {
|
|
1233
1263
|
const result = await db.tallyPeerVotes();
|
|
1234
1264
|
res.json(result);
|
|
@@ -1244,7 +1274,7 @@ module.exports = function buildTasksRouter() {
|
|
|
1244
1274
|
// - 409 if the task isn't votable, or the vote was already folded into karma
|
|
1245
1275
|
// - vote: 0 clears (deletes the row, only while still untallied)
|
|
1246
1276
|
// rank: metic+archon — a trusted-builder reputation action.
|
|
1247
|
-
router.post('/tasks/:id/vote', auth.requireBuilder, auth.
|
|
1277
|
+
router.post('/tasks/:id/vote', auth.requireBuilder, auth.requirePermission('task.vote'), async (req, res) => {
|
|
1248
1278
|
if (validateOrRespond(req, res, {
|
|
1249
1279
|
vote: { required: true, type: 'integer', enum: [-1, 0, 1] },
|
|
1250
1280
|
})) return;
|
|
@@ -1685,7 +1715,7 @@ module.exports = function buildTasksRouter() {
|
|
|
1685
1715
|
// GET /override-requests?status=open — Archon queue. Status defaults to open;
|
|
1686
1716
|
// pass ?status=approved or ?status=denied for the resolved history.
|
|
1687
1717
|
// rank: archon — same gate as the audit log + Archon view.
|
|
1688
|
-
router.get('/override-requests', auth.requireBuilder, auth.
|
|
1718
|
+
router.get('/override-requests', auth.requireBuilder, auth.requirePermission('override_request.decide'), async (req, res) => {
|
|
1689
1719
|
const status = req.query.status || 'open';
|
|
1690
1720
|
if (!['open', 'approved', 'denied'].includes(status)) {
|
|
1691
1721
|
return res.fail('bad_status', 400);
|
|
@@ -1705,7 +1735,7 @@ module.exports = function buildTasksRouter() {
|
|
|
1705
1735
|
// updates; the task stays at completed and the builder must release or
|
|
1706
1736
|
// re-ship after fixing whatever the grader caught.
|
|
1707
1737
|
// rank: archon — grader-bypass authority; same gate as POST /tasks/:id/confirm.
|
|
1708
|
-
router.post('/override-requests/:id/decide', auth.requireBuilder, auth.
|
|
1738
|
+
router.post('/override-requests/:id/decide', auth.requireBuilder, auth.requirePermission('override_request.decide'), async (req, res) => {
|
|
1709
1739
|
const id = parseId(req, res, { code: 'bad_request_id' });
|
|
1710
1740
|
if (id === null) return;
|
|
1711
1741
|
if (validateOrRespond(req, res, {
|
|
@@ -11,6 +11,11 @@ const { asyncHandler, validateOrRespond } = api;
|
|
|
11
11
|
// 'building'; 'shipped'/'frozen' are end states reached via the close flow.
|
|
12
12
|
const VALID_VERSION_STATUSES = new Set(['planning', 'building', 'shipped', 'frozen']);
|
|
13
13
|
|
|
14
|
+
// versions.track CHECK (migration 003) — NOT NULL with no DB default, so the
|
|
15
|
+
// create path must always supply one. 'product' is the caller-facing default:
|
|
16
|
+
// an instance seeding its first version is seeding its product track.
|
|
17
|
+
const VALID_VERSION_TRACKS = new Set(['product', 'internal']);
|
|
18
|
+
|
|
14
19
|
module.exports = function buildVersionsRouter() {
|
|
15
20
|
const router = express.Router();
|
|
16
21
|
|
|
@@ -62,11 +67,12 @@ module.exports = function buildVersionsRouter() {
|
|
|
62
67
|
// no hand-written seed-*.js script. Archon-only: creating a version is a
|
|
63
68
|
// plan/scope mutation, same gate as POST /tasks (ADR 0016).
|
|
64
69
|
// rank: archon — creates a version + its done-when criteria.
|
|
65
|
-
router.post('/versions', auth.requireBuilder, auth.
|
|
70
|
+
router.post('/versions', auth.requireBuilder, auth.requirePermission('version.create'), asyncHandler('POST /versions', async (req, res) => {
|
|
66
71
|
if (validateOrRespond(req, res, {
|
|
67
72
|
id: { required: true, type: 'string', maxLength: 64, minLength: 1 },
|
|
68
73
|
name: { required: true, type: 'string', maxLength: 200, minLength: 1 },
|
|
69
74
|
status: { type: 'string', maxLength: 32 },
|
|
75
|
+
track: { type: 'string', maxLength: 16 },
|
|
70
76
|
done_when: { type: 'string', maxLength: 8000 },
|
|
71
77
|
scope_doc_path: { type: 'string', maxLength: 256 },
|
|
72
78
|
criteria: { type: 'array', maxItems: 100 },
|
|
@@ -81,6 +87,11 @@ module.exports = function buildVersionsRouter() {
|
|
|
81
87
|
return res.fail('bad_status', 400, { valid: Array.from(VALID_VERSION_STATUSES) });
|
|
82
88
|
}
|
|
83
89
|
|
|
90
|
+
const track = body.track || 'product';
|
|
91
|
+
if (!VALID_VERSION_TRACKS.has(track)) {
|
|
92
|
+
return res.fail('bad_track', 400, { valid: Array.from(VALID_VERSION_TRACKS) });
|
|
93
|
+
}
|
|
94
|
+
|
|
84
95
|
if (await db.getVersion(id)) {
|
|
85
96
|
return res.fail('version_exists', { status: 409, message: `Version '${id}' already exists.` });
|
|
86
97
|
}
|
|
@@ -104,7 +115,7 @@ module.exports = function buildVersionsRouter() {
|
|
|
104
115
|
let version, created;
|
|
105
116
|
try {
|
|
106
117
|
({ version, criteria: created } = await db.createVersion({
|
|
107
|
-
id, name, status,
|
|
118
|
+
id, name, status, track,
|
|
108
119
|
doneWhen: body.done_when ?? null,
|
|
109
120
|
scopeDocPath: body.scope_doc_path ?? null,
|
|
110
121
|
criteria,
|
|
@@ -195,7 +195,21 @@ function safeBrand() {
|
|
|
195
195
|
// Stage-specific field selection lives HERE; the renderers are generic.
|
|
196
196
|
function buildCardModel(input = {}) {
|
|
197
197
|
const stage = input.stage || stageFor(input.postGradeStatus);
|
|
198
|
-
const
|
|
198
|
+
const baseCfg = STAGES[stage] || STAGES.failed;
|
|
199
|
+
// task 1002541: did the land actually bail? `input.land` is the ship chain's
|
|
200
|
+
// outcome ({ ok, pending, reason, nextStep }). A task sits at 'confirmed' both
|
|
201
|
+
// when it IS landing on its own (pending — CI/the reconciler will finish it)
|
|
202
|
+
// and when the merge gave up. Only the second is a lie worth shouting about,
|
|
203
|
+
// so this needs an explicit reason — never a bare "not ok".
|
|
204
|
+
// ABSENT `land` (every pre-existing caller, and breadcrumbs written by older
|
|
205
|
+
// versions) ⇒ false ⇒ the card renders exactly as it did before.
|
|
206
|
+
const land = input.land || null;
|
|
207
|
+
const landBailed = !!(stage === 'confirmed' && land && land.ok === false && !land.pending && land.reason);
|
|
208
|
+
// A bailed land is not "confirmed — landing". Recolor the banner to match the
|
|
209
|
+
// truth; the underlying STAGES entry is left untouched (it's frozen + shared).
|
|
210
|
+
const cfg = landBailed
|
|
211
|
+
? { ...baseCfg, tone: 'warning', icon: 'ti-alert-triangle', statusWord: 'Not landed', headline: (id) => `Task #${id} confirmed — NOT landed` }
|
|
212
|
+
: baseCfg;
|
|
199
213
|
const brand = input.brand || safeBrand();
|
|
200
214
|
const taskId = input.taskId;
|
|
201
215
|
const grade = input.grade || {};
|
|
@@ -285,11 +299,20 @@ function buildCardModel(input = {}) {
|
|
|
285
299
|
deployCommit = input.deploy && input.deploy.commit ? String(input.deploy.commit) : null;
|
|
286
300
|
deployText = deployCommit ? `live · ${deployCommit}` : 'live';
|
|
287
301
|
} else if (stage === 'confirmed') {
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
|
|
302
|
+
// task 1002541: this line used to be unconditional, driven by the STATUS
|
|
303
|
+
// alone — so a merge that bailed still reported "landing automatically".
|
|
304
|
+
// Nothing was landing, no PR existed, and under ADR 0120 (pay on land) the
|
|
305
|
+
// builder was unpaid: the failure announced itself as success, which is
|
|
306
|
+
// why these strands survived for days. Say what actually happened.
|
|
307
|
+
if (landBailed) {
|
|
308
|
+
deployText = `NOT landed — ${land.reason}`;
|
|
309
|
+
} else {
|
|
310
|
+
// The server lands confirmed tasks automatically (publish-reconciler +
|
|
311
|
+
// merge driver + ADR 0082 resolver); /merge-mode is a rare manual
|
|
312
|
+
// fallback, kept as the card's optional 'Land it' override — so the fact
|
|
313
|
+
// states the automatic reality rather than implying a required command.
|
|
314
|
+
deployText = 'landing automatically';
|
|
315
|
+
}
|
|
293
316
|
} else {
|
|
294
317
|
deployText = 'not deployed';
|
|
295
318
|
}
|
|
@@ -300,12 +323,20 @@ function buildCardModel(input = {}) {
|
|
|
300
323
|
addFact('', changed);
|
|
301
324
|
if (gradeText) addFact('grade', gradeText, { cls: gradeCls });
|
|
302
325
|
if (stage === 'shipped') addFact('live', deployCommit, { mono: true });
|
|
326
|
+
// The reason can be a full sentence — too long for a fact chip, so the chip
|
|
327
|
+
// carries the alarm and the reason rides in its tooltip (the Deploy row above
|
|
328
|
+
// always shows it in full).
|
|
329
|
+
else if (landBailed) addFact('', 'NOT landed', { cls: 'cc-bad', title: land.reason });
|
|
303
330
|
else addFact('', deployText);
|
|
304
331
|
addFact('', usageText, usageTitle ? { title: usageTitle } : {});
|
|
305
332
|
if (creditsText) addFact('', creditsText);
|
|
306
333
|
}
|
|
307
334
|
|
|
308
|
-
|
|
335
|
+
// task 1002541: a bail knows the precise recovery command (re-run the ship,
|
|
336
|
+
// re-auth, pass --db-only…), which beats the stage's generic '/merge-mode'.
|
|
337
|
+
const action = (landBailed && land.nextStep)
|
|
338
|
+
? { kind: 'prompt', label: 'Land it', cmd: land.nextStep }
|
|
339
|
+
: nextActionFor(stage, { taskId, discipline: input.discipline, sandboxUrl: input.sandboxUrl });
|
|
309
340
|
|
|
310
341
|
return {
|
|
311
342
|
stage, cfg, brand, taskId,
|
|
@@ -556,7 +556,7 @@ module.exports = function buildMemoryRouter() {
|
|
|
556
556
|
router.get(
|
|
557
557
|
'/memory/builders/:builderId/files',
|
|
558
558
|
auth.requireBuilder,
|
|
559
|
-
auth.
|
|
559
|
+
auth.requirePermission('memory.read.any'),
|
|
560
560
|
readRateLimit,
|
|
561
561
|
async (req, res) => {
|
|
562
562
|
const targetId = Number(req.params.builderId);
|
|
@@ -607,7 +607,7 @@ module.exports = function buildMemoryRouter() {
|
|
|
607
607
|
router.get(
|
|
608
608
|
'/memory/builders/:builderId/file',
|
|
609
609
|
auth.requireBuilder,
|
|
610
|
-
auth.
|
|
610
|
+
auth.requirePermission('memory.read.any'),
|
|
611
611
|
readRateLimit,
|
|
612
612
|
async (req, res) => {
|
|
613
613
|
const targetId = Number(req.params.builderId);
|
|
@@ -26,7 +26,7 @@ const express = require('express');
|
|
|
26
26
|
// core file a module may require, ADR 0083); approval-broadcast travels WITH this
|
|
27
27
|
// admission route (its sole consumer) and is an intra-module require.
|
|
28
28
|
const api = require('../../../src/module-api');
|
|
29
|
-
const auth = { requireBuilder: api.requireBuilder,
|
|
29
|
+
const auth = { requireBuilder: api.requireBuilder, requirePermission: api.requirePermission };
|
|
30
30
|
const { pool } = api;
|
|
31
31
|
const { validateOrRespond, parseId } = api;
|
|
32
32
|
const approvalBroadcast = require('../approval-broadcast');
|
|
@@ -145,7 +145,7 @@ module.exports = function buildAccessRequestsRouter() {
|
|
|
145
145
|
// GET /access-requests?status=pending — Archon queue. Defaults to pending;
|
|
146
146
|
// pass ?status=invited|dismissed for the resolved history.
|
|
147
147
|
// rank: archon — the queue exposes would-be builders' usernames.
|
|
148
|
-
router.get('/access-requests', auth.requireBuilder, auth.
|
|
148
|
+
router.get('/access-requests', auth.requireBuilder, auth.requirePermission('access_request.review'), async (req, res) => {
|
|
149
149
|
const status = req.query.status || 'pending';
|
|
150
150
|
if (!['pending', 'invited', 'dismissed'].includes(status)) {
|
|
151
151
|
return res.fail('bad_status', 400);
|
|
@@ -174,7 +174,7 @@ module.exports = function buildAccessRequestsRouter() {
|
|
|
174
174
|
// (the only sign-in gate — no repo access needed); 'dismissed' = declined /
|
|
175
175
|
// spam / dup. The row stays as the audit trail either way.
|
|
176
176
|
// rank: archon — same gate as the rest of the roster management surface.
|
|
177
|
-
router.patch('/access-requests/:id', auth.requireBuilder, auth.
|
|
177
|
+
router.patch('/access-requests/:id', auth.requireBuilder, auth.requirePermission('access_request.review'), async (req, res) => {
|
|
178
178
|
const id = parseId(req, res, { code: 'bad_request_id' });
|
|
179
179
|
if (id === null) return;
|
|
180
180
|
if (validateOrRespond(req, res, {
|
|
@@ -14,13 +14,17 @@ const express = require('express');
|
|
|
14
14
|
const api = require('../../../src/module-api');
|
|
15
15
|
const pi = require('../platform-identity');
|
|
16
16
|
|
|
17
|
-
const { pool, requireBuilder,
|
|
17
|
+
const { pool, requireBuilder, requirePermission, validateOrRespond, parseId, corsPublicGet } = api;
|
|
18
18
|
|
|
19
19
|
module.exports = function projectsRoutes() {
|
|
20
20
|
const router = express.Router();
|
|
21
21
|
|
|
22
|
-
// rank: archon — the full curation list (includes unfeatured + owner attribution).
|
|
23
|
-
|
|
22
|
+
// rank: metic+archon — the full curation list (includes unfeatured + owner attribution).
|
|
23
|
+
// BV1.R105 left this on requireRank because `project.curate` was declared over the
|
|
24
|
+
// writes + GET /scouting, not over this read. ADR 0157 extends that atom's guards to
|
|
25
|
+
// name GET /projects and lowers its floor archon→metic, so the mapping now exists and
|
|
26
|
+
// the audience is the working rank.
|
|
27
|
+
router.get('/projects', requireBuilder, requirePermission('project.curate'), async (_req, res) => {
|
|
24
28
|
try {
|
|
25
29
|
res.json({ projects: await pi.listAllProjects({ pool }) });
|
|
26
30
|
} catch (err) {
|
|
@@ -31,7 +35,7 @@ module.exports = function projectsRoutes() {
|
|
|
31
35
|
|
|
32
36
|
// rank: archon — create / update a catalog entry (manual seed for hand-wired
|
|
33
37
|
// instances; federated projects auto-catalogue at registration).
|
|
34
|
-
router.post('/projects', requireBuilder,
|
|
38
|
+
router.post('/projects', requireBuilder, requirePermission('project.curate'), async (req, res) => {
|
|
35
39
|
if (validateOrRespond(req, res, {
|
|
36
40
|
origin: { required: true, type: 'string', maxLength: 512 },
|
|
37
41
|
name: { required: true, type: 'string', maxLength: 200 },
|
|
@@ -57,7 +61,7 @@ module.exports = function projectsRoutes() {
|
|
|
57
61
|
// by github_login and requires the invitee to already have a platform account (they
|
|
58
62
|
// build on some project); inviting someone entirely new to the platform (GitHub-id
|
|
59
63
|
// resolution) is a follow-up.
|
|
60
|
-
router.post('/projects/invite', requireBuilder,
|
|
64
|
+
router.post('/projects/invite', requireBuilder, requirePermission('project.curate'), async (req, res) => {
|
|
61
65
|
if (validateOrRespond(req, res, {
|
|
62
66
|
client_id: { required: true, type: 'string', maxLength: 200 },
|
|
63
67
|
github_login: { required: true, type: 'string', maxLength: 200 },
|
|
@@ -76,7 +80,7 @@ module.exports = function projectsRoutes() {
|
|
|
76
80
|
});
|
|
77
81
|
|
|
78
82
|
// rank: archon — the feature / unfeature control (the ADR 0141 §5 curation gate).
|
|
79
|
-
router.patch('/projects/:id/featured', requireBuilder,
|
|
83
|
+
router.patch('/projects/:id/featured', requireBuilder, requirePermission('project.curate'), async (req, res) => {
|
|
80
84
|
const id = parseId(req, res, { code: 'bad_project_id', positiveInt: true });
|
|
81
85
|
if (id === null) return;
|
|
82
86
|
if (validateOrRespond(req, res, { featured: { required: true, type: 'boolean' } })) return;
|
|
@@ -12,7 +12,7 @@ const express = require('express');
|
|
|
12
12
|
const api = require('../../../src/module-api');
|
|
13
13
|
const pi = require('../platform-identity');
|
|
14
14
|
|
|
15
|
-
const { pool, requireBuilder,
|
|
15
|
+
const { pool, requireBuilder, requirePermission, parsePagination } = api;
|
|
16
16
|
|
|
17
17
|
module.exports = function scoutingRoutes() {
|
|
18
18
|
const router = express.Router();
|
|
@@ -20,7 +20,7 @@ module.exports = function scoutingRoutes() {
|
|
|
20
20
|
// rank: archon — the internal talent directory across every project. The Archon
|
|
21
21
|
// gate here is the load-bearing protection ("NOT shown to builders"): a non-Archon
|
|
22
22
|
// is 403'd before any cross-builder data is read.
|
|
23
|
-
router.get('/scouting', requireBuilder,
|
|
23
|
+
router.get('/scouting', requireBuilder, requirePermission('project.curate'), async (req, res) => {
|
|
24
24
|
const { limit } = parsePagination(req, { defaultLimit: 100, maxLimit: 500 });
|
|
25
25
|
try {
|
|
26
26
|
const builders = await pi.listScoutingRollup({ limit, pool });
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
-- provisioning_004_onboard_mode.sql — record HOW an instance was onboarded:
|
|
2
|
+
-- greenfield (a fresh empty repo) vs adopt (layer onto a repo the owner already has).
|
|
3
|
+
-- ADR 0121 / task 1002562, BONGOS-V1 goal 32.
|
|
4
|
+
--
|
|
5
|
+
-- WHY THIS COLUMN EXISTS. `bongos init --adopt` (ADR 0121, tasks 2042-2046) built the
|
|
6
|
+
-- LOCAL half of brownfield onboarding, and `GET /provisioning/onboard-plan?mode=…`
|
|
7
|
+
-- branches the checklist TEXT the user is shown — but the mode was never PERSISTED, so
|
|
8
|
+
-- the control-plane runner had no way to know. scripts/gds/provision.js therefore ran
|
|
9
|
+
-- the greenfield scaffold unconditionally: `git init` a fresh tree, commit, and push
|
|
10
|
+
-- `HEAD:main`. Against a repo that already has history that is an unrelated-history
|
|
11
|
+
-- non-fast-forward and git rejects it, so adopting an existing repo could never
|
|
12
|
+
-- succeed. This column is what lets the runner take the layering path instead.
|
|
13
|
+
--
|
|
14
|
+
-- DEFAULT 'greenfield' is the correct backfill: every instance provisioned before this
|
|
15
|
+
-- migration went through the greenfield scaffold by construction (there was no other
|
|
16
|
+
-- path), so no existing row is mislabelled.
|
|
17
|
+
--
|
|
18
|
+
-- Widening/adding is ADDITIVE — a new nullable-with-default column plus a CHECK that
|
|
19
|
+
-- only constrains the new column. No existing row changes and no data is lost, so this
|
|
20
|
+
-- is safe under the module-owned-migration rule (ADR 0083 §Decision #5). Idempotent:
|
|
21
|
+
-- safe to re-run.
|
|
22
|
+
ALTER TABLE provisioning_instances
|
|
23
|
+
ADD COLUMN IF NOT EXISTS onboard_mode text NOT NULL DEFAULT 'greenfield';
|
|
24
|
+
|
|
25
|
+
ALTER TABLE provisioning_instances DROP CONSTRAINT IF EXISTS provisioning_instances_onboard_mode_check;
|
|
26
|
+
ALTER TABLE provisioning_instances ADD CONSTRAINT provisioning_instances_onboard_mode_check
|
|
27
|
+
CHECK (onboard_mode IN ('greenfield', 'adopt'));
|
|
28
|
+
|
|
29
|
+
INSERT INTO schema_migrations (version) VALUES ('provisioning_004_onboard_mode')
|
|
30
|
+
ON CONFLICT (version) DO NOTHING;
|