Commit Graph
1119 Commits
Author SHA1 Message Date
bchanot ca9645833c chore(memory): LRN-207/208/209 + EVAL-041 — availability signal, blind security brief, scoped test runs, W1-C challenge value 2026-10-09 15:36:59 +02:00
bchanot b09e84497a chore(memory): journal — make test summary hotfix 2026-10-09 15:31:27 +02:00
bchanot efdd491d63 fix(make): test target names every red suite and prints a summary
A full make test printed only the == headers and an aggregate exit code,
so finding the red suite meant re-running every suite one by one (the
pre-merge check of 2026-10-09 took 7.5 min for that reason). The loop now
prints FAIL <suite> as it happens and ends with 'all suites green' or
'<n> suite(s) red: <names>'; the exit code is unchanged.
2026-10-09 15:31:26 +02:00
bchanot ff741e3a82 chore(memory): journal — model-router wave 1 merged into develop 2026-10-09 15:25:39 +02:00
bchanot abbdf7926d Merge feature/model-router-mod into develop 2026-10-09 15:25:06 +02:00
bchanot a4f660d0b6 chore(tasks): model-router W1-C live checks part 1 done, part 2 queued; journal 2026-10-09 15:14:55 +02:00
bchanot 6f31f49d7c chore(tasks): model-router W1-C done — contract evidence, TODO (accepted MEDIUMs, residuals, live checks), journal 2026-10-09 15:09:07 +02:00
bchanot d0fa1001bb feat(mods): model-router adaptive tiers — absolute tiers, availability breaker, derived phases
Phases name absolute tiers (best fable>opus>sonnet, big opus>fable>sonnet,
work sonnet>opus, cheap haiku>sonnet) resolved to the first available full
id; per-model circuit breaker fed by StopFailure kinds (rate_limit,
overloaded, billing_error, model_not_found) and PostModelSwitch auto, with
episode backoff 15→300 min, cleared by a user /model or /route reload and
kept across /clear; fallback chain fable→opus→sonnet→haiku with the effort
unchanged; main loop upgrades to a phase's tier by itself under a context
cap (fails closed on unknown usage), downgrades only with the switch on,
sticky within a turn; derived orchestrate on background dispatches;
prompt default rules (plan/reflect, Unicode guards, skipped on slash
commands, floor matches and mid-turn). 58 plugin tests.
2026-10-09 15:08:49 +02:00
bchanot 977be7cad8 chore(tasks): model-router W1-C plan r3 + r4 after two confirmation passes 2026-10-09 13:22:07 +02:00
bchanot 140c16a67f chore(tasks): model-router W1-C plan r2 + contract amendments after the FATAL round 2026-10-09 12:55:14 +02:00
bchanot 2d8cd6bf4c chore(tasks): model-router W1-C contract + plan (absolute tiers, breaker fallback, derived phases) 2026-10-09 12:40:19 +02:00
bchanot e79db7e6df chore(memory): model-router wave 1 closed — TODO W2 queued, journal 2026-10-09 11:16:32 +02:00
bchanot b22f8947f9 docs: README effort routing + /route, USAGE, ARCHITECTURE mods/, CHANGELOG — model-router wave 1 2026-10-09 11:04:10 +02:00
bchanot a6e200392c chore(memory): BDR-115 amendment (skills-dir load, floor, kill switch) + journal B2 2026-10-09 10:52:28 +02:00
bchanot 3c44dd00d3 chore(tasks): model-router B2 done — contract evidence, TODO close-out queue 2026-10-09 10:51:51 +02:00
bchanot 6430ac65ec feat(mods): model-router active in every session — skills-dir link, mods suite, doctor section, CLAUDE.md
Tracked relative symlink skills/model-router -> ../mods/model-router: Claude
Code loads the mod in place as model-router@skills-dir wherever link.sh
links ~/.claude/skills (no CLAUDE_CODE_PLUGIN_DIRS: absolute paths in the
tracked settings.json). Engine-laid mods/*/tsconfig.json gitignored.
lib/tests/mods.test.sh: manifest name, link target, claude plugin validate
and test per mod, capability-probed, time-bounded, SKIP with reason.
doctor.sh: fail-soft Mods section (link by -ef, one guarded plugin list).
CLAUDE.md: mods/ section (loading, per-machine enabled:false switch,
dev-copy shadowing, tests).
2026-10-09 10:51:13 +02:00
bchanot 24e180ade0 chore(tasks): model-router B1 done — contract evidence, TODO, journal 2026-10-09 10:13:03 +02:00
bchanot 1ff608a68c feat(mods): model-router user effort floor — ultrathink and typed /effort-<l> set the main turn's default and minimum
One decision helper (mainEffort) feeds the plan and every answer text;
per-axis precedence (sticky > turn route > floor > engine); a mid-turn
prompt floors the running turn and the next; per-machine kill switch
"enabled": false in ~/.claude/model-router.json, kept across /clear and
across a failed reload; typed /effort-<l> attested at prompt.submit so a
sub-agent preload cannot floor the main loop. 30 plugin tests.
2026-10-09 10:12:28 +02:00
bchanot 868a7f0515 chore(tasks): model-router B1/B2 plans r2 after the 6-lens challenge round 2026-10-09 09:24:32 +02:00
bchanot 77ad7cf494 chore(tasks): model-router W1-B split — floor + wiring contracts/plans, skills-dir loading decision, journal 2026-10-08 18:37:32 +02:00
bchanot ae0179f491 chore(tasks): model-router contract criteria 7-11, TODO hardening done + residuals, journal 2026-10-08 16:53:43 +02:00
bchanot 346d6aeab2 feat(mods): model-router hardening — user-only /route, effort-only agent routes, config caps, visible fail-open
Security-gate round on the wave 1-A mod: /route answers only a composer
origin; an in-agent route call can no longer change the agent's model
(effort only, model fixed at spawn); config patterns capped (200 chars,
4096-char scan), phase keys restricted, override file refused above 64 KB,
additionalProperties false on the tool schema; every .catch logs once per
session; post-next bookkeeping isolated. 14 plugin tests, verifier 11/11.
2026-10-08 16:53:43 +02:00
bchanot 64702d50ea chore(memory): BDR-115 + LRN-205/206 + EVAL-040 — model-router architecture, tool output schema, plugin test kit, challenge value 2026-10-08 16:38:08 +02:00
bchanot 6dc2d748fc chore(memory): journal + TODO — model-router wave 1-A done, hardening + 1-B queued 2026-10-08 16:27:43 +02:00
bchanot e8ca713d9e chore(tasks): model-router w1a contract + plan r3 2026-10-08 16:26:57 +02:00
bchanot b721c94dcb feat(mods): model-router mod, wave 1-A — per-request model/effort routing
Function-hooks plugin under mods/model-router: routes effort (and, behind a
flag, the model) of every main-loop request, sets built-in sub-agents' model
at spawn with full ids, answers Skill(effort-*) itself (single writer, no
pairing rule), exposes the route tool and /route, validates the optional
~/.claude/model-router.json. 11 plugin tests, validate + tsc clean.
Contract .claude/tasks/contracts/2026-10-08-model-router-w1a-1533.md.
2026-10-08 16:26:57 +02:00
bchanot b73d1b127e chore(memory): model-router wave 0 — plan, LRN-203/204, BLK-029, journal 2026-10-08 15:25:11 +02:00
bchanot f24682b3f3 chore(memory): BDR-112 amendment — manual-push mode user-tested, dotfiles prompt handed over 2026-10-07 17:45:01 +02:00
bchanot 6b528dc85f chore(memory): journal + TODO — manual-push-mode merged into develop (669db06) 2026-10-07 17:37:57 +02:00
bchanot 669db06485 Merge feature/manual-push-mode into develop 2026-10-07 17:37:38 +02:00
bchanot 3721cf522a chore(memory): BDR-114 + LRN-200..202 + journal — feat manual-push-mode run D 2026-10-07 17:24:31 +02:00
bchanot 1203a9a735 docs(gitflow): run D — invalid autopush value fails closed everywhere; CHANGELOG, SETTINGS, gitflow skill 2026-10-07 17:24:30 +02:00
bchanot 4a747c8144 docs(doctrine): manual-push mode — invalid value counts as manual; Claude never pushes, even when asked 2026-10-07 17:24:29 +02:00
bchanot 64ca0f8e09 docs(skills): invalid autopush value is fail-closed everywhere; prose aligned
Run D3 of manual-push mode (BDR-114). With every reader now failing
closed, the skill prose stops saying the lib and hooks still push on an
invalid value:

- capitalize STEP 5C / STEP 6: the invalid outcome is split on the ahead
  count (nothing pushed vs pushed anyway by a stale fail-open hook or a
  manual push); the verb's stderr line is quoted verbatim; neighbouring
  closing lines carry push-mode qualifiers so none shadows the invalid
  case; the --no-push lines follow the same rule.
- client-handover: "COMMIT + PUSH" labels become "COMMIT + PUSH STATE
  READ"; the STEP 5 residual sentences no longer imply the pipeline
  pushes; the invalid value is named as a case where the user pushes.
- release-executor: prep span checks the version format by reading the
  string (never in a Bash command); manual mode and an invalid value
  both leave main/develop local.
2026-10-07 17:11:58 +02:00
bchanot 3c59333fcf fix(push-guard): single reader, whole-word dir tokens, payload fallback, bad-value banner
Run D2 of manual-push mode (BDR-114).

- push-guard sources lib/gitflow.sh once (absolute path) and reads each
  candidate dir through gitflow_push_mode; a missing lib denies.
- Dir tokens are extracted as whole shell words: a fully quoted token
  (inner apostrophe allowed) is resolved, a backslash-escaped space is
  unescaped deterministically, a token mixing quoted and unquoted parts
  is refused (fail closed) instead of resolving to its parent.
- A payload jq cannot parse is scanned as raw text with its JSON escapes
  folded; a push-looking one gets the static deny through the trap.
- The 20-token cap runs before any per-token classification (a flood of
  20 000 tokens is refused in 0.13 s; T58 locks it under 5 s).
- `case "$mode"` has a deny default; missing core tools warn and allow.
- T42 compares the deny list against main (the last release) instead of
  HEAD; literal-true, mixed-token, broken-payload, lib-missing and
  banner-on-bad-value cases added (98 checks).
- session-start banner reads the mode through the verb and shows
  `push : manual (autopush bad)` on an unparseable value.
- tour hints quote "<abs project>".
2026-10-07 17:11:56 +02:00
bchanot 472cccbc52 fix(gitflow): every autopush reader fails closed and names an invalid value
Run D1 of manual-push mode (BDR-114). `git config --bool --default true
gitflow.autopush` only covered a MISSING key: an unparseable value made
git die with empty output, the `= false` test failed, and every push ran
again. A typo on a work machine silently re-enabled the pushes it was
meant to stop.

- lib/gitflow.sh: `_gitflow_push_off` reads the mode through the lib
  verb (`push-mode`); anything but `auto` is push-off, and the verb's
  stderr line names an invalid value during start/finish.
- Emitted post-commit/post-merge hooks (POSIX sh, standalone): push only
  when the key reads `true` or is unset; `false` exits quietly; any
  other result prints one stderr line ("NOT pushed, treated as manual
  push mode") and exits 0. Mirrors gitflow_push_mode.
- .githooks/ and githooks/ regenerated files-only through `emit-hook`
  (no config read or write; .git/config hash unchanged).
- hooks/unpushed-guard.sh: mode from the lib verb (absolute lib path
  resolved before any cd, no temp file); anything but auto is manual;
  the SessionStart line names an invalid or unreadable value.
- Tests: gitflow-test T18q block (invalid → start, hook and finish push
  nothing and say so; `true` → the hook pushes; emitted hook is
  POSIX-clean), unpushed-guard T14 rewritten.
2026-10-07 16:45:31 +02:00
bchanot e4bc6212ef docs(gitflow): run C — push-mode verb, skills never push; CHANGELOG, SETTINGS, gitflow skill, README, USAGE 2026-10-07 14:48:27 +02:00
bchanot 0b08ceda97 chore(memory): BDR-113 + LRN-197..199 + journal — feat manual-push-mode run C 2026-10-07 14:48:07 +02:00
bchanot 3881f462c6 fix(gitflow): run C polish — 5C coherence, sanitized verb stderr, hermetic suite
Closes the non-gap observations the gates left on runs C1/C2:

- capitalize STEP 5C/6: heading no longer says "+ push"; the --no-push
  fact read is its own paragraph and scoped to that path; the
  auto-persisted line requires finish rc 0 AND ahead = 0; rc 5/2/6
  (merged, branch not deleted) still report the push state; the
  "not on origin" line carries the once-a-remote-exists hint.
- gitflow.sh push-mode: the raw config value echoed on stderr is reduced
  to printable characters (LC_ALL=C, BSD tr safe) and capped at 64.
- gitflow-test.sh exports the hermetic git config env in the file, so a
  bare run on a global-manual machine stays green.
- client-handover-writer: the branch allowlist refuses a leading dash.
2026-10-07 14:35:08 +02:00
bchanot 6104545e76 feat(skills): push state read from facts, never pushed by the skills
Run C2 of manual-push mode (BDR-111/BDR-112). The four flows that pushed
on their own, or claimed the branch was on origin, now read the truth
after the fact and hand the user the exact command:

- client-handover-writer: the "Push to origin now?" question and its
  push block are gone (the hooks had already pushed in auto-push mode;
  push-guard denies it in manual mode). A reusable PUSH STATE READ
  (branch, origin probe, `git rev-list --count origin/<br>..<br>`, the
  verb only to word the reason) runs after commit-change, at the top of
  the deploy pause, after "Deployed" and before each end report. The
  branch name is validated against an allowlist before it is placed in
  any command or hint (a hostile branch name is otherwise a shell
  injection). Pending → the user pushes BEFORE the deploy pause; the
  deploy brief says "after your push". `Push:` line in both reports.
- release-candidate STEP 6: two ahead counts + the verb; anything other
  than auto with both counts 0 prints one user command
  `! git push --atomic origin main develop v<X.Y.Z>` and stops; the tag
  gate stays for auto mode; `hold` notes --follow-tags; version regex.
- release-executor: push claims qualified (auto-push mode, best effort).
- tour: mode-agnostic rule; STEP 3 reads one `git -C <project>` fact per
  project (suffix-aware branch, --remotes=origin, origin probe) and the
  summary row says on origin / local only with the user command.
2026-10-07 14:02:51 +02:00
bchanot 5cf049d235 feat(gitflow): push-mode verb; /close reports the push state instead of pushing
Run C1 of manual-push mode (BDR-111/BDR-112).

- lib/gitflow.sh: `gitflow.sh push-mode` prints auto | manual | invalid
  (rc 0; an invalid value is named on stderr). It is the one reader a
  skill may call: the bare `git config … gitflow.*` read is denied to
  Claude since run B. Ignores GITFLOW_NO_PUSH by design (documented).
- skills/capitalize/SKILL.md STEP 5C: the explicit `git push origin
  develop` is gone — `finish` has pushed develop itself since BDR-095,
  mode-aware since run A. 5C is now three separate read-only calls
  (finish; push-mode; `git rev-list --count origin/develop..develop`)
  and prose outcomes keyed on the real ahead count: pushed / manual push
  mode, you push / not on origin / push FAILED / invalid value named,
  plus a finish-failure outcome (merge vs delete rc distinguished).
  STEP 6 closing lines and the recap carry every outcome; the
  `--no-push` line reads the branch's own ahead count ("this disk only"
  only when true). Invariant: no `git push` inside any Bash call; the
  user hints are prose.
- skills/close/SKILL.md, lib/gitflow-aiguillage.md: "push" claims
  qualified "in auto-push mode".
- lib/gitflow-test.sh T11b: six cases for the verb (default, true,
  false, non-boolean with stderr + rc 0, corrupt config, usage).

Polish items from the gates are listed in TODO.md (C1 polish).
2026-10-07 13:39:01 +02:00
bchanot 472168d432 docs(gitflow): push-guard — SETTINGS push discipline + guardrails table, gitflow skill, ARCHITECTURE, README, CHANGELOG 2026-10-07 12:42:15 +02:00
bchanot 4630b625f7 chore(memory): BDR-112 + LRN-194..196 + journal — feat manual-push-guard run B 2026-10-07 12:41:46 +02:00
bchanot 6468eda495 fix(push-guard): fail closed on token floods, git failures and quoted cd targets
Hardening after the security gate on a2ac018 (3 MEDIUM, all closed and
re-measured):

- dir tokens are deduplicated and capped: more than 20 distinct cd/-C
  targets in one command denies before any git fork (20000 tokens: 0.15 s
  against the 10 s hook timeout that used to turn a flood into an allow)
- a git or cd failure while reading gitflow.autopush denies instead of
  reading as auto (git absent, usage error, unenterable dir); the key
  being unset is the only "auto" answer; the decision is recorded only
  after one candidate was evaluated cleanly, else the EXIT trap denies
- cd/pushd/-C targets that follow a quote or backtick (bash -c '…') are
  extracted; quote characters are excluded from unquoted tokens

User decision (contract, gated): both fail-closed cases also fire in
auto mode on such pathological commands; silence in auto mode holds for
every ordinary push. Header limits list the residual misses (quotes or
backslashes inside a token, cumulative relative cd, unparseable payload)
backed by the soft_deny rule. Tests: 71 checks (T48–T50b added).
2026-10-07 12:34:56 +02:00
bchanot a2ac0189f7 feat(gitflow): push-guard hook denies Claude's git push in manual-push mode
Run B of manual-push mode (BDR-111). With `gitflow.autopush false`
nothing stops Claude from typing `git push` itself: the `ask` tier is
inert under auto mode. This adds the mechanical block the user chose.

- hooks/push-guard.sh (PreToolUse, matcher Bash|Monitor, timeout 10):
  detects a push in the command text (strict, quote-stripped loose and
  alias patterns; backslash-newline folded in bash, BSD sed/grep only),
  reads gitflow.autopush in the payload cwd and in every literal -C/cd
  dir the command names (global config counts outside a repo), denies
  with the documented JSON form and a reason that tells the user to run
  the command with `!`. Unparseable value = manual (fail closed); once
  a push is detected an EXIT trap emits a static deny on any internal
  error. jq missing = one stderr warning, allow (sibling-hook policy).
- settings.json: hook wiring; 18 deny entries closing the write forms
  of the human-only toggle (any `git … config` spelling, section
  removal/rename, `-c`, config env overrides, direct edits of git config
  files); one soft_deny on pushing in manual mode in any form, with no
  per-turn clearance; the routing-around rule names hook refusals.
- hooks/session-start.sh: `🔒 push : manual (autopush=false) — ! git push`
  banner line when the key reads false (padding in bytes).
- lib/tests/push-guard.test.sh: 61 checks (push forms, over-blocks,
  invalid value, global key, fail-closed trap, no-jq, wiring, banner).

Known limits are listed in the hook header; the soft_deny rule is the
backstop. Run C (skills that push on their own) and run D (fail-closed
readers everywhere) follow. Do not enable manual mode at work before C.
2026-10-07 12:24:35 +02:00
bchanot 16c3dc8fbb chore(memory): BDR-111 + LRN-191..193 — feat manual-push-mode run A 2026-10-06 18:03:36 +02:00
bchanot afd6073371 docs(gitflow): manual-push mode — SETTINGS push discipline, gitflow skill rows, CHANGELOG 2026-10-06 18:03:35 +02:00
bchanot e6cccc1740 chore(memory): journal + contract/plan — feat manual-push-mode run A 2026-10-06 17:50:24 +02:00
bchanot 2fc88304ac feat(gitflow): manual-push mode honoured by the lib, quiet unpushed-guard
`gitflow.autopush false` (human-set git config) now means "nothing is
pushed" end to end, not only in the post-commit/post-merge hooks:

- lib/gitflow.sh: `_gitflow_push_off` is the single reader of
  GITFLOW_NO_PUSH / gitflow.autopush for the lib's push sites; `start`
  and `finish` stop pushing in manual mode. `gitflow_delete` checks out
  the base that contains the branch and drops a lagging upstream before
  `git branch -d` (LRN-161: `-d` judges against the upstream when set).
  Skipped remote deletes say `left in place`; `_gitflow_sync_base`
  replaces the silent `pull --ff-only || true` and warns when a base is
  behind origin and cannot fast-forward.
- hooks/unpushed-guard.sh: manual mode is silent at Stop and gives one
  `ℹ manual push mode:` line at SessionStart counting every local
  branch; an unparseable value is named and treated as auto.
- CLAUDE.global.md: manual-push mode doctrine, "ahead = defect" scoped
  to auto mode.
- Tests: gitflow-test T18m block (T18m0, T18i-T18o, 7 cases),
  unpushed-guard T10-T16.

Follow-ups (TODO.md): run B push-guard hook + settings deny widening +
banner; run C skills that push on their own (/close STEP 5C, …).
Do not enable manual mode on the work machine before B and C land.
2026-10-06 17:49:20 +02:00
bchanot fa67664bac chore(memory): journal — release 2.0.0 cut and tagged 2026-10-06 2026-10-06 16:15:10 +02:00