@oneie/claude 0.7.0 → 0.9.0
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/agents/abm-strategist.md +89 -0
- package/agents/ads-meta.md +97 -0
- package/agents/analyst.md +173 -0
- package/agents/architect.md +280 -0
- package/agents/brand-guardian.md +88 -0
- package/agents/brand-strategist.md +92 -0
- package/agents/campaign-content.md +90 -0
- package/agents/campaign-email.md +88 -0
- package/agents/campaign-sms.md +87 -0
- package/agents/campaign-social.md +87 -0
- package/agents/cco.md +99 -0
- package/agents/ceo.md +106 -0
- package/agents/chairman.md +120 -0
- package/agents/cmo.md +101 -0
- package/agents/community-greeter.md +89 -0
- package/agents/community-moderator.md +92 -0
- package/agents/compliance.md +92 -0
- package/agents/copywriter.md +100 -0
- package/agents/creative-strategist.md +95 -0
- package/agents/cro.md +98 -0
- package/agents/cto.md +267 -0
- package/agents/customer-interviewer.md +93 -0
- package/agents/customer-researcher.md +89 -0
- package/agents/customer-success-manager.md +90 -0
- package/agents/customer-trainer.md +90 -0
- package/agents/cxo.md +100 -0
- package/agents/demand-creator.md +89 -0
- package/agents/demo-mover.md +83 -0
- package/agents/demo-specialist.md +89 -0
- package/agents/demo-thai-family-law.md +87 -0
- package/agents/designer.md +112 -0
- package/agents/discovery-caller.md +98 -0
- package/agents/doctor.md +139 -0
- package/agents/educate-coach.md +100 -0
- package/agents/elevate-tutor.md +83 -0
- package/agents/email-lifecycle-marketer.md +91 -0
- package/agents/engage-specialist.md +99 -0
- package/agents/events-coordinator.md +90 -0
- package/agents/foundation-builder.md +94 -0
- package/agents/funnel-architect.md +89 -0
- package/agents/gift-creator.md +100 -0
- package/agents/google-ads.md +103 -0
- package/agents/guide.md +292 -0
- package/agents/helpdesk-dispatcher.md +88 -0
- package/agents/hook-specialist.md +91 -0
- package/agents/identify-optimizer.md +101 -0
- package/agents/implementer.md +305 -0
- package/agents/incident-commander.md +120 -0
- package/agents/insights-lead.md +93 -0
- package/agents/journey-runner.md +113 -0
- package/agents/linkedin-ads.md +123 -0
- package/agents/live-sales-chat.md +90 -0
- package/agents/market-researcher.md +88 -0
- package/agents/media-buyer.md +96 -0
- package/agents/movers-customer-researcher.md +95 -0
- package/agents/movers-foundation-builder.md +96 -0
- package/agents/movers-market-researcher.md +97 -0
- package/agents/movers-pricing-strategist.md +171 -0
- package/agents/nurture-architect.md +99 -0
- package/agents/offer-architect.md +92 -0
- package/agents/onboarder.md +222 -0
- package/agents/onboarding-specialist.md +92 -0
- package/agents/operations-dashboard.md +98 -0
- package/agents/perf-engineer.md +348 -0
- package/agents/playbook-writer.md +71 -0
- package/agents/plg-strategist.md +91 -0
- package/agents/positioning-architect.md +88 -0
- package/agents/press-officer.md +89 -0
- package/agents/pricing-strategist.md +94 -0
- package/agents/privacy-officer.md +92 -0
- package/agents/referral-manager.md +91 -0
- package/agents/refine-analyst.md +102 -0
- package/agents/release-manager.md +261 -0
- package/agents/renewals-upsell-rep.md +90 -0
- package/agents/review-engineer.md +327 -0
- package/agents/rewards-steward.md +82 -0
- package/agents/sales-call-coach.md +94 -0
- package/agents/sales-closer.md +98 -0
- package/agents/security-auditor.md +313 -0
- package/agents/sell-closer.md +101 -0
- package/agents/share-amplifier.md +99 -0
- package/agents/social-media-manager.md +92 -0
- package/agents/storyteller.md +250 -0
- package/agents/strategist.md +83 -0
- package/agents/strategy-aligner.md +94 -0
- package/agents/support-agent.md +90 -0
- package/agents/tagger.md +245 -0
- package/agents/tech-writer.md +216 -0
- package/agents/test-engineer.md +243 -0
- package/agents/tiktok-ads.md +105 -0
- package/agents/tracking-engineer.md +92 -0
- package/agents/upsell-strategist.md +101 -0
- package/agents/voice.md +85 -0
- package/agents/w4-tools.md +153 -0
- package/agents/w4-verify.md +20 -138
- package/agents/workflow-optimiser.md +84 -0
- package/commands/close.md +814 -160
- package/commands/create.md +2 -2
- package/commands/deploy.md +554 -175
- package/commands/do.md +119 -109
- package/commands/fast.md +67 -0
- package/commands/improve.md +2 -2
- package/commands/one.md +418 -0
- package/commands/see.md +1 -1
- package/commands/sync.md +1 -1
- package/commands/tasks.md +222 -0
- package/commands/vespio.md +110 -0
- package/commands/vespio.remote.md +227 -0
- package/hooks/hooks.json +25 -79
- package/hooks/lib/governor-escape-match.sh +88 -0
- package/hooks/lib/hook.sh +4 -13
- package/hooks/lib/signal.sh +9 -2
- package/hooks/scripts/branch-pin.sh +6 -0
- package/hooks/scripts/config-protect.sh +6 -0
- package/hooks/scripts/do-outcome-gate.sh +19 -2
- package/hooks/scripts/git-add-guard.sh +43 -2
- package/hooks/scripts/governor-escape.sh +96 -0
- package/hooks/scripts/load-guard.sh +127 -0
- package/hooks/scripts/post-edit-check.sh +36 -29
- package/hooks/scripts/session-start.sh +34 -80
- package/hooks/scripts/task-complete-verify.sh +46 -40
- package/package.json +1 -1
- package/rules/documentation.md +9 -0
- package/scripts/ad-copy-lint.sh +656 -0
- package/scripts/agentverse-audit.sh +256 -0
- package/scripts/asi-walk.sh +435 -0
- package/scripts/astro-build-cached.sh +143 -0
- package/scripts/blocks-manifest-cached.sh +100 -0
- package/scripts/blocks-manifest.mjs +497 -0
- package/scripts/blocks-ratchet.sh +116 -0
- package/scripts/blocks-render-probe.mjs +529 -0
- package/scripts/blocks-usage.mjs +334 -0
- package/scripts/chat-context-check.sh +89 -0
- package/scripts/close-metrics.sh +558 -0
- package/scripts/close-owner.sh +268 -0
- package/scripts/db-sync-lock-check.sh +116 -0
- package/scripts/deploy-dev.sh +35 -0
- package/scripts/deploy-emit.sh +298 -0
- package/scripts/deploy-gate-check.sh +128 -0
- package/scripts/deploy-ready.sh +78 -0
- package/scripts/deploy-record.sh +605 -0
- package/scripts/deploy.sh +1273 -0
- package/scripts/do-auto.sh +598 -48
- package/scripts/do-brief.sh +113 -0
- package/scripts/do-close.sh +1137 -0
- package/scripts/do-commit.sh +75 -0
- package/scripts/do-consumer-sweep.sh +18 -1
- package/scripts/do-cycle-shape-check.sh +160 -0
- package/scripts/do-decide.sh +476 -0
- package/scripts/do-derive-check.sh +436 -0
- package/scripts/do-fleet.sh +106 -28
- package/scripts/do-folder.sh +10 -1
- package/scripts/do-next.sh +106 -0
- package/scripts/do-orchestrate.sh +17 -5
- package/scripts/do-plan-json.mjs +201 -0
- package/scripts/do-plan-json.sh +8 -0
- package/scripts/do-preflight.sh +117 -0
- package/scripts/do-project.sh +157 -0
- package/scripts/do-prove-selftest.sh +108 -0
- package/scripts/do-prove.sh +295 -23
- package/scripts/do-rank.py +31 -1
- package/scripts/do-recon-cache.sh +7 -1
- package/scripts/do-recon-pack.sh +196 -0
- package/scripts/do-reconcile.sh +121 -4
- package/scripts/do-signal.sh +280 -23
- package/scripts/do-smoke.sh +18 -1
- package/scripts/do-test-gate.sh +80 -0
- package/scripts/do-tick.sh +102 -0
- package/scripts/do-tier.sh +6 -0
- package/scripts/do-triage.sh +182 -0
- package/scripts/do-ui-gate.sh +1 -1
- package/scripts/do-w4-gates.sh +451 -0
- package/scripts/do-walk.sh +12 -1
- package/scripts/env-sync.sh +173 -0
- package/scripts/factory-brief-check.sh +330 -0
- package/scripts/factory-check.sh +68 -14
- package/scripts/factory-close-check.sh +257 -0
- package/scripts/factory-emit.sh +211 -0
- package/scripts/factory-executor-check.mjs +353 -0
- package/scripts/factory-peak.sh +301 -0
- package/scripts/factory-repo.sh +118 -3
- package/scripts/factory-review-check.mjs +61 -0
- package/scripts/factory-ship.sh +61 -0
- package/scripts/factory-tasks-check.sh +18 -1
- package/scripts/factory-turn.sh +326 -0
- package/scripts/factory-walk.sh +396 -0
- package/scripts/factory-width.sh +57 -0
- package/scripts/fade-toxic.sh +4 -3
- package/scripts/fixtures/factory-brief-real.md +44 -0
- package/scripts/fixtures/triage-dupe.md +5 -0
- package/scripts/fleet-manifest.mjs +108 -0
- package/scripts/fleet-status.sh +110 -0
- package/scripts/full-suite-paths-check.sh +144 -0
- package/scripts/gate-reaper-check.sh +98 -0
- package/scripts/gate-reaper.sh +125 -0
- package/scripts/gate-run.sh +73 -0
- package/scripts/gc-content-check.sh +140 -0
- package/scripts/gen-dev-config.py +20 -0
- package/scripts/govern-bound-check.sh +60 -0
- package/scripts/govern-claims-check.sh +233 -0
- package/scripts/govern-mem-check.sh +290 -0
- package/scripts/governor-doors-check.sh +362 -0
- package/scripts/governor-escape-check.sh +171 -0
- package/scripts/health.sh +413 -0
- package/scripts/id-inventory.mjs +418 -0
- package/scripts/land.sh +551 -0
- package/scripts/lib/gc-finished.sh +77 -0
- package/scripts/lib/govern.sh +361 -0
- package/scripts/lib/govern.ts +756 -0
- package/scripts/lighthouse-run.sh +187 -0
- package/scripts/livekit-live-check.sh +61 -0
- package/scripts/livekit-ratchet.sh +46 -0
- package/scripts/load-guard-check.sh +49 -0
- package/scripts/machine-check.sh +102 -0
- package/scripts/machine-watch.sh +177 -0
- package/scripts/one-agents.mjs +415 -0
- package/scripts/one-resume.sh +78 -0
- package/scripts/orphan-baseline.json +182 -0
- package/scripts/orphan-modules.mjs +179 -0
- package/scripts/pr-body.sh +335 -0
- package/scripts/preview-fd-check.sh +289 -0
- package/scripts/promise-manifest.mjs +24 -1
- package/scripts/release.sh +322 -0
- package/scripts/roles-check.sh +946 -0
- package/scripts/sdk-build-cached.sh +64 -0
- package/scripts/signal-watch.sh +241 -0
- package/scripts/skills-publish.sh +94 -0
- package/scripts/speed-cache-check.sh +173 -0
- package/scripts/speed-check.mjs +907 -0
- package/scripts/speed-parity-check.sh +648 -0
- package/scripts/speed-waterfall-check.sh +355 -0
- package/scripts/substrate-env-parity.mjs +156 -0
- package/scripts/tasks-claim-race.mjs +108 -0
- package/scripts/tasks-loop.sh +185 -0
- package/scripts/test-cached.sh +255 -0
- package/scripts/test-full.sh +87 -0
- package/scripts/test-honesty.mjs +137 -0
- package/scripts/test-lanes.sh +166 -0
- package/scripts/test-speed.sh +94 -0
- package/scripts/triage-shape-check.sh +149 -0
- package/scripts/tsc-cached.sh +179 -0
- package/scripts/typedb-flake-check.sh +197 -0
- package/scripts/urls-lint.sh +15 -0
- package/scripts/verify-fast.sh +445 -0
- package/scripts/vespio-sync.sh +149 -0
- package/scripts/wf-check.mjs +104 -1
- package/scripts/worktree-preview.sh +879 -0
- package/scripts/worktree-up.sh +208 -0
- package/skills/livekit-agents/SKILL.md +285 -0
- package/skills/livekit-agents/references/freshness-rules.md +168 -0
- package/skills/shadcn/SKILL.md +1 -1
- package/skills/signal/SKILL.md +0 -1
- package/skills/voice/SKILL.md +94 -6
- package/skills/voice/corpus-check.sh +87 -0
- package/hooks/scripts/compact-hint.sh +0 -35
- package/hooks/scripts/gate-guard.sh +0 -83
- package/hooks/scripts/read-tracker.sh +0 -26
- package/hooks/scripts/session-end-verify.sh +0 -51
- package/hooks/scripts/stop-reflect.sh +0 -140
- package/hooks/scripts/sync-priority-todo.sh +0 -57
- package/hooks/scripts/sync-todo-docs.sh +0 -46
- package/hooks/scripts/tool-signal.sh +0 -48
- package/scripts/do-tasks-bridge.py +0 -366
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# do-derive-check.sh — the five checks that hold the agent-native promise.
|
|
3
|
+
#
|
|
4
|
+
# Contract: text/agent-native-plan.md § 3.5. Six of the promise's eight proof
|
|
5
|
+
# legs are this script, so that section is the specification and this file is
|
|
6
|
+
# the implementation of it.
|
|
7
|
+
#
|
|
8
|
+
# contract 60+ receivers advertised to MCP
|
|
9
|
+
# mcp the derived tool list is computed, complete, and schema-faithful
|
|
10
|
+
# binder an unauthorised caller is stopped BY THE WRAPPER, affordance kept
|
|
11
|
+
# routes no new JSON endpoint duplicates a receiver reachable by name
|
|
12
|
+
# counts no hand-written tool ships beside its derived twin
|
|
13
|
+
#
|
|
14
|
+
# THREE EXIT CODES, NOT TWO:
|
|
15
|
+
# 0 ok delivered and behaving
|
|
16
|
+
# 1 RED genuinely missing or wrong
|
|
17
|
+
# 3 CANNOT RUN the check could not reach its evidence
|
|
18
|
+
#
|
|
19
|
+
# Red and cannot-run send a person to opposite places. `contract` and `mcp`
|
|
20
|
+
# import TypeScript through bun; an import that fails because a package is
|
|
21
|
+
# unbuilt is CANNOT RUN, not deliverable-missing — the difference between "go
|
|
22
|
+
# build the SDK" and "go re-scope the work".
|
|
23
|
+
#
|
|
24
|
+
# Every threshold is env-overridable. That is not a convenience: --self-test
|
|
25
|
+
# drives each check to red by moving its threshold against the real tree, which
|
|
26
|
+
# proves the comparison is wired rather than that a fixture can be written.
|
|
27
|
+
#
|
|
28
|
+
# monorepo-only
|
|
29
|
+
set -uo pipefail
|
|
30
|
+
|
|
31
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
32
|
+
|
|
33
|
+
MIN_MCP_FLAGGED="${DERIVE_MIN_MCP_FLAGGED:-60}"
|
|
34
|
+
MIN_DERIVED_TOOLS="${DERIVE_MIN_DERIVED_TOOLS:-60}"
|
|
35
|
+
MAX_HANDWRITTEN_MCP="${DERIVE_MAX_HANDWRITTEN_MCP:-95}" # strictly below
|
|
36
|
+
MAX_CURATED_CHAT="${DERIVE_MAX_CURATED_CHAT:-125}" # at or below
|
|
37
|
+
API_DIR="${DERIVE_API_DIR:-$ROOT/one.ie/web/src/pages/api}"
|
|
38
|
+
MCP_TOOLS_DIR="${DERIVE_MCP_TOOLS_DIR:-$ROOT/packages/mcp/src/tools}"
|
|
39
|
+
CHAT_TOOLS_DIR="${DERIVE_CHAT_TOOLS_DIR:-$ROOT/channels/src/tools}"
|
|
40
|
+
ALLOWLIST="${DERIVE_ROUTE_ALLOWLIST:-$ROOT/.claude/derive-route-allowlist.txt}"
|
|
41
|
+
|
|
42
|
+
say() { printf '[derive-check] %s\n' "$*"; }
|
|
43
|
+
red() { printf '[derive-check] RED — %s\n' "$*" >&2; return 1; }
|
|
44
|
+
cant() { printf '[derive-check] CANNOT RUN — %s\n' "$*" >&2; return 3; }
|
|
45
|
+
|
|
46
|
+
need_bun() { command -v bun >/dev/null || return 3; return 0; }
|
|
47
|
+
|
|
48
|
+
# ── contract ─────────────────────────────────────────────────────────────────
|
|
49
|
+
# The Receiver contract carries surfaces.mcp, populated.
|
|
50
|
+
check_contract() {
|
|
51
|
+
need_bun || { cant "bun is not on PATH"; return 3; }
|
|
52
|
+
local src="$ROOT/packages/sdk/src/receivers.ts"
|
|
53
|
+
[ -f "$src" ] || { cant "receivers.ts absent at $src"; return 3; }
|
|
54
|
+
|
|
55
|
+
local out rc
|
|
56
|
+
out="$(cd "$ROOT" && bun -e "
|
|
57
|
+
import('./packages/sdk/src/receivers.ts')
|
|
58
|
+
.then(m => {
|
|
59
|
+
const R = Object.values(m.RECEIVERS);
|
|
60
|
+
const flagged = R.filter(r => r.surfaces && r.surfaces.mcp);
|
|
61
|
+
const named = flagged.filter(r => typeof r.surfaces.mcp === 'object' && r.surfaces.mcp.name);
|
|
62
|
+
const dupes = named.length - new Set(named.map(r => r.surfaces.mcp.name)).size;
|
|
63
|
+
console.log(JSON.stringify({ total: R.length, flagged: flagged.length, named: named.length, dupes }));
|
|
64
|
+
})
|
|
65
|
+
.catch(e => { console.error('IMPORT ' + e); process.exit(3); });
|
|
66
|
+
" 2>&1)"; rc=$?
|
|
67
|
+
|
|
68
|
+
[ "$rc" -eq 3 ] && { cant "receivers.ts did not import: ${out:0:160}"; return 3; }
|
|
69
|
+
[ "$rc" -ne 0 ] && { cant "bun exited $rc: ${out:0:160}"; return 3; }
|
|
70
|
+
|
|
71
|
+
local flagged named dupes total
|
|
72
|
+
flagged=$(printf '%s' "$out" | grep -o '"flagged":[0-9]*' | cut -d: -f2)
|
|
73
|
+
named=$(printf '%s' "$out" | grep -o '"named":[0-9]*' | cut -d: -f2)
|
|
74
|
+
dupes=$(printf '%s' "$out" | grep -o '"dupes":[0-9]*' | cut -d: -f2)
|
|
75
|
+
total=$(printf '%s' "$out" | grep -o '"total":[0-9]*' | cut -d: -f2)
|
|
76
|
+
[ -z "$flagged" ] && { cant "could not parse the receiver census: ${out:0:160}"; return 3; }
|
|
77
|
+
|
|
78
|
+
# A duplicate declared name would silently shadow one tool with another.
|
|
79
|
+
[ "${dupes:-0}" -gt 0 ] && { red "$dupes duplicate declared MCP tool name(s)"; return 1; }
|
|
80
|
+
[ "$flagged" -lt "$MIN_MCP_FLAGGED" ] && {
|
|
81
|
+
red "surfaces.mcp on $flagged receivers, need >= $MIN_MCP_FLAGGED"; return 1; }
|
|
82
|
+
|
|
83
|
+
say "contract ok — $flagged/$total flagged ($named carry an existing tool name)"
|
|
84
|
+
return 0
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# ── mcp ──────────────────────────────────────────────────────────────────────
|
|
88
|
+
# The tool list is COMPUTED from the registry, complete, and schema-faithful.
|
|
89
|
+
check_mcp() {
|
|
90
|
+
need_bun || { cant "bun is not on PATH"; return 3; }
|
|
91
|
+
local src="$ROOT/packages/mcp/src/tools/from-registry.ts"
|
|
92
|
+
local dist="$ROOT/packages/mcp/dist/index.js"
|
|
93
|
+
[ -f "$src" ] || { red "from-registry.ts absent — the derivation was never written"; return 1; }
|
|
94
|
+
[ -f "$dist" ] || { cant "packages/mcp is unbuilt (no dist/index.js) — run bun run build"; return 3; }
|
|
95
|
+
|
|
96
|
+
local out rc
|
|
97
|
+
out="$(cd "$ROOT/packages/mcp" && bun -e "
|
|
98
|
+
import('./dist/index.js')
|
|
99
|
+
.then(async m => {
|
|
100
|
+
const tools = m.mcpToolsFromRegistry();
|
|
101
|
+
const meta = await import('@oneie/sdk/meta');
|
|
102
|
+
const { RECEIVERS } = await import('@oneie/sdk/receivers');
|
|
103
|
+
// Schema fidelity: a derived tool must advertise ITS OWN receiver's request
|
|
104
|
+
// schema. A tool carrying the wrong schema passes every count and lies to
|
|
105
|
+
// every caller, so this is checked per tool, not sampled.
|
|
106
|
+
let wrong = 0;
|
|
107
|
+
for (const r of Object.values(RECEIVERS)) {
|
|
108
|
+
if (!r.surfaces || !r.surfaces.mcp) continue;
|
|
109
|
+
const want = m.toolNameFor(r.receiver, r.surfaces.mcp);
|
|
110
|
+
const t = tools.find(x => x.name === want);
|
|
111
|
+
if (!t) { wrong++; continue; }
|
|
112
|
+
const expect = JSON.stringify(meta.metaSchema(r.receiver).request);
|
|
113
|
+
if (JSON.stringify(t.inputSchema) !== expect) wrong++;
|
|
114
|
+
}
|
|
115
|
+
const noSchema = tools.filter(t => !t.inputSchema || typeof t.inputSchema !== 'object').length;
|
|
116
|
+
console.log(JSON.stringify({ derived: tools.length, unconvertible: m.UNCONVERTIBLE.length, wrong, noSchema }));
|
|
117
|
+
})
|
|
118
|
+
.catch(e => { console.error('IMPORT ' + e); process.exit(3); });
|
|
119
|
+
" 2>&1)"; rc=$?
|
|
120
|
+
|
|
121
|
+
[ "$rc" -eq 3 ] && { cant "mcp dist did not import: ${out:0:160}"; return 3; }
|
|
122
|
+
[ "$rc" -ne 0 ] && { cant "bun exited $rc: ${out:0:160}"; return 3; }
|
|
123
|
+
|
|
124
|
+
local derived unconv wrong noschema
|
|
125
|
+
derived=$(printf '%s' "$out" | grep -o '"derived":[0-9]*' | cut -d: -f2)
|
|
126
|
+
unconv=$(printf '%s' "$out" | grep -o '"unconvertible":[0-9]*' | cut -d: -f2)
|
|
127
|
+
wrong=$(printf '%s' "$out" | grep -o '"wrong":[0-9]*' | cut -d: -f2)
|
|
128
|
+
noschema=$(printf '%s' "$out" | grep -o '"noSchema":[0-9]*' | cut -d: -f2)
|
|
129
|
+
[ -z "$derived" ] && { cant "could not parse the derivation census: ${out:0:160}"; return 3; }
|
|
130
|
+
|
|
131
|
+
[ "${unconv:-0}" -gt 0 ] && { red "$unconv receiver schema(s) could not convert to JSON Schema"; return 1; }
|
|
132
|
+
[ "${noschema:-0}" -gt 0 ] && { red "$noschema derived tool(s) carry no inputSchema"; return 1; }
|
|
133
|
+
[ "${wrong:-0}" -gt 0 ] && { red "$wrong derived tool(s) missing or advertising the wrong receiver's schema"; return 1; }
|
|
134
|
+
[ "$derived" -lt "$MIN_DERIVED_TOOLS" ] && {
|
|
135
|
+
red "$derived derived tools, need >= $MIN_DERIVED_TOOLS"; return 1; }
|
|
136
|
+
|
|
137
|
+
say "mcp ok — $derived derived tools, every schema matches its receiver"
|
|
138
|
+
return 0
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# ── binder ───────────────────────────────────────────────────────────────────
|
|
142
|
+
# Behavioural first, structural second. Counting handler tables is true the
|
|
143
|
+
# moment everything routes through one function whether or not that function
|
|
144
|
+
# ever consults the policy — so the count is the WEAKER half and runs last.
|
|
145
|
+
check_binder() {
|
|
146
|
+
need_bun || { cant "bun is not on PATH"; return 3; }
|
|
147
|
+
local f="$ROOT/one.ie/web/src/lib/bind-receiver.ts"
|
|
148
|
+
[ -f "$f" ] || { red "bind-receiver.ts absent — the wrapper was never written"; return 1; }
|
|
149
|
+
|
|
150
|
+
local out rc
|
|
151
|
+
out="$(cd "$ROOT/one.ie/web" && bun -e "
|
|
152
|
+
import('./src/lib/bind-receiver.ts')
|
|
153
|
+
.then(async m => {
|
|
154
|
+
const fail = [];
|
|
155
|
+
|
|
156
|
+
// A1 — a tenant-label receiver, called by a caller holding nothing, is
|
|
157
|
+
// rejected BY THE WRAPPER and the handler never runs.
|
|
158
|
+
// Assert the OUTCOME (refused, handler never reached), not the mechanism.
|
|
159
|
+
// The floor returns a refusal object rather than throwing, because that
|
|
160
|
+
// is how every handler in the codebase signals refusal and what the ask
|
|
161
|
+
// route already reads. A check that demanded a throw would fail a
|
|
162
|
+
// correct implementation.
|
|
163
|
+
let ran = false;
|
|
164
|
+
const bound = m.bindReceiver('world:create-group', async () => { ran = true; return { ok: true }; });
|
|
165
|
+
let refused = false;
|
|
166
|
+
try {
|
|
167
|
+
const r = await bound({ name: 'x', slug: 'y' }, {}, {});
|
|
168
|
+
refused = !!(r && typeof r === 'object' && (r.forbidden === true || r.ok === false));
|
|
169
|
+
} catch { refused = true; }
|
|
170
|
+
if (!refused) fail.push('A1: unauthorised caller was NOT refused');
|
|
171
|
+
if (ran) fail.push('A1: handler RAN despite the auth gate — the wrapper consulted the policy and ignored it');
|
|
172
|
+
|
|
173
|
+
// A2 — the error-as-affordance body still exists and still teaches.
|
|
174
|
+
// Checked at its OWNER, receiver-envelope, not at the binder: the
|
|
175
|
+
// binder's request check is warn-only (the declared schemas had
|
|
176
|
+
// drifted from real in-process callers), while /api/ask and
|
|
177
|
+
// /api/signal still reject with this exact body. Asserting it on
|
|
178
|
+
// the binder would assert a contract this cycle deliberately does
|
|
179
|
+
// not ship; asserting it here checks the one that does.
|
|
180
|
+
const env = await import('./src/lib/receiver-envelope.ts');
|
|
181
|
+
const v = env.validateReceiver('world:create-group', {});
|
|
182
|
+
if (v.ok) {
|
|
183
|
+
fail.push('A2: an invalid request validated clean — the affordance gate is gone');
|
|
184
|
+
} else {
|
|
185
|
+
const body = v.body || {};
|
|
186
|
+
if (!('hint' in body) || !('expected' in body) || !('error' in body)) {
|
|
187
|
+
fail.push('A2: affordance body lost a teaching field: ' + JSON.stringify(body).slice(0, 120));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// A2b — and the binder must still NOTICE the mismatch, or warn-only has
|
|
191
|
+
// silently become no-op.
|
|
192
|
+
const seenReq = [];
|
|
193
|
+
const rw = console.warn;
|
|
194
|
+
console.warn = (...a) => seenReq.push(a.join(' '));
|
|
195
|
+
const b2 = m.bindReceiver('world:create-group', async () => ({ ok: true }));
|
|
196
|
+
await b2({}, {}, { ownerSlug: 'someone' }).catch(() => {});
|
|
197
|
+
console.warn = rw;
|
|
198
|
+
if (!seenReq.some(s => /request-mismatch/.test(s))) {
|
|
199
|
+
fail.push('A2b: an invalid request produced no request-mismatch warning — warn-only became no-op');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// A3 — a declared response is actually consulted. WARN mode counts, but
|
|
203
|
+
// the mismatch must be OBSERVABLE, not swallowed.
|
|
204
|
+
const seen = [];
|
|
205
|
+
const realWarn = console.warn;
|
|
206
|
+
console.warn = (...a) => seen.push(a.join(' '));
|
|
207
|
+
const b3 = m.bindReceiver('world:create-group', async () => ({ nonsense: true }));
|
|
208
|
+
await b3({ name: 'x', slug: 'y' }, {}, { ownerSlug: 'someone', staff: true }).catch(() => {});
|
|
209
|
+
console.warn = realWarn;
|
|
210
|
+
if (!seen.some(s => /response-mismatch/.test(s))) {
|
|
211
|
+
fail.push('A3: a handler returning the wrong shape produced no response-mismatch warning');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// A4 — an unlisted auth label must never fall OPEN.
|
|
215
|
+
if (m.authClassFor('a-label-that-does-not-exist') === 'open') {
|
|
216
|
+
fail.push('A4: an unknown auth label resolves to open');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
console.log(JSON.stringify({ fail }));
|
|
220
|
+
})
|
|
221
|
+
.catch(e => { console.error('IMPORT ' + e); process.exit(3); });
|
|
222
|
+
" 2>&1)"; rc=$?
|
|
223
|
+
|
|
224
|
+
[ "$rc" -eq 3 ] && { cant "bind-receiver.ts did not import: ${out:0:200}"; return 3; }
|
|
225
|
+
[ "$rc" -ne 0 ] && { cant "bun exited $rc: ${out:0:200}"; return 3; }
|
|
226
|
+
|
|
227
|
+
local fails
|
|
228
|
+
fails="$(printf '%s' "$out" | sed -n 's/.*"fail":\[\(.*\)\]}.*/\1/p')"
|
|
229
|
+
[ -n "$fails" ] && [ "$fails" != "" ] && { red "binder behaviour: $fails"; return 1; }
|
|
230
|
+
|
|
231
|
+
# Structural, and deliberately last: a raw handler table that never mentions
|
|
232
|
+
# bindReceiver is a dispatcher that has to remember.
|
|
233
|
+
local raw=0 f2
|
|
234
|
+
while IFS= read -r f2; do
|
|
235
|
+
grep -q "bindReceiver" "$f2" || { say " raw handler table: ${f2#$ROOT/}"; raw=$((raw + 1)); }
|
|
236
|
+
done < <(grep -rlE '^const (HANDLERS|RESOLVERS)' "$ROOT/one.ie/web/src/lib" --include='*.ts' 2>/dev/null || true)
|
|
237
|
+
[ "$raw" -gt 0 ] && { red "$raw handler table(s) bind outside bindReceiver"; return 1; }
|
|
238
|
+
|
|
239
|
+
say "binder ok — rejects before the handler, keeps the affordance, checks the response, 0 raw tables"
|
|
240
|
+
return 0
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
# ── routes ───────────────────────────────────────────────────────────────────
|
|
244
|
+
# A new JSON endpoint duplicating a receiver reachable by name. NOT a cap on
|
|
245
|
+
# routes: auth callbacks, webhooks, uploads and non-JSON responses are exempt,
|
|
246
|
+
# and today's tree is grandfathered in an allowlist that may shrink, never grow.
|
|
247
|
+
EXEMPT_RE='(^|/)(auth|webhook|webhooks|upload|uploads|oauth|callback|stripe|sse|stream|rss|sitemap|feed|og|image|images|avatar|media|export|cron|health)(/|$)'
|
|
248
|
+
|
|
249
|
+
check_routes() {
|
|
250
|
+
[ -d "$API_DIR" ] || { cant "api dir absent at $API_DIR"; return 3; }
|
|
251
|
+
[ -f "$ALLOWLIST" ] || { cant "route allowlist absent at $ALLOWLIST — freeze it first (--freeze-routes)"; return 3; }
|
|
252
|
+
need_bun || { cant "bun is not on PATH"; return 3; }
|
|
253
|
+
|
|
254
|
+
local receivers
|
|
255
|
+
receivers="$(cd "$ROOT" && bun -e "
|
|
256
|
+
import('./packages/sdk/src/receivers.ts')
|
|
257
|
+
.then(m => console.log(Object.keys(m.RECEIVERS).join('\n')))
|
|
258
|
+
.catch(() => process.exit(3));
|
|
259
|
+
" 2>/dev/null)" || { cant "could not read the receiver list"; return 3; }
|
|
260
|
+
[ -z "$receivers" ] && { cant "receiver list came back empty"; return 3; }
|
|
261
|
+
|
|
262
|
+
# noun set: the second half of every receiver name, singularised
|
|
263
|
+
local nouns
|
|
264
|
+
nouns="$(printf '%s\n' "$receivers" | sed 's/^[a-z-]*://' | sed 's/s$//' | sort -u)"
|
|
265
|
+
|
|
266
|
+
local offenders=0 rel key
|
|
267
|
+
while IFS= read -r f; do
|
|
268
|
+
rel="${f#$API_DIR/}"
|
|
269
|
+
printf '%s\n' "$rel" | grep -qE "$EXEMPT_RE" && continue
|
|
270
|
+
grep -qF -- "$rel" "$ALLOWLIST" && continue
|
|
271
|
+
# op key: path minus dynamic segments and extension, singularised
|
|
272
|
+
key="$(printf '%s' "$rel" | sed 's/\.ts$//' | sed 's/\[[^]]*\]//g' | tr '/' ' ' | tr -s ' ')"
|
|
273
|
+
for n in $key; do
|
|
274
|
+
n="$(printf '%s' "$n" | sed 's/s$//')"
|
|
275
|
+
[ -z "$n" ] && continue
|
|
276
|
+
if printf '%s\n' "$nouns" | grep -qx -- "$n"; then
|
|
277
|
+
say " twin: $rel duplicates a receiver reachable by name (noun '$n')"
|
|
278
|
+
offenders=$((offenders + 1))
|
|
279
|
+
break
|
|
280
|
+
fi
|
|
281
|
+
done
|
|
282
|
+
done < <(find "$API_DIR" -name '*.ts' -type f 2>/dev/null)
|
|
283
|
+
|
|
284
|
+
[ "$offenders" -gt 0 ] && {
|
|
285
|
+
red "$offenders new route(s) duplicate a by-name receiver — use the door, or add an exempt kind"; return 1; }
|
|
286
|
+
|
|
287
|
+
say "routes ok — no new twin against $(wc -l < "$ALLOWLIST" | tr -d ' ') grandfathered entries"
|
|
288
|
+
return 0
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
freeze_routes() {
|
|
292
|
+
[ -d "$API_DIR" ] || { cant "api dir absent at $API_DIR"; return 3; }
|
|
293
|
+
find "$API_DIR" -name '*.ts' -type f | sed "s|^$API_DIR/||" | sort > "$ALLOWLIST"
|
|
294
|
+
say "froze $(wc -l < "$ALLOWLIST" | tr -d ' ') route(s) into ${ALLOWLIST#$ROOT/} — this list may shrink, never grow"
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
# ── counts ───────────────────────────────────────────────────────────────────
|
|
298
|
+
# The deletion, measured. The load-bearing assertion is COEXISTENCE, not the
|
|
299
|
+
# number: "strictly below 95" alone is satisfied by deleting one tool.
|
|
300
|
+
check_counts() {
|
|
301
|
+
[ -d "$MCP_TOOLS_DIR" ] || { cant "mcp tools dir absent at $MCP_TOOLS_DIR"; return 3; }
|
|
302
|
+
need_bun || { cant "bun is not on PATH"; return 3; }
|
|
303
|
+
|
|
304
|
+
local flagged
|
|
305
|
+
flagged="$(cd "$ROOT" && bun -e "
|
|
306
|
+
import('./packages/sdk/src/receivers.ts')
|
|
307
|
+
.then(m => {
|
|
308
|
+
for (const r of Object.values(m.RECEIVERS)) if (r.surfaces && r.surfaces.mcp) console.log(r.receiver);
|
|
309
|
+
})
|
|
310
|
+
.catch(() => process.exit(3));
|
|
311
|
+
" 2>/dev/null)" || { cant "could not read the flagged receivers"; return 3; }
|
|
312
|
+
|
|
313
|
+
# Coexistence: a flagged receiver must not still have a SINGLE-RECEIVER
|
|
314
|
+
# hand-written wrapper. Scoped to single-receiver tools on purpose — a
|
|
315
|
+
# surviving multi-receiver recipe legitimately names a flagged receiver among
|
|
316
|
+
# several, and the pragma covers anything this still catches wrongly.
|
|
317
|
+
local coexist=0 r hits
|
|
318
|
+
while IFS= read -r r; do
|
|
319
|
+
[ -z "$r" ] && continue
|
|
320
|
+
while IFS= read -r tf; do
|
|
321
|
+
grep -qF -- 'derive:allow-handwritten' "$tf" && continue
|
|
322
|
+
# a module naming exactly ONE distinct receiver is a single-receiver wrapper
|
|
323
|
+
hits="$(grep -ohE '"[a-z][a-z-]*:[a-z][a-z-]*"' "$tf" 2>/dev/null | sort -u | wc -l | tr -d ' ')"
|
|
324
|
+
[ "${hits:-0}" -eq 1 ] || continue
|
|
325
|
+
say " coexists: $r still has a hand-written wrapper in ${tf#$ROOT/}"
|
|
326
|
+
coexist=$((coexist + 1))
|
|
327
|
+
done < <(grep -rlF -- "\"$r\"" "$MCP_TOOLS_DIR" --include='*.ts' 2>/dev/null || true)
|
|
328
|
+
done <<< "$flagged"
|
|
329
|
+
|
|
330
|
+
[ "$coexist" -gt 0 ] && { red "$coexist hand-written tool(s) ship beside a derived twin"; return 1; }
|
|
331
|
+
|
|
332
|
+
# from-registry.ts is the DERIVATION, not a hand-written tool. Counting it
|
|
333
|
+
# inflated the census by one and would have let the file that deletes
|
|
334
|
+
# hand-written tools count as one itself.
|
|
335
|
+
local hw cur
|
|
336
|
+
hw="$(grep -h 'name: "' $(ls "$MCP_TOOLS_DIR"/*.ts 2>/dev/null | grep -v '/from-registry\.ts$') 2>/dev/null | wc -l | tr -d ' ')"
|
|
337
|
+
[ "$hw" -ge "$MAX_HANDWRITTEN_MCP" ] && {
|
|
338
|
+
red "$hw hand-written MCP tool definitions, must be strictly below $MAX_HANDWRITTEN_MCP"; return 1; }
|
|
339
|
+
|
|
340
|
+
if [ -d "$CHAT_TOOLS_DIR" ]; then
|
|
341
|
+
cur="$(grep -rho 'defineWorkspaceTool(' "$CHAT_TOOLS_DIR"/*.ts 2>/dev/null | wc -l | tr -d ' ')"
|
|
342
|
+
[ "${cur:-0}" -gt "$MAX_CURATED_CHAT" ] && {
|
|
343
|
+
red "$cur curated chat tools, ceiling is $MAX_CURATED_CHAT"; return 1; }
|
|
344
|
+
else
|
|
345
|
+
cur="n/a"
|
|
346
|
+
fi
|
|
347
|
+
|
|
348
|
+
say "counts ok — no coexistence, $hw hand-written MCP defs (< $MAX_HANDWRITTEN_MCP), $cur curated chat tools"
|
|
349
|
+
return 0
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
# ── self-test ────────────────────────────────────────────────────────────────
|
|
353
|
+
# Each check is driven to RED against the real tree by moving its own threshold
|
|
354
|
+
# or pointing it at a fixture carrying the defect. A checker that has only ever
|
|
355
|
+
# run against a healthy tree proves nothing about what it would catch.
|
|
356
|
+
self_test() {
|
|
357
|
+
local fails=0 rc tmp
|
|
358
|
+
tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' RETURN
|
|
359
|
+
|
|
360
|
+
expect_red() { # expect_red <label> <cmd...>
|
|
361
|
+
local label="$1"; shift
|
|
362
|
+
"$@" >/dev/null 2>&1; rc=$?
|
|
363
|
+
if [ "$rc" -eq 1 ]; then printf ' ok %s -> RED\n' "$label"
|
|
364
|
+
else printf ' FAIL %s -> expected 1, got %s\n' "$label" "$rc"; fails=$((fails + 1)); fi
|
|
365
|
+
}
|
|
366
|
+
expect_cant() { # expect_cant <label> <cmd...>
|
|
367
|
+
local label="$1"; shift
|
|
368
|
+
"$@" >/dev/null 2>&1; rc=$?
|
|
369
|
+
if [ "$rc" -eq 3 ]; then printf ' ok %s -> CANNOT RUN\n' "$label"
|
|
370
|
+
else printf ' FAIL %s -> expected 3, got %s\n' "$label" "$rc"; fails=$((fails + 1)); fi
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
# contract: an unreachable floor must go red, not pass
|
|
374
|
+
expect_red "contract (floor 99999)" env DERIVE_MIN_MCP_FLAGGED=99999 bash "$0" contract
|
|
375
|
+
# mcp: same
|
|
376
|
+
expect_red "mcp (floor 99999)" env DERIVE_MIN_DERIVED_TOOLS=99999 bash "$0" mcp
|
|
377
|
+
# counts: a ceiling of 0 hand-written defs must go red while any survive
|
|
378
|
+
expect_red "counts (max-handwritten 1)" env DERIVE_MAX_HANDWRITTEN_MCP=1 bash "$0" counts
|
|
379
|
+
# routes: a fixture tree carrying a twin of a real receiver
|
|
380
|
+
mkdir -p "$tmp/api/groups"
|
|
381
|
+
: > "$tmp/api/groups/create.ts"
|
|
382
|
+
: > "$tmp/allow.txt"
|
|
383
|
+
expect_red "routes (fixture twin)" env DERIVE_API_DIR="$tmp/api" DERIVE_ROUTE_ALLOWLIST="$tmp/allow.txt" bash "$0" routes
|
|
384
|
+
# routes: the same twin, allowlisted, must pass — the ratchet grandfathers
|
|
385
|
+
printf 'groups/create.ts\n' > "$tmp/allow.txt"
|
|
386
|
+
env DERIVE_API_DIR="$tmp/api" DERIVE_ROUTE_ALLOWLIST="$tmp/allow.txt" bash "$0" routes >/dev/null 2>&1
|
|
387
|
+
rc=$?
|
|
388
|
+
if [ "$rc" -eq 0 ]; then printf ' ok routes (allowlisted) -> ok\n'
|
|
389
|
+
else printf ' FAIL routes (allowlisted) -> expected 0, got %s\n' "$rc"; fails=$((fails + 1)); fi
|
|
390
|
+
# cannot-run is distinguishable from red
|
|
391
|
+
expect_cant "routes (missing allowlist)" env DERIVE_API_DIR="$tmp/api" DERIVE_ROUTE_ALLOWLIST="$tmp/nope.txt" bash "$0" routes
|
|
392
|
+
expect_cant "counts (missing tools dir)" env DERIVE_MCP_TOOLS_DIR="$tmp/nope" bash "$0" counts
|
|
393
|
+
|
|
394
|
+
# binder: the defining fixture — a wrapper that consults the policy and calls
|
|
395
|
+
# the handler anyway passes every structural count and must fail A1.
|
|
396
|
+
cat > "$tmp/leaky.mjs" <<'EOF'
|
|
397
|
+
const fail = [];
|
|
398
|
+
let ran = false;
|
|
399
|
+
const leaky = (receiver, handler) => async (d, e, c) => {
|
|
400
|
+
const authed = c && (c.staff === true || c.ownerSlug); // consulted...
|
|
401
|
+
if (!authed) console.warn('would reject'); // ...and ignored
|
|
402
|
+
return handler(d, e, c);
|
|
403
|
+
};
|
|
404
|
+
const bound = leaky('world:create-group', async () => { ran = true; return { ok: true }; });
|
|
405
|
+
let threw = false;
|
|
406
|
+
try { await bound({}, {}, {}); } catch { threw = true; }
|
|
407
|
+
if (!threw) fail.push('A1: unauthorised caller was NOT rejected');
|
|
408
|
+
if (ran) fail.push('A1: handler RAN despite the auth gate');
|
|
409
|
+
process.exit(fail.length ? 1 : 0);
|
|
410
|
+
EOF
|
|
411
|
+
node "$tmp/leaky.mjs" >/dev/null 2>&1; rc=$?
|
|
412
|
+
if [ "$rc" -eq 1 ]; then printf ' ok binder (leaky-wrapper fixture) -> RED\n'
|
|
413
|
+
else printf ' FAIL binder (leaky-wrapper fixture) -> expected 1, got %s\n' "$rc"; fails=$((fails + 1)); fi
|
|
414
|
+
|
|
415
|
+
if [ "$fails" -eq 0 ]; then say "self-test PASS — every check can go red, and red is distinguishable from cannot-run"; return 0; fi
|
|
416
|
+
printf '[derive-check] self-test FAILED (%s)\n' "$fails" >&2
|
|
417
|
+
return 1
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
# ── main ─────────────────────────────────────────────────────────────────────
|
|
421
|
+
case "${1:-}" in
|
|
422
|
+
contract) check_contract ;;
|
|
423
|
+
mcp) check_mcp ;;
|
|
424
|
+
binder) check_binder ;;
|
|
425
|
+
routes) check_routes ;;
|
|
426
|
+
counts) check_counts ;;
|
|
427
|
+
--freeze-routes) freeze_routes ;;
|
|
428
|
+
--self-test) self_test ;;
|
|
429
|
+
all|"")
|
|
430
|
+
rc=0
|
|
431
|
+
for c in contract mcp binder routes counts; do
|
|
432
|
+
bash "$0" "$c" || rc=$?
|
|
433
|
+
done
|
|
434
|
+
exit "$rc" ;;
|
|
435
|
+
*) printf 'usage: %s {contract|mcp|binder|routes|counts|all|--self-test|--freeze-routes}\n' "$(basename "$0")" >&2; exit 2 ;;
|
|
436
|
+
esac
|
package/scripts/do-fleet.sh
CHANGED
|
@@ -33,7 +33,7 @@ WT_BASE="$ROOT/.do-worktrees"
|
|
|
33
33
|
|
|
34
34
|
# Worktree branches cut from trunk, never HEAD — a shared tree parked on a
|
|
35
35
|
# feature branch must not seed fleet plans with its commits. DO_BASE_REF overrides.
|
|
36
|
-
BASE_REF="${DO_BASE_REF:-
|
|
36
|
+
BASE_REF="${DO_BASE_REF:-dev}"
|
|
37
37
|
git -C "$ROOT" show-ref --verify --quiet "refs/heads/$BASE_REF" || BASE_REF=HEAD
|
|
38
38
|
|
|
39
39
|
GO=false
|
|
@@ -57,9 +57,33 @@ done
|
|
|
57
57
|
# Cap slots by (cores - 2), never exceed --slots.
|
|
58
58
|
CORES=$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 4)
|
|
59
59
|
CORE_CAP=$(( CORES - 2 )); [ "$CORE_CAP" -lt 1 ] && CORE_CAP=1
|
|
60
|
-
|
|
60
|
+
BIND="--slots"
|
|
61
|
+
[ "$SLOTS" -gt "$CORE_CAP" ] && { SLOTS="$CORE_CAP"; BIND="cores"; }
|
|
61
62
|
|
|
62
|
-
|
|
63
|
+
# …and by MEMORY, which is the constraint that actually bites.
|
|
64
|
+
#
|
|
65
|
+
# Cores were never the limit. On a 10-core/24GB box `cores - 2` authorised 8
|
|
66
|
+
# concurrent worktrees; 8 cycles is ~16GB of gates before the editors, the
|
|
67
|
+
# sessions and the OS get a byte. The box reached load 57 and filled 16GB of
|
|
68
|
+
# swap with fewer than that. gate_headroom prices a cycle in GB and divides
|
|
69
|
+
# what is actually free, so the fleet shrinks on a loaded box and opens back
|
|
70
|
+
# up on an idle one. It only ever LOWERS the count — --slots and the core cap
|
|
71
|
+
# still hold.
|
|
72
|
+
# shellcheck source=lib/govern.sh
|
|
73
|
+
. "$ROOT/.claude/scripts/lib/govern.sh"
|
|
74
|
+
MEM_CAP=$(gate_headroom)
|
|
75
|
+
[ "$SLOTS" -gt "$MEM_CAP" ] && { SLOTS="$MEM_CAP"; BIND="memory"; }
|
|
76
|
+
|
|
77
|
+
# A worktree cut before the governor landed runs the OLD unbounded gates, so
|
|
78
|
+
# the cap above is the only thing protecting the box from it. Say so — a silent
|
|
79
|
+
# stale worktree is how this problem returns.
|
|
80
|
+
for _wt in "$WT_BASE"/*/; do
|
|
81
|
+
[ -f "$_wt/.claude/scripts/lib/govern.sh" ] && continue
|
|
82
|
+
[ -d "$_wt/.claude/scripts" ] || continue
|
|
83
|
+
echo "[do-fleet] ⚠ $(basename "$_wt") predates the governor — its gates are unbounded; rebase it onto $BASE_REF"
|
|
84
|
+
done
|
|
85
|
+
|
|
86
|
+
echo "[do-fleet] ranking… (top $TOP candidates, $SLOTS slots — bound by $BIND; cores≤$CORE_CAP mem≤$MEM_CAP, $( $GO && echo LIVE || echo DRY-RUN ))"
|
|
63
87
|
|
|
64
88
|
# Load ranked queue into an array (bash 3.2 compatible — no mapfile).
|
|
65
89
|
QUEUE=()
|
|
@@ -89,47 +113,82 @@ link_deps() {
|
|
|
89
113
|
done
|
|
90
114
|
}
|
|
91
115
|
|
|
92
|
-
#
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
116
|
+
# Region-claim registry — MACHINE-WIDE, in $GOVERN_DIR/claims (lib/govern.sh).
|
|
117
|
+
#
|
|
118
|
+
# This used to be `LOCK_FILE="$(mktemp)"`: a map private to one fleet process.
|
|
119
|
+
# Nine concurrent fleet invocations held nine private maps, so two fleets could
|
|
120
|
+
# each believe a region was free and both take it — the duplicate-selection
|
|
121
|
+
# collision this registry exists to remove. mkdir(2) is the arbiter; a claim
|
|
122
|
+
# evaporates on its own when the owner dies (kill -0) or its lease expires.
|
|
123
|
+
trap 'claim_release_all' EXIT
|
|
124
|
+
|
|
125
|
+
claim_dirs() { # claim_dirs <slug> → 0 = all regions claimed, 1 = lost a race
|
|
126
|
+
local slug="$1" n=0 d
|
|
127
|
+
CLAIM_TAKEN=()
|
|
98
128
|
while IFS= read -r d; do
|
|
99
|
-
[ -n "$d" ]
|
|
100
|
-
|
|
129
|
+
[ -n "$d" ] || continue
|
|
130
|
+
n=$(( n + 1 ))
|
|
131
|
+
if claim_take "$d" "$slug"; then
|
|
132
|
+
CLAIM_TAKEN+=("$d")
|
|
133
|
+
else
|
|
134
|
+
# Another fleet won this region between has_conflict and here. Give back
|
|
135
|
+
# everything already taken for this slug — a half-claimed plan would hold
|
|
136
|
+
# ground it never builds, and only the TTL would ever free it.
|
|
137
|
+
if [ "${#CLAIM_TAKEN[@]}" -gt 0 ]; then
|
|
138
|
+
for d in "${CLAIM_TAKEN[@]}"; do claim_release "$d"; done
|
|
139
|
+
fi
|
|
140
|
+
echo " ✗ $slug — lost the race for a region to another fleet, deferring" >&2
|
|
141
|
+
return 1
|
|
142
|
+
fi
|
|
143
|
+
done < <(deliverable_dirs "$slug")
|
|
101
144
|
# An unscoped plan (no deliverables:) declares zero dirs — the conflict filter
|
|
102
145
|
# would treat it as disjoint from EVERYTHING and parallelize it blindly, which
|
|
103
146
|
# clobbers shared trees (one.ie/web). Claim the __ANY__ sentinel so it conflicts
|
|
104
147
|
# with any other plan: it only ever runs as the sole slot. Declare deliverables
|
|
105
148
|
# to make it fleet-safe (see do-rank.py UNSCOPED warning).
|
|
106
|
-
[ "$n" -eq 0 ]
|
|
149
|
+
if [ "$n" -eq 0 ]; then
|
|
150
|
+
claim_take "__ANY__" "$slug" || { echo " ✗ $slug — __ANY__ taken, deferring" >&2; return 1; }
|
|
151
|
+
fi
|
|
152
|
+
return 0
|
|
107
153
|
}
|
|
108
154
|
|
|
109
155
|
free_dirs() { # free_dirs <slug>
|
|
110
|
-
local
|
|
111
|
-
|
|
112
|
-
|
|
156
|
+
local d
|
|
157
|
+
while IFS= read -r d; do
|
|
158
|
+
[ -n "$d" ] && claim_release "$d"
|
|
159
|
+
done < <(deliverable_dirs "$1")
|
|
160
|
+
# Only the slug that took the sentinel may give it back. Every claim in this
|
|
161
|
+
# process shares one pid, so an unguarded release here would let a finishing
|
|
162
|
+
# SCOPED plan hand back a live UNSCOPED plan's sole-slot guarantee.
|
|
163
|
+
[ "$(claim_owner "__ANY__" | awk '{print $1}')" = "$1" ] && claim_release "__ANY__"
|
|
164
|
+
return 0
|
|
113
165
|
}
|
|
114
166
|
|
|
115
167
|
has_conflict() { # has_conflict <slug> → exit 0 if conflict
|
|
168
|
+
# Every read below goes through claim_owner/claim_list, which reap a dead or
|
|
169
|
+
# expired owner before answering — so a fleet that was killed mid-plan never
|
|
170
|
+
# keeps a region shut.
|
|
171
|
+
local live_owner
|
|
116
172
|
# A live unscoped plan holds the __ANY__ sentinel → nothing may share with it.
|
|
117
|
-
any_owner=$(
|
|
173
|
+
any_owner=$(claim_owner "__ANY__" | awk '{print $1}')
|
|
118
174
|
if [ -n "$any_owner" ] && [ "$any_owner" != "$1" ]; then
|
|
119
175
|
echo " ✗ $1 — unscoped plan $any_owner holds the fleet (sole slot), deferring"
|
|
120
176
|
return 0
|
|
121
177
|
fi
|
|
122
178
|
# This plan is itself unscoped (no deliverable dirs) → only runs alone.
|
|
123
179
|
ndirs=$(deliverable_dirs "$1" | grep -c . || true)
|
|
124
|
-
if [ "$ndirs" -eq 0 ]
|
|
125
|
-
|
|
126
|
-
|
|
180
|
+
if [ "$ndirs" -eq 0 ]; then
|
|
181
|
+
live_owner=$(claim_list | awk -F'\t' -v s="$1" '$2!=s{print $2; exit}')
|
|
182
|
+
if [ -n "$live_owner" ]; then
|
|
183
|
+
echo " ✗ $1 — unscoped (no deliverables:), defers while $live_owner is live"
|
|
184
|
+
return 0
|
|
185
|
+
fi
|
|
127
186
|
fi
|
|
128
187
|
while IFS= read -r d; do
|
|
129
188
|
[ -z "$d" ] && continue
|
|
130
|
-
owner=$(
|
|
131
|
-
if [ -n "$owner" ]; then
|
|
132
|
-
echo " ✗ $1 — dir $d
|
|
189
|
+
owner=$(claim_owner "$d" | awk '{print $1}')
|
|
190
|
+
if [ -n "$owner" ] && [ "$owner" != "$1" ]; then
|
|
191
|
+
echo " ✗ $1 — dir $d claimed by $owner, deferring"
|
|
133
192
|
return 0
|
|
134
193
|
fi
|
|
135
194
|
done < <(deliverable_dirs "$1")
|
|
@@ -175,16 +234,22 @@ claim_task() { # claim_task <slug> → 0 ok to launch, 1 already claimed elsewh
|
|
|
175
234
|
esac
|
|
176
235
|
}
|
|
177
236
|
|
|
178
|
-
launch_plan() { # launch_plan <slug> → echoes PID
|
|
237
|
+
launch_plan() { # launch_plan <slug> → echoes PID, or nothing if the claim was lost
|
|
179
238
|
local slug="$1"
|
|
180
239
|
local wt="$WT_BASE/$slug"
|
|
240
|
+
# Claim the ground FIRST. has_conflict only read the registry; between that
|
|
241
|
+
# read and now another fleet may have deposited. mkdir decides, and a loser
|
|
242
|
+
# reroutes to the next candidate rather than waiting.
|
|
243
|
+
claim_dirs "$slug" || return 1
|
|
181
244
|
if [ ! -d "$wt" ]; then
|
|
182
245
|
git -C "$ROOT" worktree add -b "do/$slug" "$wt" "$BASE_REF" >/dev/null 2>&1 \
|
|
183
246
|
|| git -C "$ROOT" worktree add "$wt" "do/$slug" >/dev/null 2>&1 || true
|
|
184
247
|
fi
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
248
|
+
# stdout of this function IS the pid — fill_slots captures it. Chatter must go
|
|
249
|
+
# to stderr or the "pid" is a multi-line string and every kill -0 on it fails,
|
|
250
|
+
# which reads as "slot finished" the instant it launches.
|
|
251
|
+
link_deps "$wt" >&2
|
|
252
|
+
echo " ▶ launching $slug (log: $wt/.fleet.log)" >&2
|
|
188
253
|
# Direct exec (no subshell wrapper) so $! is the do-auto.sh bash process itself.
|
|
189
254
|
# A ( cmd ) & subshell forks cmd and exits immediately — the fleet would track the
|
|
190
255
|
# subshell PID, which dies before cmd finishes, misreporting the slot as done.
|
|
@@ -196,7 +261,15 @@ launch_plan() { # launch_plan <slug> → echoes PID
|
|
|
196
261
|
if ! $GO; then
|
|
197
262
|
echo ""
|
|
198
263
|
echo " DRY-RUN — first $SLOTS disjoint plans from queue:"
|
|
199
|
-
|
|
264
|
+
# Seed the preview from the LIVE machine-wide registry (read-only — a dry run
|
|
265
|
+
# never deposits). Otherwise the preview says "✓ foo" for a region another
|
|
266
|
+
# fleet is already inside, which is exactly the lie the registry removes.
|
|
267
|
+
# claim_list emits region<TAB>slug<TAB>pid and reaps dead owners as it walks,
|
|
268
|
+
# so the preview inherits evaporation for free.
|
|
269
|
+
CLAIMED_DRY="$(claim_list)"
|
|
270
|
+
# Keep "" meaning "nothing claimed" — the unscoped-plan check below tests -n.
|
|
271
|
+
if [ -n "$CLAIMED_DRY" ]; then CLAIMED_DRY="$CLAIMED_DRY
|
|
272
|
+
"; fi
|
|
200
273
|
COUNT=0
|
|
201
274
|
for slug in "${QUEUE[@]}"; do
|
|
202
275
|
[ "$COUNT" -ge "$SLOTS" ] && break
|
|
@@ -309,7 +382,12 @@ fill_slots() {
|
|
|
309
382
|
DONE_SLUGS+=("$NEXT_SLUG")
|
|
310
383
|
continue
|
|
311
384
|
fi
|
|
312
|
-
local pid
|
|
385
|
+
local pid=""
|
|
386
|
+
# A lost region race is not an error — reroute to the next candidate.
|
|
387
|
+
if ! pid=$(launch_plan "$NEXT_SLUG") || [ -z "$pid" ]; then
|
|
388
|
+
DONE_SLUGS+=("$NEXT_SLUG")
|
|
389
|
+
continue
|
|
390
|
+
fi
|
|
313
391
|
SLOT_PIDS+=("$pid")
|
|
314
392
|
SLOT_SLUGS+=("$NEXT_SLUG")
|
|
315
393
|
SLOT_START+=("$(date +%s)")
|