Skip to content

Commit 4a5523d

Browse files
authored
Complete Milestone 2 session-controller closeout (#17)
* Complete Milestone 2 session-controller closeout * Link Milestone 2 release note to PR #17 * chore(hooks): enforce semantic commits locally --------- Co-authored-by: Hanna Rosengren <4538260+hannasoderstromdev@users.noreply.github.com>
1 parent a8f2875 commit 4a5523d

11 files changed

Lines changed: 262 additions & 61 deletions

File tree

.githooks/commit-msg

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#!/bin/sh
2+
3+
set -eu
4+
5+
MSG_FILE="$1"
6+
MSG=$(head -n 1 "$MSG_FILE" | tr -d '\r')
7+
8+
# Allow merge/revert commits created by Git.
9+
case "$MSG" in
10+
Merge\ *|Revert\ *)
11+
exit 0
12+
;;
13+
esac
14+
15+
# Conventional/semantic commit format:
16+
# type(scope)!: subject
17+
# type!: subject
18+
# type: subject
19+
PATTERN='^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([a-z0-9._/-]+\))?(!)?: .+$'
20+
21+
if printf '%s' "$MSG" | grep -Eq "$PATTERN"; then
22+
exit 0
23+
fi
24+
25+
cat <<'EOF'
26+
ERROR: Commit message must follow semantic commit format.
27+
28+
Expected:
29+
type(scope): short summary
30+
type: short summary
31+
type!: short summary
32+
33+
Allowed types:
34+
feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert
35+
36+
Examples:
37+
feat(router): add session controller extraction
38+
fix(cli): handle stale resume retry path
39+
chore: update release log links
40+
EOF
41+
42+
exit 1

.githooks/pre-commit

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#!/bin/sh
2+
3+
set -eu
4+
5+
branch_name=$(git symbolic-ref --quiet --short HEAD 2>/dev/null || true)
6+
7+
if [ "$branch_name" = "main" ]; then
8+
echo "ERROR: Direct commits to main are blocked locally. Commit from a feature branch."
9+
exit 1
10+
fi
11+
12+
# Friendly reminder: semantic message validation runs in commit-msg.
13+
echo "pre-commit: branch check passed (semantic message will be validated by commit-msg)."
14+
15+
exit 0

docs/CI-CD.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,22 @@ For each release:
7070
1. Merge a PR into `main` with conventional commit text (`feat:`, `fix:`, or `!`/`BREAKING CHANGE`).
7171
2. Release workflow creates a `Release x.y.z` commit and `vX.Y.Z` tag.
7272
3. Publish workflow runs on the new tag and publishes to npm.
73+
74+
## Local Hook Guardrails
75+
76+
To reduce accidental non-semantic commit messages locally, this repo includes git hooks under `.githooks`:
77+
78+
- `commit-msg` enforces semantic commit format (`type(scope): subject`).
79+
- `pre-commit` blocks direct commits to `main` and reminds that semantic validation runs at commit message time.
80+
81+
Install once per clone:
82+
83+
```bash
84+
npm run hooks:install
85+
```
86+
87+
Verify current hook path:
88+
89+
```bash
90+
npm run hooks:doctor
91+
```

docs/ROUTER-PHASE-PLAN.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,10 @@ Acceptance criteria:
126126

127127
### Milestone 2: Session Controller And Policy Upgrade
128128

129+
Status: complete (2026-05-11)
130+
131+
Decision record: see `DEC-2026-05-11-milestone-2-session-controller-policy-closeout` in `docs/decision-log.md`.
132+
129133
Move from mostly prompt-local routing to session-aware routing.
130134

131135
Required work:

docs/decision-log.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,3 +163,43 @@ Consequences:
163163
Follow-up:
164164
- Next review milestone: Milestone 2 slice 2 planning.
165165
- Linked artifacts (logs, fixtures, docs, PRs): src/router/router.js, src/switchboard/workflow.js, test/router.test.js, test/switchboard-workflow.test.js
166+
167+
## Milestone 2 Closeout
168+
169+
Decision ID: DEC-2026-05-11-milestone-2-session-controller-policy-closeout
170+
Related deferred item: Milestone 2 staged policy depth
171+
Status: committed
172+
Date: 2026-05-11
173+
Owners: team
174+
175+
Context:
176+
- Milestone 2 required a deterministic and testable session controller boundary, continuity-aware switching, and explicit hard/soft policy inputs in explainable router outputs.
177+
178+
Options considered:
179+
- Option A: keep session mode transitions embedded in the router implementation.
180+
- Option B: extract session mode transition ownership into a dedicated controller module and lock behavior with direct tests.
181+
182+
Tradeoffs:
183+
- Option A: lower short-term change set, weaker boundary clarity and weaker transition-focused test surface.
184+
- Option B: clearer boundary ownership and stronger deterministic validation, with small refactor overhead.
185+
186+
Verification signal:
187+
- Expected signal from phase plan: mode transition logic deterministic and testable; policy decisions explainable with session/task/constraint/continuity context; route labels continue mapping cleanly.
188+
- Evidence observed:
189+
- Session mode transition logic extracted to `src/router/session_controller.js` and consumed by router.
190+
- Dedicated transition tests added in `test/session-controller.test.js`.
191+
- Full suite passes (`npm test`) with continuity, escalation, override, and hard-constraint behavior preserved.
192+
193+
Decision:
194+
- Chosen option: Option B.
195+
- Scope of commitment: Milestone 2 is formally complete as of 2026-05-11.
196+
- What remains intentionally deferred: numeric continuity scoring, taxonomy freeze, strict universal enforcement defaults, and cross-surface validation beyond Claude workflow.
197+
198+
Consequences:
199+
- Near-term implementation impact: session-controller boundary is now explicit and reusable for Milestone 3 router-client integration work.
200+
- Test and replay impact: transition behavior is isolated and easier to validate independently of broader router selection logic.
201+
- Migration impact: low; external route output behavior remains stable.
202+
203+
Follow-up:
204+
- Next review milestone: Milestone 3 plan-to-implementation checkpoint.
205+
- Linked artifacts (logs, fixtures, docs, PRs): src/router/router.js, src/router/session_controller.js, test/session-controller.test.js, test/router.test.js, docs/ROUTER-PHASE-PLAN.md

docs/release-log.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
## Unreleased
22

3+
### 2026-05-11 — Close Milestone 2 with explicit session-controller boundary
4+
5+
- What changed: Extracted deterministic session mode-transition ownership into a dedicated controller module, wired router mode resolution through that boundary, and added focused transition tests while preserving existing routing behavior.
6+
- Why it matters: Completes Milestone 2 policy/controller acceptance criteria and reduces coupling before Milestone 3 Claude-workflow boundary refit.
7+
- Who is affected: Switchboard maintainers and contributors working on router-policy evolution.
8+
- Action needed: None.
9+
- PR: https://github.com/hannasdev/model-switchboard/pull/17
10+
311
### 2026-05-10 — Add deterministic escalation policy and escalation evidence in explain output
412

513
- What changed: Added explicit escalation policy handling for low-confidence turns, user corrections, repeated failures, and high-risk implementation; propagated escalation fields into route evidence and human explain output.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
},
2929
"scripts": {
3030
"test": "node --test test/*.test.js",
31+
"hooks:install": "sh scripts/install-git-hooks.sh",
32+
"hooks:doctor": "git config --get core.hooksPath",
3133
"release": "release-it",
3234
"test:fuzz": "node --test test/router.fuzz.js",
3335
"check:anthropic": "node bin/check.js --vendor anthropic",

scripts/install-git-hooks.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/bin/sh
2+
3+
set -eu
4+
5+
git config core.hooksPath .githooks
6+
chmod +x .githooks/pre-commit .githooks/commit-msg
7+
8+
echo "Installed local git hooks at .githooks"
9+
echo "Current core.hooksPath: $(git config --get core.hooksPath)"

src/router/router.js

Lines changed: 1 addition & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,4 @@
1-
const MODE_TO_REQUIREMENTS = {
2-
plan: ["chat", "reasoning", "structured_output"],
3-
implement: ["repo_context", "file_read", "file_edit"],
4-
debug: ["repo_context", "file_read", "shell_execution", "test_execution"],
5-
review: ["repo_context", "file_read", "reasoning", "structured_output"],
6-
summarize: ["chat", "structured_output"],
7-
agent_workflow: ["chat", "reasoning", "structured_output"],
8-
out_of_domain: ["chat"]
9-
};
10-
11-
const MODE_VALUES = new Set(Object.keys(MODE_TO_REQUIREMENTS));
1+
import { MODE_TO_REQUIREMENTS, resolveSessionMode } from "./session_controller.js";
122

133
const MODE_TO_CLASS = {
144
plan: "medium_reasoning",
@@ -153,56 +143,6 @@ export function classifyPrompt(input) {
153143
};
154144
}
155145

156-
function resolveSessionMode(session = {}, classification = {}) {
157-
const currentMode = MODE_VALUES.has(session.mode) ? session.mode : null;
158-
const proposedMode = MODE_VALUES.has(classification.proposedMode)
159-
? classification.proposedMode
160-
: "plan";
161-
162-
if (!currentMode) {
163-
return {
164-
previousMode: null,
165-
proposedMode,
166-
resolvedMode: proposedMode,
167-
transitionReason: "no_previous_mode"
168-
};
169-
}
170-
171-
if (classification.explicitModeShift) {
172-
return {
173-
previousMode: currentMode,
174-
proposedMode,
175-
resolvedMode: proposedMode,
176-
transitionReason: "explicit_task_signal"
177-
};
178-
}
179-
180-
if (proposedMode === "summarize") {
181-
return {
182-
previousMode: currentMode,
183-
proposedMode,
184-
resolvedMode: proposedMode,
185-
transitionReason: "acknowledgement_summary"
186-
};
187-
}
188-
189-
if (!classification.modeStrongSignal && ["implement", "debug", "review"].includes(currentMode)) {
190-
return {
191-
previousMode: currentMode,
192-
proposedMode,
193-
resolvedMode: currentMode,
194-
transitionReason: "preserve_current_mode_for_ambiguous_turn"
195-
};
196-
}
197-
198-
return {
199-
previousMode: currentMode,
200-
proposedMode,
201-
resolvedMode: proposedMode,
202-
transitionReason: "default_mode_resolution"
203-
};
204-
}
205-
206146
function buildRequiredCapabilities(resolvedMode, taskType) {
207147
const modeRequirements = MODE_TO_REQUIREMENTS[resolvedMode] || ["chat"];
208148
const additionalRequirements = TASK_TYPE_TO_ADDITIONAL_REQUIREMENTS[taskType] || [];

src/router/session_controller.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
const MODE_TO_REQUIREMENTS = {
2+
plan: ["chat", "reasoning", "structured_output"],
3+
implement: ["repo_context", "file_read", "file_edit"],
4+
debug: ["repo_context", "file_read", "shell_execution", "test_execution"],
5+
review: ["repo_context", "file_read", "reasoning", "structured_output"],
6+
summarize: ["chat", "structured_output"],
7+
agent_workflow: ["chat", "reasoning", "structured_output"],
8+
out_of_domain: ["chat"]
9+
};
10+
11+
const MODE_VALUES = new Set(Object.keys(MODE_TO_REQUIREMENTS));
12+
13+
export { MODE_TO_REQUIREMENTS, MODE_VALUES };
14+
15+
export function resolveSessionMode(session = {}, classification = {}) {
16+
const currentMode = MODE_VALUES.has(session.mode) ? session.mode : null;
17+
const proposedMode = MODE_VALUES.has(classification.proposedMode)
18+
? classification.proposedMode
19+
: "plan";
20+
21+
if (!currentMode) {
22+
return {
23+
previousMode: null,
24+
proposedMode,
25+
resolvedMode: proposedMode,
26+
transitionReason: "no_previous_mode"
27+
};
28+
}
29+
30+
if (classification.explicitModeShift) {
31+
return {
32+
previousMode: currentMode,
33+
proposedMode,
34+
resolvedMode: proposedMode,
35+
transitionReason: "explicit_task_signal"
36+
};
37+
}
38+
39+
if (proposedMode === "summarize") {
40+
return {
41+
previousMode: currentMode,
42+
proposedMode,
43+
resolvedMode: proposedMode,
44+
transitionReason: "acknowledgement_summary"
45+
};
46+
}
47+
48+
if (!classification.modeStrongSignal && ["implement", "debug", "review"].includes(currentMode)) {
49+
return {
50+
previousMode: currentMode,
51+
proposedMode,
52+
resolvedMode: currentMode,
53+
transitionReason: "preserve_current_mode_for_ambiguous_turn"
54+
};
55+
}
56+
57+
return {
58+
previousMode: currentMode,
59+
proposedMode,
60+
resolvedMode: proposedMode,
61+
transitionReason: "default_mode_resolution"
62+
};
63+
}

0 commit comments

Comments
 (0)