Skip to content

Commit b0cfde9

Browse files
committed
feat: harden profile installs and validate council-lite artifacts
1 parent 9642892 commit b0cfde9

10 files changed

Lines changed: 246 additions & 1 deletion

File tree

.github/workflows/validate.yml

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ on:
88

99
jobs:
1010
validation:
11-
runs-on: ubuntu-latest
11+
strategy:
12+
fail-fast: false
13+
matrix:
14+
os: [ubuntu-latest, macos-latest]
15+
16+
runs-on: ${{ matrix.os }}
1217

1318
steps:
1419
- name: Checkout
@@ -30,6 +35,8 @@ jobs:
3035
run: |
3136
./install.sh --list-profiles
3237
./install.sh --status
38+
./install.sh --conflict-policy fail --profiles core,council-lite
39+
./install.sh --conflict-policy skip --profiles core,council-lite
3340
./install.sh --profiles core,council-lite
3441
./install.sh --enable-profile council-research
3542
./install.sh --disable-profile council-research
@@ -54,4 +61,19 @@ jobs:
5461
PY
5562
)
5663
./scripts/council-lite.sh resume "$SESSION_ID"
64+
./scripts/validate-council-lite.sh --latest
5765
./install.sh --uninstall
66+
67+
secret-scan:
68+
runs-on: ubuntu-latest
69+
permissions:
70+
contents: read
71+
72+
steps:
73+
- name: Checkout
74+
uses: actions/checkout@v4
75+
76+
- name: Run gitleaks
77+
uses: gitleaks/gitleaks-action@v2
78+
env:
79+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,18 @@ Common commands:
3838
# set exact profiles (dependencies auto-added)
3939
./install.sh --profiles core,council-lite
4040

41+
# choose conflict behavior when target paths already exist
42+
./install.sh --conflict-policy fail --profiles core,council-lite
43+
./install.sh --conflict-policy skip --profiles core,council-lite
44+
4145
# enable or disable one profile
4246
./install.sh --enable-profile council-lite
4347
./install.sh --disable-profile council-lite
4448

4549
# start a council-lite session scaffold
4650
./scripts/council-lite.sh run "Design safe billing webhooks"
4751
./scripts/council-lite.sh list
52+
./scripts/validate-council-lite.sh --latest
4853
```
4954

5055
Profile state is persisted at:
@@ -57,6 +62,9 @@ For profile adoption guidance, see `docs/profiles.md`.
5762

5863
`install.sh` creates symlinks into `~/.config/opencode/` so edits in this repo apply immediately.
5964

65+
If a managed target path already exists and is not a symlink, install fails by default.
66+
Use `--conflict-policy skip` to leave conflicting paths untouched and continue linking non-conflicting paths.
67+
6068
`./install.sh --uninstall` removes managed symlinks and profile state.
6169

6270
## Directory Layout
@@ -87,6 +95,7 @@ See `docs/roadmap.md`.
8795
- `scripts/ops-check.sh` - stack-aware quality gate checks
8896
- `scripts/issue-pr-loop.sh` - issue to branch to draft PR flow
8997
- `scripts/council-lite.sh` - lightweight multi-agent session scaffold
98+
- `scripts/validate-council-lite.sh` - council-lite artifact structure validator
9099

91100
See `docs/workflows-playbook.md` for usage patterns.
92101

commands/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ Script mappings:
1515
- `scripts/ops-check.sh`
1616
- `scripts/issue-pr-loop.sh`
1717
- `scripts/council-lite.sh`
18+
- `scripts/validate-council-lite.sh`

commands/council-lite.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ Usage:
1111
./scripts/council-lite.sh list
1212
./scripts/council-lite.sh run "Design safe billing webhooks"
1313
./scripts/council-lite.sh resume <session-id>
14+
./scripts/validate-council-lite.sh --latest
1415
```
1516

1617
Notes:
1718
- `council-lite` profile must be enabled.
1819
- Enable with `./install.sh --enable-profile council-lite`.
1920
- Sessions are written to `memory/council-lite/<session-id>/`.
2021
- Artifacts include intake, assembly, deliberation rounds, and execution plan.
22+
- Validate generated artifacts with `scripts/validate-council-lite.sh`.

docs/profiles.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,21 @@ Profiles let you choose how much capability to install.
2323
# enable/disable one profile
2424
./install.sh --enable-profile council-lite
2525
./install.sh --disable-profile council-lite
26+
27+
# conflict handling policy
28+
./install.sh --conflict-policy fail --profiles core,council-lite
29+
./install.sh --conflict-policy skip --profiles core,council-lite
2630
```
2731

32+
## Conflict Policy
33+
34+
When a managed path in `~/.config/opencode/` already exists and is not a symlink:
35+
36+
- `--conflict-policy fail` (default): abort install before writing state
37+
- `--conflict-policy skip`: continue, but skip conflicting paths
38+
39+
Recommended default is `fail` so you do not accidentally mix unmanaged and managed configuration.
40+
2841
## State Location
2942

3043
Profile state is stored at:
@@ -45,6 +58,7 @@ After enabling `council-lite`, run:
4558
./scripts/council-lite.sh run "Design safe billing webhooks"
4659
./scripts/council-lite.sh list
4760
./scripts/council-lite.sh resume <session-id>
61+
./scripts/validate-council-lite.sh --latest
4862
```
4963

5064
## Experimental Warning: council-research

docs/release-checklist.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,14 @@ Use this checklist before tagging a release.
1818
- [ ] `./install.sh`
1919
- [ ] `./install.sh --list-profiles`
2020
- [ ] `./install.sh --status`
21+
- [ ] `./install.sh --conflict-policy fail --profiles core,council-lite`
22+
- [ ] `./install.sh --conflict-policy skip --profiles core,council-lite`
2123
- [ ] `./install.sh --profiles core,council-lite`
2224
- [ ] `./install.sh --enable-profile council-research`
2325
- [ ] `./install.sh --disable-profile council-research`
2426
- [ ] `./scripts/council-lite.sh run "Smoke test"`
2527
- [ ] `./scripts/council-lite.sh list`
28+
- [ ] `./scripts/validate-council-lite.sh --latest`
2629
- [ ] `./install.sh --uninstall`
2730
- [ ] experimental warning shown when `council-research` is enabled
2831
- [ ] profile state removed on uninstall (`~/.config/opencode/profiles-enabled.json`)

docs/workflows-playbook.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,11 @@ This creates one branch and one draft PR per issue.
5959
./scripts/council-lite.sh list
6060
./scripts/council-lite.sh run "Design safe billing webhooks"
6161
./scripts/council-lite.sh resume <session-id>
62+
./scripts/validate-council-lite.sh --latest
6263
```
6364

6465
Notes:
6566
- requires `council-lite` profile enabled
6667
- creates session artifacts in `memory/council-lite/<session-id>/`
6768
- includes intake, assembly, deliberation rounds, and plan templates
69+
- validation script checks required files and headings

install.sh

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ TARGET_DIR="$HOME/.config/opencode"
66
PROFILES_DIR="$REPO_DIR/profiles"
77
REGISTRY_PATH="$PROFILES_DIR/registry.json"
88
STATE_PATH="$TARGET_DIR/profiles-enabled.json"
9+
CONFLICT_POLICY="fail"
910

1011
MANAGED_ITEMS=(
1112
"agents"
@@ -31,13 +32,51 @@ Profile management:
3132
--profiles <csv> Set enabled profiles (dependencies auto-added).
3233
--enable-profile <id> Enable one profile.
3334
--disable-profile <id> Disable one profile (core cannot be disabled).
35+
--conflict-policy <fail|skip> Behavior when target path already exists.
3436
3537
Other:
3638
--uninstall Remove managed symlinks and profile state.
3739
-h, --help Show this help.
3840
EOF
3941
}
4042

43+
collect_conflicts() {
44+
mkdir -p "$TARGET_DIR"
45+
local conflicts=()
46+
local item
47+
for item in "${MANAGED_ITEMS[@]}"; do
48+
local target_path="$TARGET_DIR/$item"
49+
if [[ -e "$target_path" && ! -L "$target_path" ]]; then
50+
conflicts+=("$target_path")
51+
fi
52+
done
53+
54+
if [[ "${#conflicts[@]}" -eq 0 ]]; then
55+
return 0
56+
fi
57+
58+
echo "Detected install path conflicts:"
59+
for path in "${conflicts[@]}"; do
60+
echo "- $path"
61+
done
62+
echo ""
63+
echo "Conflicts happen when files/directories already exist and are not symlinks."
64+
echo "Recommended: move or remove conflicting paths, then rerun ./install.sh"
65+
66+
if [[ "$CONFLICT_POLICY" == "fail" ]]; then
67+
echo ""
68+
echo "Aborting install due to conflict policy: fail"
69+
echo "To proceed without replacing conflicting paths, rerun with:"
70+
echo " ./install.sh --conflict-policy skip"
71+
return 1
72+
fi
73+
74+
echo ""
75+
echo "Continuing with conflict policy: skip"
76+
echo "Only non-conflicting paths will be linked."
77+
return 0
78+
}
79+
4180
require_registry() {
4281
if [[ ! -f "$REGISTRY_PATH" ]]; then
4382
echo "Profile registry missing: $REGISTRY_PATH"
@@ -234,6 +273,8 @@ install_with_csv() {
234273
exit 1
235274
}
236275

276+
collect_conflicts || exit 1
277+
237278
save_state_csv "$resolved_csv"
238279
link_managed_items
239280

@@ -323,6 +364,28 @@ uninstall_all() {
323364
}
324365

325366
main() {
367+
while [[ $# -gt 0 ]]; do
368+
case "$1" in
369+
--conflict-policy)
370+
if [[ -z "${2:-}" ]]; then
371+
echo "Error: --conflict-policy requires a value (fail|skip)."
372+
usage
373+
exit 1
374+
fi
375+
if [[ "$2" != "fail" && "$2" != "skip" ]]; then
376+
echo "Error: invalid --conflict-policy value: $2"
377+
usage
378+
exit 1
379+
fi
380+
CONFLICT_POLICY="$2"
381+
shift 2
382+
;;
383+
*)
384+
break
385+
;;
386+
esac
387+
done
388+
326389
case "${1:-}" in
327390
"")
328391
install_with_csv "$(load_enabled_profiles)"
@@ -357,6 +420,11 @@ main() {
357420
fi
358421
disable_profile "$2"
359422
;;
423+
--conflict-policy)
424+
echo "Error: --conflict-policy must be combined with an install action."
425+
usage
426+
exit 1
427+
;;
360428
--uninstall)
361429
uninstall_all
362430
;;

profiles/council-lite/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Council Lite provides a lightweight deliberation workflow on top of the core pro
2121
./scripts/council-lite.sh run "Design safe billing webhooks"
2222
./scripts/council-lite.sh list
2323
./scripts/council-lite.sh resume <session-id>
24+
./scripts/validate-council-lite.sh --latest
2425
```
2526

2627
This initializes a structured session under `memory/council-lite/<session-id>/` with:

scripts/validate-council-lite.sh

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
4+
WORKSPACE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
5+
COUNCIL_DIR="$WORKSPACE/memory/council-lite"
6+
INDEX_PATH="$COUNCIL_DIR/index.json"
7+
8+
usage() {
9+
cat <<'EOF'
10+
Usage: validate-council-lite.sh [--latest | <session-id> | <session-path>]
11+
12+
Examples:
13+
./scripts/validate-council-lite.sh --latest
14+
./scripts/validate-council-lite.sh 20260211-150000-design-safe-billing-webhooks
15+
./scripts/validate-council-lite.sh memory/council-lite/20260211-150000-design-safe-billing-webhooks
16+
EOF
17+
}
18+
19+
resolve_session_path() {
20+
local input="${1:-}"
21+
python3 - "$INDEX_PATH" "$COUNCIL_DIR" "$input" <<'PY'
22+
import json
23+
import os
24+
import sys
25+
from pathlib import Path
26+
27+
index_path = Path(sys.argv[1])
28+
council_dir = Path(sys.argv[2])
29+
input_value = sys.argv[3].strip()
30+
31+
if not index_path.exists():
32+
raise SystemExit("No council-lite index found. Run council-lite first.")
33+
34+
data = json.loads(index_path.read_text(encoding="utf-8"))
35+
sessions = data.get("sessions", [])
36+
if not sessions:
37+
raise SystemExit("No council-lite sessions found in index.")
38+
39+
if input_value in ("", "--latest"):
40+
print(sessions[-1]["path"])
41+
raise SystemExit(0)
42+
43+
input_path = Path(input_value)
44+
if input_path.exists() and input_path.is_dir():
45+
print(str(input_path.resolve()))
46+
raise SystemExit(0)
47+
48+
for session in sessions:
49+
if session.get("id") == input_value:
50+
print(session.get("path", ""))
51+
raise SystemExit(0)
52+
53+
candidate = council_dir / input_value
54+
if candidate.exists() and candidate.is_dir():
55+
print(str(candidate.resolve()))
56+
raise SystemExit(0)
57+
58+
raise SystemExit(f"Session not found: {input_value}")
59+
PY
60+
}
61+
62+
main() {
63+
case "${1:-}" in
64+
-h | --help)
65+
usage
66+
exit 0
67+
;;
68+
esac
69+
70+
local selector="${1:---latest}"
71+
local session_path
72+
session_path="$(resolve_session_path "$selector")"
73+
74+
python3 - "$session_path" <<'PY'
75+
import sys
76+
from pathlib import Path
77+
78+
session_path = Path(sys.argv[1])
79+
required_files = [
80+
"session.md",
81+
"intake.md",
82+
"assembly.md",
83+
"deliberation/round1-position.md",
84+
"deliberation/round2-challenge.md",
85+
"deliberation/round3-converge.md",
86+
"plan.md",
87+
]
88+
89+
missing = [f for f in required_files if not (session_path / f).is_file()]
90+
if missing:
91+
print("Validation failed: missing required files")
92+
for item in missing:
93+
print(f"- {item}")
94+
raise SystemExit(1)
95+
96+
checks = {
97+
"session.md": ["# Council Lite Session", "Session ID:", "Goal:"],
98+
"intake.md": ["# Intake", "## Goal", "## Success Criteria"],
99+
"assembly.md": ["# Assembly", "## Recommended Agents", "## Selection Rationale"],
100+
"deliberation/round1-position.md": ["# Round 1 - Position"],
101+
"deliberation/round2-challenge.md": ["# Round 2 - Challenge"],
102+
"deliberation/round3-converge.md": ["# Round 3 - Converge"],
103+
"plan.md": ["# Execution Plan", "## Ordered Steps", "## Verification"],
104+
}
105+
106+
errors = []
107+
for rel_path, required_tokens in checks.items():
108+
text = (session_path / rel_path).read_text(encoding="utf-8")
109+
for token in required_tokens:
110+
if token not in text:
111+
errors.append(f"{rel_path}: missing token '{token}'")
112+
113+
if errors:
114+
print("Validation failed: content checks did not pass")
115+
for err in errors:
116+
print(f"- {err}")
117+
raise SystemExit(1)
118+
119+
print(f"Council-lite artifact validation passed: {session_path}")
120+
PY
121+
}
122+
123+
main "$@"

0 commit comments

Comments
 (0)