Compare commits
3 Commits
b50e32ae74
...
1da6186c95
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1da6186c95 | ||
|
|
e058760f79 | ||
|
|
aaa1dbf3d0 |
@ -4,8 +4,8 @@ tier: T2
|
|||||||
status: active
|
status: active
|
||||||
owner: jp
|
owner: jp
|
||||||
source: generated
|
source: generated
|
||||||
last_reviewed: 2026-05-24
|
last_reviewed: 2026-05-25
|
||||||
review_by: 2026-08-22
|
review_by: 2026-08-23
|
||||||
depends_on:
|
depends_on:
|
||||||
- disclosure-schema
|
- disclosure-schema
|
||||||
- profile-distribution-protocol
|
- profile-distribution-protocol
|
||||||
@ -19,7 +19,7 @@ auto_regen_cmd: "yq '.disclosure' manifest.yaml | <renderer-script>"
|
|||||||
|
|
||||||
# `cto-planb` — Disclosure
|
# `cto-planb` — Disclosure
|
||||||
|
|
||||||
> Live as of 2026-05-24. Source: `cto/manifest.yaml → disclosure:` block (Wave-4 apply). Pre-push hook check 6 (curator/lib/pre-push.sh) enforces this == live `hermes -p cto-planb` runtime.
|
> Live as of 2026-05-25. Source: `cto/manifest.yaml → disclosure:` block (Wave-7 D2 apply — schema v2 + sandcastle external_orchestrator promoted from §12 pending to canonical §6.5 per Wave-7 Q2 decision). Pre-push hook check 6 (curator/lib/pre-push.sh) enforces this == live `hermes -p cto-planb` runtime.
|
||||||
|
|
||||||
## §1 Identity
|
## §1 Identity
|
||||||
|
|
||||||
@ -72,7 +72,7 @@ Per `disclosure.sovereign_apis`. Each entry is grep-verified against `called_by`
|
|||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| `bte-rest` | `http://localhost:5000` | http | read-write | `skills/cto-agent/SKILL.md`, `skills/cto-angular-toolkit/SKILL.md` | BTE REST `/api/export-design-md` cited as the DESIGN.md emit path for UI tasks; not auto-invoked at v1.0 (documented pattern only — CTO would `curl` when a UI task triggers DESIGN.md export). |
|
| `bte-rest` | `http://localhost:5000` | http | read-write | `skills/cto-agent/SKILL.md`, `skills/cto-angular-toolkit/SKILL.md` | BTE REST `/api/export-design-md` cited as the DESIGN.md emit path for UI tasks; not auto-invoked at v1.0 (documented pattern only — CTO would `curl` when a UI task triggers DESIGN.md export). |
|
||||||
|
|
||||||
> Sandcastle is NOT listed here in §5 — see §12 (Pending JP review). Per Wave-3 recommendations §3 A2 it is governance-critical and PAUSED awaiting JP's call on documenting it under `sovereign_apis:` with `transport: cli` vs. a schema §4.6 extension (`external_orchestrators:`).
|
> Sandcastle is NOT listed here in §5 — it has its own dedicated surface type. See §6.5 (External orchestrators). Wave-7 Q2 resolved the §12.1 open question in favor of schema §4.6's `external_orchestrators:` taxonomy (cleaner separation from HTTP/gRPC sovereign APIs).
|
||||||
|
|
||||||
## §6 Cortex tools (12)
|
## §6 Cortex tools (12)
|
||||||
|
|
||||||
@ -95,6 +95,20 @@ Per `disclosure.cortex_tools`. 2 invoked at runtime; 10 mount-and-cite routing t
|
|||||||
|
|
||||||
**Removed (Wave-4):** `PC-svrnty.tool-cortex-plugin` — declared in legacy `external_tool_deps` but never cited in any cto skill body or lib (orphan). Removed per Wave-3 recommendations §4 C13. Reversible by re-adding the entry to `external_tool_deps`.
|
**Removed (Wave-4):** `PC-svrnty.tool-cortex-plugin` — declared in legacy `external_tool_deps` but never cited in any cto skill body or lib (orphan). Removed per Wave-3 recommendations §4 C13. Reversible by re-adding the entry to `external_tool_deps`.
|
||||||
|
|
||||||
|
## §6.5 External orchestrators (1)
|
||||||
|
|
||||||
|
Per `disclosure.external_orchestrators` (schema v2, added Wave-7 D2). cto's **primary execution mechanism** — every code-modifying task routes through sandcastle's isolation boundary (CONTRACT.md §5 + §11 anti-pattern: "CTO never edits host code directly").
|
||||||
|
|
||||||
|
| ID | Transport | Mode | Version pin | Sandboxed | Hosted API | Called by | Justification |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| `sandcastle` | cli | exec | `v0.5.11` | **true** | `anthropic` | `lib/cto-worker.sh` | Isolated `claudeCode('claude-opus-4-7')` exec per CONTRACT.md §5 — the 4-layer safety stack (sandbox + git branch + PR + JP approval). Escape valve under `sovereign_only: false`; if profile were `sovereign_only: true`, schema §6 6.e v2 permits this entry IFF `sandboxed: true`. |
|
||||||
|
|
||||||
|
**Governance.** `sandboxed: true` is the load-bearing field — it declares isolation. `hosted_api: anthropic` is surfaced honestly because sandcastle wraps `claudeCode('claude-opus-4-7')` (CONTRACT.md §5 invocation pattern). cto-agent itself runs sovereign `qwen3.6-35b-a3b`; hosted Claude lives **inside** sandcastle's sandbox, never on cto's own surface.
|
||||||
|
|
||||||
|
**Pin enforcement.** `version_pin: v0.5.11` matches `manifest.yaml → external_tool_deps[0].pin` and the workspace CLAUDE.md hard rule "sandcastle pinned v0.5.11; bumps human-only via `git fetch upstream && git checkout <tag>`". Sandcastle dir is read-only — never edited from cto.
|
||||||
|
|
||||||
|
**Pre-push check 6.e (v2).** With `sovereign_only: false`, no special enforcement triggers. If the profile ever flips to `sovereign_only: true`, the check 6.e v2 amendment requires `sandboxed: true` for any orchestrator declaring `hosted_api` — which this row satisfies.
|
||||||
|
|
||||||
## §7 Credentials (0)
|
## §7 Credentials (0)
|
||||||
|
|
||||||
No active credential declarations in this disclosure block. `github-pat` (optional, vault-absent) is parked under §12 Pending JP review per Wave-3 recommendations §5 K1 — cred-adjacent rows require JP sign-off before joining the active allowlist. Legacy `credentials.optional: [github-pat]` block remains for installer back-compat (per DISCLOSURE-SCHEMA §7).
|
No active credential declarations in this disclosure block. `github-pat` (optional, vault-absent) is parked under §12 Pending JP review per Wave-3 recommendations §5 K1 — cred-adjacent rows require JP sign-off before joining the active allowlist. Legacy `credentials.optional: [github-pat]` block remains for installer back-compat (per DISCLOSURE-SCHEMA §7).
|
||||||
@ -110,6 +124,7 @@ No cron jobs. cto runs on-demand or on kanban tick (CONTRACT.md §3 + manifest `
|
|||||||
| Skills | 3 | 3 | in-sync (live verified by AUDIT-cto-2026-05-24.md §1) |
|
| Skills | 3 | 3 | in-sync (live verified by AUDIT-cto-2026-05-24.md §1) |
|
||||||
| MCP servers | 0 | 0 | in-sync (live verified by AUDIT §2) |
|
| MCP servers | 0 | 0 | in-sync (live verified by AUDIT §2) |
|
||||||
| MCP tools (total) | 0 | 0 | in-sync |
|
| MCP tools (total) | 0 | 0 | in-sync |
|
||||||
|
| External orchestrators | 1 (sandcastle) | 1 (sandcastle invoked by `lib/cto-worker.sh:50-62`) | in-sync (Wave-7 D2) |
|
||||||
| Credentials | 0 | 1 vault-absent declared in legacy block | acceptable (Pending JP — see §12) |
|
| Credentials | 0 | 1 vault-absent declared in legacy block | acceptable (Pending JP — see §12) |
|
||||||
|
|
||||||
> Pre-push hook check 6 last run: pending (Wave-4 first apply, 2026-05-24). Curator sweep will populate.
|
> Pre-push hook check 6 last run: pending (Wave-4 first apply, 2026-05-24). Curator sweep will populate.
|
||||||
@ -131,20 +146,15 @@ No cron jobs. cto runs on-demand or on kanban tick (CONTRACT.md §3 + manifest `
|
|||||||
|
|
||||||
Rows surfaced by Wave-3 audit/recommendations but paused awaiting JP sign-off. These are NOT in the active `disclosure:` block yet.
|
Rows surfaced by Wave-3 audit/recommendations but paused awaiting JP sign-off. These are NOT in the active `disclosure:` block yet.
|
||||||
|
|
||||||
### §12.1 ADD — sandcastle as `sovereign_api` (governance-critical)
|
### §12.1 RESOLVED (Wave-7 D2 / Q2) — sandcastle promoted to canonical §6.5
|
||||||
|
|
||||||
Per `RECOMMENDATIONS-cto-2026-05-24.md §3 A2` and `AUDIT-cto-2026-05-24.md §8`.
|
Per Wave-7 Q2 decision (2026-05-25): the open question on (a) `sovereign_apis: cli` vs (b) schema §4.6 `external_orchestrators:` was resolved in favor of **(b)** — schema v2 added the `external_orchestrators:` surface (cleaner taxonomy, separates HTTP/gRPC sovereign APIs from CLI orchestrators with isolation semantics).
|
||||||
|
|
||||||
| Field | Proposed value |
|
Sandcastle now lives in:
|
||||||
|---|---|
|
- `manifest.yaml → disclosure.external_orchestrators[0]` (schema v2)
|
||||||
| name | `sandcastle` |
|
- §6.5 above (canonical disclosure section)
|
||||||
| transport | `cli` (via `npx tsx -e "..."` per `lib/cto-worker.sh:50-62`) |
|
|
||||||
| endpoint | `../sandcastle` (read-only sibling, pinned v0.5.11) |
|
|
||||||
| mode | `exec` |
|
|
||||||
| called_by | `lib/cto-worker.sh` (one actual runtime invocation at lines 50-62 + 3 env/wrapper refs) |
|
|
||||||
| justification | sandcastle is cto's **primary execution mechanism** (CONTRACT.md §5 + §11 anti-patterns: "CTO never edits host code directly — always via sandcastle"). Currently only present in legacy `external_tool_deps`. DISCLOSURE-SCHEMA §4 has no `sandcastle` surface type; closest fit = `sovereign_apis` with `transport: cli` + governance note. |
|
|
||||||
|
|
||||||
**Open question for JP:** prefer (a) document under `sovereign_apis:` with `transport: cli` (zero schema churn — Karpathy Rule 2 default) OR (b) DISCLOSURE-SCHEMA §4.6 amendment adding `external_orchestrators:` surface (cleaner taxonomy, defers this row to a future wave)? Recommendation: (a).
|
Row retained here for audit trail only. No JP action required.
|
||||||
|
|
||||||
### §12.2 KEEP — `github-pat` credential declaration (cred-adjacent PAUSE)
|
### §12.2 KEEP — `github-pat` credential declaration (cred-adjacent PAUSE)
|
||||||
|
|
||||||
|
|||||||
245
install.sh
245
install.sh
@ -81,6 +81,251 @@ fi
|
|||||||
echo "== ensure cto-worker.sh executable =="
|
echo "== ensure cto-worker.sh executable =="
|
||||||
chmod +x "$REPO/lib/cto-worker.sh" 2>/dev/null && echo " ✓ lib/cto-worker.sh executable"
|
chmod +x "$REPO/lib/cto-worker.sh" 2>/dev/null && echo " ✓ lib/cto-worker.sh executable"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Wave 7 D6+D4 — disclosure → runtime config wiring (F1-F5)
|
||||||
|
# Materializes manifest.disclosure (schema v2) into the live Hermes runtime:
|
||||||
|
# F1 resolve $HERMES_WORKSPACE in inherit_dirs → skills.external_dirs
|
||||||
|
# F2 compute denylist from disclosure.skills → skills.disabled
|
||||||
|
# F3 propagate inherit_mcp_toolsets → agent.inherit_mcp_toolsets
|
||||||
|
# F4 install subrepo pre-push disclosure-drift gate
|
||||||
|
# F5 (D4) write sovereign vllm model block → model.{default,provider,base_url,…}
|
||||||
|
# Per-profile config lives at ~/.hermes/profiles/$PROFILE_NAME/config.yaml.
|
||||||
|
# cto inherit_dirs is empty by design (CONTRACT.md §1, §9) — F1 stays for template
|
||||||
|
# consistency w/ cmo/ceo workers. Per CONTRACT.md §5: cto-agent itself runs sovereign
|
||||||
|
# qwen3.6; claudeCode hosted lives only inside sandcastle isolation boundary.
|
||||||
|
# All steps idempotent + graceful (WARN + skip on missing tooling).
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
echo "== disclosure → runtime config (Wave 7 D6+D4) =="
|
||||||
|
HERMES_WORKSPACE="${HERMES_WORKSPACE:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
||||||
|
|
||||||
|
# F1 — resolve $HERMES_WORKSPACE in disclosure.inherit_dirs → skills.external_dirs (global config)
|
||||||
|
GLOBAL_CFG="$HERMES_HOME/config.yaml"
|
||||||
|
if [ "$DRY_RUN" -eq 1 ]; then
|
||||||
|
echo "DRY: F1 expand inherit_dirs (HERMES_WORKSPACE=$HERMES_WORKSPACE) → $GLOBAL_CFG"
|
||||||
|
elif command -v yq >/dev/null 2>&1; then
|
||||||
|
INHERIT_DIRS=$(yq -r '.disclosure.inherit_dirs[]?' "$REPO/manifest.yaml" 2>/dev/null || true)
|
||||||
|
if [ -n "$INHERIT_DIRS" ]; then
|
||||||
|
while IFS= read -r raw; do
|
||||||
|
[ -z "$raw" ] && continue
|
||||||
|
resolved="${raw//\$HERMES_WORKSPACE/$HERMES_WORKSPACE}"
|
||||||
|
if [ ! -d "$resolved" ]; then
|
||||||
|
echo " WARN: F1 inherit_dir not found: $resolved (declared: $raw) — skipping"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
python3 - "$GLOBAL_CFG" "$resolved" <<'PY'
|
||||||
|
import sys, os, yaml
|
||||||
|
cfg, sk = sys.argv[1], sys.argv[2]
|
||||||
|
d = yaml.safe_load(open(cfg).read()) if os.path.exists(cfg) else {}
|
||||||
|
d = d or {}
|
||||||
|
d.setdefault('skills', {}).setdefault('external_dirs', [])
|
||||||
|
if sk not in d['skills']['external_dirs']:
|
||||||
|
d['skills']['external_dirs'].append(sk)
|
||||||
|
open(cfg, 'w').write(yaml.safe_dump(d, sort_keys=False, allow_unicode=True))
|
||||||
|
print(f" F1 + {sk}")
|
||||||
|
else:
|
||||||
|
print(f" F1 = {sk} (already present)")
|
||||||
|
PY
|
||||||
|
done <<< "$INHERIT_DIRS"
|
||||||
|
else
|
||||||
|
echo " F1: no disclosure.inherit_dirs declared — skip (cto is correct-by-design)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " WARN: F1 yq not on PATH — skipping inherit_dirs resolution"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# F2 — compute denylist (all builtins NOT in disclosure.skills allowlist) → profile config
|
||||||
|
if [ "$DRY_RUN" -eq 1 ]; then
|
||||||
|
echo "DRY: F2 compute builtin denylist → $PROFILE_CFG (skills.disabled)"
|
||||||
|
elif command -v hermes >/dev/null 2>&1 && command -v yq >/dev/null 2>&1; then
|
||||||
|
# Try --json first; fall back to table parse w/ box-draw chars.
|
||||||
|
ALL_BUILTINS=$(hermes skills list --json 2>/dev/null | jq -r '.[] | select(.source=="builtin") | .name' 2>/dev/null || true)
|
||||||
|
if [ -z "$ALL_BUILTINS" ]; then
|
||||||
|
ALL_BUILTINS=$(hermes skills list 2>/dev/null | awk -F'│' 'NR>3 && /builtin/ {gsub(/^ +| +$/, "", $2); print $2}' || true)
|
||||||
|
fi
|
||||||
|
ALLOWLIST_BUILTIN=$(yq -r '.disclosure.skills[] | select(.source=="builtin") | .id' "$REPO/manifest.yaml" 2>/dev/null | sort -u)
|
||||||
|
if [ -z "$ALL_BUILTINS" ]; then
|
||||||
|
echo " WARN: F2 could not enumerate live builtins — skipping denylist"
|
||||||
|
else
|
||||||
|
SORTED_BUILTINS=$(echo "$ALL_BUILTINS" | sort -u)
|
||||||
|
DENYLIST=$(comm -23 <(echo "$SORTED_BUILTINS") <(echo "${ALLOWLIST_BUILTIN:-}") | grep -v '^$' || true)
|
||||||
|
mkdir -p "$(dirname "$PROFILE_CFG")"
|
||||||
|
[ -f "$PROFILE_CFG" ] || : > "$PROFILE_CFG"
|
||||||
|
python3 - "$PROFILE_CFG" <<PY
|
||||||
|
import sys, yaml
|
||||||
|
cfg = sys.argv[1]
|
||||||
|
denylist = [s for s in """$DENYLIST""".splitlines() if s.strip()]
|
||||||
|
d = yaml.safe_load(open(cfg).read()) or {}
|
||||||
|
d.setdefault('skills', {})['disabled'] = sorted(set(denylist))
|
||||||
|
open(cfg, 'w').write(yaml.safe_dump(d, sort_keys=False, allow_unicode=True))
|
||||||
|
print(f" F2 wrote skills.disabled: {len(denylist)} entr{'y' if len(denylist)==1 else 'ies'}")
|
||||||
|
PY
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " WARN: F2 hermes/yq missing — skipping denylist"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# F3 — propagate disclosure.inherit_mcp_toolsets to per-profile config.yaml
|
||||||
|
if [ "$DRY_RUN" -eq 1 ]; then
|
||||||
|
echo "DRY: F3 write agent.inherit_mcp_toolsets → $PROFILE_CFG"
|
||||||
|
elif command -v yq >/dev/null 2>&1; then
|
||||||
|
INHERIT_MCP=$(yq -r '.disclosure.inherit_mcp_toolsets' "$REPO/manifest.yaml" 2>/dev/null || echo "null")
|
||||||
|
if [ "$INHERIT_MCP" = "null" ] || [ -z "$INHERIT_MCP" ]; then
|
||||||
|
echo " F3: disclosure.inherit_mcp_toolsets undeclared — skip"
|
||||||
|
else
|
||||||
|
mkdir -p "$(dirname "$PROFILE_CFG")"
|
||||||
|
[ -f "$PROFILE_CFG" ] || : > "$PROFILE_CFG"
|
||||||
|
python3 - "$PROFILE_CFG" "$INHERIT_MCP" <<'PY'
|
||||||
|
import sys, yaml
|
||||||
|
cfg, val = sys.argv[1], sys.argv[2]
|
||||||
|
b = {"true": True, "false": False}.get(val.lower(), val)
|
||||||
|
d = yaml.safe_load(open(cfg).read()) or {}
|
||||||
|
d.setdefault('agent', {})['inherit_mcp_toolsets'] = b
|
||||||
|
open(cfg, 'w').write(yaml.safe_dump(d, sort_keys=False, allow_unicode=True))
|
||||||
|
print(f" F3 wrote agent.inherit_mcp_toolsets: {b}")
|
||||||
|
PY
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " WARN: F3 yq not on PATH — skipping inherit_mcp_toolsets"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# F5 (D4) — sovereign vllm model block → per-profile config.yaml
|
||||||
|
# Per CONTRACT.md §5: cto-agent runs sovereign qwen3.6 (this block).
|
||||||
|
# claudeCode hosted is constrained INSIDE sandcastle isolation only.
|
||||||
|
# Matches ceo-planb/curator-planb pattern at http://100.90.54.40:8000/v1.
|
||||||
|
if [ "$DRY_RUN" -eq 1 ]; then
|
||||||
|
echo "DRY: F5 write sovereign vllm model block → $PROFILE_CFG"
|
||||||
|
elif command -v yq >/dev/null 2>&1; then
|
||||||
|
mkdir -p "$(dirname "$PROFILE_CFG")"
|
||||||
|
[ -f "$PROFILE_CFG" ] || : > "$PROFILE_CFG"
|
||||||
|
MODEL_BLOCK=$(cat <<'YAML'
|
||||||
|
model:
|
||||||
|
default: qwen3.6-35b-a3b
|
||||||
|
provider: vllm
|
||||||
|
api_key: dummy
|
||||||
|
base_url: http://100.90.54.40:8000/v1
|
||||||
|
context_length: 262144
|
||||||
|
YAML
|
||||||
|
)
|
||||||
|
# yq eval-all merge: existing config wins on non-model keys; model block overwrites.
|
||||||
|
TMP_CFG=$(mktemp)
|
||||||
|
echo "$MODEL_BLOCK" | yq eval-all '. as $item ireduce ({}; . * $item)' "$PROFILE_CFG" - > "$TMP_CFG"
|
||||||
|
mv "$TMP_CFG" "$PROFILE_CFG"
|
||||||
|
echo " F5 wrote model block (qwen3.6-35b-a3b @ http://100.90.54.40:8000/v1)"
|
||||||
|
else
|
||||||
|
echo " WARN: F5 yq not on PATH — skipping model block (cto-agent will fall back to global default)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# F4 — install subrepo pre-push hook (disclosure-drift gate)
|
||||||
|
HOOK_DST="$REPO/.git/hooks/pre-push"
|
||||||
|
if [ "$DRY_RUN" -eq 1 ]; then
|
||||||
|
echo "DRY: F4 install pre-push hook → $HOOK_DST"
|
||||||
|
elif [ ! -d "$REPO/.git" ]; then
|
||||||
|
echo " WARN: F4 $REPO/.git missing — not a git checkout, skip"
|
||||||
|
else
|
||||||
|
cat > "$HOOK_DST" <<'HOOK_EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# pre-push.sh — Wave 7 D6 subrepo disclosure-drift gate
|
||||||
|
# Schema v2 ref: ../sot/04-STANDARDS/DISCLOSURE-SCHEMA.md
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||||
|
PROFILE_DIR_NAME="$(basename "$REPO_ROOT")" # e.g., cmo, ceo, cto
|
||||||
|
PROFILE_NAME="${PROFILE_DIR_NAME}-planb"
|
||||||
|
VIOLATIONS=0
|
||||||
|
emit() { echo "[pre-push:$PROFILE_DIR_NAME] $*" >&2; }
|
||||||
|
fail() { emit "BLOCK: $*"; VIOLATIONS=$((VIOLATIONS + 1)); }
|
||||||
|
|
||||||
|
while read -r local_ref local_sha remote_ref remote_sha; do
|
||||||
|
[ "$local_sha" = "0000000000000000000000000000000000000000" ] && continue
|
||||||
|
if [ "$remote_sha" = "0000000000000000000000000000000000000000" ]; then
|
||||||
|
base=$(git merge-base "$local_sha" jp 2>/dev/null || git rev-parse "$local_sha^" 2>/dev/null || echo "$local_sha")
|
||||||
|
else
|
||||||
|
base="$remote_sha"
|
||||||
|
fi
|
||||||
|
DELTA=$(git diff --name-only "$base..$local_sha" 2>/dev/null || true)
|
||||||
|
[ -z "$DELTA" ] && continue
|
||||||
|
|
||||||
|
# check 2: manifest governance block
|
||||||
|
if echo "$DELTA" | grep -q '^manifest\.yaml$'; then
|
||||||
|
emit "check 2: validating governance block…"
|
||||||
|
python3 - "$REPO_ROOT/manifest.yaml" <<'PY' || fail "manifest.yaml: governance block invalid"
|
||||||
|
import sys, yaml
|
||||||
|
data = yaml.safe_load(open(sys.argv[1]).read()) or {}
|
||||||
|
gov = data.get("governance")
|
||||||
|
if not isinstance(gov, dict):
|
||||||
|
print(f"missing governance: block", file=sys.stderr); sys.exit(1)
|
||||||
|
req = ["org", "owner", "approval_authority", "vision_refs", "governing_protocols", "standards", "north_star"]
|
||||||
|
missing = [k for k in req if k not in gov]
|
||||||
|
if missing:
|
||||||
|
print(f"governance missing: {', '.join(missing)}", file=sys.stderr); sys.exit(1)
|
||||||
|
PY
|
||||||
|
fi
|
||||||
|
|
||||||
|
# check 3: identity doc frontmatter
|
||||||
|
CHANGED_IDENTITY=$(echo "$DELTA" | grep -E '^(AGENT|CONTRACT|DISCLOSURE)\.md$' || true)
|
||||||
|
if [ -n "$CHANGED_IDENTITY" ]; then
|
||||||
|
emit "check 3: validating frontmatter…"
|
||||||
|
for f in $CHANGED_IDENTITY; do
|
||||||
|
python3 - "$REPO_ROOT/$f" <<'PY' || fail "$f: frontmatter invalid"
|
||||||
|
import sys, re, yaml
|
||||||
|
content = open(sys.argv[1]).read()
|
||||||
|
m = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
|
||||||
|
if not m: print(f"missing YAML frontmatter", file=sys.stderr); sys.exit(1)
|
||||||
|
fm = yaml.safe_load(m.group(1)) or {}
|
||||||
|
req = {"name", "tier", "status", "owner", "source", "last_reviewed", "description"}
|
||||||
|
missing = req - set(fm.keys())
|
||||||
|
if missing: print(f"frontmatter missing: {', '.join(missing)}", file=sys.stderr); sys.exit(1)
|
||||||
|
PY
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# check 6: disclosure drift (only if manifest changed)
|
||||||
|
if echo "$DELTA" | grep -q '^manifest\.yaml$'; then
|
||||||
|
emit "check 6: disclosure drift…"
|
||||||
|
# 6.a skills drift
|
||||||
|
if command -v hermes >/dev/null 2>&1; then
|
||||||
|
declared=$(yq -r '.disclosure.skills[].id' "$REPO_ROOT/manifest.yaml" 2>/dev/null | sort -u)
|
||||||
|
live=$(hermes -p "$PROFILE_NAME" skills list 2>/dev/null | awk 'NR>3 && /enabled|│ *enabled/ {for (i=1; i<=NF; i++) if ($i != "│" && $i != "enabled") {print $i; break}}' | sort -u || echo "")
|
||||||
|
if [ -n "$live" ]; then
|
||||||
|
drift=$(diff <(echo "$declared") <(echo "$live") 2>/dev/null || true)
|
||||||
|
[ -n "$drift" ] && fail "skills drift: $drift"
|
||||||
|
else
|
||||||
|
emit " (skip 6.a — hermes CLI live-skill query returned empty; WARN)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
emit " (skip 6.a — hermes CLI not on PATH; WARN)"
|
||||||
|
fi
|
||||||
|
# 6.b/6.c/6.d/6.e simplified: just confirm manifest yq parses + sovereign_only invariant
|
||||||
|
if [ "$(yq '.disclosure.sovereign_only' "$REPO_ROOT/manifest.yaml")" = "true" ]; then
|
||||||
|
bad=$(yq -r '.disclosure.skills[] | select(.hosted_api != null) | .id' "$REPO_ROOT/manifest.yaml" 2>/dev/null)
|
||||||
|
[ -n "$bad" ] && fail "sovereign_only=true but skills declare hosted_api: $bad"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# bypass marker categorization
|
||||||
|
for sha in $(git rev-list "$base..$local_sha"); do
|
||||||
|
MSG=$(git log -1 --format=%B "$sha")
|
||||||
|
bypass_line=$(echo "$MSG" | grep -E '^enforcement-bypass:' || true)
|
||||||
|
if [ -n "$bypass_line" ]; then
|
||||||
|
if ! echo "$bypass_line" | grep -qE '^enforcement-bypass: (emergency|upstream-blocker|schema-migration|hermes-bug|third-party-bug) — '; then
|
||||||
|
fail "$sha: bypass marker uncategorized — required: 'enforcement-bypass: <CATEGORY> — <line>'"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$VIOLATIONS" -gt 0 ]; then
|
||||||
|
emit "✗ $VIOLATIONS violation(s) — push blocked"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
emit "✓ subrepo pre-push gate passed"
|
||||||
|
exit 0
|
||||||
|
HOOK_EOF
|
||||||
|
chmod +x "$HOOK_DST"
|
||||||
|
echo " F4 installed: $HOOK_DST"
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "== done. canonical install: hermes profile install $REPO =="
|
echo "== done. canonical install: hermes profile install $REPO =="
|
||||||
echo " verify: hermes -p $PROFILE_NAME skills list | grep cto-agent"
|
echo " verify: hermes -p $PROFILE_NAME skills list | grep cto-agent"
|
||||||
|
|||||||
@ -138,7 +138,7 @@ credentials: # provisioned via `credctl set <name>` — never
|
|||||||
# Derived from Wave-3 recommendations: sot/06-REGISTRY/audits/RECOMMENDATIONS-cto-2026-05-24.md
|
# Derived from Wave-3 recommendations: sot/06-REGISTRY/audits/RECOMMENDATIONS-cto-2026-05-24.md
|
||||||
disclosure:
|
disclosure:
|
||||||
scope: org
|
scope: org
|
||||||
schema_version: 1
|
schema_version: 2 # bumped Wave-7 D2 (2026-05-25) — adds external_orchestrators surface per DISCLOSURE-SCHEMA §4.6
|
||||||
chat_facing: false # cto is kanban-driven; JP chats with steev, not cto (CONTRACT.md §3)
|
chat_facing: false # cto is kanban-driven; JP chats with steev, not cto (CONTRACT.md §3)
|
||||||
delegates_to: [] # cto consumes sandcastle as a tool, not a sub-agent (CONTRACT.md §1, §9)
|
delegates_to: [] # cto consumes sandcastle as a tool, not a sub-agent (CONTRACT.md §1, §9)
|
||||||
inherit_builtins: false # deny-by-default; cto has zero builtins enabled
|
inherit_builtins: false # deny-by-default; cto has zero builtins enabled
|
||||||
@ -266,3 +266,18 @@ disclosure:
|
|||||||
|
|
||||||
credentials: [] # github-pat declaration parked under Pending JP review in DISCLOSURE.md §12
|
credentials: [] # github-pat declaration parked under Pending JP review in DISCLOSURE.md §12
|
||||||
# (cred-adjacent PAUSE per Wave-3 recommendations §5 K1)
|
# (cred-adjacent PAUSE per Wave-3 recommendations §5 K1)
|
||||||
|
|
||||||
|
# External orchestrators (schema v2+ — Wave-7 D2). Sandcastle is cto's primary
|
||||||
|
# execution mechanism (CONTRACT.md §5). sandboxed=true + sovereign_only=false
|
||||||
|
# = the 4-layer safety stack (sandbox isolation + git branch + PR + JP approval).
|
||||||
|
external_orchestrators:
|
||||||
|
- id: sandcastle
|
||||||
|
transport: cli
|
||||||
|
mode: exec
|
||||||
|
called_by:
|
||||||
|
- lib/cto-worker.sh
|
||||||
|
version_pin: v0.5.11
|
||||||
|
sandboxed: true
|
||||||
|
sovereign_required: false
|
||||||
|
hosted_api: anthropic
|
||||||
|
justification: "isolated claudeCode exec per CONTRACT.md §5 (escape valve under sovereign_only=false)"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user