@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
|
@@ -682,21 +682,48 @@ function landConfirmed(status) {
|
|
|
682
682
|
// it must be an ancestor of main. NOTE claims.head_sha is the claim-TIME base — for a
|
|
683
683
|
// task whose base is already on main (e.g. an R03 base) an ancestry check on it
|
|
684
684
|
// false-positives — so we deliberately key on the published tip, never the base.
|
|
685
|
-
//
|
|
686
|
-
//
|
|
687
|
-
//
|
|
688
|
-
// is
|
|
685
|
+
//
|
|
686
|
+
// task 1002615 (idea 1000613) — a land made OUT-OF-BAND must still prove. A squash-
|
|
687
|
+
// or rebase-merge (e.g. the owner landing the PR through the GitHub web UI) REWRITES
|
|
688
|
+
// the landed commit, so the recorded tip is never an ancestor of main even though the
|
|
689
|
+
// work is live — the old early-return stranded exactly that shape, unpaid. When the
|
|
690
|
+
// tip check misses, fall through to a second git-history proof: the matched PR is
|
|
691
|
+
// MERGED, names THIS task, and its merge_commit_sha is contained in main (that sha is
|
|
692
|
+
// the main-side commit under all three merge methods). Reference match PLUS ancestry
|
|
693
|
+
// is strictly stronger than the legacy no-tip fallback, so it cannot widen the
|
|
694
|
+
// branch-reuse false-ship this function guards.
|
|
695
|
+
//
|
|
696
|
+
// When no tip was recorded at all (a legacy row, or a publish in flight across the
|
|
697
|
+
// deploy that introduced the column), fall back to the matched PR referencing this
|
|
698
|
+
// task id — now requiring merged:true (task 1002615), because the reconciler consults
|
|
699
|
+
// this proof while a PR is still in flight and an OPEN PR that merely references the
|
|
700
|
+
// task must never count as a land. Returns { proven, reason }. The compare calls are
|
|
701
|
+
// the only network hops and are injected-deps-pure for tests.
|
|
689
702
|
async function landProvenForTask({ taskId, publishedSha, status }, deps = {}) {
|
|
690
|
-
|
|
703
|
+
const hasTip = !!(publishedSha && SAFE_SHA_RE.test(String(publishedSha)));
|
|
704
|
+
if (hasTip) {
|
|
691
705
|
const inMain = await isAncestorOfDefaultBranch({ sha: publishedSha }, deps);
|
|
692
|
-
return
|
|
693
|
-
|
|
694
|
-
|
|
706
|
+
if (inMain) return { proven: true, reason: `published tip ${publishedSha.slice(0, 8)} in main` };
|
|
707
|
+
}
|
|
708
|
+
if (status && status.merged === true && prReferencesTask(status, taskId)
|
|
709
|
+
&& status.merge_sha && SAFE_SHA_RE.test(String(status.merge_sha))) {
|
|
710
|
+
let mergeInMain = false;
|
|
711
|
+
try {
|
|
712
|
+
mergeInMain = await isAncestorOfDefaultBranch({ sha: status.merge_sha }, deps);
|
|
713
|
+
} catch (_) {
|
|
714
|
+
/* transient compare miss → fall through; the next sweep retries */
|
|
715
|
+
}
|
|
716
|
+
if (mergeInMain) {
|
|
717
|
+
return { proven: true, reason: `merged PR for task ${taskId} — merge ${String(status.merge_sha).slice(0, 8)} in main (out-of-band/squash land)` };
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
if (hasTip) {
|
|
721
|
+
return { proven: false, reason: `published tip ${String(publishedSha).slice(0, 8)} not in main (branch-reuse/stale PR)` };
|
|
695
722
|
}
|
|
696
|
-
if (prReferencesTask(status, taskId)) {
|
|
723
|
+
if (status && status.merged === true && prReferencesTask(status, taskId)) {
|
|
697
724
|
return { proven: true, reason: `matched PR references task ${taskId} (no published tip recorded)` };
|
|
698
725
|
}
|
|
699
|
-
return { proven: false, reason: `no published tip recorded and
|
|
726
|
+
return { proven: false, reason: `no published tip recorded and no MERGED PR references task ${taskId}` };
|
|
700
727
|
}
|
|
701
728
|
|
|
702
729
|
// task 1313 — proof-of-land that does NOT depend on the PR/branch still existing.
|
|
@@ -40,6 +40,34 @@
|
|
|
40
40
|
// An 8-strand backlog (incl. a Xenos's onboarding ships that never counted) is what
|
|
41
41
|
// surfaced this.
|
|
42
42
|
//
|
|
43
|
+
// task 1002615 (idea 1000613) — recognise a land made OUT-OF-BAND. Two residual
|
|
44
|
+
// blind spots stranded a task whose work was verifiably on main: the sweep only
|
|
45
|
+
// consulted the per-task land proof when the branch's PR reported fully landed
|
|
46
|
+
// (prNotLanded suppressed it), and the proof itself gave up when the recorded
|
|
47
|
+
// published tip wasn't an ancestor of main. A human landing the PR by hand is
|
|
48
|
+
// completely normal — a web-UI squash/rebase REWRITES the landed sha (tip ancestry
|
|
49
|
+
// can never pass), and a manual `git merge` straight to main leaves the PR open or
|
|
50
|
+
// closed-unmerged (prNotLanded suppressed the proof forever). Both shapes left the
|
|
51
|
+
// builder unpaid for landed work (the 2026-07-29 reproduction on task 1002525). Now
|
|
52
|
+
// the proof answers from git history on main — tip ancestry, else the merged PR's
|
|
53
|
+
// merge_commit_sha ancestry — the same authoritative-source principle as task 879
|
|
54
|
+
// (git is the collision detector, not the bookkeeping). One deliberate exception:
|
|
55
|
+
// a MERGED PR still mid-deploy keeps governing — on a deploy-enabled instance
|
|
56
|
+
// 'shipped' means merged AND deployed, and the merge puts the tip in main minutes
|
|
57
|
+
// before the deploy finishes.
|
|
58
|
+
//
|
|
59
|
+
// task 1002606 (finding P-3 — the DETECTION half; 1002615 fixed the cause half) —
|
|
60
|
+
// a strand must reach its builder when it is DETECTED, not at the 30-minute stale
|
|
61
|
+
// WARN. Two shapes are definitive the moment the sweep sees them: a hand-written
|
|
62
|
+
// merge conflict (the ADR 0082 driver can never resolve it) and a PR closed
|
|
63
|
+
// without merging (auto-merge is dead; the land proof already refused it).
|
|
64
|
+
// Both now flag needs_rebase immediately — which surfaces in /builder-start's
|
|
65
|
+
// rebase warning and gates the builder's new claims (ADR 0120 part 3) — and
|
|
66
|
+
// every NEWLY-flagged strand additionally emits `task.strand-flagged` on the
|
|
67
|
+
// kernel seam, giving strands the push channel that previously only
|
|
68
|
+
// `task.shipped` had (the Discord module listens and posts a notice). Ambiguous
|
|
69
|
+
// shapes (open PR, CI still running, no proof yet) keep the 30-min grace.
|
|
70
|
+
//
|
|
43
71
|
// Mirrors the in-process poller precedent (src/bongos/uptime-poller.js) and the
|
|
44
72
|
// Discord reconcile timers (src/bongos/discord-bot.js): unref'd timers (never hold
|
|
45
73
|
// the process open), best-effort (a GitHub/DB hiccup logs and waits for the next
|
|
@@ -145,7 +173,7 @@ async function runSweep(deps = {}) {
|
|
|
145
173
|
`[publish-reconciler] WARN ${describeTarget({ configured: false, repo: resolveTarget(deps) })} — confirmed→shipped can NEVER land until a credential is set (GITHUB_APP_ID + GITHUB_APP_INSTALLATION_ID + GITHUB_APP_PRIVATE_KEY, or GITHUB_PUSH_TOKEN). Subsequent sweeps stay quiet until this changes.`
|
|
146
174
|
);
|
|
147
175
|
}
|
|
148
|
-
return { skipped: 'unconfigured', checked: 0, reconciled: 0, merged: 0, resolvedConflicts: 0, errors: 0 };
|
|
176
|
+
return { skipped: 'unconfigured', checked: 0, reconciled: 0, merged: 0, resolvedConflicts: 0, errors: 0, strandsFlagged: 0 };
|
|
149
177
|
}
|
|
150
178
|
warn.unconfigured = false; // re-arm: a later credential LOSS must warn again
|
|
151
179
|
|
|
@@ -154,7 +182,7 @@ async function runSweep(deps = {}) {
|
|
|
154
182
|
rows = await db.listConfirmedTasksForReconcile({ limit: deps.limit || SWEEP_LIMIT });
|
|
155
183
|
} catch (e) {
|
|
156
184
|
errFn(`[publish-reconciler] could not list confirmed tasks: ${e && e.message}`);
|
|
157
|
-
return { skipped: 'list_failed', checked: 0, reconciled: 0, merged: 0, resolvedConflicts: 0, errors: 1 };
|
|
185
|
+
return { skipped: 'list_failed', checked: 0, reconciled: 0, merged: 0, resolvedConflicts: 0, errors: 1, strandsFlagged: 0 };
|
|
158
186
|
}
|
|
159
187
|
|
|
160
188
|
// Safety: a worktree_name can be reused across sequential (released) claims, so
|
|
@@ -175,9 +203,43 @@ async function runSweep(deps = {}) {
|
|
|
175
203
|
let errors = 0;
|
|
176
204
|
let unprovable = 0; // confirmed tasks the sweep could not prove landed this pass
|
|
177
205
|
let staleStrands = 0; // of those, the ones older than STALE_CONFIRMED_MS (WARN'd)
|
|
206
|
+
let strandsFlagged = 0; // task 1002606: needs_rebase flags NEWLY set this sweep (any site)
|
|
178
207
|
const attempts = deps.healAttempts || healAttempts; // injectable for deterministic tests
|
|
179
208
|
const branchesSeen = new Set(); // to prune the attempts map post-sweep
|
|
180
209
|
const nowMs = Date.now();
|
|
210
|
+
|
|
211
|
+
// task 1002606 — the ONE exit for every strand signal: flag needs_rebase
|
|
212
|
+
// (idempotent — a re-sweep is a no-op), WARN with the concrete cause, and on a
|
|
213
|
+
// NEW flag emit `task.strand-flagged` so a listening module (the Discord
|
|
214
|
+
// mirror) can push the strand to its builder the moment it is detected instead
|
|
215
|
+
// of leaving it to be found at the next session start. Best-effort on every
|
|
216
|
+
// leg — a DB or listener hiccup logs and never breaks the sweep.
|
|
217
|
+
const announceStrand = async (row, reason, describe) => {
|
|
218
|
+
let flagged = false;
|
|
219
|
+
try {
|
|
220
|
+
flagged = await db.flagTaskNeedsRebase(row.task_id, reason);
|
|
221
|
+
} catch (e) {
|
|
222
|
+
errFn(`[publish-reconciler] flagNeedsRebase task ${row.task_id} (non-blocking): ${e && e.message}`);
|
|
223
|
+
}
|
|
224
|
+
errFn(
|
|
225
|
+
`[publish-reconciler] STRAND task ${row.task_id}: ${describe}; ${flagged ? 'flagged needs_rebase for its builder' : 'needs_rebase already set'}`
|
|
226
|
+
);
|
|
227
|
+
if (flagged) {
|
|
228
|
+
strandsFlagged++;
|
|
229
|
+
try {
|
|
230
|
+
await emit('task.strand-flagged', {
|
|
231
|
+
id: row.task_id,
|
|
232
|
+
builder_id: row.builder_id,
|
|
233
|
+
title: row.title,
|
|
234
|
+
security_sensitive: row.security_sensitive,
|
|
235
|
+
reason,
|
|
236
|
+
});
|
|
237
|
+
} catch (e) {
|
|
238
|
+
errFn(`[publish-reconciler] strand broadcast for task ${row.task_id} (non-blocking): ${e && e.message}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return flagged;
|
|
242
|
+
};
|
|
181
243
|
for (const r of rows) {
|
|
182
244
|
const branch = r.worktree_name ? `claude/${r.worktree_name}` : null;
|
|
183
245
|
if (branch) branchesSeen.add(branch);
|
|
@@ -192,9 +254,12 @@ async function runSweep(deps = {}) {
|
|
|
192
254
|
let landed = false;
|
|
193
255
|
let landNote = '';
|
|
194
256
|
let branchStatus = null;
|
|
195
|
-
// task 1002472 — set when a PR exists but has not fully landed.
|
|
196
|
-
//
|
|
197
|
-
//
|
|
257
|
+
// task 1002472 — set when a PR exists but has not fully landed. Classifies the
|
|
258
|
+
// unprovable reason for the observability below (which the old bare `continue`
|
|
259
|
+
// skipped). task 1002615: it no longer suppresses the land proof — the proof
|
|
260
|
+
// itself refuses an un-merged PR (its reference fallback requires merged:true),
|
|
261
|
+
// so an open PR still can't ship by reference, while a task whose WORK is
|
|
262
|
+
// already on main (out-of-band merge) can now prove it by ancestry.
|
|
198
263
|
let prNotLanded = false;
|
|
199
264
|
|
|
200
265
|
if (branchUsable) {
|
|
@@ -232,8 +297,16 @@ async function runSweep(deps = {}) {
|
|
|
232
297
|
`[publish-reconciler] task ${r.task_id} (${branch}) PR #${m.number} conflicted only on generated/index files — server-resolved + pushed; checks re-run, lands next sweep`
|
|
233
298
|
);
|
|
234
299
|
} else if (rr && rr.skipped === 'unresolvable_conflict') {
|
|
235
|
-
|
|
236
|
-
|
|
300
|
+
// task 1002606 (P-3 detection): a hand-written conflict is DEFINITIVE —
|
|
301
|
+
// the driver can never resolve it and the PR sits red until a human
|
|
302
|
+
// acts — so the builder is told NOW, not at the 30-min stale WARN with
|
|
303
|
+
// a vague reason. The file list rides in the reason so /builder-start's
|
|
304
|
+
// rebase warning says WHERE to look.
|
|
305
|
+
const files = (rr.conflicted || []).slice(0, 5).join(',');
|
|
306
|
+
await announceStrand(
|
|
307
|
+
r,
|
|
308
|
+
`strand:unresolvable_conflict${files ? `:${files}` : ''}`,
|
|
309
|
+
`PR #${m.number} (${branch}) has a hand-written conflict (${files || 'unknown files'}) only its builder can resolve`
|
|
237
310
|
);
|
|
238
311
|
}
|
|
239
312
|
} catch (e) {
|
|
@@ -263,13 +336,12 @@ async function runSweep(deps = {}) {
|
|
|
263
336
|
// is exhausted and the PR is still red — a real check failure or a
|
|
264
337
|
// hand-written conflict only the builder can resolve. Flag needs_rebase
|
|
265
338
|
// so their queue is gated until they fix + re-land (or release it).
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
339
|
+
// task 1002606: announceStrand also pushes task.strand-flagged.
|
|
340
|
+
await announceStrand(
|
|
341
|
+
r,
|
|
342
|
+
`failing_check_capped:${m.reason}`,
|
|
343
|
+
`PR #${m.number} (${branch}) still red after ${n} regen-heal attempt(s) (${m.reason}) — a real check failure or conflict only its builder can fix`
|
|
344
|
+
);
|
|
273
345
|
} else {
|
|
274
346
|
try {
|
|
275
347
|
const rr = await resolver.refreshStaleGenerated({ branch });
|
|
@@ -307,14 +379,24 @@ async function runSweep(deps = {}) {
|
|
|
307
379
|
}
|
|
308
380
|
if (!push.landConfirmed(status) && status && status.pr_state !== 'none') {
|
|
309
381
|
// A PR exists but hasn't fully landed yet (open / merged-but-deploy-pending /
|
|
310
|
-
// blocked). Respect
|
|
382
|
+
// blocked). Respect the PR path — wait for the deploy — rather than merge-ship
|
|
383
|
+
// early on it.
|
|
311
384
|
//
|
|
312
385
|
// task 1002472: this used to be a bare `continue`, which jumped PAST the
|
|
313
386
|
// unprovable counter, the STALE WARN and flagTaskNeedsRebase below — so this
|
|
314
387
|
// strand shape was completely invisible (tasks 1002427/1002428 sat 50h at
|
|
315
|
-
// 'confirmed' reporting needs_rebase:false). Flag it instead and fall through
|
|
316
|
-
//
|
|
317
|
-
//
|
|
388
|
+
// 'confirmed' reporting needs_rebase:false). Flag it instead and fall through.
|
|
389
|
+
//
|
|
390
|
+
// task 1002615: an UN-merged PR no longer suppresses the land proof — an
|
|
391
|
+
// out-of-band land (manual git merge to main with the PR left open or
|
|
392
|
+
// closed-unmerged) makes the PR report "not landed" while the WORK is on
|
|
393
|
+
// main, and suppressing the proof stranded exactly that shape, unpaid. The
|
|
394
|
+
// proof refuses reference-only claims on an un-merged PR, so a genuinely
|
|
395
|
+
// in-flight PR stays exactly as un-shippable as before. A MERGED PR still
|
|
396
|
+
// mid-deploy DOES keep governing (prMidDeploy below): on a deploy-enabled
|
|
397
|
+
// instance 'shipped' means merged AND deployed, and the merge puts the tip
|
|
398
|
+
// in main minutes before the deploy finishes — the tip proof must not
|
|
399
|
+
// short-circuit that window.
|
|
318
400
|
prNotLanded = true;
|
|
319
401
|
}
|
|
320
402
|
branchStatus = status;
|
|
@@ -330,7 +412,11 @@ async function runSweep(deps = {}) {
|
|
|
330
412
|
// references this task id. landProvenForTask still cures the task-1313 strand class
|
|
331
413
|
// (PR deleted post-merge / no worktree_name / ambiguous branch) — now keyed on the
|
|
332
414
|
// unambiguous published tip rather than the base.
|
|
333
|
-
|
|
415
|
+
// task 1002615: only a MERGED PR that is still deploying suppresses the proof
|
|
416
|
+
// (the deploy gate governs); an un-merged PR — open, or closed without merging —
|
|
417
|
+
// cannot veto what git history on main already shows.
|
|
418
|
+
const prMidDeploy = prNotLanded && !!(branchStatus && branchStatus.merged === true);
|
|
419
|
+
if (!landed && !prMidDeploy && typeof push.landProvenForTask === 'function') {
|
|
334
420
|
try {
|
|
335
421
|
const proof = await push.landProvenForTask({
|
|
336
422
|
taskId: r.task_id, publishedSha: r.published_head_sha, status: branchStatus,
|
|
@@ -355,9 +441,18 @@ async function runSweep(deps = {}) {
|
|
|
355
441
|
// task 1002472: 'pr_not_landed' distinguishes "a PR exists and we are waiting on
|
|
356
442
|
// it" from "we looked for proof and found none" — the two need different
|
|
357
443
|
// remediation (wait/unblock the PR vs. push or rebase the branch).
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
444
|
+
// task 1002606: 'pr_closed_unmerged' is carved out of it as DEFINITIVE — a PR
|
|
445
|
+
// closed without merging will never land on its own (auto-merge is dead and the
|
|
446
|
+
// land proof above already refused it), so it must not enjoy the 30-min grace
|
|
447
|
+
// that exists for CI legitimately still running. (A MERGED PR mid-deploy has
|
|
448
|
+
// merged=true and stays on the graced 'pr_not_landed' path.)
|
|
449
|
+
const prClosedUnmerged = prNotLanded && !!branchStatus &&
|
|
450
|
+
branchStatus.pr_state === 'closed' && branchStatus.merged !== true;
|
|
451
|
+
const reason = prClosedUnmerged
|
|
452
|
+
? 'pr_closed_unmerged'
|
|
453
|
+
: prNotLanded
|
|
454
|
+
? 'pr_not_landed'
|
|
455
|
+
: (!branch && !r.published_head_sha) ? 'no_branch_no_tip' : 'land_not_proven';
|
|
361
456
|
const ageMs = r.confirmed_at ? (nowMs - new Date(r.confirmed_at).getTime()) : null;
|
|
362
457
|
if (ageMs != null && ageMs > STALE_CONFIRMED_MS) {
|
|
363
458
|
staleStrands++;
|
|
@@ -369,14 +464,16 @@ async function runSweep(deps = {}) {
|
|
|
369
464
|
// NEW claims until they land or release it. Since the reward now lands only
|
|
370
465
|
// on ship (part 1), they are motivated to clear it. NOTE it is no longer
|
|
371
466
|
// "credited" — part 1 moved the payout from confirm to the land.
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
467
|
+
await announceStrand(
|
|
468
|
+
r,
|
|
469
|
+
`strand:${reason}`,
|
|
470
|
+
`confirmed ~${Math.round(ageMs / 3_600_000)}h ago, unprovable (${reason}) — no landed proof`
|
|
471
|
+
);
|
|
472
|
+
} else if (prClosedUnmerged) {
|
|
473
|
+
await announceStrand(
|
|
474
|
+
r,
|
|
475
|
+
`strand:${reason}`,
|
|
476
|
+
`its PR was closed without merging — it will never land on its own`
|
|
380
477
|
);
|
|
381
478
|
}
|
|
382
479
|
continue;
|
|
@@ -423,7 +520,7 @@ async function runSweep(deps = {}) {
|
|
|
423
520
|
`[publish-reconciler] ${staleStrands} stale strand(s): confirmed >${Math.round(STALE_CONFIRMED_MS / 60_000)}min with no provable land — credited but unlanded; see STRAND lines above`
|
|
424
521
|
);
|
|
425
522
|
}
|
|
426
|
-
return { checked: rows.length, reconciled, merged, resolvedConflicts, refreshedStale, errors, unprovable, staleStrands };
|
|
523
|
+
return { checked: rows.length, reconciled, merged, resolvedConflicts, refreshedStale, errors, unprovable, staleStrands, strandsFlagged };
|
|
427
524
|
}
|
|
428
525
|
|
|
429
526
|
// task 1366 — server-side session-cost reconcile, piggybacked on the same 5-min
|
|
@@ -37,7 +37,7 @@ function resolveCreatorSessionId(bindSession, sessionId) {
|
|
|
37
37
|
function taskNotReadyHint(taskId, currentStatus) {
|
|
38
38
|
const idNum = Number(taskId);
|
|
39
39
|
return currentStatus === 'backlog'
|
|
40
|
-
? `Task #${idNum} is in 'backlog' — promote it first: POST /api/bongos/tasks/${idNum}/promote (
|
|
40
|
+
? `Task #${idNum} is in 'backlog' — promote it first: POST /api/bongos/tasks/${idNum}/promote (Metic+), then claim.`
|
|
41
41
|
: `Task #${idNum} is '${currentStatus}', not 'ready' — only 'ready' tasks are claimable (promote applies to 'backlog': POST /api/bongos/tasks/${idNum}/promote).`;
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -259,6 +259,10 @@ module.exports = function buildClaimsRouter() {
|
|
|
259
259
|
// tasks (already 'ready' at migration 177) are exempt and never reach here.
|
|
260
260
|
err.code === 'REWARD_REQUIRED' ? 409 :
|
|
261
261
|
err.code === 'ALREADY_CLAIMED' ? 409 :
|
|
262
|
+
// task 1002589: migration 180's per-session index tripped at the DB
|
|
263
|
+
// layer (claimTask maps the 23505 by err.constraint). Same 409 family
|
|
264
|
+
// as the route gate's session_already_has_active_claim above.
|
|
265
|
+
err.code === 'session_already_has_bound_claim' ? 409 :
|
|
262
266
|
err.code === 'BUILDER_INACTIVE' ? 403 :
|
|
263
267
|
// V3.R15 #239: rank_too_low_for_task is the canonical Xenos-gate code;
|
|
264
268
|
// XENOS_RESTRICTED is the pre-rename alias still honored by callers
|
|
@@ -306,8 +310,10 @@ module.exports = function buildClaimsRouter() {
|
|
|
306
310
|
// task 1802: omitting the flag (the real-CLI-session default) no longer
|
|
307
311
|
// binds every row — db.claimTasksBatch only binds the FIRST inserted
|
|
308
312
|
// claim to the caller's session and leaves the rest unbound, so a real
|
|
309
|
-
// session can batch-claim 2+ tasks without tripping
|
|
310
|
-
//
|
|
313
|
+
// session can batch-claim 2+ tasks without tripping the per-session
|
|
314
|
+
// backstop index on the 2nd+ INSERT (migration 180; re-keyed per
|
|
315
|
+
// (session, worktree) by core_204, where a prefix-less batch's NULL
|
|
316
|
+
// worktrees still fold into one group — first-row binding stays). A
|
|
311
317
|
// single-task batch call is unaffected (first row IS the only row).
|
|
312
318
|
bind_session: { type: 'boolean' },
|
|
313
319
|
})) return;
|
|
@@ -502,7 +508,7 @@ module.exports = function buildClaimsRouter() {
|
|
|
502
508
|
// grep can find every third-party release.
|
|
503
509
|
router.post('/claims/:id/release-on-behalf',
|
|
504
510
|
auth.requireBuilder,
|
|
505
|
-
auth.
|
|
511
|
+
auth.requirePermission('claim.release.any'),
|
|
506
512
|
async (req, res) => {
|
|
507
513
|
if (validateOrRespond(req, res, {
|
|
508
514
|
reason: { required: true, type: 'string', maxLength: LIMITS.NOTES },
|
|
@@ -53,7 +53,7 @@ module.exports = function buildDependenciesRouter() {
|
|
|
53
53
|
|
|
54
54
|
// POST /dependencies — create an edge "from is blocked until to is done".
|
|
55
55
|
// rank: archon.
|
|
56
|
-
router.post('/dependencies', auth.requireBuilder, auth.
|
|
56
|
+
router.post('/dependencies', auth.requireBuilder, auth.requirePermission('dependency.cross_tier.manage'), async (req, res) => {
|
|
57
57
|
const edge = parseEdge(req, res);
|
|
58
58
|
if (!edge) return;
|
|
59
59
|
try {
|
|
@@ -66,7 +66,7 @@ module.exports = function buildDependenciesRouter() {
|
|
|
66
66
|
|
|
67
67
|
// DELETE /dependencies — remove an edge (body carries the 4-tuple).
|
|
68
68
|
// rank: archon.
|
|
69
|
-
router.delete('/dependencies', auth.requireBuilder, auth.
|
|
69
|
+
router.delete('/dependencies', auth.requireBuilder, auth.requirePermission('dependency.cross_tier.manage'), async (req, res) => {
|
|
70
70
|
const edge = parseEdge(req, res);
|
|
71
71
|
if (!edge) return;
|
|
72
72
|
try {
|
|
@@ -116,7 +116,7 @@ module.exports = function buildDoneWhenRouter() {
|
|
|
116
116
|
// bad_goal_id so the route is not an existence oracle (ADR 0112).
|
|
117
117
|
// Pinned in route-rank-check.MODULE_EXPECTED_RANKS.
|
|
118
118
|
// rank: metic+archon — criterion create, walled by goal ownership (ADR 0154).
|
|
119
|
-
router.post('/versions/:id/done-when', auth.requireBuilder, auth.
|
|
119
|
+
router.post('/versions/:id/done-when', auth.requireBuilder, auth.requirePermission('criterion.create'), asyncHandler('POST /versions/:id/done-when', async (req, res) => {
|
|
120
120
|
const versionId = String(req.params.id || '').trim();
|
|
121
121
|
if (!versionId) return res.fail('version_id_required', 400);
|
|
122
122
|
if (validateOrRespond(req, res, {
|
|
@@ -176,7 +176,7 @@ module.exports = function buildDoneWhenRouter() {
|
|
|
176
176
|
// match the satisfy route below — this is the planner's closing queue, not a
|
|
177
177
|
// public surface.
|
|
178
178
|
// rank: metic+archon — the planner review queue (mirrors the satisfy gate).
|
|
179
|
-
router.get('/done-when/pending-review', auth.requireBuilder, auth.
|
|
179
|
+
router.get('/done-when/pending-review', auth.requireBuilder, auth.requirePermission('criterion.review'), asyncHandler('GET /done-when/pending-review', async (req, res) => {
|
|
180
180
|
const versionId = String(req.query.version || '').trim() || null;
|
|
181
181
|
const criteria = await doneWhen.listPendingReviewCriteria({ versionId });
|
|
182
182
|
res.json({ criteria, count: criteria.length });
|
|
@@ -190,7 +190,7 @@ module.exports = function buildDoneWhenRouter() {
|
|
|
190
190
|
// pinned in route-rank-check.MODULE_EXPECTED_RANKS.lifecycle so a further
|
|
191
191
|
// downgrade is caught.
|
|
192
192
|
// rank: metic+archon — criterion confirmation + goal auto-achieve (the closing loop).
|
|
193
|
-
router.post('/done-when/:criterionId/satisfy', auth.requireBuilder, auth.
|
|
193
|
+
router.post('/done-when/:criterionId/satisfy', auth.requireBuilder, auth.requirePermission('criterion.satisfy'), asyncHandler('POST /done-when/:criterionId/satisfy', async (req, res) => {
|
|
194
194
|
if (validateOrRespond(req, res, {
|
|
195
195
|
satisfied_by_task_id: { type: 'integer', min: 1 },
|
|
196
196
|
})) return;
|
|
@@ -213,7 +213,7 @@ module.exports = function buildDoneWhenRouter() {
|
|
|
213
213
|
|
|
214
214
|
// Auth: flip a criterion back to unsatisfied (rare; for corrections).
|
|
215
215
|
// rank: archon — un-flip a criterion; same gate as satisfy.
|
|
216
|
-
router.post('/done-when/:criterionId/unsatisfy', auth.requireBuilder, auth.
|
|
216
|
+
router.post('/done-when/:criterionId/unsatisfy', auth.requireBuilder, auth.requirePermission('criterion.unsatisfy'), asyncHandler('POST /done-when/:criterionId/unsatisfy', async (req, res) => {
|
|
217
217
|
const id = parseId(req, res, { param: 'criterionId', code: 'bad_criterion_id' });
|
|
218
218
|
if (id === null) return;
|
|
219
219
|
const existing = await doneWhen.getCriterion(id);
|
|
@@ -64,8 +64,14 @@ module.exports = function buildGateApprovalsRouter() {
|
|
|
64
64
|
const router = express.Router();
|
|
65
65
|
|
|
66
66
|
// GET /gate-approvals — open PRs awaiting owner approval (gate-review red), with the
|
|
67
|
-
// verdict + per-file reasons + changed files + diff link. rank: archon.
|
|
68
|
-
|
|
67
|
+
// verdict + per-file reasons + changed files + diff link. rank: metic+archon.
|
|
68
|
+
// BV1.R105 left this on requireRank because `gate_approval.decide` was declared over
|
|
69
|
+
// the /approve WRITE only and no atom named the read. ADR 0157 extends the catalog
|
|
70
|
+
// with exactly that atom — `gate_approval.read`, floor metic — so the mapping is now
|
|
71
|
+
// one the catalog makes. The split is load-bearing, not cosmetic: `page.view.governance`
|
|
72
|
+
// opens /gate to Metic, and without a Metic-readable data endpoint that page would
|
|
73
|
+
// render nothing but a 403. Approving a held PR stays archon (`.decide`, system).
|
|
74
|
+
router.get('/gate-approvals', auth.requireBuilder, auth.requirePermission('gate_approval.read'), async (req, res) => {
|
|
69
75
|
if (!githubPush.isConfigured()) {
|
|
70
76
|
return res.fail('publish_unconfigured', { status: 503, message: 'No server GitHub credential configured — cannot read pull requests.' });
|
|
71
77
|
}
|
|
@@ -130,7 +136,7 @@ module.exports = function buildGateApprovalsRouter() {
|
|
|
130
136
|
// POST /gate-approvals/:pr/approve — record owner approval for a stuck gate PR and
|
|
131
137
|
// re-run the gate-review check so it clears. rank: archon (clears the hard floor —
|
|
132
138
|
// pinned in route-rank-check.EXPECTED_RANKS for downgrade protection).
|
|
133
|
-
router.post('/gate-approvals/:pr/approve', auth.requireBuilder, auth.
|
|
139
|
+
router.post('/gate-approvals/:pr/approve', auth.requireBuilder, auth.requirePermission('gate_approval.decide'), async (req, res) => {
|
|
134
140
|
const number = parseId(req, res, { param: 'pr', code: 'bad_pr', positiveInt: true });
|
|
135
141
|
if (number == null) return;
|
|
136
142
|
if (!githubPush.isConfigured()) {
|
|
@@ -379,7 +379,7 @@ module.exports = function buildGoalsRouter() {
|
|
|
379
379
|
// POST /goals — create a goal.
|
|
380
380
|
// rank: metic+archon — a Metic or Archon may create a goal; creation of a goal
|
|
381
381
|
// whose scope_modules includes a PROTECTED module is Archon-only (in-handler).
|
|
382
|
-
router.post('/goals', auth.requireBuilder, auth.
|
|
382
|
+
router.post('/goals', auth.requireBuilder, auth.requirePermission('goal.create'), async (req, res) => {
|
|
383
383
|
if (validateOrRespond(req, res, {
|
|
384
384
|
version_id: { required: true, type: 'string', minLength: 1, maxLength: 64 },
|
|
385
385
|
title: { required: true, type: 'string', minLength: 1, maxLength: LIMITS.TITLE },
|
|
@@ -457,7 +457,7 @@ module.exports = function buildGoalsRouter() {
|
|
|
457
457
|
// Mounted BEFORE GET /goals/:id so 'reorder' is never captured as an id.
|
|
458
458
|
// rank: archon — priority is an owner decision, never self-granted by a
|
|
459
459
|
// goal's own member/manager (unlike the goal-authority routes above).
|
|
460
|
-
router.post('/goals/reorder', auth.requireBuilder, auth.
|
|
460
|
+
router.post('/goals/reorder', auth.requireBuilder, auth.requirePermission('goal.reorder'), async (req, res) => {
|
|
461
461
|
if (validateOrRespond(req, res, {
|
|
462
462
|
version_id: { required: true, type: 'string', minLength: 1, maxLength: 64 },
|
|
463
463
|
ordered_ids: { required: true, type: 'array', maxItems: 500, itemsType: 'integer' },
|
|
@@ -1232,7 +1232,7 @@ module.exports = function buildGoalsRouter() {
|
|
|
1232
1232
|
// goals are re-openable (below). This is the lifecycle disposition the /goal-review
|
|
1233
1233
|
// skill reaches for once a goal is achieved (ADR 0086 §6 / BV1.R64).
|
|
1234
1234
|
// rank: metic+archon — a goal member (Metic+) or an Archon dispositions the goal.
|
|
1235
|
-
router.post('/goals/:id/archive', auth.requireBuilder, auth.
|
|
1235
|
+
router.post('/goals/:id/archive', auth.requireBuilder, auth.requirePermission('goal.archive'), async (req, res) => {
|
|
1236
1236
|
const id = parseId(req, res, { code: 'bad_goal_id' });
|
|
1237
1237
|
if (id === null) return;
|
|
1238
1238
|
try {
|
|
@@ -1255,7 +1255,7 @@ module.exports = function buildGoalsRouter() {
|
|
|
1255
1255
|
// POST /goals/:id/reopen — bring an archived goal back to open (ADR 0086 §6 /
|
|
1256
1256
|
// BV1.R64). Only archived → open; an already open/achieved goal is a 409.
|
|
1257
1257
|
// rank: metic+archon — same gate as archive.
|
|
1258
|
-
router.post('/goals/:id/reopen', auth.requireBuilder, auth.
|
|
1258
|
+
router.post('/goals/:id/reopen', auth.requireBuilder, auth.requirePermission('goal.archive'), async (req, res) => {
|
|
1259
1259
|
const id = parseId(req, res, { code: 'bad_goal_id' });
|
|
1260
1260
|
if (id === null) return;
|
|
1261
1261
|
try {
|
|
@@ -1283,7 +1283,7 @@ module.exports = function buildGoalsRouter() {
|
|
|
1283
1283
|
// ask a Metic+/Archon out-of-band, exactly as joining a protected goal works (R57).
|
|
1284
1284
|
// The goal must be open. Body: { add: ["module-key", ...] }; idempotent (set-union).
|
|
1285
1285
|
// rank: metic+archon — widens the module wall; pinned in route-rank-check.EXPECTED_RANKS.
|
|
1286
|
-
router.post('/goals/:id/scope', auth.requireBuilder, auth.
|
|
1286
|
+
router.post('/goals/:id/scope', auth.requireBuilder, auth.requirePermission('goal.scope.widen'), async (req, res) => {
|
|
1287
1287
|
const id = parseId(req, res, { code: 'bad_goal_id' });
|
|
1288
1288
|
if (id === null) return;
|
|
1289
1289
|
if (validateOrRespond(req, res, {
|