From ee43863e0a235823f8f6d1ccfc45e7acd8680fae Mon Sep 17 00:00:00 2001 From: NetDevAutomate Date: Sat, 19 Sep 2026 10:20:12 +0100 Subject: [PATCH 1/3] =?UTF-8?q?test(ci):=20RED=20=E2=80=94=20the=20nightly?= =?UTF-8?q?=20installer=20job=20must=20plant=20a=20harness=20before=20inst?= =?UTF-8?q?all.sh?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `installer` job in nightly-install.yml (A4, b5339db9, 2026-09-14) has failed every night since it was added (runs 34949338879 .. 35431368317). It isolates HOME to `${{ runner.temp }}/home` so `studyloop install agents` writes into scratch, but an empty HOME has no harness: on the runner no codex/opencode/pi/grok binary is on PATH and no ~/.kiro, ~/.claude, ~/.pi or ~/.grok exists, so `detect_available_agent_tools()` returns [] and `install.sh` exits 1 at "Installing agent definitions" — the script's documented behaviour when no supported AI tool is present. The two tools the job was written to prove had already installed fine by then; the verify step never ran. Reproduced locally with an empty HOME and a PATH holding only uv and the system bins: exit 1, same message. Planting ~/.kiro (or ~/.claude, ~/.pi, ~/.grok — the markers the detector reads without a binary) makes the same command exit 0 and write the links. This test pins the fixture: the run step plants a marker before the script, and a verify step reads what `install agents` wrote into the isolated HOME. Red on the current workflow for that reason (1 failed, 13 passed). --- .../tests/test_ci_workflow_contract.py | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/packages/studyloop/tests/test_ci_workflow_contract.py b/packages/studyloop/tests/test_ci_workflow_contract.py index ebd150b8b..be7101fcd 100644 --- a/packages/studyloop/tests/test_ci_workflow_contract.py +++ b/packages/studyloop/tests/test_ci_workflow_contract.py @@ -236,3 +236,57 @@ def test_sast_and_pre_commit_bandit_skip_lists_agree() -> None: ) assert sast_skipped == precommit_skipped == ci_standards_skipped + + +NIGHTLY_WORKFLOW = WORKFLOW_DIR / "nightly-install.yml" + + +def _nightly_workflow() -> dict[str, Any]: + return yaml.safe_load(NIGHTLY_WORKFLOW.read_text(encoding="utf-8")) + + +def test_nightly_installer_job_plants_a_harness_before_running_install_sh() -> None: + """The nightly `installer` job (A4, added 2026-09-14) isolates HOME so + `studyloop install agents` writes into scratch -- and had never passed: + an empty HOME has no harness, `detect_available_agent_tools()` finds + nothing, and `install.sh` exits 1 at "Installing agent definitions" + exactly as the README says it should when no supported AI tool exists. + The job died five nights running before its verify step ever ran. + + The fixture must supply the precondition the script documents: at least + one harness marker directory that the detector reads without a binary + (`~/.kiro`, `~/.claude`, `~/.pi`, `~/.grok`), created in the isolated + HOME by the same step that runs the script. And the job must then check + what `install agents` wrote, or the isolation buys nothing. + """ + data = _nightly_workflow() + installer = data["jobs"]["installer"] + steps = installer["steps"] + run_step = next(step for step in steps if step.get("name") == "Run scripts/install.sh") + + assert run_step["env"]["HOME"] == "${{ runner.temp }}/home", ( + "HOME isolation is the point of the job; it must stay" + ) + run = run_step["run"] + assert "./scripts/install.sh --non-interactive --no-smoke" in run + + planted = [ + marker + for marker in ("$HOME/.kiro", "$HOME/.claude", "$HOME/.pi", "$HOME/.grok") + if marker in run + ] + assert planted, ( + "the installer job runs install.sh in an empty HOME; plant at least one " + "directory-detected harness marker first or `install agents` exits 1" + ) + plant_at = min(run.index(marker) for marker in planted) + assert plant_at < run.index("./scripts/install.sh"), "plant the marker BEFORE the script runs" + + verify = next( + (step for step in steps if step.get("name") == "Verify installed agent definitions"), + None, + ) + assert verify is not None, "the job must verify what `install agents` wrote into HOME" + assert verify["env"]["HOME"] == "${{ runner.temp }}/home" + for planted_marker in planted: + assert planted_marker in verify["run"], f"verify step does not look inside {planted_marker}" From 57b38838698098eb53a2babafea9b4f2faf89b5d Mon Sep 17 00:00:00 2001 From: NetDevAutomate Date: Sat, 19 Sep 2026 10:22:05 +0100 Subject: [PATCH 2/3] fix(ci): plant harness markers in the nightly installer job's isolated HOME MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GREEN for ee43863e. The `installer` job isolates HOME so `install agents` writes into scratch; the fixture now creates ~/.kiro, ~/.claude, ~/.pi and ~/.grok in that HOME first — the four markers detect_available_agent_tools() reads without a binary — so `install.sh` reaches and exercises all four link sets instead of refusing with "No supported AI tools detected". A new step verifies what `install agents` wrote: two artefacts each for kiro, claude and grok, one for pi, every path taken from installers.py (_TOOL_LINKS / _configure_*). Without a read of the isolated HOME the isolation proved nothing. Proved locally the way the runner runs it: full ./scripts/install.sh --non-interactive --no-smoke with HOME, UV_TOOL_DIR and UV_TOOL_BIN_DIR in scratch and a PATH holding only uv and the system bins — exit 0, "Installation complete!", both entry points answer, all seven artefacts present. Contract module 14/14. The nightly run itself can only be observed at 03:30 UTC or via workflow_dispatch on main after merge. --- .github/workflows/nightly-install.yml | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/.github/workflows/nightly-install.yml b/.github/workflows/nightly-install.yml index 6c5b2f5de..9fedbc088 100644 --- a/.github/workflows/nightly-install.yml +++ b/.github/workflows/nightly-install.yml @@ -72,11 +72,22 @@ jobs: # (~/.kiro, ~/.claude, ~/.codex, ~/.config/opencode). With a fresh HOME # there is no Python 3.12 either, so this also exercises the script's # `uv python install` fallback every night. + # + # The script refuses, by design, when no supported AI tool is present + # ("No supported AI tools detected"), and the runner has none: no + # harness binary on PATH and nothing in the fresh HOME. So the fixture + # plants the four harness markers `detect_available_agent_tools()` + # reads without a binary -- ~/.kiro, ~/.claude, ~/.pi, ~/.grok -- and + # the install-agents step then exercises all four link sets for real. + # (Without this the job failed every night from 2026-09-15, before its + # verify steps ever ran; pinned by test_ci_workflow_contract.py.) env: HOME: ${{ runner.temp }}/home UV_TOOL_DIR: ${{ runner.temp }}/tools UV_TOOL_BIN_DIR: ${{ runner.temp }}/bin - run: mkdir -p "$HOME" && ./scripts/install.sh --non-interactive --no-smoke + run: | + mkdir -p "$HOME/.kiro" "$HOME/.claude" "$HOME/.pi" "$HOME/.grok" + ./scripts/install.sh --non-interactive --no-smoke - name: Verify installed CLI entry points env: @@ -84,3 +95,17 @@ jobs: run: | "$UV_TOOL_BIN_DIR/studyloop" --version "$UV_TOOL_BIN_DIR/session-export" --help + + - name: Verify installed agent definitions + # One artefact per planted harness, each a path `install agents` + # writes for that tool (installers.py _TOOL_LINKS / _configure_*). + env: + HOME: ${{ runner.temp }}/home + run: | + test -e "$HOME/.kiro/agents/study-mentor.json" + test -e "$HOME/.kiro/agents/study-plan-architect.json" + test -e "$HOME/.claude/agents/socratic-mentor.md" + test -e "$HOME/.claude/agents/study-plan-architect.md" + test -e "$HOME/.pi/agent/AGENTS.md" + test -e "$HOME/.grok/hooks/studyloop.json" + test -e "$HOME/.grok/rules/session-db.md" From 4f8e3e0f3c723b2a60dbd12a49d6c83aeea2f04f Mon Sep 17 00:00:00 2001 From: NetDevAutomate Date: Sat, 19 Sep 2026 10:25:20 +0100 Subject: [PATCH 3/3] =?UTF-8?q?docs(tasks):=20item=207=20step=204=20done?= =?UTF-8?q?=20=E2=80=94=20#25=20(D-D),=20#26=20(D-E)=20filed;=20#21=20stat?= =?UTF-8?q?us=20posted,=20left=20open?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both issues carry the proposals' content with every cited test id, symbol and path verified against main a03fc9bd before posting. #21 stays open because its definition of done names OpenCode in the core tuple and the harness receipt records OpenCode as preview on two named blockers. --- openspec/changes/plan-integration-followons/tasks.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openspec/changes/plan-integration-followons/tasks.md b/openspec/changes/plan-integration-followons/tasks.md index a8f912e72..8f3b6d44b 100644 --- a/openspec/changes/plan-integration-followons/tasks.md +++ b/openspec/changes/plan-integration-followons/tasks.md @@ -228,7 +228,11 @@ writer, through the existing gate, closes that. Kept out of item 3 so item 3's f byte-equal afterwards; single `main` on both sides. (3) closeout comments — done: posted on #8–#15 from the re-verified draft (`receipts/issue-closeout-draft-2026-09-16.md`, status paragraph records the shas rewritten and the rows overtaken); #8, #9, #11, #12, #13, #14 closed as completed; #10 and #15 open (row 3b); #7 - reopened (auto-closed by PR #20's body) with the parent mapping. PR #20's body left as merged. **Open:** - (4) the two item-6 issues (after T6.4) and #21's closing comment or decision; (5) the local tag + reopened (auto-closed by PR #20's body) with the parent mapping. PR #20's body left as merged. (4) the two + item-6 issues — done 2026-09-19: **#25** (D-D derived plan bias) and **#26** (D-E nudge + retire/snooze), both + `ready-for-agent`, bodies drawn from the proposals with every cited test id, symbol and path checked against + `main` `a03fc9bd`; #21 given a status comment from the harness receipt and **left open** (its DoD names OpenCode + in core; OpenCode stays preview on the `opencode.db` exporter and the no-completed-reply gaps). **Open:** + (5) the local tag `archive/feat-clean-start-2026-09-15` — owner: push or discard; (6) the GitHub Support ticket text — owner; (7) revoke both tokens and delete `~/tmp/.env` — owner (D-J).