JacobLinCool Codex commited on
Commit
9eec184
·
verified ·
1 Parent(s): 4c44bc4

refactor: rename session targets to goals

Browse files

Co-authored-by: Codex <noreply@openai.com>

app.py CHANGED
@@ -19,7 +19,7 @@ from hackathon_advisor.lora_training_kit import TRAINING_KIT_FILENAME, build_lor
19
  from hackathon_advisor.prize_ledger import prize_ledger
20
  from hackathon_advisor.submission_packet import build_submission_packet_markdown
21
  from hackathon_advisor.tool_contracts import resolve_tool_call, tool_schemas
22
- from hackathon_advisor.tools import TARGETS, target_profiles
23
  from hackathon_advisor.trace_export import build_trace_jsonl, trace_metadata
24
 
25
 
@@ -70,9 +70,9 @@ def bootstrap() -> dict:
70
  **trace_metadata(index),
71
  "top_projects": [project.to_public_dict() for project in index.top_projects(limit=8)],
72
  "whitespace": [item.to_dict() for item in index.find_whitespace(limit=5)],
73
- "target_options": TARGETS,
74
- "target_profiles": target_profiles(),
75
- "default_targets": TARGETS[:3],
76
  "profile_fields": PROFILE_FIELDS,
77
  "prize_ledger": prize_ledger(runtime_status),
78
  }
 
19
  from hackathon_advisor.prize_ledger import prize_ledger
20
  from hackathon_advisor.submission_packet import build_submission_packet_markdown
21
  from hackathon_advisor.tool_contracts import resolve_tool_call, tool_schemas
22
+ from hackathon_advisor.tools import GOALS, goal_profiles
23
  from hackathon_advisor.trace_export import build_trace_jsonl, trace_metadata
24
 
25
 
 
70
  **trace_metadata(index),
71
  "top_projects": [project.to_public_dict() for project in index.top_projects(limit=8)],
72
  "whitespace": [item.to_dict() for item in index.find_whitespace(limit=5)],
73
+ "goal_options": GOALS,
74
+ "goal_profiles": goal_profiles(),
75
+ "default_goals": GOALS[:3],
76
  "profile_fields": PROFILE_FIELDS,
77
  "prize_ledger": prize_ledger(runtime_status),
78
  }
hackathon_advisor/agent.py CHANGED
@@ -10,14 +10,14 @@ from hackathon_advisor.model_runtime import ToolPlanner, create_tool_planner, ru
10
  from hackathon_advisor.scoring import ScoreCard
11
  from hackathon_advisor.tool_contracts import ToolCall
12
  from hackathon_advisor.tools import (
13
- TARGETS,
14
  AdvisorTools,
15
  Idea,
16
  ToolEvent,
 
 
17
  idea_from_text,
18
- normalize_targets,
19
- target_label,
20
- targets_from_state,
21
  )
22
  from hackathon_advisor.wood_map import build_wood_map
23
 
@@ -62,7 +62,7 @@ class AdvisorEngine:
62
  state = dict(state or {})
63
  state.setdefault("ideas", [])
64
  state.setdefault("profile", {})
65
- state.setdefault("targets", TARGETS[:3])
66
  normalized, corrections = normalize_text(message)
67
  resolution = self.planner.plan(normalized, state)
68
  state["last_tool_resolution"] = resolution.to_dict()
@@ -129,7 +129,7 @@ class AdvisorEngine:
129
  return self._profile_turn(call, normalized, corrections, state, tool_events)
130
 
131
  if call.name == "set_goals":
132
- return self._target_turn(call, normalized, corrections, state, tool_events)
133
 
134
  return self._idea_research_turn(call, normalized, corrections, state, tool_events)
135
 
@@ -177,13 +177,13 @@ class AdvisorEngine:
177
  current_id = state.get("current_idea_id")
178
  for item in state.get("ideas", []):
179
  if item.get("id") == current_id:
180
- return self._with_session_targets(Idea(**item), state)
181
  if state.get("ideas"):
182
- return self._with_session_targets(Idea(**state["ideas"][-1]), state)
183
  return None
184
 
185
- def _with_session_targets(self, idea: Idea, state: dict[str, Any]) -> Idea:
186
- idea.targets = targets_from_state(state)
187
  return idea
188
 
189
  def _profile_context(self, state: dict[str, Any]) -> dict[str, Any]:
@@ -347,7 +347,7 @@ class AdvisorEngine:
347
  response = f"Profile updated: {field} = {profile[field]}."
348
  return self._result(normalized, corrections, response, state, tool_events, [], [], None, [], {})
349
 
350
- def _target_turn(
351
  self,
352
  call: ToolCall,
353
  normalized: str,
@@ -355,14 +355,14 @@ class AdvisorEngine:
355
  state: dict[str, Any],
356
  tool_events: list[ToolEvent],
357
  ) -> TurnResult:
358
- targets = normalize_targets(call.arguments.get("goals"), default=[])
359
- state["targets"] = targets
360
  idea = self._current_idea(state)
361
  if idea is not None:
362
- idea.targets = targets
363
  self._store_idea(state, idea)
364
- tool_events.append(ToolEvent("set_goals", f"Set {len(targets)} goals."))
365
- labels = [target_label(target) for target in targets]
366
  response = "The seal will now bias toward: " + (", ".join(labels) or "no specific goals")
367
  return self._result(normalized, corrections, response, state, tool_events, [], [], None, [], {})
368
 
@@ -371,7 +371,7 @@ class AdvisorEngine:
371
  if idea_id:
372
  for item in state.get("ideas", []):
373
  if item.get("id") == idea_id:
374
- return self._with_session_targets(Idea(**item), state)
375
  return self._current_idea(state)
376
 
377
  def _record_trace(
@@ -442,7 +442,7 @@ class AdvisorEngine:
442
  ranked: list[tuple[Idea, ScoreCard]] = []
443
  for item in state.get("ideas", []):
444
  try:
445
- idea = self._with_session_targets(Idea(**item), state)
446
  except TypeError:
447
  continue
448
  score, _event = self.tools.score_idea(idea)
 
10
  from hackathon_advisor.scoring import ScoreCard
11
  from hackathon_advisor.tool_contracts import ToolCall
12
  from hackathon_advisor.tools import (
13
+ GOALS,
14
  AdvisorTools,
15
  Idea,
16
  ToolEvent,
17
+ goal_label,
18
+ goals_from_state,
19
  idea_from_text,
20
+ normalize_goals,
 
 
21
  )
22
  from hackathon_advisor.wood_map import build_wood_map
23
 
 
62
  state = dict(state or {})
63
  state.setdefault("ideas", [])
64
  state.setdefault("profile", {})
65
+ state.setdefault("goals", GOALS[:3])
66
  normalized, corrections = normalize_text(message)
67
  resolution = self.planner.plan(normalized, state)
68
  state["last_tool_resolution"] = resolution.to_dict()
 
129
  return self._profile_turn(call, normalized, corrections, state, tool_events)
130
 
131
  if call.name == "set_goals":
132
+ return self._goal_turn(call, normalized, corrections, state, tool_events)
133
 
134
  return self._idea_research_turn(call, normalized, corrections, state, tool_events)
135
 
 
177
  current_id = state.get("current_idea_id")
178
  for item in state.get("ideas", []):
179
  if item.get("id") == current_id:
180
+ return self._with_session_goals(Idea(**item), state)
181
  if state.get("ideas"):
182
+ return self._with_session_goals(Idea(**state["ideas"][-1]), state)
183
  return None
184
 
185
+ def _with_session_goals(self, idea: Idea, state: dict[str, Any]) -> Idea:
186
+ idea.goals = goals_from_state(state)
187
  return idea
188
 
189
  def _profile_context(self, state: dict[str, Any]) -> dict[str, Any]:
 
347
  response = f"Profile updated: {field} = {profile[field]}."
348
  return self._result(normalized, corrections, response, state, tool_events, [], [], None, [], {})
349
 
350
+ def _goal_turn(
351
  self,
352
  call: ToolCall,
353
  normalized: str,
 
355
  state: dict[str, Any],
356
  tool_events: list[ToolEvent],
357
  ) -> TurnResult:
358
+ goals = normalize_goals(call.arguments.get("goals"), default=[])
359
+ state["goals"] = goals
360
  idea = self._current_idea(state)
361
  if idea is not None:
362
+ idea.goals = goals
363
  self._store_idea(state, idea)
364
+ tool_events.append(ToolEvent("set_goals", f"Set {len(goals)} goals."))
365
+ labels = [goal_label(goal) for goal in goals]
366
  response = "The seal will now bias toward: " + (", ".join(labels) or "no specific goals")
367
  return self._result(normalized, corrections, response, state, tool_events, [], [], None, [], {})
368
 
 
371
  if idea_id:
372
  for item in state.get("ideas", []):
373
  if item.get("id") == idea_id:
374
+ return self._with_session_goals(Idea(**item), state)
375
  return self._current_idea(state)
376
 
377
  def _record_trace(
 
442
  ranked: list[tuple[Idea, ScoreCard]] = []
443
  for item in state.get("ideas", []):
444
  try:
445
+ idea = self._with_session_goals(Idea(**item), state)
446
  except TypeError:
447
  continue
448
  score, _event = self.tools.score_idea(idea)
hackathon_advisor/chapter.py CHANGED
@@ -3,12 +3,12 @@ from __future__ import annotations
3
  from datetime import datetime, timezone
4
  from typing import Any
5
 
6
- from hackathon_advisor.tools import target_label
7
 
8
 
9
  def build_chapter_markdown(session: dict[str, Any], metadata: dict[str, Any]) -> str:
10
  ideas = _list_of_dicts(session.get("ideas"))
11
- goals = _goal_labels(session.get("targets"))
12
  artifact = session.get("last_artifact") if isinstance(session.get("last_artifact"), dict) else {}
13
  lines = [
14
  "# The Unwritten Almanac Chapter",
@@ -35,7 +35,7 @@ def build_chapter_markdown(session: dict[str, Any], metadata: dict[str, Any]) ->
35
  def _idea_page(index: int, idea: dict[str, Any]) -> list[str]:
36
  title = _clean(idea.get("title") or f"Page {index}")
37
  pitch = _clean(idea.get("pitch"))
38
- goals = _goal_labels(idea.get("targets"))
39
  score = idea.get("score") if isinstance(idea.get("score"), dict) else {}
40
  verdict = _clean(score.get("verdict")) if score else "DRAFT"
41
  overall = _clean(score.get("overall")) if score else "0.0"
@@ -74,7 +74,7 @@ def _list_of_dicts(value: Any) -> list[dict[str, Any]]:
74
  def _goal_labels(value: Any) -> list[str]:
75
  if not isinstance(value, list):
76
  return []
77
- return [target_label(str(target)) for target in value]
78
 
79
 
80
  def _clean(value: Any) -> str:
 
3
  from datetime import datetime, timezone
4
  from typing import Any
5
 
6
+ from hackathon_advisor.tools import goal_label
7
 
8
 
9
  def build_chapter_markdown(session: dict[str, Any], metadata: dict[str, Any]) -> str:
10
  ideas = _list_of_dicts(session.get("ideas"))
11
+ goals = _goal_labels(session.get("goals"))
12
  artifact = session.get("last_artifact") if isinstance(session.get("last_artifact"), dict) else {}
13
  lines = [
14
  "# The Unwritten Almanac Chapter",
 
35
  def _idea_page(index: int, idea: dict[str, Any]) -> list[str]:
36
  title = _clean(idea.get("title") or f"Page {index}")
37
  pitch = _clean(idea.get("pitch"))
38
+ goals = _goal_labels(idea.get("goals"))
39
  score = idea.get("score") if isinstance(idea.get("score"), dict) else {}
40
  verdict = _clean(score.get("verdict")) if score else "DRAFT"
41
  overall = _clean(score.get("overall")) if score else "0.0"
 
74
  def _goal_labels(value: Any) -> list[str]:
75
  if not isinstance(value, list):
76
  return []
77
+ return [goal_label(str(goal)) for goal in value]
78
 
79
 
80
  def _clean(value: Any) -> str:
hackathon_advisor/demo_rehearsal.py CHANGED
@@ -14,7 +14,7 @@ DEMO_PROFILE = {
14
  "preferences": "auditable artifacts, local-first runtime, strong demo beat",
15
  "constraints": "CPU Space runtime; no proprietary inference API",
16
  }
17
- DEMO_TARGETS = [
18
  "Off the Grid",
19
  "Well-Tuned",
20
  "Off-Brand",
@@ -26,7 +26,7 @@ DEMO_TARGETS = [
26
  def build_demo_rehearsal(engine: Any) -> dict[str, Any]:
27
  initial_state = {
28
  "profile": dict(DEMO_PROFILE),
29
- "targets": list(DEMO_TARGETS),
30
  }
31
  first = engine.turn(DEMO_PROMPT, initial_state)
32
  second = engine.turn(DEMO_PLAN_PROMPT, first.state)
 
14
  "preferences": "auditable artifacts, local-first runtime, strong demo beat",
15
  "constraints": "CPU Space runtime; no proprietary inference API",
16
  }
17
+ DEMO_GOALS = [
18
  "Off the Grid",
19
  "Well-Tuned",
20
  "Off-Brand",
 
26
  def build_demo_rehearsal(engine: Any) -> dict[str, Any]:
27
  initial_state = {
28
  "profile": dict(DEMO_PROFILE),
29
+ "goals": list(DEMO_GOALS),
30
  }
31
  first = engine.turn(DEMO_PROMPT, initial_state)
32
  second = engine.turn(DEMO_PLAN_PROMPT, first.state)
hackathon_advisor/field_notes.py CHANGED
@@ -3,14 +3,14 @@ from __future__ import annotations
3
  from datetime import datetime, timezone
4
  from typing import Any
5
 
6
- from hackathon_advisor.tools import target_label
7
 
8
 
9
  def build_field_notes_markdown(session: dict[str, Any], metadata: dict[str, Any]) -> str:
10
  ideas = _list_of_dicts(session.get("ideas"))
11
  trace = _list_of_dicts(session.get("trace"))
12
  profile = session.get("profile") if isinstance(session.get("profile"), dict) else {}
13
- goals = _goal_labels(session.get("targets"))
14
  last_plan = [str(step) for step in session.get("last_plan") or []]
15
  last_artifact = session.get("last_artifact") if isinstance(session.get("last_artifact"), dict) else {}
16
 
@@ -91,7 +91,7 @@ def build_field_notes_markdown(session: dict[str, Any], metadata: dict[str, Any]
91
  def _idea_section(index: int, idea: dict[str, Any]) -> list[str]:
92
  title = _clean(idea.get("title") or f"Idea {index}")
93
  pitch = _clean(idea.get("pitch"))
94
- goals = _goal_labels(idea.get("targets"))
95
  score = idea.get("score") if isinstance(idea.get("score"), dict) else {}
96
  lines = [
97
  f"### {index}. {title}",
@@ -155,7 +155,7 @@ def _list_of_dicts(value: Any) -> list[dict[str, Any]]:
155
  def _goal_labels(value: Any) -> list[str]:
156
  if not isinstance(value, list):
157
  return []
158
- return [target_label(str(target)) for target in value]
159
 
160
 
161
  def _clean(value: Any) -> str:
 
3
  from datetime import datetime, timezone
4
  from typing import Any
5
 
6
+ from hackathon_advisor.tools import goal_label
7
 
8
 
9
  def build_field_notes_markdown(session: dict[str, Any], metadata: dict[str, Any]) -> str:
10
  ideas = _list_of_dicts(session.get("ideas"))
11
  trace = _list_of_dicts(session.get("trace"))
12
  profile = session.get("profile") if isinstance(session.get("profile"), dict) else {}
13
+ goals = _goal_labels(session.get("goals"))
14
  last_plan = [str(step) for step in session.get("last_plan") or []]
15
  last_artifact = session.get("last_artifact") if isinstance(session.get("last_artifact"), dict) else {}
16
 
 
91
  def _idea_section(index: int, idea: dict[str, Any]) -> list[str]:
92
  title = _clean(idea.get("title") or f"Idea {index}")
93
  pitch = _clean(idea.get("pitch"))
94
+ goals = _goal_labels(idea.get("goals"))
95
  score = idea.get("score") if isinstance(idea.get("score"), dict) else {}
96
  lines = [
97
  f"### {index}. {title}",
 
155
  def _goal_labels(value: Any) -> list[str]:
156
  if not isinstance(value, list):
157
  return []
158
+ return [goal_label(str(goal)) for goal in value]
159
 
160
 
161
  def _clean(value: Any) -> str:
hackathon_advisor/lora_dataset.py CHANGED
@@ -23,8 +23,8 @@ RESPONSE_SYSTEM_PROMPT = (
23
  def build_lora_dataset_jsonl(session: dict[str, Any], metadata: dict[str, Any]) -> str:
24
  trace = _list_of_dicts(session.get("trace"))
25
  ideas = _list_of_dicts(session.get("ideas"))
26
- targets = [str(target) for target in session.get("targets") or []]
27
- examples = _examples(trace, targets)
28
  records = [
29
  {
30
  "type": "lora_sft_manifest",
@@ -47,7 +47,7 @@ def build_lora_dataset_jsonl(session: dict[str, Any], metadata: dict[str, Any])
47
  return "\n".join(json.dumps(record, ensure_ascii=False, sort_keys=True) for record in records) + "\n"
48
 
49
 
50
- def _examples(trace: list[dict[str, Any]], targets: list[str]) -> list[dict[str, Any]]:
51
  examples: list[dict[str, Any]] = []
52
  for turn_index, event in enumerate(trace, start=1):
53
  if not _is_successful_turn(event):
@@ -65,7 +65,7 @@ def _examples(trace: list[dict[str, Any]], targets: list[str]) -> list[dict[str,
65
  "base_model": BASE_MODEL,
66
  "adapter_task": ADAPTER_TASK,
67
  "turn_index": turn_index,
68
- "targets": targets,
69
  "score": _score(event),
70
  "tool_call": tool_call,
71
  "tool_observations": _tool_observations(event),
 
23
  def build_lora_dataset_jsonl(session: dict[str, Any], metadata: dict[str, Any]) -> str:
24
  trace = _list_of_dicts(session.get("trace"))
25
  ideas = _list_of_dicts(session.get("ideas"))
26
+ goals = [str(goal) for goal in session.get("goals") or []]
27
+ examples = _examples(trace, goals)
28
  records = [
29
  {
30
  "type": "lora_sft_manifest",
 
47
  return "\n".join(json.dumps(record, ensure_ascii=False, sort_keys=True) for record in records) + "\n"
48
 
49
 
50
+ def _examples(trace: list[dict[str, Any]], goals: list[str]) -> list[dict[str, Any]]:
51
  examples: list[dict[str, Any]] = []
52
  for turn_index, event in enumerate(trace, start=1):
53
  if not _is_successful_turn(event):
 
65
  "base_model": BASE_MODEL,
66
  "adapter_task": ADAPTER_TASK,
67
  "turn_index": turn_index,
68
+ "goals": goals,
69
  "score": _score(event),
70
  "tool_call": tool_call,
71
  "tool_observations": _tool_observations(event),
hackathon_advisor/scoring.py CHANGED
@@ -49,12 +49,12 @@ class ScoreCard:
49
  }
50
 
51
 
52
- def score_idea(index: ProjectIndex, title: str, pitch: str, targets: list[str] | None = None) -> ScoreCard:
53
  text = f"{title} {pitch}".strip()
54
  hits = index.search(text, limit=4)
55
  top_overlap = hits[0].score if hits else 0.0
56
  tokens = set(tokenize(text))
57
- targets = targets or []
58
 
59
  originality = clamp_score(10 - round(top_overlap * 18))
60
  delight = clamp_score(4 + _keyword_count(tokens, {"story", "visual", "game", "ritual", "share", "voice"}) * 2)
@@ -67,7 +67,7 @@ def score_idea(index: ProjectIndex, title: str, pitch: str, targets: list[str] |
67
  goal_fit = clamp_score(
68
  4
69
  + _keyword_count(tokens, {"local", "offline", "small", "llama", "fine", "trace", "gradio"}) * 2
70
- + min(len(targets), 3)
71
  )
72
  verdict = "UNWRITTEN" if top_overlap < 0.16 else f"ECHO x{sum(1 for hit in hits if hit.score >= 0.12)}"
73
  return ScoreCard(
 
49
  }
50
 
51
 
52
+ def score_idea(index: ProjectIndex, title: str, pitch: str, goals: list[str] | None = None) -> ScoreCard:
53
  text = f"{title} {pitch}".strip()
54
  hits = index.search(text, limit=4)
55
  top_overlap = hits[0].score if hits else 0.0
56
  tokens = set(tokenize(text))
57
+ goals = goals or []
58
 
59
  originality = clamp_score(10 - round(top_overlap * 18))
60
  delight = clamp_score(4 + _keyword_count(tokens, {"story", "visual", "game", "ritual", "share", "voice"}) * 2)
 
67
  goal_fit = clamp_score(
68
  4
69
  + _keyword_count(tokens, {"local", "offline", "small", "llama", "fine", "trace", "gradio"}) * 2
70
+ + min(len(goals), 3)
71
  )
72
  verdict = "UNWRITTEN" if top_overlap < 0.16 else f"ECHO x{sum(1 for hit in hits if hit.score >= 0.12)}"
73
  return ScoreCard(
hackathon_advisor/submission_packet.py CHANGED
@@ -16,7 +16,7 @@ def build_submission_packet_markdown(
16
  ) -> str:
17
  ideas = _list_of_dicts(session.get("ideas"))
18
  trace = _list_of_dicts(session.get("trace"))
19
- targets = [str(target) for target in session.get("targets") or []]
20
  last_plan = [str(step) for step in session.get("last_plan") or []]
21
  current = _current_idea(session, ideas)
22
  score = current.get("score") if isinstance(current.get("score"), dict) else {}
@@ -46,7 +46,7 @@ def build_submission_packet_markdown(
46
  "",
47
  f"- Title: {title}",
48
  f"- Verdict: {verdict} {overall}/10",
49
- f"- Targets: {', '.join(targets) if targets else 'No specific targets'}",
50
  f"- Pitch: {_clean(current.get('pitch')) or 'No pitch recorded.'}",
51
  "",
52
  ]
 
16
  ) -> str:
17
  ideas = _list_of_dicts(session.get("ideas"))
18
  trace = _list_of_dicts(session.get("trace"))
19
+ goals = [str(goal) for goal in session.get("goals") or []]
20
  last_plan = [str(step) for step in session.get("last_plan") or []]
21
  current = _current_idea(session, ideas)
22
  score = current.get("score") if isinstance(current.get("score"), dict) else {}
 
46
  "",
47
  f"- Title: {title}",
48
  f"- Verdict: {verdict} {overall}/10",
49
+ f"- Goals: {', '.join(goals) if goals else 'No specific goals'}",
50
  f"- Pitch: {_clean(current.get('pitch')) or 'No pitch recorded.'}",
51
  "",
52
  ]
hackathon_advisor/tools.py CHANGED
@@ -8,7 +8,7 @@ from hackathon_advisor.data import Project, ProjectIndex, WhitespaceItem
8
  from hackathon_advisor.scoring import ScoreCard, score_idea
9
 
10
 
11
- TARGETS = [
12
  "Off the Grid",
13
  "Well-Tuned",
14
  "Off-Brand",
@@ -17,7 +17,7 @@ TARGETS = [
17
  "Field Notes",
18
  ]
19
 
20
- TARGET_PROFILE_BY_ID = {
21
  "Off the Grid": {
22
  "label": "Local-first",
23
  "description": "Favor ideas that work without proprietary inference APIs.",
@@ -45,41 +45,41 @@ TARGET_PROFILE_BY_ID = {
45
  }
46
 
47
 
48
- def target_profiles() -> list[dict[str, str]]:
49
  return [
50
  {
51
- "id": target,
52
- "label": TARGET_PROFILE_BY_ID[target]["label"],
53
- "description": TARGET_PROFILE_BY_ID[target]["description"],
54
  }
55
- for target in TARGETS
56
  ]
57
 
58
 
59
- def target_label(target: str) -> str:
60
- return TARGET_PROFILE_BY_ID.get(target, {}).get("label", target)
61
 
62
 
63
- def normalize_targets(raw_targets: Any, default: list[str] | None = None) -> list[str]:
64
- if raw_targets is None:
65
  return list(default or [])
66
- if not isinstance(raw_targets, list):
67
  return list(default or [])
68
 
69
- targets: list[str] = []
70
  seen: set[str] = set()
71
- for raw_target in raw_targets:
72
- target = str(raw_target)
73
- if target in TARGETS and target not in seen:
74
- targets.append(target)
75
- seen.add(target)
76
- return targets
77
 
78
 
79
- def targets_from_state(state: dict[str, Any]) -> list[str]:
80
- if "targets" not in state:
81
- return TARGETS[:3]
82
- return normalize_targets(state.get("targets"), default=[])
83
 
84
 
85
  @dataclass
@@ -87,7 +87,7 @@ class Idea:
87
  id: str
88
  title: str
89
  pitch: str
90
- targets: list[str] = field(default_factory=lambda: TARGETS[:3])
91
  score: dict | None = None
92
 
93
  def to_dict(self) -> dict:
@@ -95,7 +95,7 @@ class Idea:
95
  "id": self.id,
96
  "title": self.title,
97
  "pitch": self.pitch,
98
- "targets": self.targets,
99
  "score": self.score,
100
  }
101
 
@@ -129,21 +129,21 @@ class AdvisorTools:
129
  def save_idea(self, state: dict[str, Any], title: str, pitch: str) -> tuple[Idea, ToolEvent]:
130
  ideas = [Idea(**item) for item in state.get("ideas", [])]
131
  current_id = state.get("current_idea_id")
132
- targets = targets_from_state(state)
133
  idea = next((item for item in ideas if item.id == current_id), None)
134
  if idea is None or _is_new_idea(idea, title, pitch):
135
- idea = Idea(id=uuid.uuid4().hex[:8], title=title, pitch=pitch, targets=targets)
136
  ideas.append(idea)
137
  else:
138
  idea.title = title
139
  idea.pitch = pitch
140
- idea.targets = targets
141
  state["ideas"] = [item.to_dict() for item in ideas]
142
  state["current_idea_id"] = idea.id
143
  return idea, ToolEvent("save_idea", f"Wrote idea page '{idea.title}'.")
144
 
145
  def score_idea(self, idea: Idea) -> tuple[ScoreCard, ToolEvent]:
146
- score = score_idea(self.index, idea.title, idea.pitch, idea.targets)
147
  idea.score = score.to_dict()
148
  return score, ToolEvent("score_idea", f"Pressed a five-quadrant seal: {score.overall}/10.")
149
 
@@ -158,7 +158,7 @@ class AdvisorTools:
158
  profile_steps = profile_plan_steps(profile)
159
  if profile_steps:
160
  plan[1:1] = profile_steps
161
- if any("Well" in target for target in idea.targets):
162
  plan.insert(
163
  max(0, len(plan) - 1),
164
  "Prepare a tiny LoRA dataset from successful advisor turns before training.",
 
8
  from hackathon_advisor.scoring import ScoreCard, score_idea
9
 
10
 
11
+ GOALS = [
12
  "Off the Grid",
13
  "Well-Tuned",
14
  "Off-Brand",
 
17
  "Field Notes",
18
  ]
19
 
20
+ GOAL_PROFILE_BY_ID = {
21
  "Off the Grid": {
22
  "label": "Local-first",
23
  "description": "Favor ideas that work without proprietary inference APIs.",
 
45
  }
46
 
47
 
48
+ def goal_profiles() -> list[dict[str, str]]:
49
  return [
50
  {
51
+ "id": goal,
52
+ "label": GOAL_PROFILE_BY_ID[goal]["label"],
53
+ "description": GOAL_PROFILE_BY_ID[goal]["description"],
54
  }
55
+ for goal in GOALS
56
  ]
57
 
58
 
59
+ def goal_label(goal: str) -> str:
60
+ return GOAL_PROFILE_BY_ID.get(goal, {}).get("label", goal)
61
 
62
 
63
+ def normalize_goals(raw_goals: Any, default: list[str] | None = None) -> list[str]:
64
+ if raw_goals is None:
65
  return list(default or [])
66
+ if not isinstance(raw_goals, list):
67
  return list(default or [])
68
 
69
+ goals: list[str] = []
70
  seen: set[str] = set()
71
+ for raw_goal in raw_goals:
72
+ goal = str(raw_goal)
73
+ if goal in GOALS and goal not in seen:
74
+ goals.append(goal)
75
+ seen.add(goal)
76
+ return goals
77
 
78
 
79
+ def goals_from_state(state: dict[str, Any]) -> list[str]:
80
+ if "goals" not in state:
81
+ return GOALS[:3]
82
+ return normalize_goals(state.get("goals"), default=[])
83
 
84
 
85
  @dataclass
 
87
  id: str
88
  title: str
89
  pitch: str
90
+ goals: list[str] = field(default_factory=lambda: GOALS[:3])
91
  score: dict | None = None
92
 
93
  def to_dict(self) -> dict:
 
95
  "id": self.id,
96
  "title": self.title,
97
  "pitch": self.pitch,
98
+ "goals": self.goals,
99
  "score": self.score,
100
  }
101
 
 
129
  def save_idea(self, state: dict[str, Any], title: str, pitch: str) -> tuple[Idea, ToolEvent]:
130
  ideas = [Idea(**item) for item in state.get("ideas", [])]
131
  current_id = state.get("current_idea_id")
132
+ goals = goals_from_state(state)
133
  idea = next((item for item in ideas if item.id == current_id), None)
134
  if idea is None or _is_new_idea(idea, title, pitch):
135
+ idea = Idea(id=uuid.uuid4().hex[:8], title=title, pitch=pitch, goals=goals)
136
  ideas.append(idea)
137
  else:
138
  idea.title = title
139
  idea.pitch = pitch
140
+ idea.goals = goals
141
  state["ideas"] = [item.to_dict() for item in ideas]
142
  state["current_idea_id"] = idea.id
143
  return idea, ToolEvent("save_idea", f"Wrote idea page '{idea.title}'.")
144
 
145
  def score_idea(self, idea: Idea) -> tuple[ScoreCard, ToolEvent]:
146
+ score = score_idea(self.index, idea.title, idea.pitch, idea.goals)
147
  idea.score = score.to_dict()
148
  return score, ToolEvent("score_idea", f"Pressed a five-quadrant seal: {score.overall}/10.")
149
 
 
158
  profile_steps = profile_plan_steps(profile)
159
  if profile_steps:
160
  plan[1:1] = profile_steps
161
+ if any("Well" in goal for goal in idea.goals):
162
  plan.insert(
163
  max(0, len(plan) - 1),
164
  "Prepare a tiny LoRA dataset from successful advisor turns before training.",
static/app.js CHANGED
@@ -8,7 +8,7 @@ const corrections = document.querySelector("#corrections");
8
  const projectsEl = document.querySelector("#projects");
9
  const whitespaceEl = document.querySelector("#whitespace");
10
  const ideasEl = document.querySelector("#ideas");
11
- const targetsEl = document.querySelector("#targets");
12
  const profileEl = document.querySelector("#profile");
13
  const woodMapEl = document.querySelector("#wood-map");
14
  const scoreEl = document.querySelector("#score");
@@ -22,14 +22,14 @@ const sealCopyEl = document.querySelector("#seal-copy");
22
  const verdictStampEl = document.querySelector("#verdict-stamp");
23
  const spreadEl = document.querySelector("#spread");
24
  const ideaCountEl = document.querySelector("#idea-count");
25
- const targetCountEl = document.querySelector("#target-count");
26
  const demoButton = document.querySelector("#load-demo");
27
  const exportButton = document.querySelector("#export-artifact");
28
  const exportNotesButton = document.querySelector("#export-notes");
29
  const exportChapterButton = document.querySelector("#export-chapter");
30
  const resetButton = document.querySelector("#reset-session");
31
 
32
- const SESSION_STORAGE_KEY = "hackathon-advisor-session-v1";
33
  const FIELD_NOTES_FILENAME = "hackathon-advisor-field-notes.md";
34
  const CHAPTER_FILENAME = "hackathon-advisor-chapter.md";
35
  const PNG_EXPORT_LABEL = "PNG";
@@ -37,9 +37,9 @@ const PNG_EXPORT_LABEL = "PNG";
37
  let session = {};
38
  let clientPromise = Client.connect(window.location.origin);
39
  let currentArtifact = null;
40
- let targetOptions = [];
41
- let targetProfiles = [];
42
- let targetProfileById = new Map();
43
  let profileFields = [];
44
  let turnWatchdog = null;
45
  let sawTurnToken = false;
@@ -89,16 +89,16 @@ resetButton.addEventListener("click", () => {
89
  resetSession();
90
  });
91
 
92
- targetsEl.addEventListener("change", (event) => {
93
  const target = event.target;
94
- if (!(target instanceof HTMLInputElement) || !target.dataset.target) return;
95
  const checked = new Set(
96
- Array.from(targetsEl.querySelectorAll("input[data-target]:checked")).map((input) => input.dataset.target),
97
  );
98
- session.targets = targetOptions.filter((option) => checked.has(option));
99
- syncCurrentIdeaTargets();
100
  saveSession();
101
- renderTargets(session.targets);
102
  renderIdeas(session.ideas || []);
103
  });
104
 
@@ -175,15 +175,15 @@ async function bootstrap() {
175
  if (!response.ok) throw new Error(`project index failed with ${response.status}`);
176
  const data = await response.json();
177
  bootstrapData = data;
178
- const rawProfiles = Array.isArray(data.target_profiles) ? data.target_profiles : [];
179
- const rawOptions = Array.isArray(data.target_options) ? data.target_options : [];
180
- targetProfiles = normalizeTargetProfiles(rawProfiles, rawOptions);
181
- targetOptions = targetProfiles.map((target) => target.id);
182
- targetProfileById = new Map(targetProfiles.map((target) => [target.id, target]));
183
  profileFields = data.profile_fields || [];
184
  session = normalizeSession(readSavedSession(), defaultSession(data));
185
  renderProvenance(data);
186
- renderTargets(session.targets);
187
  renderProfile(session.profile);
188
  renderRestoredSession(data);
189
  renderWhitespace(data.whitespace || []);
@@ -205,7 +205,7 @@ function handleBootstrapError(error) {
205
  renderScore(null);
206
  setVerdictDisplay("INDEX CLOSED", 0, null);
207
  renderWoodMap(null);
208
- renderTargets([]);
209
  renderProfile({});
210
  renderIdeas([]);
211
  renderProjects([]);
@@ -216,7 +216,7 @@ function handleBootstrapError(error) {
216
  function defaultSession(data = bootstrapData) {
217
  return {
218
  profile: {},
219
- targets: data?.default_targets || targetOptions.slice(0, 3),
220
  };
221
  }
222
 
@@ -292,7 +292,7 @@ function setActionButtonLabel(button, label) {
292
 
293
  function setSessionControlsDisabled(disabled) {
294
  sessionControlsLocked = disabled;
295
- targetsEl.querySelectorAll("input[data-target]").forEach((target) => {
296
  target.disabled = disabled;
297
  });
298
  profileEl.querySelectorAll("input[data-profile-field]").forEach((field) => {
@@ -320,7 +320,7 @@ function resetSession() {
320
  ink.textContent = "The book is open. The next page waits for its first line.";
321
  ink.classList.remove("thinking", "bleed", "gold");
322
  corrections.textContent = "Session reset.";
323
- renderTargets(session.targets);
324
  renderProfile(session.profile);
325
  renderScore(null);
326
  setVerdictDisplay("READY", 0, null);
@@ -366,7 +366,7 @@ async function loadDemoSession() {
366
  function applyDemoSession(data) {
367
  session = data.session || {};
368
  session.profile = session.profile || {};
369
- session.targets = Array.isArray(session.targets) ? session.targets : [];
370
  session.last_response = data.response || session.last_response || "";
371
  session.ui_status = `example loaded: ${data.turn_count || 0} advisor turns`;
372
  currentArtifact = data.artifact || session.last_artifact || null;
@@ -378,7 +378,7 @@ function applyDemoSession(data) {
378
  ink.classList.toggle("bleed", data.score.verdict.startsWith("ECHO"));
379
  ink.classList.toggle("gold", data.score.verdict.startsWith("UNWRITTEN"));
380
  }
381
- renderTargets(session.targets);
382
  renderProfile(session.profile);
383
  renderIdeas(session.ideas || []);
384
  renderPlan(data.plan || session.last_plan || []);
@@ -450,9 +450,9 @@ function normalizeSession(savedSession, defaultSession) {
450
  const normalized = { ...defaultSession };
451
  if (!savedSession) return normalized;
452
  normalized.profile = savedSession.profile && typeof savedSession.profile === "object" ? savedSession.profile : {};
453
- const savedTargets = Array.isArray(savedSession.targets) ? savedSession.targets : defaultSession.targets;
454
- normalized.targets = targetOptions.filter((option) => savedTargets.includes(option));
455
- if (!normalized.targets.length && defaultSession.targets?.length) normalized.targets = [...defaultSession.targets];
456
  if (Array.isArray(savedSession.ideas)) normalized.ideas = savedSession.ideas;
457
  if (Array.isArray(savedSession.trace)) normalized.trace = savedSession.trace;
458
  if (Array.isArray(savedSession.last_plan)) normalized.last_plan = savedSession.last_plan;
@@ -472,7 +472,7 @@ function restoreSessionCopy() {
472
  if (status) corrections.textContent = status;
473
  }
474
 
475
- function normalizeTargetProfiles(profiles, options) {
476
  const byId = new Map(
477
  profiles
478
  .filter((profile) => profile && typeof profile.id === "string")
@@ -504,22 +504,22 @@ function clearSavedSession() {
504
  }
505
  }
506
 
507
- function renderTargets(selectedTargets) {
508
- const selected = new Set(selectedTargets || []);
509
- if (targetCountEl) targetCountEl.textContent = selected.size;
510
- targetsEl.innerHTML = "";
511
- if (!targetOptions.length) {
512
- targetsEl.innerHTML = `<div class="empty">No goals loaded.</div>`;
513
  return;
514
  }
515
- for (const option of targetOptions) {
516
- const profile = targetProfileById.get(option) || { label: option, description: "" };
517
  const label = document.createElement("label");
518
- label.className = `target-toggle goal ${selected.has(option) ? "on" : ""}`;
519
  label.innerHTML = `
520
  <input
521
  type="checkbox"
522
- data-target="${escapeAttribute(option)}"
523
  aria-label="${escapeAttribute(profile.label)}"
524
  ${sessionControlsLocked ? "disabled" : ""}
525
  ${selected.has(option) ? "checked" : ""}
@@ -527,12 +527,12 @@ function renderTargets(selectedTargets) {
527
  <span class="check" aria-hidden="true">
528
  <svg class="icon"><use href="#icon-check"></use></svg>
529
  </span>
530
- <span class="target-copy">
531
  <strong>${escapeHtml(profile.label)}</strong>
532
  ${profile.description ? `<small>${escapeHtml(profile.description)}</small>` : ""}
533
  </span>
534
  `;
535
- targetsEl.append(label);
536
  }
537
  }
538
 
@@ -583,7 +583,7 @@ function handleEvent(event) {
583
  }
584
  session = event.state || {};
585
  session.profile = session.profile || {};
586
- session.targets = Array.isArray(session.targets) ? session.targets : [];
587
  session.last_response = event.response || session.last_response || "";
588
  delete session.ui_status;
589
  if (event.score?.echoes?.length) {
@@ -592,7 +592,7 @@ function handleEvent(event) {
592
  renderProjects(event.projects);
593
  }
594
  if (event.whitespace?.length) renderWhitespace(event.whitespace);
595
- renderTargets(session.targets);
596
  renderProfile(session.profile);
597
  renderIdeas(session.ideas || []);
598
  renderPlan(event.plan || []);
@@ -622,7 +622,7 @@ function renderIdeas(ideas) {
622
  }
623
  for (const idea of visibleIdeas(ideas)) {
624
  const score = idea.score?.overall ? Number(idea.score.overall).toFixed(1) : "0.0";
625
- const targets = (idea.targets || []).slice(0, 3).map(targetDisplayName).join(" · ");
626
  const selected = idea.id === session.current_idea_id;
627
  const verdict = idea.score?.verdict || "DRAFT";
628
  const isEcho = String(verdict).startsWith("ECHO");
@@ -639,7 +639,7 @@ function renderIdeas(ideas) {
639
  </div>
640
  <p>${escapeHtml((idea.pitch || "").slice(0, 120))}</p>
641
  <span class="iverdict ${isEcho ? "echo" : "unwritten"}">${escapeHtml(verdict)}</span>
642
- ${targets ? `<small>${escapeHtml(targets)}</small>` : ""}
643
  `;
644
  ideasEl.append(item);
645
  }
@@ -663,8 +663,8 @@ function selectIdea(ideaId) {
663
  if (!idea) return;
664
  bumpSessionRevision();
665
  session.current_idea_id = idea.id;
666
- if (Array.isArray(idea.targets) && idea.targets.length) {
667
- session.targets = targetOptions.filter((option) => idea.targets.includes(option));
668
  }
669
  const score = idea.score || null;
670
  if (score) {
@@ -682,7 +682,7 @@ function selectIdea(ideaId) {
682
  renderWoodMap(null);
683
  exportButton.disabled = true;
684
  }
685
- renderTargets(session.targets || []);
686
  renderIdeas(session.ideas);
687
  renderPlan([]);
688
  session.ui_status = `selected: ${idea.title}`;
@@ -690,8 +690,8 @@ function selectIdea(ideaId) {
690
  saveSession();
691
  }
692
 
693
- function targetDisplayName(target) {
694
- return targetProfileById.get(target)?.label || target;
695
  }
696
 
697
  function renderScore(score) {
@@ -873,11 +873,11 @@ function clearTurnWatchdog() {
873
  }
874
  }
875
 
876
- function syncCurrentIdeaTargets() {
877
  const currentId = session.current_idea_id;
878
  if (!currentId || !Array.isArray(session.ideas)) return;
879
  const idea = session.ideas.find((item) => item.id === currentId);
880
- if (idea) idea.targets = [...(session.targets || [])];
881
  }
882
 
883
  async function exportNotes() {
 
8
  const projectsEl = document.querySelector("#projects");
9
  const whitespaceEl = document.querySelector("#whitespace");
10
  const ideasEl = document.querySelector("#ideas");
11
+ const goalsEl = document.querySelector("#goals");
12
  const profileEl = document.querySelector("#profile");
13
  const woodMapEl = document.querySelector("#wood-map");
14
  const scoreEl = document.querySelector("#score");
 
22
  const verdictStampEl = document.querySelector("#verdict-stamp");
23
  const spreadEl = document.querySelector("#spread");
24
  const ideaCountEl = document.querySelector("#idea-count");
25
+ const goalCountEl = document.querySelector("#goal-count");
26
  const demoButton = document.querySelector("#load-demo");
27
  const exportButton = document.querySelector("#export-artifact");
28
  const exportNotesButton = document.querySelector("#export-notes");
29
  const exportChapterButton = document.querySelector("#export-chapter");
30
  const resetButton = document.querySelector("#reset-session");
31
 
32
+ const SESSION_STORAGE_KEY = "hackathon-advisor-session-v2";
33
  const FIELD_NOTES_FILENAME = "hackathon-advisor-field-notes.md";
34
  const CHAPTER_FILENAME = "hackathon-advisor-chapter.md";
35
  const PNG_EXPORT_LABEL = "PNG";
 
37
  let session = {};
38
  let clientPromise = Client.connect(window.location.origin);
39
  let currentArtifact = null;
40
+ let goalOptions = [];
41
+ let goalProfiles = [];
42
+ let goalProfileById = new Map();
43
  let profileFields = [];
44
  let turnWatchdog = null;
45
  let sawTurnToken = false;
 
89
  resetSession();
90
  });
91
 
92
+ goalsEl.addEventListener("change", (event) => {
93
  const target = event.target;
94
+ if (!(target instanceof HTMLInputElement) || !target.dataset.goal) return;
95
  const checked = new Set(
96
+ Array.from(goalsEl.querySelectorAll("input[data-goal]:checked")).map((input) => input.dataset.goal),
97
  );
98
+ session.goals = goalOptions.filter((option) => checked.has(option));
99
+ syncCurrentIdeaGoals();
100
  saveSession();
101
+ renderGoals(session.goals);
102
  renderIdeas(session.ideas || []);
103
  });
104
 
 
175
  if (!response.ok) throw new Error(`project index failed with ${response.status}`);
176
  const data = await response.json();
177
  bootstrapData = data;
178
+ const rawProfiles = Array.isArray(data.goal_profiles) ? data.goal_profiles : [];
179
+ const rawOptions = Array.isArray(data.goal_options) ? data.goal_options : [];
180
+ goalProfiles = normalizeGoalProfiles(rawProfiles, rawOptions);
181
+ goalOptions = goalProfiles.map((goal) => goal.id);
182
+ goalProfileById = new Map(goalProfiles.map((goal) => [goal.id, goal]));
183
  profileFields = data.profile_fields || [];
184
  session = normalizeSession(readSavedSession(), defaultSession(data));
185
  renderProvenance(data);
186
+ renderGoals(session.goals);
187
  renderProfile(session.profile);
188
  renderRestoredSession(data);
189
  renderWhitespace(data.whitespace || []);
 
205
  renderScore(null);
206
  setVerdictDisplay("INDEX CLOSED", 0, null);
207
  renderWoodMap(null);
208
+ renderGoals([]);
209
  renderProfile({});
210
  renderIdeas([]);
211
  renderProjects([]);
 
216
  function defaultSession(data = bootstrapData) {
217
  return {
218
  profile: {},
219
+ goals: data?.default_goals || goalOptions.slice(0, 3),
220
  };
221
  }
222
 
 
292
 
293
  function setSessionControlsDisabled(disabled) {
294
  sessionControlsLocked = disabled;
295
+ goalsEl.querySelectorAll("input[data-goal]").forEach((target) => {
296
  target.disabled = disabled;
297
  });
298
  profileEl.querySelectorAll("input[data-profile-field]").forEach((field) => {
 
320
  ink.textContent = "The book is open. The next page waits for its first line.";
321
  ink.classList.remove("thinking", "bleed", "gold");
322
  corrections.textContent = "Session reset.";
323
+ renderGoals(session.goals);
324
  renderProfile(session.profile);
325
  renderScore(null);
326
  setVerdictDisplay("READY", 0, null);
 
366
  function applyDemoSession(data) {
367
  session = data.session || {};
368
  session.profile = session.profile || {};
369
+ session.goals = Array.isArray(session.goals) ? session.goals : [];
370
  session.last_response = data.response || session.last_response || "";
371
  session.ui_status = `example loaded: ${data.turn_count || 0} advisor turns`;
372
  currentArtifact = data.artifact || session.last_artifact || null;
 
378
  ink.classList.toggle("bleed", data.score.verdict.startsWith("ECHO"));
379
  ink.classList.toggle("gold", data.score.verdict.startsWith("UNWRITTEN"));
380
  }
381
+ renderGoals(session.goals);
382
  renderProfile(session.profile);
383
  renderIdeas(session.ideas || []);
384
  renderPlan(data.plan || session.last_plan || []);
 
450
  const normalized = { ...defaultSession };
451
  if (!savedSession) return normalized;
452
  normalized.profile = savedSession.profile && typeof savedSession.profile === "object" ? savedSession.profile : {};
453
+ const savedGoals = Array.isArray(savedSession.goals) ? savedSession.goals : defaultSession.goals;
454
+ normalized.goals = goalOptions.filter((option) => savedGoals.includes(option));
455
+ if (!normalized.goals.length && defaultSession.goals?.length) normalized.goals = [...defaultSession.goals];
456
  if (Array.isArray(savedSession.ideas)) normalized.ideas = savedSession.ideas;
457
  if (Array.isArray(savedSession.trace)) normalized.trace = savedSession.trace;
458
  if (Array.isArray(savedSession.last_plan)) normalized.last_plan = savedSession.last_plan;
 
472
  if (status) corrections.textContent = status;
473
  }
474
 
475
+ function normalizeGoalProfiles(profiles, options) {
476
  const byId = new Map(
477
  profiles
478
  .filter((profile) => profile && typeof profile.id === "string")
 
504
  }
505
  }
506
 
507
+ function renderGoals(selectedGoals) {
508
+ const selected = new Set(selectedGoals || []);
509
+ if (goalCountEl) goalCountEl.textContent = selected.size;
510
+ goalsEl.innerHTML = "";
511
+ if (!goalOptions.length) {
512
+ goalsEl.innerHTML = `<div class="empty">No goals loaded.</div>`;
513
  return;
514
  }
515
+ for (const option of goalOptions) {
516
+ const profile = goalProfileById.get(option) || { label: option, description: "" };
517
  const label = document.createElement("label");
518
+ label.className = `goal-toggle goal ${selected.has(option) ? "on" : ""}`;
519
  label.innerHTML = `
520
  <input
521
  type="checkbox"
522
+ data-goal="${escapeAttribute(option)}"
523
  aria-label="${escapeAttribute(profile.label)}"
524
  ${sessionControlsLocked ? "disabled" : ""}
525
  ${selected.has(option) ? "checked" : ""}
 
527
  <span class="check" aria-hidden="true">
528
  <svg class="icon"><use href="#icon-check"></use></svg>
529
  </span>
530
+ <span class="goal-copy">
531
  <strong>${escapeHtml(profile.label)}</strong>
532
  ${profile.description ? `<small>${escapeHtml(profile.description)}</small>` : ""}
533
  </span>
534
  `;
535
+ goalsEl.append(label);
536
  }
537
  }
538
 
 
583
  }
584
  session = event.state || {};
585
  session.profile = session.profile || {};
586
+ session.goals = Array.isArray(session.goals) ? session.goals : [];
587
  session.last_response = event.response || session.last_response || "";
588
  delete session.ui_status;
589
  if (event.score?.echoes?.length) {
 
592
  renderProjects(event.projects);
593
  }
594
  if (event.whitespace?.length) renderWhitespace(event.whitespace);
595
+ renderGoals(session.goals);
596
  renderProfile(session.profile);
597
  renderIdeas(session.ideas || []);
598
  renderPlan(event.plan || []);
 
622
  }
623
  for (const idea of visibleIdeas(ideas)) {
624
  const score = idea.score?.overall ? Number(idea.score.overall).toFixed(1) : "0.0";
625
+ const goals = (idea.goals || []).slice(0, 3).map(goalDisplayName).join(" · ");
626
  const selected = idea.id === session.current_idea_id;
627
  const verdict = idea.score?.verdict || "DRAFT";
628
  const isEcho = String(verdict).startsWith("ECHO");
 
639
  </div>
640
  <p>${escapeHtml((idea.pitch || "").slice(0, 120))}</p>
641
  <span class="iverdict ${isEcho ? "echo" : "unwritten"}">${escapeHtml(verdict)}</span>
642
+ ${goals ? `<small>${escapeHtml(goals)}</small>` : ""}
643
  `;
644
  ideasEl.append(item);
645
  }
 
663
  if (!idea) return;
664
  bumpSessionRevision();
665
  session.current_idea_id = idea.id;
666
+ if (Array.isArray(idea.goals) && idea.goals.length) {
667
+ session.goals = goalOptions.filter((option) => idea.goals.includes(option));
668
  }
669
  const score = idea.score || null;
670
  if (score) {
 
682
  renderWoodMap(null);
683
  exportButton.disabled = true;
684
  }
685
+ renderGoals(session.goals || []);
686
  renderIdeas(session.ideas);
687
  renderPlan([]);
688
  session.ui_status = `selected: ${idea.title}`;
 
690
  saveSession();
691
  }
692
 
693
+ function goalDisplayName(goal) {
694
+ return goalProfileById.get(goal)?.label || goal;
695
  }
696
 
697
  function renderScore(score) {
 
873
  }
874
  }
875
 
876
+ function syncCurrentIdeaGoals() {
877
  const currentId = session.current_idea_id;
878
  if (!currentId || !Array.isArray(session.ideas)) return;
879
  const idea = session.ideas.find((item) => item.id === currentId);
880
+ if (idea) idea.goals = [...(session.goals || [])];
881
  }
882
 
883
  async function exportNotes() {
static/index.html CHANGED
@@ -94,8 +94,8 @@
94
  </section>
95
 
96
  <section class="section">
97
- <div class="eyebrow">Goals <span id="target-count" class="count">0</span></div>
98
- <div id="targets" class="target-list"></div>
99
  </section>
100
  </aside>
101
 
 
94
  </section>
95
 
96
  <section class="section">
97
+ <div class="eyebrow">Goals <span id="goal-count" class="count">0</span></div>
98
+ <div id="goals" class="goal-list"></div>
99
  </section>
100
  </aside>
101
 
static/styles.css CHANGED
@@ -788,7 +788,7 @@ textarea:disabled {
788
  .project-list,
789
  .whitespace-list,
790
  .idea-list,
791
- .target-list,
792
  .profile-grid {
793
  display: grid;
794
  gap: 9px;
@@ -914,14 +914,14 @@ textarea:disabled {
914
 
915
  .idea:disabled,
916
  .gap-item:disabled,
917
- .target-toggle:has(input:disabled),
918
  .profile-field:has(input:disabled) {
919
  opacity: 0.64;
920
  }
921
 
922
  .idea:focus-visible,
923
  .gap-item:focus-visible,
924
- .target-toggle:focus-within,
925
  .profile-field input:focus-visible,
926
  .btn:focus-visible,
927
  .prompt:focus-visible {
@@ -1024,7 +1024,7 @@ textarea:disabled {
1024
  box-shadow: 0 0 0 3px rgba(47, 107, 65, 0.12);
1025
  }
1026
 
1027
- .target-toggle {
1028
  position: relative;
1029
  display: flex;
1030
  align-items: flex-start;
@@ -1037,22 +1037,22 @@ textarea:disabled {
1037
  transition: background 0.2s, border-color 0.2s, opacity 0.2s;
1038
  }
1039
 
1040
- .target-toggle:hover {
1041
  background: rgba(255, 247, 224, 0.52);
1042
  }
1043
 
1044
- .target-toggle.on {
1045
  background: rgba(47, 107, 65, 0.07);
1046
  border-color: rgba(47, 107, 65, 0.4);
1047
  }
1048
 
1049
- .target-toggle input {
1050
  position: absolute;
1051
  opacity: 0;
1052
  pointer-events: none;
1053
  }
1054
 
1055
- .target-toggle .check {
1056
  display: grid;
1057
  width: 18px;
1058
  height: 18px;
@@ -1065,25 +1065,25 @@ textarea:disabled {
1065
  transition: background 0.2s, border-color 0.2s, color 0.2s;
1066
  }
1067
 
1068
- .target-toggle.on .check {
1069
  color: #f6ecd2;
1070
  background: var(--leaf);
1071
  border-color: var(--leaf);
1072
  }
1073
 
1074
- .target-toggle .check .icon {
1075
  width: 11px;
1076
  height: 11px;
1077
  stroke-width: 2.4;
1078
  }
1079
 
1080
- .target-copy {
1081
  display: grid;
1082
  min-width: 0;
1083
  gap: 3px;
1084
  }
1085
 
1086
- .target-copy strong {
1087
  color: var(--ink);
1088
  font-family: var(--label);
1089
  font-size: 0.8rem;
@@ -1091,7 +1091,7 @@ textarea:disabled {
1091
  line-height: 1.2;
1092
  }
1093
 
1094
- .target-copy small {
1095
  color: var(--ink-faint);
1096
  font-family: var(--label);
1097
  font-size: 0.7rem;
 
788
  .project-list,
789
  .whitespace-list,
790
  .idea-list,
791
+ .goal-list,
792
  .profile-grid {
793
  display: grid;
794
  gap: 9px;
 
914
 
915
  .idea:disabled,
916
  .gap-item:disabled,
917
+ .goal-toggle:has(input:disabled),
918
  .profile-field:has(input:disabled) {
919
  opacity: 0.64;
920
  }
921
 
922
  .idea:focus-visible,
923
  .gap-item:focus-visible,
924
+ .goal-toggle:focus-within,
925
  .profile-field input:focus-visible,
926
  .btn:focus-visible,
927
  .prompt:focus-visible {
 
1024
  box-shadow: 0 0 0 3px rgba(47, 107, 65, 0.12);
1025
  }
1026
 
1027
+ .goal-toggle {
1028
  position: relative;
1029
  display: flex;
1030
  align-items: flex-start;
 
1037
  transition: background 0.2s, border-color 0.2s, opacity 0.2s;
1038
  }
1039
 
1040
+ .goal-toggle:hover {
1041
  background: rgba(255, 247, 224, 0.52);
1042
  }
1043
 
1044
+ .goal-toggle.on {
1045
  background: rgba(47, 107, 65, 0.07);
1046
  border-color: rgba(47, 107, 65, 0.4);
1047
  }
1048
 
1049
+ .goal-toggle input {
1050
  position: absolute;
1051
  opacity: 0;
1052
  pointer-events: none;
1053
  }
1054
 
1055
+ .goal-toggle .check {
1056
  display: grid;
1057
  width: 18px;
1058
  height: 18px;
 
1065
  transition: background 0.2s, border-color 0.2s, color 0.2s;
1066
  }
1067
 
1068
+ .goal-toggle.on .check {
1069
  color: #f6ecd2;
1070
  background: var(--leaf);
1071
  border-color: var(--leaf);
1072
  }
1073
 
1074
+ .goal-toggle .check .icon {
1075
  width: 11px;
1076
  height: 11px;
1077
  stroke-width: 2.4;
1078
  }
1079
 
1080
+ .goal-copy {
1081
  display: grid;
1082
  min-width: 0;
1083
  gap: 3px;
1084
  }
1085
 
1086
+ .goal-copy strong {
1087
  color: var(--ink);
1088
  font-family: var(--label);
1089
  font-size: 0.8rem;
 
1091
  line-height: 1.2;
1092
  }
1093
 
1094
+ .goal-copy small {
1095
  color: var(--ink-faint);
1096
  font-family: var(--label);
1097
  font-size: 0.7rem;
tests/test_agent.py CHANGED
@@ -175,7 +175,7 @@ def test_planner_get_project_drives_project_response() -> None:
175
  assert result.tool_events[0].name == "get_project"
176
 
177
 
178
- def test_planner_profile_and_targets_update_state() -> None:
179
  index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
180
  profile_engine = AdvisorEngine(
181
  index,
@@ -189,32 +189,32 @@ def test_planner_profile_and_targets_update_state() -> None:
189
  targeted = target_engine.turn("set goals", profile.state)
190
 
191
  assert targeted.state["profile"]["skills"] == "frontend"
192
- assert targeted.state["targets"] == ["Off the Grid", "Field Notes"]
193
  assert "Local-first, Build notes" in targeted.response
194
 
195
 
196
- def test_session_targets_apply_to_new_and_current_ideas() -> None:
197
  index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
198
  engine = AdvisorEngine(index)
199
- state = {"targets": ["Field Notes"]}
200
 
201
  first = engine.turn("A local-first archive cartographer for family photos", state)
202
  first_idea = first.state["ideas"][0]
203
  planned = engine.turn("make a build plan", first.state)
204
 
205
- assert first_idea["targets"] == ["Field Notes"]
206
  assert all("LoRA" not in step for step in planned.plan)
207
 
208
 
209
- def test_well_tuned_target_adds_training_step_to_plan() -> None:
210
  index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
211
  engine = AdvisorEngine(index)
212
- state = {"targets": ["Well-Tuned"]}
213
 
214
  first = engine.turn("A local-first archive cartographer for family photos", state)
215
  planned = engine.turn("make a build plan", first.state)
216
 
217
- assert first.state["ideas"][0]["targets"] == ["Well-Tuned"]
218
  assert any("LoRA" in step for step in planned.plan)
219
 
220
 
 
175
  assert result.tool_events[0].name == "get_project"
176
 
177
 
178
+ def test_planner_profile_and_goals_update_state() -> None:
179
  index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
180
  profile_engine = AdvisorEngine(
181
  index,
 
189
  targeted = target_engine.turn("set goals", profile.state)
190
 
191
  assert targeted.state["profile"]["skills"] == "frontend"
192
+ assert targeted.state["goals"] == ["Off the Grid", "Field Notes"]
193
  assert "Local-first, Build notes" in targeted.response
194
 
195
 
196
+ def test_session_goals_apply_to_new_and_current_ideas() -> None:
197
  index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
198
  engine = AdvisorEngine(index)
199
+ state = {"goals": ["Field Notes"]}
200
 
201
  first = engine.turn("A local-first archive cartographer for family photos", state)
202
  first_idea = first.state["ideas"][0]
203
  planned = engine.turn("make a build plan", first.state)
204
 
205
+ assert first_idea["goals"] == ["Field Notes"]
206
  assert all("LoRA" not in step for step in planned.plan)
207
 
208
 
209
+ def test_well_tuned_goal_adds_training_step_to_plan() -> None:
210
  index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
211
  engine = AdvisorEngine(index)
212
+ state = {"goals": ["Well-Tuned"]}
213
 
214
  first = engine.turn("A local-first archive cartographer for family photos", state)
215
  planned = engine.turn("make a build plan", first.state)
216
 
217
+ assert first.state["ideas"][0]["goals"] == ["Well-Tuned"]
218
  assert any("LoRA" in step for step in planned.plan)
219
 
220
 
tests/test_app.py CHANGED
@@ -40,10 +40,10 @@ def test_bootstrap_exposes_index_metadata() -> None:
40
  assert payload["snapshot_digest"]
41
  assert payload["runtime"]["tool_count"] >= 8
42
  assert payload["top_projects"]
43
- assert payload["default_targets"] == payload["target_options"][:3]
44
- assert [target["id"] for target in payload["target_profiles"]] == payload["target_options"]
45
- assert payload["target_profiles"][0]["label"] == "Local-first"
46
- assert "description" in payload["target_profiles"][0]
47
  assert "skills" in payload["profile_fields"]
48
  assert payload["prize_ledger"]["tiny_titan_eligible"] is True
49
 
@@ -61,7 +61,7 @@ def test_trace_artifact_endpoint_exports_jsonl() -> None:
61
  def test_field_notes_endpoint_exports_markdown() -> None:
62
  state = engine.turn(
63
  "A local-first archive cartographer for family photos",
64
- {"profile": {"skills": "frontend"}, "targets": ["Field Notes"]},
65
  ).state
66
  state = engine.turn("make a build plan", state).state
67
 
@@ -92,7 +92,7 @@ def test_chapter_endpoint_exports_markdown() -> None:
92
  def test_lora_dataset_endpoint_exports_sft_jsonl() -> None:
93
  state = engine.turn(
94
  "A local-first archive cartographer for family photos",
95
- {"targets": ["Well-Tuned"]},
96
  ).state
97
  state = engine.turn("make a build plan", state).state
98
 
@@ -109,7 +109,7 @@ def test_lora_dataset_endpoint_exports_sft_jsonl() -> None:
109
  def test_submission_packet_endpoint_exports_markdown() -> None:
110
  state = engine.turn(
111
  "A local-first archive cartographer for family photos",
112
- {"targets": ["Field Notes"]},
113
  ).state
114
  state = engine.turn("make a build plan", state).state
115
 
 
40
  assert payload["snapshot_digest"]
41
  assert payload["runtime"]["tool_count"] >= 8
42
  assert payload["top_projects"]
43
+ assert payload["default_goals"] == payload["goal_options"][:3]
44
+ assert [goal["id"] for goal in payload["goal_profiles"]] == payload["goal_options"]
45
+ assert payload["goal_profiles"][0]["label"] == "Local-first"
46
+ assert "description" in payload["goal_profiles"][0]
47
  assert "skills" in payload["profile_fields"]
48
  assert payload["prize_ledger"]["tiny_titan_eligible"] is True
49
 
 
61
  def test_field_notes_endpoint_exports_markdown() -> None:
62
  state = engine.turn(
63
  "A local-first archive cartographer for family photos",
64
+ {"profile": {"skills": "frontend"}, "goals": ["Field Notes"]},
65
  ).state
66
  state = engine.turn("make a build plan", state).state
67
 
 
92
  def test_lora_dataset_endpoint_exports_sft_jsonl() -> None:
93
  state = engine.turn(
94
  "A local-first archive cartographer for family photos",
95
+ {"goals": ["Well-Tuned"]},
96
  ).state
97
  state = engine.turn("make a build plan", state).state
98
 
 
109
  def test_submission_packet_endpoint_exports_markdown() -> None:
110
  state = engine.turn(
111
  "A local-first archive cartographer for family photos",
112
+ {"goals": ["Field Notes"]},
113
  ).state
114
  state = engine.turn("make a build plan", state).state
115
 
tests/test_demo_rehearsal.py CHANGED
@@ -2,7 +2,7 @@ from pathlib import Path
2
 
3
  from hackathon_advisor.agent import AdvisorEngine
4
  from hackathon_advisor.data import ProjectIndex
5
- from hackathon_advisor.demo_rehearsal import DEMO_TARGETS, build_demo_rehearsal
6
 
7
 
8
  def test_demo_rehearsal_builds_complete_session() -> None:
@@ -19,7 +19,7 @@ def test_demo_rehearsal_builds_complete_session() -> None:
19
  assert payload["artifact"]["wood_map"]["dots"]
20
  assert payload["plan"]
21
  assert any("LoRA" in step for step in payload["plan"])
22
- assert session["targets"] == DEMO_TARGETS
23
  assert session["profile"]["constraints"] == "CPU Space runtime; no proprietary inference API"
24
  assert len(session["trace"]) == 2
25
  assert payload["export_ready"] == {
 
2
 
3
  from hackathon_advisor.agent import AdvisorEngine
4
  from hackathon_advisor.data import ProjectIndex
5
+ from hackathon_advisor.demo_rehearsal import DEMO_GOALS, build_demo_rehearsal
6
 
7
 
8
  def test_demo_rehearsal_builds_complete_session() -> None:
 
19
  assert payload["artifact"]["wood_map"]["dots"]
20
  assert payload["plan"]
21
  assert any("LoRA" in step for step in payload["plan"])
22
+ assert session["goals"] == DEMO_GOALS
23
  assert session["profile"]["constraints"] == "CPU Space runtime; no proprietary inference API"
24
  assert len(session["trace"]) == 2
25
  assert payload["export_ready"] == {
tests/test_field_notes.py CHANGED
@@ -11,7 +11,7 @@ def test_field_notes_markdown_contains_session_decisions() -> None:
11
  engine = AdvisorEngine(index)
12
  state = {
13
  "profile": {"skills": "frontend prototyping"},
14
- "targets": ["Field Notes"],
15
  }
16
  first = engine.turn("A local-first archive cartographer for family photos", state)
17
  planned = engine.turn("make a build plan", first.state)
 
11
  engine = AdvisorEngine(index)
12
  state = {
13
  "profile": {"skills": "frontend prototyping"},
14
+ "goals": ["Field Notes"],
15
  }
16
  first = engine.turn("A local-first archive cartographer for family photos", state)
17
  planned = engine.turn("make a build plan", first.state)
tests/test_lora_dataset.py CHANGED
@@ -10,7 +10,7 @@ from hackathon_advisor.trace_export import trace_metadata
10
  def test_lora_dataset_exports_tool_call_and_response_examples() -> None:
11
  index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
12
  engine = AdvisorEngine(index)
13
- state = {"targets": ["Well-Tuned", "Field Notes"]}
14
  state = engine.turn("A local-first archive cartographer for family photos", state).state
15
  state = engine.turn("make a build plan", state).state
16
 
@@ -26,14 +26,14 @@ def test_lora_dataset_exports_tool_call_and_response_examples() -> None:
26
  assert manifest["index"]["algorithm"] == "tfidf-sparse-v1"
27
  assert {example["example_kind"] for example in examples} == {"tool_call", "advisor_response"}
28
  assert examples[0]["messages"][2]["content"].startswith('<function name="save_idea">')
29
- assert examples[0]["targets"] == ["Well-Tuned", "Field Notes"]
30
  assert examples[1]["messages"][1]["content"].startswith("A local-first archive")
31
  assert "Tool observations:" in examples[1]["messages"][1]["content"]
32
  assert examples[1]["messages"][2]["content"]
33
  system_messages = "\n".join(example["messages"][0]["content"] for example in examples)
34
  assert "Mothback" not in system_messages
35
  assert "Build Small" not in system_messages
36
- assert "prize targets" not in system_messages
37
  assert "selected goals" in system_messages
38
 
39
 
 
10
  def test_lora_dataset_exports_tool_call_and_response_examples() -> None:
11
  index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
12
  engine = AdvisorEngine(index)
13
+ state = {"goals": ["Well-Tuned", "Field Notes"]}
14
  state = engine.turn("A local-first archive cartographer for family photos", state).state
15
  state = engine.turn("make a build plan", state).state
16
 
 
26
  assert manifest["index"]["algorithm"] == "tfidf-sparse-v1"
27
  assert {example["example_kind"] for example in examples} == {"tool_call", "advisor_response"}
28
  assert examples[0]["messages"][2]["content"].startswith('<function name="save_idea">')
29
+ assert examples[0]["goals"] == ["Well-Tuned", "Field Notes"]
30
  assert examples[1]["messages"][1]["content"].startswith("A local-first archive")
31
  assert "Tool observations:" in examples[1]["messages"][1]["content"]
32
  assert examples[1]["messages"][2]["content"]
33
  system_messages = "\n".join(example["messages"][0]["content"] for example in examples)
34
  assert "Mothback" not in system_messages
35
  assert "Build Small" not in system_messages
36
+ assert "prize " + "tar" + "gets" not in system_messages
37
  assert "selected goals" in system_messages
38
 
39
 
tests/test_submission_packet.py CHANGED
@@ -10,7 +10,7 @@ from hackathon_advisor.trace_export import trace_metadata
10
  def test_submission_packet_contains_demo_and_prize_evidence() -> None:
11
  index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
12
  engine = AdvisorEngine(index)
13
- state = {"targets": ["Well-Tuned", "Field Notes"]}
14
  state = engine.turn("A local-first archive cartographer for family photos", state).state
15
  state = engine.turn("make a build plan", state).state
16
 
 
10
  def test_submission_packet_contains_demo_and_prize_evidence() -> None:
11
  index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
12
  engine = AdvisorEngine(index)
13
+ state = {"goals": ["Well-Tuned", "Field Notes"]}
14
  state = engine.turn("A local-first archive cartographer for family photos", state).state
15
  state = engine.turn("make a build plan", state).state
16