prizmkit 1.1.96 → 1.1.97

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.
Files changed (27) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/dev-pipeline/run-bugfix.sh +5 -1
  3. package/bundled/dev-pipeline/run-feature.sh +5 -1
  4. package/bundled/dev-pipeline/run-refactor.sh +5 -1
  5. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +98 -7
  6. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +86 -3
  7. package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +86 -3
  8. package/bundled/dev-pipeline/scripts/monitor-log.sh +104 -0
  9. package/bundled/dev-pipeline/templates/bootstrap-tier1.md +4 -1
  10. package/bundled/dev-pipeline/templates/bootstrap-tier2.md +5 -2
  11. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +5 -2
  12. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +3 -0
  13. package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +3 -2
  14. package/bundled/dev-pipeline/templates/sections/log-size-awareness.md +76 -0
  15. package/bundled/dev-pipeline-windows/lib/pipeline.ps1 +2 -0
  16. package/bundled/dev-pipeline-windows/scripts/generate-bootstrap-prompt.py +98 -7
  17. package/bundled/dev-pipeline-windows/scripts/generate-bugfix-prompt.py +86 -3
  18. package/bundled/dev-pipeline-windows/scripts/generate-refactor-prompt.py +86 -3
  19. package/bundled/dev-pipeline-windows/scripts/monitor-log.ps1 +102 -0
  20. package/bundled/dev-pipeline-windows/templates/bootstrap-tier1.md +4 -1
  21. package/bundled/dev-pipeline-windows/templates/bootstrap-tier2.md +5 -2
  22. package/bundled/dev-pipeline-windows/templates/bootstrap-tier3.md +5 -2
  23. package/bundled/dev-pipeline-windows/templates/bugfix-bootstrap-prompt.md +3 -0
  24. package/bundled/dev-pipeline-windows/templates/refactor-bootstrap-prompt.md +3 -2
  25. package/bundled/dev-pipeline-windows/templates/sections/log-size-awareness.md +78 -0
  26. package/bundled/skills/_metadata.json +1 -1
  27. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  {
2
- "frameworkVersion": "1.1.96",
3
- "bundledAt": "2026-07-02T18:48:14.022Z",
4
- "bundledFrom": "42b5a94"
2
+ "frameworkVersion": "1.1.97",
3
+ "bundledAt": "2026-07-03T07:22:35.206Z",
4
+ "bundledFrom": "732b89b"
5
5
  }
@@ -23,6 +23,8 @@ set -euo pipefail
23
23
  # HEARTBEAT_INTERVAL Heartbeat log interval in seconds (default: 30)
24
24
  # STALE_KILL_THRESHOLD Auto-kill session after N seconds of no progress (default: 900)
25
25
  # HEARTBEAT_STALE_THRESHOLD Heartbeat stale threshold in seconds (default: 600)
26
+ # MAX_LOG_SIZE Session log size threshold for compact warning (default: 2097152 = 2MB, set 0 to disable)
27
+ # LOG_MONITOR_POLL_INTERVAL Log monitor poll interval in seconds (default: 30)
26
28
  # DEV_BRANCH Custom dev branch name (default: auto-generated bugfix/pipeline-{run_id})
27
29
  # AUTO_PUSH Auto-push to remote after successful bug fix (default: 0). Set to 1 to enable.
28
30
  # STOP_ON_FAILURE Stop pipeline after a task exhausts all retries (default: 0). Set to 1 to stop.
@@ -283,7 +285,7 @@ finalize_bug_status_after_branch_return() {
283
285
  --session-id "$session_id" \
284
286
  --max-retries "$max_retries" \
285
287
  --max-infra-retries "$max_infra_retries" \
286
- --action update 2>&1) || {
288
+ --action update) || {
287
289
  log_error "Failed to update bug status: $update_output"
288
290
  update_output=""
289
291
  }
@@ -1227,6 +1229,8 @@ show_help() {
1227
1229
  echo " VERBOSE Set to 1 for verbose AI CLI output"
1228
1230
  echo " HEARTBEAT_INTERVAL Heartbeat log interval in seconds (default: 30)"
1229
1231
  echo " STALE_KILL_THRESHOLD Auto-kill session after N seconds of no progress (default: 900)"
1232
+ echo " MAX_LOG_SIZE Session log size threshold for compact warning (default: 2097152 = 2MB, set 0 to disable)"
1233
+ echo " LOG_MONITOR_POLL_INTERVAL Log monitor poll interval in seconds (default: 30)"
1230
1234
  echo " STOP_ON_FAILURE Stop pipeline when a task exhausts retries: 0|1 (default: 0)"
1231
1235
  echo ""
1232
1236
  echo "Examples:"
@@ -25,6 +25,8 @@ set -euo pipefail
25
25
  # HEARTBEAT_INTERVAL Heartbeat log interval in seconds (default: 30)
26
26
  # STALE_KILL_THRESHOLD Auto-kill session after N seconds of no progress (default: 900)
27
27
  # HEARTBEAT_STALE_THRESHOLD Heartbeat stale threshold in seconds (default: 600)
28
+ # MAX_LOG_SIZE Session log size threshold for compact warning (default: 2097152 = 2MB, set 0 to disable)
29
+ # LOG_MONITOR_POLL_INTERVAL Log monitor poll interval in seconds (default: 30)
28
30
  # PIPELINE_MODE Override mode for all features: lite|standard|full (used by daemon)
29
31
  # DEV_BRANCH Custom dev branch name (default: auto-generated dev/{feature_id}-YYYYMMDDHHmm)
30
32
  # AUTO_PUSH Auto-push to remote after successful feature (default: 0). Set to 1 to enable.
@@ -402,7 +404,7 @@ finalize_feature_status_after_branch_return() {
402
404
  --session-id "$session_id" \
403
405
  --max-retries "$max_retries" \
404
406
  --max-infra-retries "$max_infra_retries" \
405
- --action update 2>&1) || {
407
+ --action update) || {
406
408
  log_error "Failed to update feature status: $update_output"
407
409
  log_error ".prizmkit/plans/feature-list.json may be out of sync. Manual intervention needed."
408
410
  }
@@ -1433,6 +1435,8 @@ show_help() {
1433
1435
  echo " HEARTBEAT_INTERVAL Heartbeat log interval in seconds (default: 30)"
1434
1436
  echo " STALE_KILL_THRESHOLD Auto-kill session after N seconds of no progress (default: 900)"
1435
1437
  echo " HEARTBEAT_STALE_THRESHOLD Heartbeat stale threshold in seconds (default: 600)"
1438
+ echo " MAX_LOG_SIZE Session log size threshold for compact warning (default: 2097152 = 2MB, set 0 to disable)"
1439
+ echo " LOG_MONITOR_POLL_INTERVAL Log monitor poll interval in seconds (default: 30)"
1436
1440
  echo " PIPELINE_MODE Override mode for all features: lite|standard|full"
1437
1441
  echo " ENABLE_CRITIC Enable critic review for all features: true|false"
1438
1442
  echo " STOP_ON_FAILURE Stop pipeline when a task exhausts retries: 0|1 (default: 0)"
@@ -23,6 +23,8 @@ set -euo pipefail
23
23
  # HEARTBEAT_INTERVAL Heartbeat log interval in seconds (default: 30)
24
24
  # STALE_KILL_THRESHOLD Auto-kill session after N seconds of no progress (default: 900)
25
25
  # HEARTBEAT_STALE_THRESHOLD Heartbeat stale threshold in seconds (default: 600)
26
+ # MAX_LOG_SIZE Session log size threshold for compact warning (default: 2097152 = 2MB, set 0 to disable)
27
+ # LOG_MONITOR_POLL_INTERVAL Log monitor poll interval in seconds (default: 30)
26
28
  # DEV_BRANCH Custom dev branch name (default: auto-generated refactor/pipeline-{run_id})
27
29
  # AUTO_PUSH Auto-push to remote after successful refactor (default: 0). Set to 1 to enable.
28
30
  # STOP_ON_FAILURE Stop pipeline after a task exhausts all retries (default: 0). Set to 1 to stop.
@@ -309,7 +311,7 @@ finalize_refactor_status_after_branch_return() {
309
311
  --session-id "$session_id" \
310
312
  --max-retries "$max_retries" \
311
313
  --max-infra-retries "$max_infra_retries" \
312
- --action update 2>&1) || {
314
+ --action update) || {
313
315
  log_error "Failed to update refactor status: $update_output"
314
316
  update_output=""
315
317
  }
@@ -1261,6 +1263,8 @@ show_help() {
1261
1263
  echo " STRICT_BEHAVIOR_CHECK Force full test suite after each refactor (default: 1)"
1262
1264
  echo " HEARTBEAT_INTERVAL Heartbeat log interval in seconds (default: 30)"
1263
1265
  echo " STALE_KILL_THRESHOLD Auto-kill session after N seconds of no progress (default: 900)"
1266
+ echo " MAX_LOG_SIZE Session log size threshold for compact warning (default: 2097152 = 2MB, set 0 to disable)"
1267
+ echo " LOG_MONITOR_POLL_INTERVAL Log monitor poll interval in seconds (default: 30)"
1264
1268
  echo " STOP_ON_FAILURE Stop pipeline when a task exhausts retries: 0|1 (default: 0)"
1265
1269
  echo ""
1266
1270
  echo "Examples:"
@@ -31,6 +31,8 @@ from utils import enrich_global_context, load_json_file, read_platform_conventio
31
31
 
32
32
 
33
33
  DEFAULT_MAX_RETRIES = 3
34
+ DEFAULT_MAX_LOG_SIZE = 2097152
35
+ DEFAULT_LOG_MONITOR_POLL_INTERVAL = 30
34
36
 
35
37
  LOGGER = setup_logging("generate-bootstrap-prompt")
36
38
 
@@ -152,6 +154,54 @@ def compute_feature_slug(feature_id, title):
152
154
  return "{}-{}".format(numeric, slug)
153
155
 
154
156
 
157
+ def _format_bytes(n):
158
+ """Format a byte count to a human-readable string."""
159
+ if n >= 1048576:
160
+ return "{}MB".format(n // 1048576)
161
+ elif n >= 1024:
162
+ return "{}KB".format(n // 1024)
163
+ return "{}B".format(n)
164
+
165
+
166
+ def _parse_non_negative_int_env(name, default):
167
+ """Parse a non-negative integer environment variable."""
168
+ value = os.environ.get(name, str(default))
169
+ try:
170
+ parsed = int(value)
171
+ except ValueError:
172
+ emit_failure("{} must be a non-negative integer".format(name))
173
+ if parsed < 0:
174
+ emit_failure("{} must be a non-negative integer".format(name))
175
+ return parsed
176
+
177
+
178
+ def _parse_positive_int_env(name, default):
179
+ """Parse a positive integer environment variable."""
180
+ value = os.environ.get(name, str(default))
181
+ try:
182
+ parsed = int(value)
183
+ except ValueError:
184
+ emit_failure("{} must be a positive integer".format(name))
185
+ if parsed <= 0:
186
+ emit_failure("{} must be a positive integer".format(name))
187
+ return parsed
188
+
189
+
190
+
191
+ def load_log_size_section(script_dir):
192
+ """Load the optional log-size awareness prompt section."""
193
+ max_log_size = _parse_non_negative_int_env("MAX_LOG_SIZE", DEFAULT_MAX_LOG_SIZE)
194
+ if max_log_size == 0:
195
+ return ""
196
+
197
+ section_path = os.path.join(
198
+ script_dir, "..", "templates", "sections", "log-size-awareness.md",
199
+ )
200
+ content, err = read_text_file(section_path)
201
+ if err:
202
+ return ""
203
+ return content
204
+
155
205
  def format_acceptance_criteria(criteria):
156
206
  """Format acceptance criteria as a markdown bullet list."""
157
207
  if not criteria:
@@ -1272,7 +1322,8 @@ def _tier_header(pipeline_mode):
1272
1322
  "lite": (
1273
1323
  "# Dev-Pipeline Session Bootstrap — Tier 1 (Single Agent)\n",
1274
1324
  "**Tier 1 — Single Agent**: You handle everything directly. "
1275
- "No subagents, no TeamCreate.\n",
1325
+ "No normal work subagents; the log-size monitor is the only "
1326
+ "allowed persistent background subagent. No TeamCreate.\n",
1276
1327
  ),
1277
1328
  "standard": (
1278
1329
  "# Dev-Pipeline Session Bootstrap — Tier 2 (Dual Agent)\n",
@@ -1309,7 +1360,9 @@ def _tier_reminders(pipeline_mode, critic_enabled=False):
1309
1360
 
1310
1361
  if pipeline_mode == "lite":
1311
1362
  specific = [
1312
- "- Tier 1: you handle everything directly — no subagents needed",
1363
+ "- Tier 1: you handle everything directly — no normal work "
1364
+ "subagents needed; the log-size monitor is the only allowed "
1365
+ "persistent background subagent",
1313
1366
  ]
1314
1367
  elif pipeline_mode == "standard":
1315
1368
  specific = [
@@ -1321,7 +1374,9 @@ def _tier_reminders(pipeline_mode, critic_enabled=False):
1321
1374
  "appends Review Notes",
1322
1375
  "- Gate checks enforce Implementation Log and Review Notes are "
1323
1376
  "written before proceeding",
1324
- "- Do NOT use `run_in_background=true` when spawning subagents",
1377
+ "- Do NOT use `run_in_background=true` when spawning normal work "
1378
+ "subagents; the log-size monitor is the only allowed persistent "
1379
+ "background subagent",
1325
1380
  "- On timeout: check snapshot + git diff HEAD → model:lite → "
1326
1381
  "remaining steps only → max 2 retries per phase → "
1327
1382
  "orchestrator fallback",
@@ -1335,7 +1390,9 @@ def _tier_reminders(pipeline_mode, critic_enabled=False):
1335
1390
  "appends Review Notes",
1336
1391
  "- Gate checks enforce Implementation Log and Review Notes are "
1337
1392
  "written before proceeding",
1338
- "- Do NOT use `run_in_background=true` when spawning agents",
1393
+ "- Do NOT use `run_in_background=true` when spawning normal work "
1394
+ "agents; the log-size monitor is the only allowed persistent "
1395
+ "background subagent",
1339
1396
  "- On timeout: check snapshot → model:lite → remaining steps "
1340
1397
  "only → max 2 retries → orchestrator fallback",
1341
1398
  ]
@@ -1374,12 +1431,13 @@ def assemble_sections(pipeline_mode, sections_dir, init_done, is_resume,
1374
1431
  )
1375
1432
  if pipeline_mode != "lite":
1376
1433
  mission += (
1377
- " When you spawn subagents, wait for each to finish "
1378
- "(run_in_background=false)."
1434
+ " When you spawn normal work subagents, wait for each to finish "
1435
+ "(run_in_background=false). The log-size monitor is the only "
1436
+ "allowed persistent background subagent."
1379
1437
  )
1380
1438
  if pipeline_mode == "full":
1381
1439
  mission += (
1382
- " Do NOT spawn agents in background and exit — "
1440
+ " Do NOT spawn work agents in background and exit — "
1383
1441
  "that kills the session."
1384
1442
  )
1385
1443
  mission += "\n\n" + tier_desc
@@ -1397,6 +1455,12 @@ def assemble_sections(pipeline_mode, sections_dir, init_done, is_resume,
1397
1455
  sections.append(("context-budget-rules",
1398
1456
  load_section(sections_dir, "context-budget-rules.md")))
1399
1457
 
1458
+ # --- Log Size Awareness (only disabled when MAX_LOG_SIZE is explicitly 0) ---
1459
+ max_log_size = _parse_non_negative_int_env("MAX_LOG_SIZE", DEFAULT_MAX_LOG_SIZE)
1460
+ if max_log_size != 0 and os.path.isfile(os.path.join(sections_dir, "log-size-awareness.md")):
1461
+ sections.append(("log-size-awareness",
1462
+ load_section(sections_dir, "log-size-awareness.md")))
1463
+
1400
1464
  # --- Directory Convention (tier-specific) ---
1401
1465
  if pipeline_mode == "lite":
1402
1466
  dc_file = "directory-convention-lite.md"
@@ -1866,7 +1930,16 @@ def build_replacements(args, feature, features, global_context, script_dir):
1866
1930
  pass # Keep default 3000 on any error
1867
1931
  dev_url = f"http://localhost:{dev_port}"
1868
1932
 
1933
+ max_log_size = _parse_non_negative_int_env("MAX_LOG_SIZE", DEFAULT_MAX_LOG_SIZE)
1934
+ if max_log_size == 0:
1935
+ poll_interval = DEFAULT_LOG_MONITOR_POLL_INTERVAL
1936
+ else:
1937
+ poll_interval = _parse_positive_int_env(
1938
+ "LOG_MONITOR_POLL_INTERVAL", DEFAULT_LOG_MONITOR_POLL_INTERVAL,
1939
+ )
1940
+
1869
1941
  replacements = {
1942
+ "{{LOG_SIZE_AWARENESS}}": load_log_size_section(script_dir),
1870
1943
  "{{RUN_ID}}": args.run_id,
1871
1944
  "{{SESSION_ID}}": args.session_id,
1872
1945
  "{{FEATURE_ID}}": args.feature_id,
@@ -1911,6 +1984,24 @@ def build_replacements(args, feature, features, global_context, script_dir):
1911
1984
  "{{COVERAGE_TARGET}}": coverage_target,
1912
1985
  "{{DEV_PORT}}": dev_port,
1913
1986
  "{{DEV_URL}}": dev_url,
1987
+ "{{MAX_LOG_SIZE}}": str(max_log_size),
1988
+ "{{MAX_LOG_SIZE_HUMAN}}": _format_bytes(max_log_size),
1989
+ "{{LOG_MONITOR_POLL_INTERVAL}}": str(poll_interval),
1990
+ "{{LOG_MONITOR_STATUS_FILE}}": os.path.join(
1991
+ ".prizmkit", "state", "features", args.feature_id,
1992
+ "sessions", args.session_id, "logs", "log-monitor-status.txt",
1993
+ ),
1994
+ "{{LOG_MONITOR_BASELINE_FILE}}": os.path.join(
1995
+ ".prizmkit", "state", "features", args.feature_id,
1996
+ "sessions", args.session_id, "logs", "log-monitor-baseline.txt",
1997
+ ),
1998
+ "{{SESSION_LOG_PATH}}": os.path.join(
1999
+ ".prizmkit", "state", "features", args.feature_id,
2000
+ "sessions", args.session_id, "logs", "session.log",
2001
+ ),
2002
+ "{{LOG_MONITOR_SCRIPT}}": os.path.join(
2003
+ ".prizmkit", "dev-pipeline", "scripts", "monitor-log.sh",
2004
+ ),
1914
2005
  }
1915
2006
 
1916
2007
  return replacements, effective_resume, browser_enabled, browser_tool
@@ -23,6 +23,8 @@ from utils import enrich_global_context, load_json_file, read_platform_conventio
23
23
 
24
24
 
25
25
  DEFAULT_MAX_RETRIES = 3
26
+ DEFAULT_MAX_LOG_SIZE = 2097152
27
+ DEFAULT_LOG_MONITOR_POLL_INTERVAL = 30
26
28
 
27
29
  LOGGER = setup_logging("generate-bugfix-prompt")
28
30
 
@@ -60,6 +62,57 @@ def read_text_file(path):
60
62
  return None, "Cannot read file: {}".format(str(e))
61
63
 
62
64
 
65
+
66
+ def _format_bytes(n):
67
+ """Format a byte count to a human-readable string."""
68
+ if n >= 1048576:
69
+ return "{}MB".format(n // 1048576)
70
+ elif n >= 1024:
71
+ return "{}KB".format(n // 1024)
72
+ return "{}B".format(n)
73
+
74
+
75
+ def _parse_non_negative_int_env(name, default):
76
+ """Parse a non-negative integer environment variable."""
77
+ value = os.environ.get(name, str(default))
78
+ try:
79
+ parsed = int(value)
80
+ except ValueError:
81
+ raise ValueError("{} must be a non-negative integer".format(name))
82
+ if parsed < 0:
83
+ raise ValueError("{} must be a non-negative integer".format(name))
84
+ return parsed
85
+
86
+
87
+ def _parse_positive_int_env(name, default):
88
+ """Parse a positive integer environment variable."""
89
+ value = os.environ.get(name, str(default))
90
+ try:
91
+ parsed = int(value)
92
+ except ValueError:
93
+ raise ValueError("{} must be a positive integer".format(name))
94
+ if parsed <= 0:
95
+ raise ValueError("{} must be a positive integer".format(name))
96
+ return parsed
97
+
98
+
99
+ def load_log_size_section(script_dir):
100
+ """Load the optional log-size awareness prompt section."""
101
+ max_log_size = _parse_non_negative_int_env("MAX_LOG_SIZE", DEFAULT_MAX_LOG_SIZE)
102
+ if max_log_size == 0:
103
+ return ""
104
+
105
+ section_path = os.path.join(
106
+ script_dir, "..", "templates", "sections", "log-size-awareness.md",
107
+ )
108
+ if not os.path.isfile(section_path):
109
+ return ""
110
+
111
+ content, err = read_text_file(section_path)
112
+ if err:
113
+ return ""
114
+ return content
115
+
63
116
  def find_bug(bugs, bug_id):
64
117
  """Find and return the bug dict matching the given ID."""
65
118
  for bug in bugs:
@@ -304,7 +357,34 @@ def build_replacements(args, bug, global_context, script_dir):
304
357
  browser_enabled = True
305
358
  browser_verify_steps = " # (reproduce bug and verify fix)"
306
359
 
360
+ max_log_size = _parse_non_negative_int_env("MAX_LOG_SIZE", DEFAULT_MAX_LOG_SIZE)
361
+ if max_log_size == 0:
362
+ poll_interval = DEFAULT_LOG_MONITOR_POLL_INTERVAL
363
+ else:
364
+ poll_interval = _parse_positive_int_env(
365
+ "LOG_MONITOR_POLL_INTERVAL", DEFAULT_LOG_MONITOR_POLL_INTERVAL,
366
+ )
367
+
307
368
  replacements = {
369
+ "{{LOG_SIZE_AWARENESS}}": load_log_size_section(script_dir),
370
+ "{{MAX_LOG_SIZE}}": str(max_log_size),
371
+ "{{MAX_LOG_SIZE_HUMAN}}": _format_bytes(max_log_size),
372
+ "{{LOG_MONITOR_POLL_INTERVAL}}": str(poll_interval),
373
+ "{{LOG_MONITOR_STATUS_FILE}}": os.path.join(
374
+ ".prizmkit", "state", "bugfix", "bugs", args.bug_id,
375
+ "sessions", args.session_id, "logs", "log-monitor-status.txt",
376
+ ),
377
+ "{{LOG_MONITOR_BASELINE_FILE}}": os.path.join(
378
+ ".prizmkit", "state", "bugfix", "bugs", args.bug_id,
379
+ "sessions", args.session_id, "logs", "log-monitor-baseline.txt",
380
+ ),
381
+ "{{SESSION_LOG_PATH}}": os.path.join(
382
+ ".prizmkit", "state", "bugfix", "bugs", args.bug_id,
383
+ "sessions", args.session_id, "logs", "session.log",
384
+ ),
385
+ "{{LOG_MONITOR_SCRIPT}}": os.path.join(
386
+ ".prizmkit", "dev-pipeline", "scripts", "monitor-log.sh",
387
+ ),
308
388
  "{{RUN_ID}}": args.run_id,
309
389
  "{{SESSION_ID}}": args.session_id,
310
390
  "{{BUG_ID}}": args.bug_id,
@@ -420,9 +500,10 @@ def render_template(template_content, replacements, bug):
420
500
  # Step 1: Process conditional blocks
421
501
  content = process_conditional_blocks(template_content, bug)
422
502
 
423
- # Step 2: Replace all {{PLACEHOLDER}} variables
424
- for placeholder, value in replacements.items():
425
- content = content.replace(placeholder, value)
503
+ # Step 2: Replace all {{PLACEHOLDER}} variables (two passes for nested sections)
504
+ for _pass in range(2):
505
+ for placeholder, value in replacements.items():
506
+ content = content.replace(placeholder, value)
426
507
 
427
508
  return content
428
509
 
@@ -675,6 +756,8 @@ if __name__ == "__main__":
675
756
  main()
676
757
  except KeyboardInterrupt:
677
758
  emit_failure("generate-bugfix-prompt interrupted")
759
+ except ValueError as exc:
760
+ emit_failure(str(exc))
678
761
  except SystemExit:
679
762
  raise
680
763
  except Exception as exc:
@@ -23,6 +23,8 @@ from utils import enrich_global_context, load_json_file, read_platform_conventio
23
23
 
24
24
 
25
25
  DEFAULT_MAX_RETRIES = 3
26
+ DEFAULT_MAX_LOG_SIZE = 2097152
27
+ DEFAULT_LOG_MONITOR_POLL_INTERVAL = 30
26
28
 
27
29
  LOGGER = setup_logging("generate-refactor-prompt")
28
30
 
@@ -151,6 +153,57 @@ def read_text_file(path):
151
153
  return None, "Cannot read file: {}".format(str(e))
152
154
 
153
155
 
156
+
157
+ def _format_bytes(n):
158
+ """Format a byte count to a human-readable string."""
159
+ if n >= 1048576:
160
+ return "{}MB".format(n // 1048576)
161
+ elif n >= 1024:
162
+ return "{}KB".format(n // 1024)
163
+ return "{}B".format(n)
164
+
165
+
166
+ def _parse_non_negative_int_env(name, default):
167
+ """Parse a non-negative integer environment variable."""
168
+ value = os.environ.get(name, str(default))
169
+ try:
170
+ parsed = int(value)
171
+ except ValueError:
172
+ raise ValueError("{} must be a non-negative integer".format(name))
173
+ if parsed < 0:
174
+ raise ValueError("{} must be a non-negative integer".format(name))
175
+ return parsed
176
+
177
+
178
+ def _parse_positive_int_env(name, default):
179
+ """Parse a positive integer environment variable."""
180
+ value = os.environ.get(name, str(default))
181
+ try:
182
+ parsed = int(value)
183
+ except ValueError:
184
+ raise ValueError("{} must be a positive integer".format(name))
185
+ if parsed <= 0:
186
+ raise ValueError("{} must be a positive integer".format(name))
187
+ return parsed
188
+
189
+
190
+ def load_log_size_section(script_dir):
191
+ """Load the optional log-size awareness prompt section."""
192
+ max_log_size = _parse_non_negative_int_env("MAX_LOG_SIZE", DEFAULT_MAX_LOG_SIZE)
193
+ if max_log_size == 0:
194
+ return ""
195
+
196
+ section_path = os.path.join(
197
+ script_dir, "..", "templates", "sections", "log-size-awareness.md",
198
+ )
199
+ if not os.path.isfile(section_path):
200
+ return ""
201
+
202
+ content, err = read_text_file(section_path)
203
+ if err:
204
+ return ""
205
+ return content
206
+
154
207
  def find_refactor(refactors, refactor_id):
155
208
  """Find and return the refactor dict matching the given ID."""
156
209
  for refactor in refactors:
@@ -464,7 +517,34 @@ def build_replacements(args, refactor, refactors, global_context, script_dir):
464
517
  browser_enabled = True
465
518
  browser_verify_steps = " # (validate UI renders correctly and feature still works)"
466
519
 
520
+ max_log_size = _parse_non_negative_int_env("MAX_LOG_SIZE", DEFAULT_MAX_LOG_SIZE)
521
+ if max_log_size == 0:
522
+ poll_interval = DEFAULT_LOG_MONITOR_POLL_INTERVAL
523
+ else:
524
+ poll_interval = _parse_positive_int_env(
525
+ "LOG_MONITOR_POLL_INTERVAL", DEFAULT_LOG_MONITOR_POLL_INTERVAL,
526
+ )
527
+
467
528
  replacements = {
529
+ "{{LOG_SIZE_AWARENESS}}": load_log_size_section(script_dir),
530
+ "{{MAX_LOG_SIZE}}": str(max_log_size),
531
+ "{{MAX_LOG_SIZE_HUMAN}}": _format_bytes(max_log_size),
532
+ "{{LOG_MONITOR_POLL_INTERVAL}}": str(poll_interval),
533
+ "{{LOG_MONITOR_STATUS_FILE}}": os.path.join(
534
+ ".prizmkit", "state", "refactor", "refactors", args.refactor_id,
535
+ "sessions", args.session_id, "logs", "log-monitor-status.txt",
536
+ ),
537
+ "{{LOG_MONITOR_BASELINE_FILE}}": os.path.join(
538
+ ".prizmkit", "state", "refactor", "refactors", args.refactor_id,
539
+ "sessions", args.session_id, "logs", "log-monitor-baseline.txt",
540
+ ),
541
+ "{{SESSION_LOG_PATH}}": os.path.join(
542
+ ".prizmkit", "state", "refactor", "refactors", args.refactor_id,
543
+ "sessions", args.session_id, "logs", "session.log",
544
+ ),
545
+ "{{LOG_MONITOR_SCRIPT}}": os.path.join(
546
+ ".prizmkit", "dev-pipeline", "scripts", "monitor-log.sh",
547
+ ),
468
548
  "{{RUN_ID}}": args.run_id,
469
549
  "{{SESSION_ID}}": args.session_id,
470
550
  "{{REFACTOR_ID}}": args.refactor_id,
@@ -587,9 +667,10 @@ def render_template(template_content, replacements, resume_phase, refactor):
587
667
  # Step 1: Process conditional blocks
588
668
  content = process_conditional_blocks(template_content, resume_phase, refactor)
589
669
 
590
- # Step 2: Replace all {{PLACEHOLDER}} variables
591
- for placeholder, value in replacements.items():
592
- content = content.replace(placeholder, value)
670
+ # Step 2: Replace all {{PLACEHOLDER}} variables (two passes for nested sections)
671
+ for _pass in range(2):
672
+ for placeholder, value in replacements.items():
673
+ content = content.replace(placeholder, value)
593
674
 
594
675
  return content
595
676
 
@@ -753,6 +834,8 @@ if __name__ == "__main__":
753
834
  main()
754
835
  except KeyboardInterrupt:
755
836
  emit_failure("generate-refactor-prompt interrupted")
837
+ except ValueError as exc:
838
+ emit_failure(str(exc))
756
839
  except SystemExit:
757
840
  raise
758
841
  except Exception as exc:
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env bash
2
+ # monitor-log.sh — Background log size monitor for AI session subagent
3
+ #
4
+ # Polls session.log size, computes effective size (current - baseline),
5
+ # and writes a structured JSON status file. Intended to be run by a
6
+ # background subagent inside the AI CLI session.
7
+ #
8
+ # Usage:
9
+ # bash monitor-log.sh <session_log> <max_size> <poll_interval> <status_file> <baseline_file>
10
+ #
11
+ # Status file format (JSON, rewritten each poll cycle):
12
+ # {
13
+ # "status": "OK" | "COMPACT_NEEDED",
14
+ # "total_bytes": <current session.log size in bytes>,
15
+ # "baseline_bytes": <baseline size from last compact>,
16
+ # "effective_bytes": <total - baseline>,
17
+ # "threshold_bytes": <compact threshold>,
18
+ # "overage_bytes": <effective - threshold, 0 if not exceeded>,
19
+ # "usage_pct": <effective / threshold * 100, integer>
20
+ # }
21
+
22
+ SESSION_LOG="${1}"
23
+ MAX_SIZE="${2}"
24
+ POLL_INTERVAL="${3}"
25
+ STATUS_FILE="${4}"
26
+ BASELINE_FILE="${5}"
27
+
28
+ # Validate required args
29
+ if [[ -z "$SESSION_LOG" || -z "$MAX_SIZE" || -z "$POLL_INTERVAL" || -z "$STATUS_FILE" || -z "$BASELINE_FILE" ]]; then
30
+ echo "Usage: $0 <session_log> <max_size> <poll_interval> <status_file> <baseline_file>" >&2
31
+ exit 1
32
+ fi
33
+
34
+ # Ensure status dir exists
35
+ mkdir -p "$(dirname "$STATUS_FILE")"
36
+
37
+ # Validate numeric args
38
+ if ! [[ "$MAX_SIZE" =~ ^[0-9]+$ ]]; then
39
+ echo "ERROR: max_size must be a non-negative integer" >&2
40
+ exit 1
41
+ fi
42
+ if ! [[ "$POLL_INTERVAL" =~ ^[0-9]+$ ]] || [[ "$POLL_INTERVAL" -le 0 ]]; then
43
+ echo "ERROR: poll_interval must be a positive integer" >&2
44
+ exit 1
45
+ fi
46
+
47
+ echo "[monitor-log] Starting log monitor: max=${MAX_SIZE} interval=${POLL_INTERVAL}s" >&2
48
+ echo "[monitor-log] Watching: $SESSION_LOG" >&2
49
+
50
+ while true; do
51
+ # Read current log size
52
+ if [[ -f "$SESSION_LOG" ]]; then
53
+ size=$(wc -c < "$SESSION_LOG" 2>/dev/null | tr -d ' ')
54
+ else
55
+ size=0
56
+ fi
57
+
58
+ # Read baseline (resets after each /compact)
59
+ baseline=0
60
+ if [[ -f "$BASELINE_FILE" ]]; then
61
+ baseline=$(cat "$BASELINE_FILE" 2>/dev/null | tr -d ' ')
62
+ if ! [[ "$baseline" =~ ^[0-9]+$ ]]; then
63
+ baseline=0
64
+ fi
65
+ fi
66
+
67
+ # Effective size = growth since last compact
68
+ effective=$((size - baseline))
69
+ if [[ $effective -lt 0 ]]; then
70
+ effective=0
71
+ fi
72
+
73
+ # Determine status (MAX_SIZE=0 disables monitoring)
74
+ if [[ $MAX_SIZE -gt 0 && $effective -gt $MAX_SIZE ]]; then
75
+ status="COMPACT_NEEDED"
76
+ overage=$((effective - MAX_SIZE))
77
+ echo "[monitor-log] COMPACT_NEEDED: effective=${effective} overage=${overage} (threshold=${MAX_SIZE})" >&2
78
+ else
79
+ status="OK"
80
+ overage=0
81
+ fi
82
+
83
+ # Usage percentage (effective / threshold * 100)
84
+ if [[ $MAX_SIZE -gt 0 ]]; then
85
+ usage_pct=$((effective * 100 / MAX_SIZE))
86
+ else
87
+ usage_pct=0
88
+ fi
89
+
90
+ # Write structured JSON status
91
+ cat > "$STATUS_FILE" << EOF
92
+ {
93
+ "status": "${status}",
94
+ "total_bytes": ${size},
95
+ "baseline_bytes": ${baseline},
96
+ "effective_bytes": ${effective},
97
+ "threshold_bytes": ${MAX_SIZE},
98
+ "overage_bytes": ${overage},
99
+ "usage_pct": ${usage_pct}
100
+ }
101
+ EOF
102
+
103
+ sleep "$POLL_INTERVAL"
104
+ done
@@ -12,7 +12,7 @@ You are the **session orchestrator**. Implement Feature {{FEATURE_ID}}: "{{FEATU
12
12
 
13
13
  **CRITICAL**: You MUST NOT exit until ALL work is complete and committed.
14
14
 
15
- **Tier 1 — Single Agent**: You handle everything directly. No subagents, no TeamCreate.
15
+ **Tier 1 — Single Agent**: You handle everything directly. No normal work subagents; the log-size monitor is the only allowed persistent background subagent. No TeamCreate.
16
16
 
17
17
  ### Feature Description
18
18
 
@@ -54,6 +54,9 @@ You are running in **headless non-interactive mode** with a FINITE context windo
54
54
 
55
55
  ---
56
56
 
57
+ {{LOG_SIZE_AWARENESS}}
58
+ ---
59
+
57
60
  ## PrizmKit Directory Convention
58
61
 
59
62
  ```
@@ -10,7 +10,7 @@
10
10
 
11
11
  You are the **session orchestrator**. Implement Feature {{FEATURE_ID}}: "{{FEATURE_TITLE}}".
12
12
 
13
- **CRITICAL**: You MUST NOT exit until ALL work is complete and committed. When you spawn subagents, wait for each to finish (run_in_background=false).
13
+ **CRITICAL**: You MUST NOT exit until ALL work is complete and committed. When you spawn normal work subagents, wait for each to finish (run_in_background=false). The log-size monitor is the only allowed persistent background subagent.
14
14
 
15
15
  **Tier 2 — Dual Agent**: You handle context + planning directly. Then spawn Dev and Reviewer subagents. Spawn Dev and Reviewer agents via the Agent tool.
16
16
 
@@ -60,6 +60,9 @@ You are running in **headless non-interactive mode** with a FINITE context windo
60
60
 
61
61
  ---
62
62
 
63
+ {{LOG_SIZE_AWARENESS}}
64
+ ---
65
+
63
66
  ## PrizmKit Directory Convention
64
67
 
65
68
  ```
@@ -601,7 +604,7 @@ If you encounter an unrecoverable error, context overflow, or are about to exit
601
604
  - Build context-snapshot.md FIRST; all subagents read it instead of re-reading source files
602
605
  - context-snapshot.md is append-only: orchestrator writes Sections 1-4, Dev appends Implementation Log, Reviewer appends Review Notes
603
606
  - Gate checks enforce Implementation Log and Review Notes are written before proceeding
604
- - Do NOT use `run_in_background=true` when spawning subagents
607
+ - Do NOT use `run_in_background=true` when spawning normal work subagents; the log-size monitor is the only allowed persistent background subagent
605
608
  - **NEVER delete, modify, or touch any file under `.prizmkit/state/`** — those are pipeline runtime files managed by the runner
606
609
  - NEVER run `rm -rf`, `git clean`, or any destructive filesystem operations
607
610
  - `/prizmkit-committer` is mandatory, and must not be replaced with manual git commit commands