kairos-chain 3.79.0 → 3.81.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +97 -0
- data/lib/kairos_mcp/version.rb +1 -1
- data/templates/skillsets/kairos_hook_projector/hooks/readable_gate.rb +119 -86
- data/templates/skillsets/kairos_hook_projector/skillset.json +1 -1
- data/templates/skillsets/kairos_hook_projector/test/mutation_check_readable_gate.rb +134 -12
- data/templates/skillsets/kairos_hook_projector/test/test_readable_gate.rb +530 -43
- data/templates/skillsets/project_manager/plugin/SKILL.md +27 -15
- data/templates/skillsets/project_manager/plugin/hooks.json +19 -2
- data/templates/skillsets/project_manager/scripts/l2_scan.rb +510 -0
- data/templates/skillsets/project_manager/scripts/pm_l2_report.rb +655 -0
- data/templates/skillsets/project_manager/skillset.json +2 -2
- data/templates/skillsets/project_manager/test/test_pm_l2_report.rb +1496 -0
- metadata +4 -4
- data/templates/skillsets/project_manager/scripts/l2_scan.py +0 -284
- data/templates/skillsets/project_manager/scripts/pm_l2_report.py +0 -597
- data/templates/skillsets/project_manager/test/test_pm_l2_report.py +0 -685
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0bc251869ebf7ce64e94fbae5baf21ef0eea655b6d1c42d55dfb2f7ee7c2ccc0
|
|
4
|
+
data.tar.gz: aea009fc75974ff42a05e2bc54a352c19b1192157b9fe8eefa311ae9fd096c65
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 498212b88feb4e563e745be00467f8e206aff2c2103761c12d21695a1829201f2edae98295f61e2aaf6695f9d5e82be4f9f24a2c3ff10e868965a8cc1f1eae91
|
|
7
|
+
data.tar.gz: efe4a6d07531e77648bd175452e72036a0261d0bfb3b426b5f571b54b14a1f3f1695acc3b7d0136b64f04c33eac0e9c04dcebfb58481373af0ddbed355a8d1c4
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,103 @@ All notable changes to the `kairos-chain` gem will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
This project follows [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [3.81.0] - 2026-08-27
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **`project_manager` ships its L2 comparison in Ruby, 0.7.0 -> 0.8.0.** The
|
|
12
|
+
three hook-side scripts move from Python to Ruby (`pm_l2_report.rb`,
|
|
13
|
+
`l2_scan.rb`, and the ported test), and the SessionStart hook it contributes
|
|
14
|
+
now invokes `ruby` rather than `python3`. python3 was never guaranteed on a
|
|
15
|
+
host the gem ships to; the interpreter the rest of the SkillSet already runs
|
|
16
|
+
on is. The port itself was committed before this release (`1ec9bd5`) and is
|
|
17
|
+
unchanged here — 505 execution comparisons over 101 input classes with zero
|
|
18
|
+
undeclared differences, 100 unit runs / 485 assertions green, 6/6 mutations
|
|
19
|
+
killed.
|
|
20
|
+
|
|
21
|
+
The version move is the load-bearing part of this release. Without it
|
|
22
|
+
`skillset upgrade` compares versions, finds them equal, reports UNCHANGED and
|
|
23
|
+
copies nothing, so an instance would take the new gem and keep the Python
|
|
24
|
+
scripts while the projected hook still names them.
|
|
25
|
+
|
|
26
|
+
- **`project_manager/plugin/hooks.json`: the bare `ruby` is now a ruling, not
|
|
27
|
+
an open question.** The comment block previously said the resolution was
|
|
28
|
+
undecided and named two candidates — the projector embedding an absolute
|
|
29
|
+
interpreter path, or this file checking `RUBY_VERSION`. Neither ships
|
|
30
|
+
(operator ruling, 2026-08-27). The comment now records why each was declined:
|
|
31
|
+
an embedded path has to be written by the projector, which turns an
|
|
32
|
+
interpreter choice into a core change and pins every host to one machine's
|
|
33
|
+
layout; a version check in this file trades the loud failure for a quiet one;
|
|
34
|
+
and `rbenv exec` fails outright wherever rbenv is absent — a Docker ruby
|
|
35
|
+
image, a homebrew install, asdf — which is strictly worse than the bare form,
|
|
36
|
+
since those hosts run the bare form fine.
|
|
37
|
+
|
|
38
|
+
Resolution therefore stays with the host's PATH, and the residual risk is
|
|
39
|
+
stated rather than hidden: `/usr/bin/ruby` 2.6.10 fails this script at parse
|
|
40
|
+
time with a 43-line syntax error and writes no page (measured). That failure
|
|
41
|
+
is visible at session start rather than silent, and the visibility is what
|
|
42
|
+
makes the bare form acceptable rather than merely convenient. The command
|
|
43
|
+
string is unchanged by this release; only the comment moved.
|
|
44
|
+
|
|
45
|
+
## [3.80.0] - 2026-08-27
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
|
|
49
|
+
- **`readable_gate`: the measurement bound is the hook's own remaining time,
|
|
50
|
+
not a mode's declaration.** `measure_timeout_seconds` is retired, and its
|
|
51
|
+
whole observable surface goes with it — the clamp, the type-error clause,
|
|
52
|
+
and the silent death where `0` or a negative passed the Integer check,
|
|
53
|
+
reached `Regexp.timeout=` and killed the gate with exit 0, empty stdout and
|
|
54
|
+
no log file. A mode file must not be able to silence the gate by arithmetic
|
|
55
|
+
(operator ruling, 2026-08-27). A config still declaring the key flows
|
|
56
|
+
through `checked()` as an unknown key and is read by nobody.
|
|
57
|
+
|
|
58
|
+
Time protection is now `bounded_match`, the one seam every mode-supplied
|
|
59
|
+
match passes through: remaining computed from an absolute deadline fixed at
|
|
60
|
+
`main`'s first statement, a floor at one millisecond (`Regexp.timeout=`
|
|
61
|
+
silently stores nil for positive values under 1e-9), the grant set to the
|
|
62
|
+
remaining time, and the grant revoked as the match ends — no match runs
|
|
63
|
+
under an authorisation granted to another. Announce, specimen, shorthand and
|
|
64
|
+
both gloss operands route through it; `each_match` starts every match inside
|
|
65
|
+
the seam, so no ordering exists in which a match begins before the clock is
|
|
66
|
+
read. The recheck's poll is charged as time actually spent, which retires
|
|
67
|
+
the old static arithmetic that billed a first read for the four seconds only
|
|
68
|
+
a recheck uses. `HOOK_TIMEOUT_MARGIN` drops from 1.0 to 0.5 (start-up
|
|
69
|
+
measured at a 0.033s median), recovering half the band the old margin
|
|
70
|
+
discarded.
|
|
71
|
+
|
|
72
|
+
**Three declared output differences** against the pre-bound gate: a config
|
|
73
|
+
declaring the key loses its problem clause and the gate runs when no other
|
|
74
|
+
problem remains; the measurement-exceeded banner quotes elapsed time against
|
|
75
|
+
the live budget instead of a mode-declared value; and the budget itself
|
|
76
|
+
grows from a flat 5s to the deadline's remainder (≈9.4s on a first read,
|
|
77
|
+
≈5.4s on a recheck), so a measurement finishing between 5s and the budget
|
|
78
|
+
now yields a verdict where it used to yield NOT RUN. Probed at 10, 12 and 14
|
|
79
|
+
configurations by three independent contexts: no undeclared difference.
|
|
80
|
+
|
|
81
|
+
Design frozen at v0.7 after six review rounds; implementation conformance
|
|
82
|
+
round 1 REVISE, round 2 APPROVE 3/3 with zero DOES NOT HOLD. Suite 152 runs
|
|
83
|
+
/ 948 assertions green; the mutation harness gains 19 rows paired one-to-one
|
|
84
|
+
with the design's table (anchors verified 72/72), plus a label filter and
|
|
85
|
+
early exit so a narrowed sweep is possible and says so in its summary.
|
|
86
|
+
|
|
87
|
+
### Fixed
|
|
88
|
+
|
|
89
|
+
- **`readable_gate`: the carry-over note's truncation fallback refuses to
|
|
90
|
+
follow a symlink.** When a stale note cannot be deleted the gate empties it
|
|
91
|
+
in place, and that write followed whatever the note's name pointed at: with
|
|
92
|
+
a symlink planted at the note key and the notes directory made unwritable so
|
|
93
|
+
the unlink is refused, the emptying truncated a file outside the notes
|
|
94
|
+
directory. The open now carries `File::NOFOLLOW`, so a final-component
|
|
95
|
+
symlink fails with `ELOOP` and lands in the existing refusal arm — the
|
|
96
|
+
leaning is to refuse, never to write through a name to somewhere the path
|
|
97
|
+
did not say. The leftover link stays unspent and every later recheck refuses
|
|
98
|
+
it. A hard link and an intermediate-component symlink still reach through;
|
|
99
|
+
both need the same outside hand and are declared rather than closed.
|
|
100
|
+
|
|
101
|
+
- `kairos_hook_projector` SkillSet version 0.5.0 → 0.6.0, so instances
|
|
102
|
+
actually receive the two changes above on `skillset upgrade`.
|
|
103
|
+
|
|
7
104
|
## [3.79.0] - 2026-08-26
|
|
8
105
|
|
|
9
106
|
### Fixed
|
data/lib/kairos_mcp/version.rb
CHANGED
|
@@ -64,30 +64,26 @@ module KairosHookProjector
|
|
|
64
64
|
# the limit emits nothing at all — no verdict, no banner, no log line — so
|
|
65
65
|
# the gate stops enforcing and the measurement loses rows, both silently.
|
|
66
66
|
HOOK_TIMEOUT = 10.0
|
|
67
|
-
# Held back
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
HOOK_TIMEOUT_MARGIN =
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
#
|
|
82
|
-
#
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
#
|
|
86
|
-
|
|
87
|
-
# spends. The bound is therefore carved out of this design and carried in
|
|
88
|
-
# its own, and this line is back to the shape shipped since 6d7fe02.
|
|
89
|
-
MEASURE_TIMEOUT_CEILING =
|
|
90
|
-
HOOK_TIMEOUT - HOOK_TIMEOUT_MARGIN - (RECHECK_POLL_ATTEMPTS * POLL_DELAY)
|
|
67
|
+
# Held back for what the deadline cannot see: interpreter start-up before
|
|
68
|
+
# main's first clock read, and the output written after measurement ends.
|
|
69
|
+
# Start-up measured at 0.033s median over 8 runs through the projected
|
|
70
|
+
# `sh -c ruby …` form (2026-08-27, consistent with round 5's 0.033-0.039s);
|
|
71
|
+
# the log append and JSON emit are under 10ms. The previous value, 1.0, was
|
|
72
|
+
# about thirty times the measured start-up and discarded ~0.95s in which
|
|
73
|
+
# the pre-bound gate would have measured and blocked. Pinned by an equality
|
|
74
|
+
# assert, not by the wall-time band: the band's flake headroom and kill
|
|
75
|
+
# margin always sum to exactly this value, so no band can pin it robustly
|
|
76
|
+
# from both sides.
|
|
77
|
+
HOOK_TIMEOUT_MARGIN = 0.5
|
|
78
|
+
# The least remaining time bounded_match will grant. Two grounds. First,
|
|
79
|
+
# Regexp.timeout= silently stores nil — no bound at all — for positive
|
|
80
|
+
# values below 1e-9 (measured on Ruby 3.4.10: 1e-9 is kept, 7.5e-10 reads
|
|
81
|
+
# back nil), so a remaining that small must refuse rather than arm nothing.
|
|
82
|
+
# Second, a measurement with under a millisecond left cannot produce a
|
|
83
|
+
# verdict anyway; raising is the honest outcome. Pinned by equality in both
|
|
84
|
+
# directions: lowering it re-opens the silent-nil window, raising it
|
|
85
|
+
# quietly shrinks every budget.
|
|
86
|
+
MIN_TIMEOUT = 0.001
|
|
91
87
|
|
|
92
88
|
# Built from single-quoted strings so that `#{` in a pattern stays literal
|
|
93
89
|
# rather than becoming interpolation, and anchored with \A because Python's
|
|
@@ -110,12 +106,14 @@ module KairosHookProjector
|
|
|
110
106
|
'specimen_patterns' => [],
|
|
111
107
|
'vocab_min_lines' => 1,
|
|
112
108
|
'blocking' => true,
|
|
113
|
-
#
|
|
114
|
-
# bound
|
|
115
|
-
#
|
|
116
|
-
#
|
|
117
|
-
#
|
|
118
|
-
|
|
109
|
+
# measure_timeout_seconds is deliberately absent. It was a mode-declared
|
|
110
|
+
# bound on measurement time, and its whole observable surface — the
|
|
111
|
+
# clamp, the type-error clause, the 0-or-negative silent death, and the
|
|
112
|
+
# 5-second budget itself — was retired by the 2026-08-27 operator ruling
|
|
113
|
+
# ("no mode-facing bound setting"): a mode file must not be able to
|
|
114
|
+
# silence the gate by arithmetic. A config still declaring it is passed
|
|
115
|
+
# through checked() as an unknown key and read by nobody. Time protection
|
|
116
|
+
# is bounded_match's, derived from the hook's own remaining time.
|
|
119
117
|
'log_max_bytes' => 1024 * 1024,
|
|
120
118
|
'banner_prefix' => 'gate',
|
|
121
119
|
# nil, and pinned here, because nil is what note() reads as "this mode
|
|
@@ -126,7 +124,7 @@ module KairosHookProjector
|
|
|
126
124
|
}.freeze
|
|
127
125
|
|
|
128
126
|
INT_KEYS = %w[max_lines max_headings max_tables diagram_required_over_lines
|
|
129
|
-
vocab_min_lines
|
|
127
|
+
vocab_min_lines log_max_bytes].freeze
|
|
130
128
|
LIST_KEYS = %w[announce_patterns shorthand_patterns gloss_patterns
|
|
131
129
|
specimen_patterns].freeze
|
|
132
130
|
# log_path is deliberately absent. It was added here once, and the cost was
|
|
@@ -151,7 +149,7 @@ module KairosHookProjector
|
|
|
151
149
|
:max_lines, :max_headings, :max_tables, :diagram_over_lines,
|
|
152
150
|
:vocab_min_lines,
|
|
153
151
|
:blocking, :banner_prefix, :rewrite_instruction, :log_path,
|
|
154
|
-
:announce, :shorthand, :gloss, :specimen,
|
|
152
|
+
:announce, :shorthand, :gloss, :specimen,
|
|
155
153
|
:log_max_bytes
|
|
156
154
|
|
|
157
155
|
def initialize(raw, path)
|
|
@@ -188,18 +186,6 @@ module KairosHookProjector
|
|
|
188
186
|
# list. Text explaining the vocabulary rule has to name the shapes it
|
|
189
187
|
# governs, and naming them is not using them.
|
|
190
188
|
@specimen = merged['specimen_patterns'].map { |p| Regexp.new(p) }
|
|
191
|
-
# Clamped, not rejected: a mode that asks for 6 seconds gets what fits
|
|
192
|
-
# rather than a refusal it cannot act on. 4 seconds of recheck poll plus
|
|
193
|
-
# 6 of measurement is 10.4 against a 10-second hook limit, and the hook
|
|
194
|
-
# dies with no output, so the gate would silently stop enforcing for
|
|
195
|
-
# exactly the modes whose patterns are expensive enough to need the time.
|
|
196
|
-
requested = merged['measure_timeout_seconds']
|
|
197
|
-
@measure_timeout =
|
|
198
|
-
if requested.is_a?(Numeric) && requested > MEASURE_TIMEOUT_CEILING
|
|
199
|
-
MEASURE_TIMEOUT_CEILING
|
|
200
|
-
else
|
|
201
|
-
requested
|
|
202
|
-
end
|
|
203
189
|
@log_max_bytes = merged['log_max_bytes']
|
|
204
190
|
end
|
|
205
191
|
|
|
@@ -474,6 +460,16 @@ module KairosHookProjector
|
|
|
474
460
|
# the parse on every later read whatever the directory permits by then.
|
|
475
461
|
# Only when both are refused does the leftover stay valid, and that state
|
|
476
462
|
# is declared in design §7 rather than closed.
|
|
463
|
+
#
|
|
464
|
+
# The truncation refuses to follow a symlink. The note's name sits in
|
|
465
|
+
# operator space, and round 4 planted a link there — the directory then
|
|
466
|
+
# made unwritable so the unlink cannot spend it — and pointed the old
|
|
467
|
+
# File.write at a file outside the notes directory. NOFOLLOW turns that
|
|
468
|
+
# open into an ELOOP, which lands in the refusal arm below: the leaning is
|
|
469
|
+
# to refuse, never to write through a name to somewhere the path did not
|
|
470
|
+
# say. A hard link, or a link planted higher up the path, still reaches
|
|
471
|
+
# through — each needs the same outside hand that plants the link, and both
|
|
472
|
+
# are declared rather than closed.
|
|
477
473
|
def spend_stale_note(path, reason)
|
|
478
474
|
File.unlink(path)
|
|
479
475
|
"#{reason}; a stale note was deleted"
|
|
@@ -481,7 +477,7 @@ module KairosHookProjector
|
|
|
481
477
|
reason
|
|
482
478
|
rescue StandardError
|
|
483
479
|
begin
|
|
484
|
-
File.
|
|
480
|
+
File.open(path, File::WRONLY | File::TRUNC | File::NOFOLLOW).close
|
|
485
481
|
"#{reason}; a stale note could not be deleted and was emptied instead"
|
|
486
482
|
rescue StandardError => e
|
|
487
483
|
"#{reason}; a stale note could not be deleted (#{e.class})"
|
|
@@ -694,44 +690,74 @@ module KairosHookProjector
|
|
|
694
690
|
|
|
695
691
|
# --- measurement ---------------------------------------------------------
|
|
696
692
|
|
|
697
|
-
# The
|
|
698
|
-
#
|
|
699
|
-
#
|
|
700
|
-
#
|
|
701
|
-
#
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
693
|
+
# The one gate every mode-supplied match passes through. The invariant is
|
|
694
|
+
# over match operations, not call sites: no mode pattern ever starts a
|
|
695
|
+
# match without a bound computed from the time actually remaining, and no
|
|
696
|
+
# match runs under an authorisation granted to another — the grant is
|
|
697
|
+
# revoked as the match ends, whichever way it ends. Both halves are
|
|
698
|
+
# review-taught: naming three sites and bounding those left announce and
|
|
699
|
+
# both gloss operands unbounded, and granting without revoking let a match
|
|
700
|
+
# outside the seam inherit the previous grant, which made every bypass
|
|
701
|
+
# unobservable (measured at +65µs against a band needing +0.25s).
|
|
702
|
+
#
|
|
703
|
+
# The floor: a remaining below MIN_TIMEOUT raises instead of arming the
|
|
704
|
+
# bound, because Regexp.timeout= silently stores nil — no bound at all —
|
|
705
|
+
# for positive values under 1e-9, and a sub-millisecond measurement cannot
|
|
706
|
+
# finish regardless. The check precedes the match, so no ordering exists
|
|
707
|
+
# in which a match starts and the clock is read only after it.
|
|
708
|
+
#
|
|
709
|
+
# A nil deadline is the --report path: offline calibration, run by the
|
|
710
|
+
# operator outside the hook limit, deliberately unbounded.
|
|
711
|
+
#
|
|
712
|
+
# monotonic is read through the module helper and must stay that way: the
|
|
713
|
+
# floor fixture stubs it and counts the calls, so inlining the clock here
|
|
714
|
+
# is a mutation that fixture kills.
|
|
715
|
+
def bounded_match(regexp, string, pos, deadline)
|
|
716
|
+
return regexp.match(string, pos) if deadline.nil?
|
|
717
|
+
|
|
718
|
+
remaining = deadline - monotonic
|
|
719
|
+
raise MeasureTimeout if remaining < MIN_TIMEOUT
|
|
720
|
+
|
|
721
|
+
Regexp.timeout = remaining
|
|
722
|
+
begin
|
|
723
|
+
regexp.match(string, pos)
|
|
724
|
+
ensure
|
|
725
|
+
Regexp.timeout = nil
|
|
726
|
+
end
|
|
727
|
+
end
|
|
728
|
+
|
|
729
|
+
# In a named helper so the formula is assertable: an assert against an
|
|
730
|
+
# inline expression only re-derives it, which is a test that cannot fail.
|
|
731
|
+
def deadline_for(started)
|
|
732
|
+
started + (HOOK_TIMEOUT - HOOK_TIMEOUT_MARGIN)
|
|
733
|
+
end
|
|
706
734
|
|
|
735
|
+
def each_match(regexp, string, deadline)
|
|
736
|
+
pos = 0
|
|
737
|
+
while pos <= string.length && (m = bounded_match(regexp, string, pos, deadline))
|
|
707
738
|
yield m
|
|
708
739
|
pos = m.end(0) > m.begin(0) ? m.end(0) : m.begin(0) + 1
|
|
709
740
|
end
|
|
710
741
|
end
|
|
711
742
|
|
|
712
|
-
def specimen_spans(line, cfg, deadline
|
|
743
|
+
def specimen_spans(line, cfg, deadline)
|
|
713
744
|
spans = []
|
|
714
745
|
cfg.specimen.each do |pattern|
|
|
715
|
-
raise MeasureTimeout if deadline && monotonic > deadline
|
|
716
|
-
|
|
717
746
|
each_match(pattern, line, deadline) { |m| spans << [m.begin(0), m.end(0)] }
|
|
718
747
|
end
|
|
719
748
|
spans
|
|
720
749
|
end
|
|
721
750
|
|
|
722
|
-
# measure()
|
|
723
|
-
#
|
|
724
|
-
#
|
|
725
|
-
#
|
|
726
|
-
#
|
|
727
|
-
#
|
|
728
|
-
def measure_bounded(text, cfg)
|
|
729
|
-
return measure(text, cfg, nil) unless cfg.measure_timeout
|
|
730
|
-
|
|
751
|
+
# measure() with the deadline threaded through. This wrapper owns the two
|
|
752
|
+
# things that sit outside any single match: whatever Regexp.timeout the
|
|
753
|
+
# caller had is saved and restored here, outside every per-match grant,
|
|
754
|
+
# and a match cut by Ruby's own Regexp::TimeoutError maps to the same
|
|
755
|
+
# MeasureTimeout the deadline raises — one banner for both ways of
|
|
756
|
+
# running out.
|
|
757
|
+
def measure_bounded(text, cfg, deadline)
|
|
731
758
|
previous = Regexp.timeout
|
|
732
|
-
Regexp.timeout = cfg.measure_timeout
|
|
733
759
|
begin
|
|
734
|
-
measure(text, cfg,
|
|
760
|
+
measure(text, cfg, deadline)
|
|
735
761
|
rescue Regexp::TimeoutError
|
|
736
762
|
raise MeasureTimeout
|
|
737
763
|
ensure
|
|
@@ -744,7 +770,7 @@ module KairosHookProjector
|
|
|
744
770
|
end
|
|
745
771
|
|
|
746
772
|
# Pure. [metrics, failures]. The whole judgement lives here.
|
|
747
|
-
def measure(text, cfg, deadline
|
|
773
|
+
def measure(text, cfg, deadline)
|
|
748
774
|
raw = text.split("\n", -1)
|
|
749
775
|
|
|
750
776
|
prose = []
|
|
@@ -765,27 +791,20 @@ module KairosHookProjector
|
|
|
765
791
|
tables = prose.count { |l| TABLE_SEP.match(l) }
|
|
766
792
|
|
|
767
793
|
first = raw.find { |l| !l.strip.empty? } || ''
|
|
768
|
-
announced = !(cfg.announce && cfg.announce
|
|
794
|
+
announced = !(cfg.announce && bounded_match(cfg.announce, first, 0, deadline)).nil?
|
|
769
795
|
|
|
770
796
|
seen = {}
|
|
771
797
|
unglossed = []
|
|
772
798
|
if !cfg.shorthand.empty? && prose.length >= cfg.vocab_min_lines
|
|
773
799
|
prose.each_with_index do |line, i|
|
|
774
|
-
# No per-line check.
|
|
775
|
-
#
|
|
776
|
-
#
|
|
777
|
-
#
|
|
778
|
-
#
|
|
800
|
+
# No per-line or per-pattern check. Every match on this line starts
|
|
801
|
+
# inside bounded_match, which reads the clock before it; a check here
|
|
802
|
+
# would be a guard no fixture can redden — the shape this file
|
|
803
|
+
# deleted once already ("an unfalsifiable guard is not defence, it
|
|
804
|
+
# is a claim") and declines to reintroduce.
|
|
779
805
|
nxt = i + 1 < prose.length ? prose[i + 1] : ''
|
|
780
806
|
spans = specimen_spans(line, cfg, deadline)
|
|
781
807
|
cfg.shorthand.each do |pattern|
|
|
782
|
-
# Per pattern, not only per line. Regexp.timeout bounds one match,
|
|
783
|
-
# so a line carrying n patterns could overshoot the deadline by
|
|
784
|
-
# n times that bound while the clock was read only once, between
|
|
785
|
-
# lines. The check above still stands: it catches a line whose
|
|
786
|
-
# specimen scan alone is what runs long.
|
|
787
|
-
raise MeasureTimeout if deadline && monotonic > deadline
|
|
788
|
-
|
|
789
808
|
each_match(pattern, line, deadline) do |m|
|
|
790
809
|
tok = m.size > 1 ? m[1] : m[0]
|
|
791
810
|
next if tok.nil? || seen.key?(tok)
|
|
@@ -794,7 +813,8 @@ module KairosHookProjector
|
|
|
794
813
|
|
|
795
814
|
seen[tok] = true
|
|
796
815
|
after = line[m.end(0)..] || ''
|
|
797
|
-
if cfg.gloss && !(cfg.gloss
|
|
816
|
+
if cfg.gloss && !(bounded_match(cfg.gloss, after, 0, deadline) ||
|
|
817
|
+
bounded_match(cfg.gloss, nxt, 0, deadline))
|
|
798
818
|
unglossed << tok
|
|
799
819
|
end
|
|
800
820
|
end
|
|
@@ -990,6 +1010,14 @@ module KairosHookProjector
|
|
|
990
1010
|
end
|
|
991
1011
|
|
|
992
1012
|
def main(argv = ARGV, stdin = $stdin)
|
|
1013
|
+
# The first executable statement, before config or stdin are read: the
|
|
1014
|
+
# deadline is absolute, so everything main does — config load, transcript
|
|
1015
|
+
# reads, the recheck's poll sleeps — is charged against it as time
|
|
1016
|
+
# actually spent, and measurement bounds itself by whatever is left.
|
|
1017
|
+
# This is what retires the old static arithmetic that pre-charged a
|
|
1018
|
+
# first read for the four seconds only a recheck spends.
|
|
1019
|
+
started = monotonic
|
|
1020
|
+
deadline = deadline_for(started)
|
|
993
1021
|
config_path, report_paths = parse_args(argv)
|
|
994
1022
|
if config_path.nil?
|
|
995
1023
|
# Zero, not two. The header two hundred lines up says the exit status is
|
|
@@ -1084,13 +1112,18 @@ module KairosHookProjector
|
|
|
1084
1112
|
end
|
|
1085
1113
|
|
|
1086
1114
|
begin
|
|
1087
|
-
metrics, failures = measure_bounded(text, cfg)
|
|
1115
|
+
metrics, failures = measure_bounded(text, cfg, deadline)
|
|
1088
1116
|
rescue MeasureTimeout
|
|
1089
1117
|
lost = note(cfg, 'SKIP-measure-timeout')
|
|
1118
|
+
# Quotes the time actually spent and the budget it ran out of. The old
|
|
1119
|
+
# banner quoted the mode's declared bound — post-clamp, so a declared
|
|
1120
|
+
# 60 read "exceeded 5.0s" — and both the declaration and the clamp are
|
|
1121
|
+
# gone with the key.
|
|
1090
1122
|
emit(
|
|
1091
1123
|
'systemMessage' =>
|
|
1092
|
-
|
|
1093
|
-
|
|
1124
|
+
format('%s: NOT RUN — measurement ran out of the hook budget (%.1fs elapsed ' \
|
|
1125
|
+
"of %.1fs); check the mode's patterns for unbounded backtracking",
|
|
1126
|
+
cfg.banner_prefix, monotonic - started, deadline - started) +
|
|
1094
1127
|
(lost ? "; #{log_note(lost)}" : '')
|
|
1095
1128
|
)
|
|
1096
1129
|
return 0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kairos_hook_projector",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Projects an instruction mode's declared hooks into the harness configuration. Stage 1 compiles a mode_hooks declaration to a hook artifact plus a compile record. Stage 2 installs it: mode_hooks_project proposes by default and writes nothing, applying only when the caller echoes back the artifact hash from the proposal, and rewriting only hook groups carrying this SkillSet's marker for that mode — hand-added hooks and other modes' hooks are counted and left alone. Ships the generic readable_gate implementation; a mode supplies its own thresholds, the core supplies none.",
|
|
5
5
|
"author": "Masaomi Hatakeyama",
|
|
6
6
|
"layer": "L1",
|