@bongos/core 1.19.1080 → 1.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +573 -188
- package/.claude/skills/planning-session/SKILL.md +6 -2
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +34 -0
- package/clients/bongos-client/index.cjs +34 -0
- package/clients/bongos-client/index.d.ts +53 -4
- package/clients/bongos-client/index.mjs +34 -0
- package/docs/adr/0099-delayed-redacted-mirror-export.md +5 -1
- package/docs/adr/0111-instance-hosting-provisioning-module.md +1 -0
- package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +17 -0
- package/docs/adr/0176-private-repo-deploy-keys.md +2 -0
- package/docs/adr/0310-a-speciality-offers-skills-and-the-adopter-chooses-them.md +1 -1
- package/docs/adr/0343-a-module-score-is-a-security-gate-then-an-average-of-visible-parts.md +2 -0
- package/docs/adr/0347-every-store-module-ships-a-how-to.md +135 -0
- package/docs/adr/0348-the-web-tier-may-look-read-only-at-what-an-owners-render-key-can-see.md +53 -0
- package/docs/adr/0349-a-version-preview-is-a-sandboxed-child-the-web-tier-launches.md +106 -0
- package/docs/adr/0350-the-hub-holds-a-per-project-write-deploy-key-so-a-hosted-upgrade-reaches-github.md +103 -0
- package/docs/adr/README.md +4 -0
- package/docs/api/openapi.json +995 -40
- package/docs/api-reference.md +33 -8
- package/docs/architecture.md +6 -2
- package/docs/copy-inventory.md +620 -553
- package/docs/copy-registry.json +1467 -820
- package/docs/file-map.md +2 -0
- package/docs/module-api-changelog.md +8 -0
- package/docs/modules-contract.md +1 -0
- package/docs/page-inventory.json +38 -4
- package/docs/page-readings.json +1674 -1566
- package/migrations/core_260_goals_working_area.sql +63 -0
- package/migrations/core_261_grade_attempts_verdicts.sql +24 -0
- package/migrations/core_262_drop_builder_box_blocked.sql +33 -0
- package/modules/agents/lib/validate.js +43 -0
- package/modules/autonomy/gauge.js +38 -2
- package/modules/grading/grader-subagent.js +37 -2
- package/modules/grading/grader-workers/reader.js +65 -5
- package/modules/hall-ui/public/approval-queue.css +33 -7
- package/modules/hall-ui/public/approval-queue.js +9 -2
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/brand-holes.js +121 -0
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/collab.js +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +6 -1
- package/modules/hall-ui/public/deploy.js +22 -8
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/dom-utils.js +20 -0
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/idea.js +28 -0
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/ideas.js +19 -3
- package/modules/hall-ui/public/index.html +5 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile-nudge.js +21 -4
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/profile.js +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings.html +19 -30
- package/modules/hall-ui/public/settings.js +24 -177
- package/modules/hall-ui/public/settings.states.json +1 -1
- package/modules/hall-ui/public/shell.js +4 -2
- package/modules/hall-ui/public/studio.css +14 -8
- package/modules/hall-ui/public/studio.html +5 -4
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.css +6 -3
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/tweak-editor-lib.js +82 -1
- package/modules/hall-ui/public/tweak-editor.css +142 -11
- package/modules/hall-ui/public/tweak-editor.html +25 -9
- package/modules/hall-ui/public/tweak-editor.js +243 -10
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/hall-ui/records/approval-queue.md +6 -0
- package/modules/hall-ui/records/tweak-editor.md +6 -0
- package/modules/ideas/ratify.js +36 -8
- package/modules/ideas/routes/ratify-goal.js +6 -0
- package/modules/lifecycle/db-analytics.js +79 -10
- package/modules/lifecycle/db-claims.js +48 -1
- package/modules/lifecycle/db-deps-criteria.js +94 -40
- package/modules/lifecycle/db-goals.js +4 -1
- package/modules/lifecycle/db-grade.js +9 -1
- package/modules/lifecycle/db-relevance-flags.js +183 -0
- package/modules/lifecycle/db-versions.js +4 -3
- package/modules/lifecycle/db.js +19 -0
- package/modules/lifecycle/done-when.js +5 -3
- package/modules/lifecycle/est-advisory.js +31 -4
- package/modules/lifecycle/goal-task-relevance-advisory.js +153 -0
- package/modules/lifecycle/goal-task-relevance-judge.js +226 -0
- package/modules/lifecycle/migrations/lifecycle_014_goal_task_relevance_flags.sql +82 -0
- package/modules/lifecycle/module.json +2 -1
- package/modules/lifecycle/routes/claims.js +8 -1
- package/modules/lifecycle/routes/goal-task-relevance.js +72 -0
- package/modules/lifecycle/routes/goals.js +1 -1
- package/modules/lifecycle/routes/tasks.js +27 -24
- package/modules/lifecycle/routes/visuals.js +9 -0
- package/modules/lifecycle/ship-preflight.js +7 -11
- package/modules/lifecycle/task-visuals.js +109 -5
- package/modules/npm-release/module.json +2 -1
- package/modules/npm-release/preview/commands.js +81 -0
- package/modules/npm-release/preview/divert.js +95 -0
- package/modules/npm-release/preview/env.js +78 -0
- package/modules/npm-release/preview/proxy.js +107 -0
- package/modules/npm-release/preview/runtime.js +83 -0
- package/modules/npm-release/preview/supervisor.js +175 -0
- package/modules/npm-release/public/work.js +65 -1
- package/modules/npm-release/routes/preview.js +133 -0
- package/modules/provisioning/app-status.js +83 -0
- package/modules/provisioning/migrations/provisioning_028_render_standup.sql +46 -0
- package/modules/provisioning/module.json +4 -2
- package/modules/provisioning/pollers/app-liveness.js +110 -0
- package/modules/provisioning/provisioning.js +5 -5
- package/modules/provisioning/render-lookup.js +68 -0
- package/modules/provisioning/render-standup.js +134 -0
- package/modules/provisioning/routes/render-standup.js +165 -0
- package/modules/provisioning/starter-bundles.js +4 -1
- package/modules/public-landing/public/account-private.states.json +14 -0
- package/modules/public-landing/public/account.html +513 -0
- package/modules/public-landing/public/account.probes.json +37 -0
- package/modules/public-landing/public/account.states.json +16 -0
- package/modules/public-landing/public/index.html +5 -2
- package/modules/public-landing/public/projects.html +661 -18
- package/modules/public-landing/public/projects.states.json +8 -1
- package/modules/render-deploy/deploys.js +203 -0
- package/modules/render-deploy/migrations/render_deploy_001_app.sql +22 -0
- package/modules/render-deploy/module.json +25 -0
- package/modules/render-deploy/public/deploy.css +10 -0
- package/modules/render-deploy/public/deploy.js +249 -0
- package/modules/render-deploy/render.js +56 -0
- package/modules/render-deploy/routes/act.js +88 -0
- package/modules/render-deploy/routes/app.js +65 -0
- package/modules/render-deploy/routes/history.js +38 -0
- package/modules/specialities/db.js +27 -8
- package/modules/specialities/migrations/specialities_003_skills.sql +46 -0
- package/modules/specialities/routes/specialities.js +51 -3
- package/modules/specialities/skills.js +94 -0
- package/modules/specialities/specialities.js +56 -9
- package/modules/ui-design/kit/fixtures/me-cross-project-private.json +12 -0
- package/modules/ui-design/kit/fixtures/me__cross-project.json +15 -0
- package/modules/ui-design/kit/fixtures/provisioning-instance-render.json +39 -0
- package/modules/ui-design/kit/lib.js +3 -1
- package/modules/ui-design/kit/serve.js +96 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +173 -0
- package/scripts/gds/agents-sync.js +13 -3
- package/scripts/gds/autobongos-run.js +80 -2
- package/scripts/gds/autobongos-service.cmd +12 -0
- package/scripts/gds/copy-apply.js +14 -0
- package/scripts/gds/dev-box-guard.js +3 -2
- package/scripts/gds/fitness.js +9 -0
- package/scripts/gds/grade-correlation-audit.js +42 -12
- package/scripts/gds/grade-replay.js +1 -1
- package/scripts/gds/provision-core-upgrade.js +38 -7
- package/scripts/gds/provision-pin-key.js +176 -0
- package/scripts/gds/provision-render.js +189 -0
- package/scripts/gds/provision-teardown.js +9 -2
- package/scripts/gds/provision-units.js +31 -0
- package/scripts/gds/provision.js +12 -12
- package/scripts/gds/publish-manifest.js +1 -0
- package/scripts/gds/render-api.js +197 -0
- package/scripts/gds/render-payload.js +84 -0
- package/scripts/gds/run-unit-tests.js +10 -0
- package/scripts/gds/ship-finish.js +19 -20
- package/scripts/gds/smoke-dependencies.sh +37 -8
- package/scripts/gds/status.js +12 -4
- package/scripts/gds/upgrade.js +2 -2
- package/scripts/public-mirror-export.js +7 -1
- package/src/bongos/module-scope-map.js +13 -0
- package/src/bongos/serve-internal.js +3 -1
- package/src/module-api.js +10 -1
- package/src/platform-server.js +39 -0
- package/tests/account_privacy_flags.mjs +10 -6
- package/tests/agents_authoring.mjs +15 -1
- package/tests/agents_sync.mjs +106 -5
- package/tests/agents_validate.mjs +45 -0
- package/tests/api_path_404.mjs +6 -0
- package/tests/autobongos_cadence.mjs +7 -0
- package/tests/autobongos_loop.mjs +136 -1
- package/tests/autonomy_gauge.mjs +62 -0
- package/tests/blocker_hall_live_proof.mjs +275 -0
- package/tests/claim_gate_ci_unblock.mjs +116 -0
- package/tests/claim_gate_rebase.mjs +15 -3
- package/tests/collab_page.mjs +10 -0
- package/tests/conductor_main_e2e.mjs +97 -0
- package/tests/core_262_drop_box_blocked_db.mjs +143 -0
- package/tests/core_upgrade_runner.mjs +1 -0
- package/tests/dependency_writes_atomic.mjs +191 -0
- package/tests/deploy_page_projects.mjs +2 -1
- package/tests/effective_visibility_predicate.mjs +15 -0
- package/tests/est_advisory.mjs +27 -0
- package/tests/goal_map_page.mjs +26 -0
- package/tests/goal_task_relevance.mjs +518 -0
- package/tests/goal_working_area.mjs +146 -0
- package/tests/grade_attempts.mjs +61 -0
- package/tests/grade_attribution.mjs +23 -0
- package/tests/grade_correlation_audit.mjs +54 -1
- package/tests/grader_reader_lens.mjs +55 -2
- package/tests/grader_root_outage.mjs +6 -1
- package/tests/grader_subagent_tools_arg.mjs +102 -0
- package/tests/hall_approval_queue.mjs +54 -2
- package/tests/hall_settings_world.mjs +3 -1
- package/tests/hall_tweak_editor.mjs +316 -11
- package/tests/hub_account_page.mjs +394 -0
- package/tests/idea_detail_page.mjs +64 -0
- package/tests/idea_goal_ratification.mjs +64 -0
- package/tests/idea_spark_hall.mjs +60 -9
- package/tests/ideator_full_idea_shapes_space_proof.mjs +1 -1
- package/tests/landing_page.mjs +6 -3
- package/tests/module-scope-map.mjs +31 -8
- package/tests/module_api.mjs +1 -0
- package/tests/module_loader.mjs +1 -1
- package/tests/nav_permission_atoms.mjs +1 -1
- package/tests/no_phantom_mirror_workflow.mjs +24 -0
- package/tests/npm_release_preview_commands.mjs +89 -0
- package/tests/npm_release_preview_divert.mjs +164 -0
- package/tests/npm_release_preview_env.mjs +100 -0
- package/tests/npm_release_preview_proxy.mjs +162 -0
- package/tests/npm_release_preview_routes.mjs +208 -0
- package/tests/npm_release_preview_supervisor.mjs +203 -0
- package/tests/pin_write_key.mjs +383 -0
- package/tests/planning_session_skill.mjs +27 -0
- package/tests/platform_boot.mjs +71 -0
- package/tests/profile_nudge_links.mjs +86 -0
- package/tests/profile_ui_cross_project.mjs +1 -1
- package/tests/projects_hub.mjs +2 -2
- package/tests/projects_hub_app_status.mjs +249 -0
- package/tests/projects_hub_app_step.mjs +282 -52
- package/tests/projects_hub_render_connect.mjs +295 -0
- package/tests/provision_render.mjs +362 -0
- package/tests/provision_settings_apply.mjs +62 -0
- package/tests/provisioning_app_status.mjs +204 -0
- package/tests/provisioning_render_route.mjs +336 -0
- package/tests/provisioning_settings_apply.mjs +1 -1
- package/tests/provisioning_teardown_intent.mjs +2 -2
- package/tests/render_api.mjs +153 -0
- package/tests/render_check.mjs +2 -1
- package/tests/render_deploy.mjs +495 -0
- package/tests/ship_preflight.mjs +61 -8
- package/tests/smoke_dependencies_witness.mjs +46 -0
- package/tests/speciality_skills.mjs +214 -0
- package/tests/studio_room_height.mjs +70 -0
- package/tests/task_detail_includes.mjs +10 -0
- package/tests/task_visuals_instance_root.mjs +150 -0
- package/tests/tweak_batch_apply.mjs +21 -0
- package/tests/ui_design_kit.mjs +36 -4
- package/tests/upgrade.mjs +1 -1
- package/tests/upgrade_persist_pin.mjs +24 -0
package/scripts/gds/status.js
CHANGED
|
@@ -167,6 +167,14 @@ function shortTitle(title, max = 52) {
|
|
|
167
167
|
return t.length > max ? t.slice(0, max - 1) + '…' : t;
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
// "area 6" for a working-area goal, else nothing (task 1004057). Read from the
|
|
171
|
+
// goal's working_area FIELD, never parsed from its title and never taken from
|
|
172
|
+
// sort_order or the id, which both look like it and are not.
|
|
173
|
+
function areaTag(g) {
|
|
174
|
+
const n = g && g.working_area;
|
|
175
|
+
return Number.isInteger(n) && n > 0 ? 'area ' + n : '';
|
|
176
|
+
}
|
|
177
|
+
|
|
170
178
|
// "1 confirmed, 2 backlog" — non-shipped tasks grouped by status, ordered.
|
|
171
179
|
function remainingSummary(remaining) {
|
|
172
180
|
if (remaining.length === 0) return 'none — all shipped ✓';
|
|
@@ -283,7 +291,7 @@ function renderStatusWidget(data, criterion) {
|
|
|
283
291
|
const rows = goals
|
|
284
292
|
? goals.map((g) => {
|
|
285
293
|
const ach = g.status === 'achieved';
|
|
286
|
-
const head = `<tr><td class="st-td" colspan="5" style="background:var(--color-background-secondary);font-weight:500;"><i class="ti ti-target" aria-hidden="true"></i> ${sesc(g.title)} <span class="st-sub">· ${ach ? '<span class="st-ok">achieved</span>' : sesc(g.status)}${g.criteria.length ? '' : ' · no criteria yet'}</span></td></tr>`;
|
|
294
|
+
const head = `<tr><td class="st-td" colspan="5" style="background:var(--color-background-secondary);font-weight:500;"><i class="ti ti-target" aria-hidden="true"></i> ${sesc(g.title)} <span class="st-sub">· ${areaTag(g) ? sesc(areaTag(g)) + ' · ' : ''}${ach ? '<span class="st-ok">achieved</span>' : sesc(g.status)}${g.criteria.length ? '' : ' · no criteria yet'}</span></td></tr>`;
|
|
287
295
|
return head + g.criteria.map(critRow).join('');
|
|
288
296
|
}).join('')
|
|
289
297
|
: data.criteria.map(critRow).join('');
|
|
@@ -336,7 +344,7 @@ function renderStatusMarkdown(data, criterion) {
|
|
|
336
344
|
const tableHeader = () => { lines.push('| | Criterion | Pos | Shipped | Remaining |'); lines.push('|---|---|---|---|---|'); };
|
|
337
345
|
if (goals) {
|
|
338
346
|
for (const g of goals) {
|
|
339
|
-
lines.push(`**🎯 ${g.title}** — ${g.status === 'achieved' ? 'achieved ✓' : g.status}${g.criteria.length ? '' : ' · no criteria yet'}`);
|
|
347
|
+
lines.push(`**🎯 ${g.title}** — ${areaTag(g) ? areaTag(g) + ' · ' : ''}${g.status === 'achieved' ? 'achieved ✓' : g.status}${g.criteria.length ? '' : ' · no criteria yet'}`);
|
|
340
348
|
if (g.criteria.length) { tableHeader(); for (const c of g.criteria) lines.push(critRow(c)); }
|
|
341
349
|
lines.push('');
|
|
342
350
|
}
|
|
@@ -383,7 +391,7 @@ function printAll(data) {
|
|
|
383
391
|
};
|
|
384
392
|
if (goals) {
|
|
385
393
|
for (const g of goals) {
|
|
386
|
-
console.log(` 🎯 ${g.title} [${g.status === 'achieved' ? 'achieved ✓' : g.status}]${g.criteria.length ? '' : ' (no criteria yet)'}`);
|
|
394
|
+
console.log(` 🎯 ${g.title} [${areaTag(g) ? areaTag(g) + ' · ' : ''}${g.status === 'achieved' ? 'achieved ✓' : g.status}]${g.criteria.length ? '' : ' (no criteria yet)'}`);
|
|
387
395
|
for (const c of g.criteria) printCrit(c);
|
|
388
396
|
if (g.criteria.length) console.log('');
|
|
389
397
|
}
|
|
@@ -523,4 +531,4 @@ if (require.main === module) {
|
|
|
523
531
|
});
|
|
524
532
|
}
|
|
525
533
|
|
|
526
|
-
module.exports = { parseArgs, resolveVersion, defaultedVersionNote, positionalRefNote, renderStatusWidget, renderStatusMarkdown };
|
|
534
|
+
module.exports = { parseArgs, resolveVersion, defaultedVersionNote, positionalRefNote, renderStatusWidget, renderStatusMarkdown, areaTag };
|
package/scripts/gds/upgrade.js
CHANGED
|
@@ -127,9 +127,9 @@ function restorePin(instanceDir, depString, fsImpl = fs) {
|
|
|
127
127
|
|
|
128
128
|
// ---- steps (each injectable for tests) --------------------------------------
|
|
129
129
|
|
|
130
|
-
// `ignore` is waived (it comes from — and is argued for in — claude-materialize.js materializeWillRewrite, task 1004122); every OTHER dirty path still halts the bump.
|
|
130
|
+
// `ignore` is waived (it comes from — and is argued for in — claude-materialize.js materializeWillRewrite, task 1004122); every OTHER dirty path still halts the bump. -uall (task 1004188): a bare status collapses a NEW untracked dir to one '?? dir/' entry the file-path waiver never matches.
|
|
131
131
|
function gitTreeClean(instanceDir, run = spawnSync, ignore = []) {
|
|
132
|
-
const r = run('git', ['status', '--porcelain'], { cwd: instanceDir, encoding: 'utf8' });
|
|
132
|
+
const r = run('git', ['status', '--porcelain', '-uall'], { cwd: instanceDir, encoding: 'utf8' });
|
|
133
133
|
if (!r || r.error || r.status !== 0) return { ok: false, reason: 'git status failed (not a git repo?)' };
|
|
134
134
|
const skip = new Set(ignore), all = (r.stdout || '').split('\n').filter((l) => l.trim());
|
|
135
135
|
const kept = all.filter((l) => !skip.has(l.slice(3).trim()));
|
|
@@ -22,7 +22,13 @@
|
|
|
22
22
|
//
|
|
23
23
|
// The 6-month delay alone does NOT scrub secrets/topology — redaction is mandatory and
|
|
24
24
|
// the gate is the backstop. Reuses the status-mirror-sync transport shape (--repo-dir /
|
|
25
|
-
// --push + a scoped deploy key
|
|
25
|
+
// --push + a scoped deploy key).
|
|
26
|
+
//
|
|
27
|
+
// PUBLISHING IS A MANUAL STEP. No workflow or schedule runs this script (there is
|
|
28
|
+
// no sync-public-mirror workflow) and nothing publishes the public mirror automatically.
|
|
29
|
+
// A person runs it by hand, e.g.
|
|
30
|
+
// node scripts/public-mirror-export.js --repo-dir <mirror-checkout> --push
|
|
31
|
+
// (a scheduled public release channel needs the owner's approval first).
|
|
26
32
|
//
|
|
27
33
|
// USAGE:
|
|
28
34
|
// node scripts/public-mirror-export.js --repo-dir <mirror-checkout> [--push]
|
|
@@ -73,6 +73,16 @@ const MODULE_GLOBS = {
|
|
|
73
73
|
// protected - ADR 0314 in a second costume. Ownership follows what the
|
|
74
74
|
// file IS, and what it is is the core read facade over ten modules.
|
|
75
75
|
'src/bongos/routes/me.js',
|
|
76
|
+
// The rest of the core route directory (task 1004116). It is a BLANKET
|
|
77
|
+
// protected glob, so every file in it is walled, but until now only the two
|
|
78
|
+
// entries above were authorable: sixteen live routes (auth, public,
|
|
79
|
+
// core-update, builders, ...) sat in no module's territory and no goal could
|
|
80
|
+
// author a task on them. They are the core HTTP surface that stayed in core
|
|
81
|
+
// while domain routes carved into modules/<key>/routes/, so they are kernel's.
|
|
82
|
+
// Kernel was already protected, so this changes no module's protected status;
|
|
83
|
+
// the me.js overlap the task feared is gone (task 1004110 moved it here too).
|
|
84
|
+
// The two explicit entries above stay for their reasons.
|
|
85
|
+
'src/bongos/routes/',
|
|
76
86
|
'server.js', 'config/',
|
|
77
87
|
|
|
78
88
|
// ADOPTED (task 1004111): a protected path owned by no module is authorable
|
|
@@ -340,6 +350,9 @@ const MODULE_GLOBS = {
|
|
|
340
350
|
// (tests/npm_release_*.mjs) because a default-off module's own tests/ is skipped
|
|
341
351
|
// by the unit runner. The /deploy page shell stays hall-ui's (one key per surface).
|
|
342
352
|
'npm-release': ['modules/npm-release/', 'tests/npm_release_*.mjs'],
|
|
353
|
+
// --- the render-deploy module (task 1004270): a Render project's deploy page. Tests live at
|
|
354
|
+
// the repo root (tests/render_deploy_*.mjs) for the same reason as npm-release above.
|
|
355
|
+
'render-deploy': ['modules/render-deploy/', 'tests/render_deploy_*.mjs'],
|
|
343
356
|
// --- instance provisioning: the web-tier REQUEST + STATE surface (module home)
|
|
344
357
|
// plus the control-plane runner CLI — a command travels with its domain (the
|
|
345
358
|
// lifecycle precedent). Covers the projects-surface work (goal
|
|
@@ -986,7 +986,9 @@ function mountInternalSurfaces(app) {
|
|
|
986
986
|
if (req.method !== 'GET' && req.method !== 'HEAD') return next();
|
|
987
987
|
if (!DEPLOY_PAGE_RE.test(path.posix.normalize(req.path))) return next();
|
|
988
988
|
let door = false;
|
|
989
|
-
|
|
989
|
+
// The page also exists where the render-deploy module is on: a Render project's own deploy
|
|
990
|
+
// page (task 1004270). Neither module on and the page still answers with Settings.
|
|
991
|
+
try { door = isModuleEnabled('provisioning') || isModuleEnabled('render-deploy'); } catch { door = false; }
|
|
990
992
|
if (door) return next();
|
|
991
993
|
return res.redirect(302, `${isBuildersHost(req) ? '' : '/builders'}/settings#software-update`);
|
|
992
994
|
});
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.
|
|
78
|
+
const CORE_VERSION = '1.20.1'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -255,6 +255,15 @@ module.exports = {
|
|
|
255
255
|
// second protected list — so a module reaches it via the doorway, not a deep
|
|
256
256
|
// core require (which the module-boundary fitness check would red). PERMANENT.
|
|
257
257
|
get goalScopeCheck() { return require('./bongos/goal-scope-check'); },
|
|
258
|
+
// cascadeDispatch — the cheapest-model-first cascade router (src/bongos/
|
|
259
|
+
// cascade-dispatch.js, task 1001691 / ADR 0101 Part 1). KERNEL (it composes
|
|
260
|
+
// no domain state — a pure ladder + an injected runner), grandfathered
|
|
261
|
+
// dormant core since System 3 (grading/CLAUDE.md's own note: "stays in
|
|
262
|
+
// src/gds/... nothing calls it but its mock unit test"). The lifecycle
|
|
263
|
+
// module's relevance/anti-hack judge (goal-task-relevance-judge.js) is its
|
|
264
|
+
// FIRST real caller, injecting grade.runSubagent (resolved via the `grade`
|
|
265
|
+
// port, never required directly) as the runner — never a deep core require.
|
|
266
|
+
get cascadeDispatch() { return require('./bongos/cascade-dispatch'); },
|
|
258
267
|
|
|
259
268
|
// === DOMAIN capabilities still on the doorway — SCHEDULED TO RELOCATE ======
|
|
260
269
|
// NOT kernel: these leak a domain's queries through the doorway (ADR 0091
|
package/src/platform-server.js
CHANGED
|
@@ -57,6 +57,7 @@ const { isModuleEnabled } = require('./modules');
|
|
|
57
57
|
const { refreshOverrides } = require('./bongos/module-overrides');
|
|
58
58
|
const { schemaPending } = require('./bongos/schema-lag');
|
|
59
59
|
const { logger, requestIdMiddleware } = require('./bongos/logger');
|
|
60
|
+
const seams = require('./module-seams');
|
|
60
61
|
|
|
61
62
|
const PORT = Number(process.env.PORT) || 3000;
|
|
62
63
|
const HOST = process.env.HOST || '127.0.0.1';
|
|
@@ -199,6 +200,32 @@ function buildPlatformApp() {
|
|
|
199
200
|
// same reqId. Mounted before any surface so nothing logs uncorrelated.
|
|
200
201
|
app.use(requestIdMiddleware);
|
|
201
202
|
|
|
203
|
+
// THE request.divert SEAM (task 1004300, ADR 0349). A module may register a provider that
|
|
204
|
+
// takes a request BEFORE any surface answers and either handles it or passes it on —
|
|
205
|
+
// today the npm-release module's version preview, which hands a browser holding its
|
|
206
|
+
// cookie to a second process. The provider is looked up on EVERY request, not once at
|
|
207
|
+
// build: modules mount inside mountInternalSurfaces below, after this line runs, so a
|
|
208
|
+
// lookup here at build time would always find nothing.
|
|
209
|
+
//
|
|
210
|
+
// WITH NO PROVIDER THIS IS ONE MAP LOOKUP AND next(): an instance that does not have such
|
|
211
|
+
// a module is served exactly as it was. A provider that throws is treated as absent for
|
|
212
|
+
// that request, never as a failed request — a broken preview must not take the live hall
|
|
213
|
+
// down with it — unless it had already started answering.
|
|
214
|
+
app.use((req, res, next) => {
|
|
215
|
+
const divert = seams.resolveOptional('request.divert');
|
|
216
|
+
if (!divert) return next();
|
|
217
|
+
let passed = false;
|
|
218
|
+
const pass = (e) => { passed = true; next(e); };
|
|
219
|
+
Promise.resolve()
|
|
220
|
+
.then(() => divert(req, res, pass))
|
|
221
|
+
.catch((err) => {
|
|
222
|
+
logger.error({ err }, 'request.divert provider failed — serving the request normally');
|
|
223
|
+
if (passed) return;
|
|
224
|
+
if (res.headersSent) { res.destroy(); return; }
|
|
225
|
+
pass();
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
202
229
|
// The whole internal surface — shared verbatim with server.js (ADR 0062 R51).
|
|
203
230
|
mountInternalSurfaces(app);
|
|
204
231
|
|
|
@@ -266,6 +293,18 @@ function buildPlatformApp() {
|
|
|
266
293
|
res.type('html').send(html);
|
|
267
294
|
});
|
|
268
295
|
}
|
|
296
|
+
// THE ACCOUNT PAGE (task 1004259, ADR 0337 D4.3): where a person edits what
|
|
297
|
+
// belongs to their platform account rather than to one project — handle, name,
|
|
298
|
+
// bio, links, and who can find them. Hub-only, unlike the doors above: every
|
|
299
|
+
// read and write it makes is a platform-identity route, and that module IS the
|
|
300
|
+
// hub flag (the stripApexOnly note above), so a child instance serves no such
|
|
301
|
+
// page rather than one whose every request 404s.
|
|
302
|
+
app.get('/account', (req, res, next) => {
|
|
303
|
+
if (!isApexHost(req) || !isModuleEnabled('platform-identity')) return next();
|
|
304
|
+
const html = landingPage('account.html');
|
|
305
|
+
if (!html) return next();
|
|
306
|
+
res.type('html').send(html);
|
|
307
|
+
});
|
|
269
308
|
// Assets fall through to the 404 tail when no module claims the apex root — the
|
|
270
309
|
// handler is not mounted at all rather than pointed at a missing directory.
|
|
271
310
|
const landingAssets = landingDir() && express.static(landingDir(), { index: false, fallthrough: true });
|
|
@@ -335,9 +335,13 @@ const GRANT_WORDS = {
|
|
|
335
335
|
active_recently: /active in the last 30 days/,
|
|
336
336
|
};
|
|
337
337
|
|
|
338
|
+
// The toggles live on the hub's account page since task 1004259 (ADR 0337 D4.3) —
|
|
339
|
+
// one inline-script page, so the copy and the markup are read from the same file.
|
|
340
|
+
const ACCOUNT_PAGE = join(ROOT, 'modules/public-landing/public/account.html');
|
|
341
|
+
|
|
338
342
|
test('the discoverable toggle names EXACTLY what it grants — checked against SLIVER_KEYS itself', () => {
|
|
339
|
-
const js = readFileSync(
|
|
340
|
-
const html =
|
|
343
|
+
const js = readFileSync(ACCOUNT_PAGE, 'utf8');
|
|
344
|
+
const html = js;
|
|
341
345
|
// Privacy spec D3 makes this a MUST: "The discoverable toggle's settings copy
|
|
342
346
|
// must state exactly this grant." It was checked against three transcribed
|
|
343
347
|
// phrases, and the transcription had gone stale in the direction that matters:
|
|
@@ -363,13 +367,13 @@ test('the discoverable toggle names EXACTLY what it grants — checked against S
|
|
|
363
367
|
'and the copy says what it does NOT grant');
|
|
364
368
|
assert.match(js, /the one thing a recruiter can see while your profile page is private/,
|
|
365
369
|
'a private account is told the sliver is the exception to "not found to every visitor" — the surprising half of D2/D3');
|
|
366
|
-
assert.match(html, /<
|
|
367
|
-
'the
|
|
370
|
+
assert.match(html, /<div id="acctBody" hidden>/,
|
|
371
|
+
'the controls ship hidden — they reveal only once the hub module answers for this account');
|
|
368
372
|
});
|
|
369
373
|
|
|
370
374
|
test('each toggle claims exactly what is enforced — and BOTH are now in force', () => {
|
|
371
|
-
const js = readFileSync(
|
|
372
|
-
const html =
|
|
375
|
+
const js = readFileSync(ACCOUNT_PAGE, 'utf8');
|
|
376
|
+
const html = js;
|
|
373
377
|
// A toggle may promise only what a read path actually enforces. The two rows
|
|
374
378
|
// reached that point one task apart. Task 1002967 wired VISIBILITY into every
|
|
375
379
|
// public read, so its row moved to the present tense — and it must also say
|
|
@@ -113,8 +113,10 @@ t('FAIL-CLOSED: a throwing matcher is a hit, and only an explicit false clears a
|
|
|
113
113
|
|
|
114
114
|
t('this wall is STRICTER than the validator, which is the point', () => {
|
|
115
115
|
// An Archon clears validate.js's Metic+ rank floor on a protected scope...
|
|
116
|
+
// Through mergeForValidation, as the PATCH route does: the validator refuses
|
|
117
|
+
// a stored row's server-owned columns as unknown fields (task 1004007).
|
|
116
118
|
const verdict = validate.validateAgentDefinition(
|
|
117
|
-
|
|
119
|
+
authoring.mergeForValidation(DB_ROW, { scope_modules: ['government'] }),
|
|
118
120
|
{ authorRank: 'archon', allowedModules: ['government'], protectedModules: PROTECTED_MODULES, isProtectedPath: () => false },
|
|
119
121
|
);
|
|
120
122
|
assert.equal(verdict.ok, true, 'the rank floor admits an Archon');
|
|
@@ -126,6 +128,18 @@ t('this wall is STRICTER than the validator, which is the point', () => {
|
|
|
126
128
|
);
|
|
127
129
|
});
|
|
128
130
|
|
|
131
|
+
t('the fields the routes let through are exactly the fields the validator accepts (task 1004007)', () => {
|
|
132
|
+
// The validator refuses any key outside its list; the routes project onto
|
|
133
|
+
// AUTHORABLE_FIELDS. If the two drift, either an authorable field is refused
|
|
134
|
+
// on every write, or the validator's list names something no route can set.
|
|
135
|
+
const v = validate.validateAgentDefinition(
|
|
136
|
+
authoring.mergeForValidation(DB_ROW, {}),
|
|
137
|
+
{ authorRank: 'metic', allowedModules: [], protectedModules: [] },
|
|
138
|
+
);
|
|
139
|
+
assert.equal(v.ok, true, JSON.stringify(v.errors));
|
|
140
|
+
assert.deepEqual([...Object.keys(v.value)].sort(), [...authoring.AUTHORABLE_FIELDS].sort());
|
|
141
|
+
});
|
|
142
|
+
|
|
129
143
|
console.log('\nrule 4 — the sync owns its own rows:');
|
|
130
144
|
|
|
131
145
|
t('a file-sourced definition is not editable here, and the reason names the file', () => {
|
package/tests/agents_sync.mjs
CHANGED
|
@@ -140,12 +140,22 @@ t('a file claiming author_rank cannot promote itself', () => {
|
|
|
140
140
|
const p = sync.planOne(claiming, {
|
|
141
141
|
authorRank: 'xenos', allowedModules: ['government'], protectedModules: ['government'],
|
|
142
142
|
});
|
|
143
|
-
|
|
143
|
+
// Since task 1004007 the file is not even imported: author_rank is not a field
|
|
144
|
+
// of a definition, so the validator refuses it BY NAME rather than dropping it.
|
|
145
|
+
// That is a stronger form of this rule than the flagged import it used to be.
|
|
146
|
+
assert.equal(p.action, 'skip', 'a file claiming author_rank is refused, not imported');
|
|
147
|
+
assert.match(p.reason, /author_rank/);
|
|
144
148
|
// NULL, not 'xenos': this definition reaches a protected module, so no
|
|
145
|
-
// git-derived rank is stamped at all (stampableAuthorRank).
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
+
// git-derived rank is stamped at all (stampableAuthorRank).
|
|
150
|
+
assert.equal(p.authorRank ?? null, null, 'nothing git-derived is stamped on a protected-scope row');
|
|
151
|
+
// And without the claim, the same scope still imports only flagged: the
|
|
152
|
+
// protected wall is what holds, not the refusal above.
|
|
153
|
+
const plain = sync.planOne(
|
|
154
|
+
sync.parseAgentFile('---\nname: x\ntrigger: on-demand\nscope_modules: [government]\n---\nbody', {}),
|
|
155
|
+
{ authorRank: 'xenos', allowedModules: ['government'], protectedModules: ['government'] },
|
|
156
|
+
);
|
|
157
|
+
assert.equal(plain.action, 'flagged');
|
|
158
|
+
assert.match(plain.scopeViolation, /metic\+ author/i);
|
|
149
159
|
});
|
|
150
160
|
|
|
151
161
|
t('THE ESCALATION PATH: a spoofed git identity cannot arm a protected scope', () => {
|
|
@@ -447,5 +457,96 @@ t('migrate.sh invokes agents-sync, module-gated and fail-open', () => {
|
|
|
447
457
|
assert.doesNotMatch(call.slice(0, 200), /exit 1/, 'no exit 1 on the agents-sync path');
|
|
448
458
|
});
|
|
449
459
|
|
|
460
|
+
// ---- task 1004007: a file-declared filter and sink SURVIVE the round trip ----
|
|
461
|
+
//
|
|
462
|
+
// Tested at the round-trip altitude on purpose: file text in, the VALIDATOR's
|
|
463
|
+
// normalized row out, and then the event router's own reading of that row. A
|
|
464
|
+
// test over parseAgentFile alone stayed green through the whole bug, because
|
|
465
|
+
// the parser kept `touches` — it was the validator that dropped it.
|
|
466
|
+
{
|
|
467
|
+
const router = require('../modules/agents/pollers/event-router.js');
|
|
468
|
+
const compile = (globs) => (file) => globs.some((g) => file.startsWith(g.replace(/\*\*.*$/, '')));
|
|
469
|
+
const roundTrip = (text, name = 'review-lens') => validate.validateAgentDefinition(
|
|
470
|
+
sync.parseAgentFile(text, { name }),
|
|
471
|
+
{ authorRank: 'archon', allowedModules: [], protectedModules: [], isProtectedPath: () => false },
|
|
472
|
+
);
|
|
473
|
+
const LINES = [
|
|
474
|
+
'---', 'title: Review lens', 'trigger: event', 'event: task.shipped',
|
|
475
|
+
'touches:', ' - docs/**', ' - modules/hall-ui/**', 'sink: discord', '---',
|
|
476
|
+
'You review what shipped.', '',
|
|
477
|
+
];
|
|
478
|
+
const FILE = LINES.join('\n');
|
|
479
|
+
const without = (...drop) => LINES.filter((l) => !drop.includes(l)).join('\n');
|
|
480
|
+
|
|
481
|
+
t('a file-declared touches[] filter reaches trigger_spec and the router reads it (task 1004007)', () => {
|
|
482
|
+
const v = roundTrip(FILE);
|
|
483
|
+
assert.equal(v.ok, true, JSON.stringify(v.errors));
|
|
484
|
+
assert.deepEqual(v.value.trigger_spec.touches, ['docs/**', 'modules/hall-ui/**']);
|
|
485
|
+
assert.deepEqual(router.declaredGlobs(v.value), ['docs/**', 'modules/hall-ui/**']);
|
|
486
|
+
// The point: a ship that touched nothing in scope must NOT fire.
|
|
487
|
+
assert.equal(router.matchesTouches(v.value, ['src/bongos/db.js'], compile).match, false);
|
|
488
|
+
assert.equal(router.matchesTouches(v.value, ['docs/voice.md'], compile).match, true);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
t('a file-declared sink reaches trigger_spec and the router routes to it (task 1004007)', () => {
|
|
492
|
+
const v = roundTrip(FILE);
|
|
493
|
+
assert.equal(v.value.trigger_spec.sink, 'discord');
|
|
494
|
+
assert.equal(router.sinkNameFor(v.value), 'discord');
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
t('touches_globs is the same filter as touches (the router reads both spellings)', () => {
|
|
498
|
+
const v = roundTrip(FILE.replace('touches:', 'touches_globs:'));
|
|
499
|
+
assert.equal(v.ok, true, JSON.stringify(v.errors));
|
|
500
|
+
assert.deepEqual(router.declaredGlobs(v.value), ['docs/**', 'modules/hall-ui/**']);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
t('a file-declared cadence reaches trigger_spec, so a cron agent can be written as a file', () => {
|
|
504
|
+
const v = roundTrip(['---', 'trigger: cron', 'cadence: daily', '---', 'A digest.'].join('\n'), 'digest');
|
|
505
|
+
assert.equal(v.ok, true, JSON.stringify(v.errors));
|
|
506
|
+
assert.equal(v.value.trigger_spec.cadence, 'daily');
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
t('a definition with no filter still means every ship: the default is unchanged', () => {
|
|
510
|
+
const v = roundTrip(without('touches:', ' - docs/**', ' - modules/hall-ui/**'));
|
|
511
|
+
assert.equal(v.ok, true, JSON.stringify(v.errors));
|
|
512
|
+
assert.equal(v.value.trigger_spec.touches, undefined);
|
|
513
|
+
assert.equal(router.matchesTouches(v.value, ['src/bongos/db.js'], compile).reason, 'no_filter_declared');
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
t('a malformed filter is REFUSED, never widened silently to fire on every ship', () => {
|
|
517
|
+
// A scalar is what "touches: docs/**" parses to. The router treats a non-list
|
|
518
|
+
// as ABSENT, i.e. every ship, so it has to be caught at authoring time.
|
|
519
|
+
const text = without(' - docs/**', ' - modules/hall-ui/**').replace('touches:', 'touches: docs/**');
|
|
520
|
+
const v = roundTrip(text);
|
|
521
|
+
assert.equal(v.ok, false);
|
|
522
|
+
assert.ok(v.errors.some((e) => e.code === 'trigger_touches_invalid'), JSON.stringify(v.errors));
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
t('an unknown top-level key is refused, not dropped: a typo is an error, not a default', () => {
|
|
526
|
+
const v = roundTrip(FILE.replace('sink: discord', 'sink: discord\ntouchess: [docs/**]'));
|
|
527
|
+
assert.equal(v.ok, false);
|
|
528
|
+
const e = v.errors.find((x) => x.code === 'unknown_field');
|
|
529
|
+
assert.ok(e, JSON.stringify(v.errors));
|
|
530
|
+
assert.match(e.message, /touchess/);
|
|
531
|
+
// and the exact probe from the task: a top-level touches on a row handed to
|
|
532
|
+
// the validator directly no longer returns ok with the filter gone.
|
|
533
|
+
const direct = validate.validateAgentDefinition(
|
|
534
|
+
{ name: 'x', persona: 'p', trigger_type: 'event', trigger_spec: { event: 'task.shipped' }, touches: ['src/**'] },
|
|
535
|
+
{ authorRank: 'archon', allowedModules: [], protectedModules: [] },
|
|
536
|
+
);
|
|
537
|
+
assert.equal(direct.ok, false, 'a touches that lands nowhere must not pass silently');
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
t('every agent file this repo ships still validates under the stricter check', () => {
|
|
541
|
+
const dir = path.join(process.cwd(), '.claude', 'agents');
|
|
542
|
+
const files = fs.readdirSync(dir).filter((x) => x.endsWith('.md'));
|
|
543
|
+
assert.ok(files.length >= 3, 'expected the shipped agent files');
|
|
544
|
+
for (const x of files) {
|
|
545
|
+
const v = roundTrip(fs.readFileSync(path.join(dir, x), 'utf8'), x.replace(/\.md$/, ''));
|
|
546
|
+
assert.equal(v.ok, true, x + ': ' + JSON.stringify(v.errors));
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
|
|
450
551
|
console.log(`\nagents_sync: ${passed} passed, ${failed} failed`);
|
|
451
552
|
process.exit(failed ? 1 : 0);
|
|
@@ -477,6 +477,51 @@ await test('a hostile getter cannot escape the validator', () => {
|
|
|
477
477
|
assert.equal(r.ok, false);
|
|
478
478
|
});
|
|
479
479
|
|
|
480
|
+
await test('an unknown top-level key is refused by name, not dropped (task 1004007)', () => {
|
|
481
|
+
// The validator's normalized row names a fixed set of fields, so an extra key
|
|
482
|
+
// used to pass with ok:true and vanish, which is how a file's top-level
|
|
483
|
+
// touches: filter became "fires on every ship".
|
|
484
|
+
const r = validateAgentDefinition(
|
|
485
|
+
{ name: 'historian', persona: 'x', trigger_type: 'event', trigger_spec: { event: 'task.shipped' }, touches: ['src/**'] },
|
|
486
|
+
OPTS,
|
|
487
|
+
);
|
|
488
|
+
assert.equal(r.ok, false);
|
|
489
|
+
const e = r.errors.find((x) => x.code === 'unknown_field');
|
|
490
|
+
assert.ok(e, JSON.stringify(codes(r)));
|
|
491
|
+
assert.equal(e.field, 'touches');
|
|
492
|
+
assert.equal(r.value, null);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
await test('a malformed touches filter is refused, because the router reads it as "every ship" (task 1004007)', () => {
|
|
496
|
+
for (const bad of ['docs/**', [], [''], [3], { a: 1 }]) {
|
|
497
|
+
const r = validateAgentDefinition(
|
|
498
|
+
{ name: 'historian', persona: 'x', trigger_type: 'event', trigger_spec: { event: 'task.shipped', touches: bad } },
|
|
499
|
+
OPTS,
|
|
500
|
+
);
|
|
501
|
+
assert.equal(r.ok, false, JSON.stringify(bad));
|
|
502
|
+
assert.ok(codes(r).includes('trigger_touches_invalid'), JSON.stringify(bad) + ' ' + JSON.stringify(codes(r)));
|
|
503
|
+
}
|
|
504
|
+
const good = validateAgentDefinition(
|
|
505
|
+
{ name: 'historian', persona: 'x', trigger_type: 'event', trigger_spec: { event: 'task.shipped', touches_globs: ['docs/**'] } },
|
|
506
|
+
OPTS,
|
|
507
|
+
);
|
|
508
|
+
assert.equal(good.ok, true, JSON.stringify(codes(good)));
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
await test('a Proxy whose ownKeys trap throws cannot escape the validator (task 1004007)', () => {
|
|
512
|
+
// The unknown-field check is the one place the validator enumerates the
|
|
513
|
+
// definition, so it is the one place a throwing ownKeys trap could escape.
|
|
514
|
+
const hostile = new Proxy(
|
|
515
|
+
{ name: 'historian', persona: 'x', trigger_type: 'on-demand' },
|
|
516
|
+
{ ownKeys() { throw new Error('boom'); } },
|
|
517
|
+
);
|
|
518
|
+
let r;
|
|
519
|
+
assert.doesNotThrow(() => { r = validateAgentDefinition(hostile, OPTS); });
|
|
520
|
+
assert.equal(r.ok, false);
|
|
521
|
+
assert.ok(codes(r).includes('keys_unreadable'), JSON.stringify(codes(r)));
|
|
522
|
+
assert.equal(r.value, null);
|
|
523
|
+
});
|
|
524
|
+
|
|
480
525
|
await test('EVERY error carries a code, a field and a human message', () => {
|
|
481
526
|
const r = validateAgentDefinition(
|
|
482
527
|
{ name: 'BAD NAME', trigger_type: 'webhook', trigger_spec: 'nope', scope_modules: ['government'] },
|
package/tests/api_path_404.mjs
CHANGED
|
@@ -172,5 +172,11 @@ t('rebaseRequiredHint: still leads with /strand-fix — merge is the manual fall
|
|
|
172
172
|
'the command that works on any machine must still come first (task 1003768)');
|
|
173
173
|
});
|
|
174
174
|
|
|
175
|
+
// task 1004370: when main's CI is red for a reason outside every branch, the gated
|
|
176
|
+
// builder must learn from the refusal itself that the fix is still claimable.
|
|
177
|
+
t('rebaseRequiredHint: names the CI-unblock exit the gate lets through', () => {
|
|
178
|
+
assert.match(rebaseRequiredHint([{ id: 7 }]), /P1 kind=infra task passes this gate/);
|
|
179
|
+
});
|
|
180
|
+
|
|
175
181
|
console.log(`\napi_path_404: ${passed} passed, ${failed} failed`);
|
|
176
182
|
process.exit(failed ? 1 : 0);
|
|
@@ -270,6 +270,13 @@ function loopHarness({ rows, maxLoops = 6 }) {
|
|
|
270
270
|
// network call is not a unit suite, it is a flake waiting for a slow day.
|
|
271
271
|
writeHeartbeat: () => true,
|
|
272
272
|
postHeartbeat: async () => true,
|
|
273
|
+
// These tests drive the BACKOFF LADDER, not the upgrade path (task 1004374).
|
|
274
|
+
// forever() checks for code drift once a loop, and the generic run() stub
|
|
275
|
+
// above answers rev-parse and ls-remote with different output — which reads
|
|
276
|
+
// as "main moved" and exits the loop before it can ever reach probing.
|
|
277
|
+
// Declaring no drift keeps each test about the one thing it is testing;
|
|
278
|
+
// tests/autobongos_loop.mjs covers the drift exit itself.
|
|
279
|
+
codeDrifted: async () => false,
|
|
273
280
|
};
|
|
274
281
|
return { deps, waits: emitted, recorded, opts: { goals: [], maxTasks: 3, maxLoops } };
|
|
275
282
|
}
|
|
@@ -664,7 +664,7 @@ test('workerEnv passes Claude auth through, and still refuses the rest', () => {
|
|
|
664
664
|
// cap has to work, and it has to survive a restart: the count comes from the
|
|
665
665
|
// run log, not from memory.
|
|
666
666
|
|
|
667
|
-
import { mkdtempSync, writeFileSync } from 'node:fs';
|
|
667
|
+
import { mkdtempSync, writeFileSync, readFileSync } from 'node:fs';
|
|
668
668
|
import { tmpdir } from 'node:os';
|
|
669
669
|
import { join } from 'node:path';
|
|
670
670
|
|
|
@@ -768,3 +768,138 @@ test('countWorkedSince stops at the first row older than the mark', () => {
|
|
|
768
768
|
assert.equal(runner.countWorkedSince(8000), 1);
|
|
769
769
|
delete process.env.AUTOBONGOS_LOG_FILE;
|
|
770
770
|
});
|
|
771
|
+
|
|
772
|
+
// --- a --forever runner must be able to take a fix (task 1004374) -----------
|
|
773
|
+
//
|
|
774
|
+
// autobongos-service.cmd refreshes the checkout only BEFORE launching node, and
|
|
775
|
+
// --forever never returns (task 1003904). Together: a process never re-reads its
|
|
776
|
+
// code, a shipped fix never reaches the runner, and nothing restarts it. On the
|
|
777
|
+
// host this ran week-old code for four days across three landed fixes while the
|
|
778
|
+
// heartbeat reported it alive and the hall showed a healthy runner.
|
|
779
|
+
|
|
780
|
+
test('codeDrifted reports a drift when main has moved past the loaded head', async () => {
|
|
781
|
+
const calls = [];
|
|
782
|
+
const run = async (bin, args) => {
|
|
783
|
+
calls.push(args.join(' '));
|
|
784
|
+
if (args[0] === 'rev-parse') return { ok: true, stdout: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n' };
|
|
785
|
+
return { ok: true, stdout: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\trefs/heads/main\n' };
|
|
786
|
+
};
|
|
787
|
+
const d = await runner.codeDrifted({ run });
|
|
788
|
+
assert.ok(d, 'a moved main is a drift');
|
|
789
|
+
assert.equal(d.from, 'aaaaaaaa');
|
|
790
|
+
assert.equal(d.to, 'bbbbbbbb');
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
test('codeDrifted is false when the loaded head IS the tip', async () => {
|
|
794
|
+
const loadedHead = async () => 'cccccccccccccccccccccccccccccccccccccccc';
|
|
795
|
+
const run = async (bin, args) => (args[0] === 'rev-parse'
|
|
796
|
+
? { ok: true, stdout: 'cccccccccccccccccccccccccccccccccccccccc\n' }
|
|
797
|
+
: { ok: true, stdout: 'cccccccccccccccccccccccccccccccccccccccc\trefs/heads/main\n' });
|
|
798
|
+
assert.equal(await runner.codeDrifted({ run, loadedHead }), false);
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
test('codeDrifted returns null when the LOCAL head cannot be read', async () => {
|
|
802
|
+
// Injected rather than stubbed through run(): loadedHead caches into a
|
|
803
|
+
// module-level, and an earlier test in this file already populated it — so a
|
|
804
|
+
// run() stub that fails rev-parse is never consulted, and the case passes for
|
|
805
|
+
// an unrelated reason. That is exactly what the first cut of this test did.
|
|
806
|
+
const okTip = async () => ({ ok: true, stdout: 'dddddddddddddddddddddddddddddddddddddddd refs/heads/main' });
|
|
807
|
+
assert.equal(await runner.codeDrifted({ loadedHead: async () => null, run: okTip }), null,
|
|
808
|
+
'no local head means we cannot tell, and cannot-tell must never cycle the runner');
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
test('codeDrifted returns null when ORIGIN cannot be reached', async () => {
|
|
812
|
+
assert.equal(await runner.codeDrifted({ loadedHead: async () => 'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', run: async () => ({ ok: false }) }), null);
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
test('codeDrifted returns null when the git call throws outright', async () => {
|
|
816
|
+
const thrown = async () => { throw new Error('no network'); };
|
|
817
|
+
assert.equal(await runner.codeDrifted({ loadedHead: async () => 'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', run: thrown }), null);
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
test('forever EXITS with the upgrade code on drift, and logs why', async () => {
|
|
821
|
+
const rows = [];
|
|
822
|
+
const code = await runner.forever({ goals: [], maxTasks: 1, maxLoops: 3 }, {
|
|
823
|
+
iteration: async () => ({ event: 'hold', reason: 'test' }),
|
|
824
|
+
reconcileLeftoverClaims: async () => ({ event: 'boot_reconcile', ok: true }),
|
|
825
|
+
waitOrJump: async () => ({ waited: 0, jumped: false }),
|
|
826
|
+
writeHeartbeat: () => true,
|
|
827
|
+
postHeartbeat: async () => true,
|
|
828
|
+
emit: () => {},
|
|
829
|
+
record: (r) => { rows.push(r); return r; },
|
|
830
|
+
now: () => Date.now() + runner.MIN_UPTIME_BEFORE_UPGRADE_MS + 1000,
|
|
831
|
+
codeDrifted: async () => ({ from: '1111aaaa', to: '2222bbbb' }),
|
|
832
|
+
});
|
|
833
|
+
assert.equal(code, runner.UPGRADE_EXIT_CODE, 'the wrapper loops on exactly this code');
|
|
834
|
+
const exit = rows.find((r) => r.event === 'upgrade_exit');
|
|
835
|
+
assert.ok(exit, 'a silent exit is the failure mode task 1003904 forbids — it must say why');
|
|
836
|
+
assert.match(exit.reason, /main moved from 1111aaaa to 2222bbbb/);
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
test('forever does NOT exit while the code is current', async () => {
|
|
840
|
+
const code = await runner.forever({ goals: [], maxTasks: 1, maxLoops: 2 }, {
|
|
841
|
+
iteration: async () => ({ event: 'hold', reason: 'test' }),
|
|
842
|
+
reconcileLeftoverClaims: async () => ({ event: 'boot_reconcile', ok: true }),
|
|
843
|
+
waitOrJump: async () => ({ waited: 0, jumped: false }),
|
|
844
|
+
writeHeartbeat: () => true,
|
|
845
|
+
postHeartbeat: async () => true,
|
|
846
|
+
emit: () => {},
|
|
847
|
+
record: (r) => r,
|
|
848
|
+
codeDrifted: async () => false,
|
|
849
|
+
});
|
|
850
|
+
assert.equal(code, 0, 'the no-terminal-stop rule still holds for every other case');
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
test('the wrapper loops on the upgrade code and only on that code', () => {
|
|
854
|
+
const raw = readFileSync(new URL('../scripts/gds/autobongos-service.cmd', import.meta.url), 'utf8');
|
|
855
|
+
// LIVE lines only, and no regex here on purpose. A first cut of this test
|
|
856
|
+
// matched the raw file text and passed happily against a commented-out
|
|
857
|
+
// "REM goto run" — which is precisely the regression it exists to catch.
|
|
858
|
+
const live = raw
|
|
859
|
+
.split('\n')
|
|
860
|
+
.map((l) => l.replace('\r', '').trim())
|
|
861
|
+
.filter((l) => l && !l.toUpperCase().startsWith('REM ') && !l.startsWith('::'));
|
|
862
|
+
const joined = live.join('\n');
|
|
863
|
+
assert.ok(live.includes(':run'), 'there must be a live label to go back to');
|
|
864
|
+
assert.ok(live.some((l) => l.startsWith('if "%EXITCODE%"=="75"')), 'and a live branch on the upgrade code');
|
|
865
|
+
assert.ok(live.includes('goto run'), 'and a live relaunch, not a commented-out one');
|
|
866
|
+
// Ordering: the relaunch must precede the final exit, or it can never run.
|
|
867
|
+
assert.ok(joined.indexOf('goto run') < joined.indexOf('exit /b %EXITCODE%'));
|
|
868
|
+
// And the refresh must sit INSIDE the loop, or a relaunch re-runs the old code,
|
|
869
|
+
// which is the entire bug.
|
|
870
|
+
assert.ok(joined.indexOf(':run') < joined.indexOf('git reset --quiet --hard origin/main'));
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
test('a runner that just started will NOT upgrade-exit, so a failed fetch cannot spin', async () => {
|
|
874
|
+
// The wrapper's reset is best-effort by design. Without this guard, starting
|
|
875
|
+
// behind means: see drift, exit, relaunch into the same state, forever.
|
|
876
|
+
let askedForDrift = false;
|
|
877
|
+
const code = await runner.forever({ goals: [], maxTasks: 1, maxLoops: 2 }, {
|
|
878
|
+
iteration: async () => ({ event: 'hold', reason: 'test' }),
|
|
879
|
+
reconcileLeftoverClaims: async () => ({ event: 'boot_reconcile', ok: true }),
|
|
880
|
+
waitOrJump: async () => ({ waited: 0, jumped: false }),
|
|
881
|
+
writeHeartbeat: () => true,
|
|
882
|
+
postHeartbeat: async () => true,
|
|
883
|
+
emit: () => {},
|
|
884
|
+
record: (r) => r,
|
|
885
|
+
now: () => Date.now(), // uptime is ~0, well under the floor
|
|
886
|
+
codeDrifted: async () => { askedForDrift = true; return { from: 'aaaa1111', to: 'bbbb2222' }; },
|
|
887
|
+
});
|
|
888
|
+
assert.equal(code, 0, 'it must keep running rather than bounce');
|
|
889
|
+
assert.equal(askedForDrift, false, 'and not even ask — the check costs a network call');
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
test('past the uptime floor, a drift still exits', async () => {
|
|
893
|
+
const code = await runner.forever({ goals: [], maxTasks: 1, maxLoops: 2 }, {
|
|
894
|
+
iteration: async () => ({ event: 'hold', reason: 'test' }),
|
|
895
|
+
reconcileLeftoverClaims: async () => ({ event: 'boot_reconcile', ok: true }),
|
|
896
|
+
waitOrJump: async () => ({ waited: 0, jumped: false }),
|
|
897
|
+
writeHeartbeat: () => true,
|
|
898
|
+
postHeartbeat: async () => true,
|
|
899
|
+
emit: () => {},
|
|
900
|
+
record: (r) => r,
|
|
901
|
+
now: () => Date.now() + runner.MIN_UPTIME_BEFORE_UPGRADE_MS + 1000,
|
|
902
|
+
codeDrifted: async () => ({ from: 'aaaa1111', to: 'bbbb2222' }),
|
|
903
|
+
});
|
|
904
|
+
assert.equal(code, runner.UPGRADE_EXIT_CODE);
|
|
905
|
+
});
|