cadence-skill-installer 0.2.45 → 0.2.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cadence-skill-installer",
3
- "version": "0.2.45",
3
+ "version": "0.2.47",
4
4
  "description": "Install the Cadence skill into supported AI tool skill directories.",
5
5
  "repository": "https://github.com/snowdamiz/cadence",
6
6
  "private": false,
package/skill/SKILL.md CHANGED
@@ -94,10 +94,11 @@ description: Structured project operating system for end-to-end greenfield or br
94
94
 
95
95
  ## Manual Subskill Safety Gate
96
96
  1. If the user manually requests a Cadence subskill, first resolve `PROJECT_ROOT` with `python3 scripts/resolve-project-root.py --project-root "$PWD"`.
97
- 2. Run `python3 scripts/assert-workflow-route.py --skill-name <subskill> --project-root "$PROJECT_ROOT"` before executing that subskill.
98
- 3. Ensure that direct subskill execution still applies this skill's Repo Status Gate and Git Checkpoints rules.
99
- 4. If route assertion fails, stop and surface the exact script error.
100
- 5. Do not execute state-changing subskill steps when assertion fails.
97
+ 2. If the requested subskill is the read-only utility `project-overview`, skip route assertion and run it directly.
98
+ 3. For all other subskills, run `python3 scripts/assert-workflow-route.py --skill-name <subskill> --project-root "$PROJECT_ROOT"` before executing that subskill.
99
+ 4. Ensure that direct subskill execution still applies this skill's Repo Status Gate and Git Checkpoints rules.
100
+ 5. If route assertion fails, stop and surface the exact script error.
101
+ 6. Do not execute state-changing subskill steps when assertion fails.
101
102
 
102
103
  ## Ideation Flow
103
104
  1. When scaffold, prerequisite, and project mode intake complete in this same conversation for a net-new project and route advances to `ideator`, force a subskill handoff and end with this exact line: `Start a new chat and either say "help me define my project" or share your project brief.`
@@ -114,8 +115,8 @@ description: Structured project operating system for end-to-end greenfield or br
114
115
 
115
116
  ## Research Flow
116
117
  1. If the workflow route is `researcher`, invoke `skills/researcher/SKILL.md`.
117
- 2. Enforce one research pass per conversation so context stays bounded.
118
- 3. When the researcher flow reports additional passes remain (`handoff_required=true`), end with this exact line: `Start a new chat and say "continue research".`
118
+ 2. Run bounded multi-pass research in the same conversation while `handoff_required=false`.
119
+ 3. Treat `handoff_required=true` from `run-research-pass.py complete` as the context-boundary stop signal (triggered by token-budget estimate or per-chat pass cap); then end with this exact line: `Start a new chat and say "continue research".`
119
120
  4. Continue routing to researcher on subsequent chats until workflow reports the research task complete.
120
121
  5. For greenfield projects, when research completes and route advances to `planner`, invoke `skills/planner/SKILL.md` in the next routed conversation.
121
122
 
@@ -11,10 +11,12 @@ interface:
11
11
  project mode intake (brownfield-intake) in one continuous run and do not stop between these gates unless a user
12
12
  decision is required. In user-facing replies before mode resolution, refer to this step as project mode intake.
13
13
  For manual subskill requests, assert route first with
14
- scripts/assert-workflow-route.py --skill-name <subskill> --project-root "$PROJECT_ROOT".
14
+ scripts/assert-workflow-route.py --skill-name <subskill> --project-root "$PROJECT_ROOT",
15
+ except for read-only project-overview utility requests.
15
16
  Never read or edit .cadence/cadence.json directly (including cat/rg/jq/Read); use Cadence scripts for all state reads and writes. Keep replies concise, do not expose internal traces unless asked,
16
17
  and do not announce successful internal checks.
17
18
  For each successful subskill conversation, run scripts/finalize-skill-checkpoint.py from PROJECT_ROOT with
18
19
  that subskill's --scope/--checkpoint and --paths ., allow status=no_changes, and treat checkpoint/push
19
20
  failures as blocking.
20
- Use the exact handoff lines in SKILL.md for ideator, brownfield-documenter, researcher, and planner transitions.
21
+ Use the exact handoff lines in SKILL.md for ideator, brownfield-documenter, and planner transitions.
22
+ For researcher, use the exact handoff line only when run-research-pass output indicates a context-boundary handoff is required.
@@ -127,23 +127,58 @@
127
127
  "planning": {
128
128
  "target_effort_per_pass": 12,
129
129
  "max_topics_per_pass": 4,
130
+ "max_passes_per_topic": 3,
131
+ "max_total_passes": 120,
132
+ "max_passes_per_chat": 6,
133
+ "context_window_tokens": 128000,
134
+ "handoff_context_threshold_percent": 70,
135
+ "estimated_fixed_tokens_per_chat": 12000,
136
+ "estimated_tokens_in_overhead_per_pass": 1200,
137
+ "estimated_tokens_out_overhead_per_pass": 400,
130
138
  "latest_round": 0
131
139
  },
132
140
  "summary": {
133
141
  "topic_total": 0,
134
142
  "topic_complete": 0,
143
+ "topic_caveated": 0,
135
144
  "topic_needs_followup": 0,
136
145
  "topic_pending": 0,
137
146
  "pass_pending": 0,
138
147
  "pass_complete": 0,
139
- "next_pass_id": ""
148
+ "next_pass_id": "",
149
+ "context_budget_tokens": 128000,
150
+ "context_threshold_tokens": 89600,
151
+ "context_threshold_percent": 70,
152
+ "context_tokens_in": 0,
153
+ "context_tokens_out": 0,
154
+ "context_tokens_total": 12000,
155
+ "context_percent_estimate": 9.38,
156
+ "context_passes_completed": 0
140
157
  },
141
158
  "topic_status": {},
142
159
  "pass_queue": [],
143
160
  "pass_history": [],
144
161
  "source_registry": [],
162
+ "chat_context": {
163
+ "session_index": 0,
164
+ "passes_completed": 0,
165
+ "estimated_tokens_fixed": 12000,
166
+ "estimated_tokens_in": 0,
167
+ "estimated_tokens_out": 0,
168
+ "estimated_tokens_total": 12000,
169
+ "estimated_context_percent": 9.38,
170
+ "budget_tokens": 128000,
171
+ "threshold_tokens": 89600,
172
+ "threshold_percent": 70,
173
+ "last_reset_at": "",
174
+ "last_updated_at": "",
175
+ "last_pass_id": "",
176
+ "last_pass_tokens_in": 0,
177
+ "last_pass_tokens_out": 0
178
+ },
145
179
  "handoff_required": false,
146
- "handoff_message": "Start a new chat and say \"continue research\"."
180
+ "handoff_message": "Start a new chat and say \"continue research\".",
181
+ "handoff_reason": ""
147
182
  }
148
183
  },
149
184
  "planning": {
@@ -128,6 +128,12 @@
128
128
  "progress-checked": "check progress and route next action"
129
129
  }
130
130
  },
131
+ "project-overview": {
132
+ "description": "Project overview reporting checks",
133
+ "checkpoints": {
134
+ "overview-reviewed": "read full roadmap and report current project position"
135
+ }
136
+ },
131
137
  "researcher": {
132
138
  "description": "Ideation research pass execution",
133
139
  "checkpoints": {
@@ -93,6 +93,12 @@ def default_research_execution() -> dict[str, Any]:
93
93
  "max_topics_per_pass": 4,
94
94
  "max_passes_per_topic": 3,
95
95
  "max_total_passes": 120,
96
+ "max_passes_per_chat": 6,
97
+ "context_window_tokens": 128000,
98
+ "handoff_context_threshold_percent": 70,
99
+ "estimated_fixed_tokens_per_chat": 12000,
100
+ "estimated_tokens_in_overhead_per_pass": 1200,
101
+ "estimated_tokens_out_overhead_per_pass": 400,
96
102
  "latest_round": 0,
97
103
  },
98
104
  "summary": {
@@ -104,13 +110,39 @@ def default_research_execution() -> dict[str, Any]:
104
110
  "pass_pending": 0,
105
111
  "pass_complete": 0,
106
112
  "next_pass_id": "",
113
+ "context_budget_tokens": 128000,
114
+ "context_threshold_tokens": 89600,
115
+ "context_threshold_percent": 70,
116
+ "context_tokens_in": 0,
117
+ "context_tokens_out": 0,
118
+ "context_tokens_total": 12000,
119
+ "context_percent_estimate": 9.38,
120
+ "context_passes_completed": 0,
107
121
  },
108
122
  "topic_status": {},
109
123
  "pass_queue": [],
110
124
  "pass_history": [],
111
125
  "source_registry": [],
126
+ "chat_context": {
127
+ "session_index": 0,
128
+ "passes_completed": 0,
129
+ "estimated_tokens_fixed": 12000,
130
+ "estimated_tokens_in": 0,
131
+ "estimated_tokens_out": 0,
132
+ "estimated_tokens_total": 12000,
133
+ "estimated_context_percent": 9.38,
134
+ "budget_tokens": 128000,
135
+ "threshold_tokens": 89600,
136
+ "threshold_percent": 70,
137
+ "last_reset_at": "",
138
+ "last_updated_at": "",
139
+ "last_pass_id": "",
140
+ "last_pass_tokens_in": 0,
141
+ "last_pass_tokens_out": 0,
142
+ },
112
143
  "handoff_required": False,
113
144
  "handoff_message": DEFAULT_RESEARCH_HANDOFF_MESSAGE,
145
+ "handoff_reason": "",
114
146
  }
115
147
 
116
148
 
@@ -205,6 +237,50 @@ def _normalize_research_execution(agenda: dict[str, Any], raw_execution: Any) ->
205
237
  if max_total_passes < 1:
206
238
  max_total_passes = 1
207
239
 
240
+ try:
241
+ max_passes_per_chat = int(planning.get("max_passes_per_chat", 6))
242
+ except (TypeError, ValueError):
243
+ max_passes_per_chat = 6
244
+ if max_passes_per_chat < 1:
245
+ max_passes_per_chat = 1
246
+
247
+ try:
248
+ context_window_tokens = int(planning.get("context_window_tokens", 128000))
249
+ except (TypeError, ValueError):
250
+ context_window_tokens = 128000
251
+ if context_window_tokens < 1000:
252
+ context_window_tokens = 1000
253
+
254
+ try:
255
+ handoff_context_threshold_percent = int(planning.get("handoff_context_threshold_percent", 70))
256
+ except (TypeError, ValueError):
257
+ handoff_context_threshold_percent = 70
258
+ if handoff_context_threshold_percent < 1:
259
+ handoff_context_threshold_percent = 1
260
+ if handoff_context_threshold_percent > 95:
261
+ handoff_context_threshold_percent = 95
262
+
263
+ try:
264
+ estimated_fixed_tokens_per_chat = int(planning.get("estimated_fixed_tokens_per_chat", 12000))
265
+ except (TypeError, ValueError):
266
+ estimated_fixed_tokens_per_chat = 12000
267
+ if estimated_fixed_tokens_per_chat < 0:
268
+ estimated_fixed_tokens_per_chat = 0
269
+
270
+ try:
271
+ estimated_tokens_in_overhead_per_pass = int(planning.get("estimated_tokens_in_overhead_per_pass", 1200))
272
+ except (TypeError, ValueError):
273
+ estimated_tokens_in_overhead_per_pass = 1200
274
+ if estimated_tokens_in_overhead_per_pass < 0:
275
+ estimated_tokens_in_overhead_per_pass = 0
276
+
277
+ try:
278
+ estimated_tokens_out_overhead_per_pass = int(planning.get("estimated_tokens_out_overhead_per_pass", 400))
279
+ except (TypeError, ValueError):
280
+ estimated_tokens_out_overhead_per_pass = 400
281
+ if estimated_tokens_out_overhead_per_pass < 0:
282
+ estimated_tokens_out_overhead_per_pass = 0
283
+
208
284
  try:
209
285
  latest_round = int(planning.get("latest_round", 0))
210
286
  except (TypeError, ValueError):
@@ -217,6 +293,12 @@ def _normalize_research_execution(agenda: dict[str, Any], raw_execution: Any) ->
217
293
  "max_topics_per_pass": max_topics,
218
294
  "max_passes_per_topic": max_passes_per_topic,
219
295
  "max_total_passes": max_total_passes,
296
+ "max_passes_per_chat": max_passes_per_chat,
297
+ "context_window_tokens": context_window_tokens,
298
+ "handoff_context_threshold_percent": handoff_context_threshold_percent,
299
+ "estimated_fixed_tokens_per_chat": estimated_fixed_tokens_per_chat,
300
+ "estimated_tokens_in_overhead_per_pass": estimated_tokens_in_overhead_per_pass,
301
+ "estimated_tokens_out_overhead_per_pass": estimated_tokens_out_overhead_per_pass,
220
302
  "latest_round": latest_round,
221
303
  }
222
304
 
@@ -277,6 +359,13 @@ def _normalize_research_execution(agenda: dict[str, Any], raw_execution: Any) ->
277
359
  if planned_effort < 0:
278
360
  planned_effort = 0
279
361
 
362
+ try:
363
+ estimated_tokens_in = int(entry.get("estimated_tokens_in", 0) or 0)
364
+ except (TypeError, ValueError):
365
+ estimated_tokens_in = 0
366
+ if estimated_tokens_in < 0:
367
+ estimated_tokens_in = 0
368
+
280
369
  pass_queue.append(
281
370
  {
282
371
  "pass_id": pass_id,
@@ -286,6 +375,7 @@ def _normalize_research_execution(agenda: dict[str, Any], raw_execution: Any) ->
286
375
  "planned_effort": planned_effort,
287
376
  "created_at": _string(entry.get("created_at")),
288
377
  "started_at": _string(entry.get("started_at")),
378
+ "estimated_tokens_in": estimated_tokens_in,
289
379
  }
290
380
  )
291
381
 
@@ -330,6 +420,88 @@ def _normalize_research_execution(agenda: dict[str, Any], raw_execution: Any) ->
330
420
  normalized["pass_history"] = pass_history
331
421
  normalized["source_registry"] = source_registry
332
422
 
423
+ chat_context_raw = raw_execution.get("chat_context")
424
+ chat_context_raw = dict(chat_context_raw) if isinstance(chat_context_raw, dict) else {}
425
+ budget_tokens = context_window_tokens
426
+ threshold_tokens = max(1, int((budget_tokens * handoff_context_threshold_percent) / 100.0))
427
+
428
+ try:
429
+ session_index = int(chat_context_raw.get("session_index", 0))
430
+ except (TypeError, ValueError):
431
+ session_index = 0
432
+ if session_index < 0:
433
+ session_index = 0
434
+
435
+ try:
436
+ passes_completed = int(chat_context_raw.get("passes_completed", 0))
437
+ except (TypeError, ValueError):
438
+ passes_completed = 0
439
+ if passes_completed < 0:
440
+ passes_completed = 0
441
+
442
+ try:
443
+ estimated_tokens_fixed = int(
444
+ chat_context_raw.get("estimated_tokens_fixed", estimated_fixed_tokens_per_chat)
445
+ )
446
+ except (TypeError, ValueError):
447
+ estimated_tokens_fixed = estimated_fixed_tokens_per_chat
448
+ if estimated_tokens_fixed < 0:
449
+ estimated_tokens_fixed = 0
450
+
451
+ try:
452
+ estimated_tokens_in = int(chat_context_raw.get("estimated_tokens_in", 0))
453
+ except (TypeError, ValueError):
454
+ estimated_tokens_in = 0
455
+ if estimated_tokens_in < 0:
456
+ estimated_tokens_in = 0
457
+
458
+ try:
459
+ estimated_tokens_out = int(chat_context_raw.get("estimated_tokens_out", 0))
460
+ except (TypeError, ValueError):
461
+ estimated_tokens_out = 0
462
+ if estimated_tokens_out < 0:
463
+ estimated_tokens_out = 0
464
+
465
+ estimated_tokens_total = estimated_tokens_fixed + estimated_tokens_in + estimated_tokens_out
466
+ estimated_context_percent = round((estimated_tokens_total / float(budget_tokens)) * 100.0, 2)
467
+
468
+ try:
469
+ last_pass_tokens_in = int(chat_context_raw.get("last_pass_tokens_in", 0))
470
+ except (TypeError, ValueError):
471
+ last_pass_tokens_in = 0
472
+ if last_pass_tokens_in < 0:
473
+ last_pass_tokens_in = 0
474
+
475
+ try:
476
+ last_pass_tokens_out = int(chat_context_raw.get("last_pass_tokens_out", 0))
477
+ except (TypeError, ValueError):
478
+ last_pass_tokens_out = 0
479
+ if last_pass_tokens_out < 0:
480
+ last_pass_tokens_out = 0
481
+
482
+ normalized["chat_context"] = {
483
+ "session_index": session_index,
484
+ "passes_completed": passes_completed,
485
+ "estimated_tokens_fixed": estimated_tokens_fixed,
486
+ "estimated_tokens_in": estimated_tokens_in,
487
+ "estimated_tokens_out": estimated_tokens_out,
488
+ "estimated_tokens_total": estimated_tokens_total,
489
+ "estimated_context_percent": estimated_context_percent,
490
+ "budget_tokens": budget_tokens,
491
+ "threshold_tokens": threshold_tokens,
492
+ "threshold_percent": handoff_context_threshold_percent,
493
+ "last_reset_at": _string(chat_context_raw.get("last_reset_at")),
494
+ "last_updated_at": _string(chat_context_raw.get("last_updated_at")),
495
+ "last_pass_id": _string(chat_context_raw.get("last_pass_id")),
496
+ "last_pass_tokens_in": last_pass_tokens_in,
497
+ "last_pass_tokens_out": last_pass_tokens_out,
498
+ }
499
+
500
+ handoff_reason = _string(raw_execution.get("handoff_reason")).lower()
501
+ if handoff_reason not in {"", "context_budget", "pass_cap"}:
502
+ handoff_reason = ""
503
+ normalized["handoff_reason"] = handoff_reason
504
+
333
505
  total_topics = len(topic_status)
334
506
  topic_complete = len(
335
507
  [entry for entry in topic_status.values() if entry.get("status") in RESEARCH_TOPIC_COMPLETE_STATUSES]
@@ -366,6 +538,14 @@ def _normalize_research_execution(agenda: dict[str, Any], raw_execution: Any) ->
366
538
  "pass_pending": len(pass_queue),
367
539
  "pass_complete": len(pass_history),
368
540
  "next_pass_id": next_pass_id,
541
+ "context_budget_tokens": budget_tokens,
542
+ "context_threshold_tokens": threshold_tokens,
543
+ "context_threshold_percent": handoff_context_threshold_percent,
544
+ "context_tokens_in": estimated_tokens_in,
545
+ "context_tokens_out": estimated_tokens_out,
546
+ "context_tokens_total": estimated_tokens_total,
547
+ "context_percent_estimate": estimated_context_percent,
548
+ "context_passes_completed": passes_completed,
369
549
  }
370
550
  return normalized
371
551
 
@@ -441,6 +621,24 @@ def reset_research_execution(ideation: Any) -> dict[str, Any]:
441
621
  "pass_pending": 0,
442
622
  "pass_complete": 0,
443
623
  "next_pass_id": "",
624
+ "context_budget_tokens": execution["planning"]["context_window_tokens"],
625
+ "context_threshold_tokens": int(
626
+ (execution["planning"]["context_window_tokens"] * execution["planning"]["handoff_context_threshold_percent"])
627
+ / 100.0
628
+ ),
629
+ "context_threshold_percent": execution["planning"]["handoff_context_threshold_percent"],
630
+ "context_tokens_in": 0,
631
+ "context_tokens_out": 0,
632
+ "context_tokens_total": execution["planning"]["estimated_fixed_tokens_per_chat"],
633
+ "context_percent_estimate": round(
634
+ (
635
+ execution["planning"]["estimated_fixed_tokens_per_chat"]
636
+ / float(max(1, execution["planning"]["context_window_tokens"]))
637
+ )
638
+ * 100.0,
639
+ 2,
640
+ ),
641
+ "context_passes_completed": 0,
444
642
  }
445
643
 
446
644
  normalized["research_execution"] = execution
@@ -201,6 +201,36 @@ def planning_contract() -> dict[str, Any]:
201
201
  }
202
202
 
203
203
 
204
+ def roadmap_outline(milestones_raw: Any) -> list[dict[str, Any]]:
205
+ milestones = milestones_raw if isinstance(milestones_raw, list) else []
206
+ outline: list[dict[str, Any]] = []
207
+ for milestone in milestones:
208
+ if not isinstance(milestone, dict):
209
+ continue
210
+ milestone_id = _coerce_text(milestone.get("milestone_id"))
211
+ title = _coerce_text(milestone.get("title")) or milestone_id or "Untitled milestone"
212
+ raw_phases = milestone.get("phases")
213
+ phases = raw_phases if isinstance(raw_phases, list) else []
214
+ phase_titles: list[str] = []
215
+ for phase in phases:
216
+ if not isinstance(phase, dict):
217
+ continue
218
+ phase_id = _coerce_text(phase.get("phase_id"))
219
+ phase_title = _coerce_text(phase.get("title")) or phase_id
220
+ if phase_title:
221
+ phase_titles.append(phase_title)
222
+
223
+ outline.append(
224
+ {
225
+ "milestone_id": milestone_id,
226
+ "title": title,
227
+ "phase_count": len(phases),
228
+ "phase_titles": phase_titles,
229
+ }
230
+ )
231
+ return outline
232
+
233
+
204
234
  def summarize_context(data: dict[str, Any]) -> dict[str, Any]:
205
235
  ideation = data.get("ideation")
206
236
  ideation = ideation if isinstance(ideation, dict) else {}
@@ -284,6 +314,7 @@ def summarize_context(data: dict[str, Any]) -> dict[str, Any]:
284
314
  "detail_level": _coerce_text(planning.get("detail_level")),
285
315
  "milestone_count": len(milestones),
286
316
  "updated_at": _coerce_text(planning.get("updated_at")),
317
+ "milestone_outline": roadmap_outline(milestones),
287
318
  },
288
319
  "planner_payload_contract": planning_contract(),
289
320
  }
@@ -532,6 +563,7 @@ def complete_flow(args: argparse.Namespace, project_root: Path, data: dict[str,
532
563
  "milestone_count": len(milestones),
533
564
  "phase_count": phase_count,
534
565
  "decomposition_pending": bool(normalized.get("decomposition_pending", True)),
566
+ "milestone_outline": roadmap_outline(milestones),
535
567
  },
536
568
  "next_route": data.get("workflow", {}).get("next_route", {}),
537
569
  }