@antoneeo/kb-agentic-skill 1.13.0 → 1.15.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.
- package/CHANGELOG.md +81 -0
- package/README.md +30 -1
- package/gemini-extension.json +6 -6
- package/package.json +51 -51
- package/skills/kb-agentic-skill/ENFORCEMENT.md +33 -16
- package/skills/kb-agentic-skill/SKILL.md +288 -274
- package/skills/kb-agentic-skill/dispatch.md +39 -1
- package/skills/kb-agentic-skill/distillation.md +38 -7
- package/skills/kb-agentic-skill/guides.md +16 -12
- package/skills/kb-agentic-skill/reconciliation.md +22 -0
- package/skills/kb-agentic-skill/review.md +80 -3
- package/skills/kb-agentic-skill/scripts/sdlc_check.py +46 -28
- package/skills/kb-agentic-skill/scripts/sdlc_core.py +293 -4
- package/skills/kb-agentic-skill/templates.md +17 -9
|
@@ -268,16 +268,18 @@ _PROFILE = {
|
|
|
268
268
|
"support_files": (),
|
|
269
269
|
"capabilities": frozenset(),
|
|
270
270
|
"design_gate_between": (),
|
|
271
|
+
"remind_line": "",
|
|
271
272
|
}
|
|
272
273
|
|
|
273
274
|
|
|
274
275
|
def set_profile(skill_name, support_files=(), capabilities=(), unit_noun="feature",
|
|
275
|
-
design_gate_between=()):
|
|
276
|
+
design_gate_between=(), remind_line=""):
|
|
276
277
|
_PROFILE.update(skill_name=skill_name,
|
|
277
278
|
unit_noun=unit_noun,
|
|
278
279
|
support_files=tuple(support_files),
|
|
279
280
|
capabilities=frozenset(capabilities),
|
|
280
|
-
design_gate_between=tuple(design_gate_between)
|
|
281
|
+
design_gate_between=tuple(design_gate_between),
|
|
282
|
+
remind_line=remind_line)
|
|
281
283
|
|
|
282
284
|
|
|
283
285
|
def profile():
|
|
@@ -1318,7 +1320,7 @@ def cmd_validate(root, strict=False, hybrid=False):
|
|
|
1318
1320
|
# comment-stripped, anchored: a '<!-- TODO: the ## Capability Ledger -->'
|
|
1319
1321
|
# must not read as the section being present
|
|
1320
1322
|
# Hybrid: the design lives in devPNT and its §4.5 gate owns this slot
|
|
1321
|
-
# (
|
|
1323
|
+
# (the Hybrid ownership matrix: "run ONE of them, never both"), and its log
|
|
1322
1324
|
# rows are keyed on e_isp_/e_tdd_ doc_keys, not on this filename -- so
|
|
1323
1325
|
# firing here would be a permanent, unfixable false positive.
|
|
1324
1326
|
if not hybrid and design_review_due(meta) and not review_logged(root, p.name):
|
|
@@ -1796,6 +1798,43 @@ def cmd_check(root, strict=False, hybrid=False):
|
|
|
1796
1798
|
|
|
1797
1799
|
# --------------------------------------------------------------------- gate
|
|
1798
1800
|
|
|
1801
|
+
# --- UserPromptSubmit reminder hook (F-046) --------------------------------
|
|
1802
|
+
# `orient` fires once per session and decays; a session that never invokes the
|
|
1803
|
+
# skill never had the protocol at all. Measured 2026-09-06: a release ran in
|
|
1804
|
+
# this repo against a `GUIDE_release.md` the session never consulted, because
|
|
1805
|
+
# SKILL.md -- where the duty is written -- had not been read.
|
|
1806
|
+
#
|
|
1807
|
+
# The carrier is kb's (F-041 item B), lifted here so all three lenses share one
|
|
1808
|
+
# machine instead of two that do the same thing. Its contract is kept: the line
|
|
1809
|
+
# is CONSTANT and reads NOTHING -- nothing repo- or session-controlled may ride
|
|
1810
|
+
# text injected every turn, and zero reads is what keeps the per-turn cost flat.
|
|
1811
|
+
# What changed is the payload's first duty and the wiring. First duty: decide
|
|
1812
|
+
# whether the skill governs this work and, if it does, read it once -- an
|
|
1813
|
+
# unread protocol cannot be applied, which is the failure above. Wiring: a
|
|
1814
|
+
# default instead of opt-in, on the owner's explicit acceptance of the per-turn
|
|
1815
|
+
# cost (`vision/rulings.md` r19; the "no ceremony ratchet" Non-Goal admits a
|
|
1816
|
+
# never-zero cost by that acceptance, and a trivial turn pays a yes/no
|
|
1817
|
+
# judgement, never a load).
|
|
1818
|
+
|
|
1819
|
+
REMIND_FALLBACK = (
|
|
1820
|
+
"decide whether the {skill} skill governs this work; if it does, load it "
|
|
1821
|
+
"once before acting -- an unread protocol cannot be applied."
|
|
1822
|
+
)
|
|
1823
|
+
|
|
1824
|
+
|
|
1825
|
+
def cmd_remind(args):
|
|
1826
|
+
"""UserPromptSubmit hook: print the lens's constant per-turn line. Always 0.
|
|
1827
|
+
|
|
1828
|
+
argv is ignored whole and nothing is read: a hook surface must not fail, and
|
|
1829
|
+
a flag typo must not cost a turn's context."""
|
|
1830
|
+
try:
|
|
1831
|
+
prof = profile()
|
|
1832
|
+
line = prof.get("remind_line") or REMIND_FALLBACK.format(skill=prof["skill_name"])
|
|
1833
|
+
print(f"{prof['skill_name']}: {line}")
|
|
1834
|
+
except Exception:
|
|
1835
|
+
pass
|
|
1836
|
+
return 0
|
|
1837
|
+
|
|
1799
1838
|
def cmd_gate(args):
|
|
1800
1839
|
file_path = args.file or ""
|
|
1801
1840
|
if args.hook:
|
|
@@ -2013,6 +2052,237 @@ def cmd_plan(root, args):
|
|
|
2013
2052
|
return 0
|
|
2014
2053
|
|
|
2015
2054
|
|
|
2055
|
+
# --- F-050: the benefit report -------------------------------------------
|
|
2056
|
+
# The process's central claim is that it makes divergence visible BEFORE
|
|
2057
|
+
# implementation. That claim lived in prose for four months while the evidence
|
|
2058
|
+
# for it sat unread in REVIEW_LOG. This reads it.
|
|
2059
|
+
#
|
|
2060
|
+
# Columns are resolved BY HEADER NAME, never by width. `templates.md` states
|
|
2061
|
+
# that contract ("extra or reordered columns are fine -- but the header must
|
|
2062
|
+
# say `tier`") and three widths are legal today: the original 8, Standalone's 9
|
|
2063
|
+
# (F-048 added `model`), and Hybrid's 10 (`instrument` + `notes`, and no
|
|
2064
|
+
# `reviewer` at all). A width whitelist reads none of the last, and silently
|
|
2065
|
+
# reports a devPNT-governed project as having no data at all.
|
|
2066
|
+
#
|
|
2067
|
+
# Two honesty rules, both learned from this unit's own review: whatever cannot
|
|
2068
|
+
# be read is COUNTED and NAMED, at the row level AND at the cell level. A ratio
|
|
2069
|
+
# over a silently shrunken denominator is worse than no ratio.
|
|
2070
|
+
REVIEW_CORE = ("date", "doc_key", "tier", "findings_real", "verdict")
|
|
2071
|
+
REVIEW_POSITIONAL = {
|
|
2072
|
+
8: ("date", "doc_key", "tier", "reviewer", "findings_raised",
|
|
2073
|
+
"findings_real", "verdict", "revise_rounds"),
|
|
2074
|
+
9: ("date", "doc_key", "tier", "model", "reviewer", "findings_raised",
|
|
2075
|
+
"findings_real", "verdict", "revise_rounds"),
|
|
2076
|
+
10: ("date", "doc_key", "tier", "model", "instrument", "findings_raised",
|
|
2077
|
+
"findings_real", "verdict", "revise_rounds", "notes"),
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
|
|
2081
|
+
def _table_cells(line):
|
|
2082
|
+
"""Cells of a Markdown table row, or None when the line is not one.
|
|
2083
|
+
Separator rows (---, :---:, any alignment colons) are not data."""
|
|
2084
|
+
s = line.strip()
|
|
2085
|
+
if not s.startswith("|"):
|
|
2086
|
+
return None
|
|
2087
|
+
if set(s) <= set("|-: "):
|
|
2088
|
+
return None
|
|
2089
|
+
return [c.strip() for c in s.strip("|").split("|")]
|
|
2090
|
+
|
|
2091
|
+
|
|
2092
|
+
def parse_review_log(text):
|
|
2093
|
+
"""-> (rows, unparsed). Columns come from the header when there is one.
|
|
2094
|
+
|
|
2095
|
+
Falls back to the positional maps only for a table with no header, so a
|
|
2096
|
+
hand-kept log still reads. A row whose cell count does not match the
|
|
2097
|
+
header's is unparsed and named -- never dropped."""
|
|
2098
|
+
rows, unparsed, names = [], [], None
|
|
2099
|
+
for n, line in enumerate(text.splitlines(), 1):
|
|
2100
|
+
cells = _table_cells(line)
|
|
2101
|
+
if cells is None:
|
|
2102
|
+
continue
|
|
2103
|
+
lowered = [c.lower() for c in cells]
|
|
2104
|
+
if names is None and "tier" in lowered and "verdict" in lowered:
|
|
2105
|
+
names = lowered # the header IS the column map
|
|
2106
|
+
continue
|
|
2107
|
+
# The header supplies the map -- that is what makes reordered and extra
|
|
2108
|
+
# columns readable. But a log legitimately carries MIXED widths (F-048
|
|
2109
|
+
# widened the schema and did not rewrite history), so a row that does
|
|
2110
|
+
# not match the header is read by the positional map for its own width
|
|
2111
|
+
# before it is given up on. Only a width no map knows is unparsed.
|
|
2112
|
+
if names is not None and len(cells) == len(names):
|
|
2113
|
+
row = dict(zip(names, cells))
|
|
2114
|
+
else:
|
|
2115
|
+
positional = REVIEW_POSITIONAL.get(len(cells))
|
|
2116
|
+
if positional is None:
|
|
2117
|
+
unparsed.append((n, "%d cells: %s" % (
|
|
2118
|
+
len(cells),
|
|
2119
|
+
"header declares %d and no positional map fits"
|
|
2120
|
+
% len(names) if names else "no header, and no map fits")))
|
|
2121
|
+
continue
|
|
2122
|
+
row = dict(zip(positional, cells))
|
|
2123
|
+
missing = [c for c in REVIEW_CORE if c not in row]
|
|
2124
|
+
if missing:
|
|
2125
|
+
unparsed.append((n, "no %s column" % "/".join(missing)))
|
|
2126
|
+
continue
|
|
2127
|
+
if not re.match(r"^\d{4}-\d{2}-\d{2}$", row["date"]):
|
|
2128
|
+
unparsed.append((n, "date cell %r is not a date" % row["date"][:24]))
|
|
2129
|
+
continue
|
|
2130
|
+
rows.append(row)
|
|
2131
|
+
return rows, unparsed
|
|
2132
|
+
|
|
2133
|
+
|
|
2134
|
+
def review_findings(value):
|
|
2135
|
+
"""The count in a `findings_real` cell, or None when it states no number.
|
|
2136
|
+
|
|
2137
|
+
None is NOT zero. `all` and `VOID -- instrument error` appear in real logs,
|
|
2138
|
+
and counting them as zero findings silently deflates the one ratio this
|
|
2139
|
+
report exists to state. Anchored at the START of the cell so a prose cell
|
|
2140
|
+
like `R1 4 BLOCK + 8 WARN` does not yield a confident 4."""
|
|
2141
|
+
m = re.match(r"^(\d+)\b", (value or "").strip())
|
|
2142
|
+
return int(m.group(1)) if m else None
|
|
2143
|
+
|
|
2144
|
+
|
|
2145
|
+
def _is_fail(verdict):
|
|
2146
|
+
"""FAIL as a WORD, so `PASS (no failures)` is not counted as a failure."""
|
|
2147
|
+
return re.search(r"\bFAIL\b", (verdict or "").upper()) is not None
|
|
2148
|
+
|
|
2149
|
+
|
|
2150
|
+
def _is_pass(verdict):
|
|
2151
|
+
return re.search(r"\bPASS\b", (verdict or "").upper()) is not None
|
|
2152
|
+
|
|
2153
|
+
|
|
2154
|
+
def _print_read_cost():
|
|
2155
|
+
"""The cost side, printed with the benefit and NEVER without it: a catch
|
|
2156
|
+
rate with no cost term justifies unbounded doctrine, and a cost term with
|
|
2157
|
+
no catch rate justifies deleting the process. When it cannot be measured
|
|
2158
|
+
the line still prints, saying so -- silence here IS the catch rate quoted
|
|
2159
|
+
alone, which is the thing this report exists to prevent."""
|
|
2160
|
+
skill_dir = Path(__file__).resolve().parent.parent
|
|
2161
|
+
contract = skill_dir / "SKILL.md"
|
|
2162
|
+
print()
|
|
2163
|
+
if not contract.is_file():
|
|
2164
|
+
print("Read cost: unavailable -- SKILL.md not found beside this "
|
|
2165
|
+
"validator (%s)" % skill_dir)
|
|
2166
|
+
return
|
|
2167
|
+
files = [p for p in sorted(skill_dir.glob("*.md")) if p.name != "SKILL.md"]
|
|
2168
|
+
support = sum(p.stat().st_size for p in files)
|
|
2169
|
+
head = contract.stat().st_size
|
|
2170
|
+
# The TOTAL, and the files itemised. Printing only the two terms lets the
|
|
2171
|
+
# one that fell be quoted while the one that grew stays lumped -- which is
|
|
2172
|
+
# exactly how a -7.8% headline coexisted with a read surface that grew.
|
|
2173
|
+
print("Read cost of %s: SKILL.md %d bytes (every session) + %d bytes "
|
|
2174
|
+
"across %d support files (read on trigger) = %d bytes total"
|
|
2175
|
+
% (skill_dir.name, head, support, len(files), head + support))
|
|
2176
|
+
for p in files:
|
|
2177
|
+
print(" %-24s %8d" % (p.name, p.stat().st_size))
|
|
2178
|
+
|
|
2179
|
+
|
|
2180
|
+
def cmd_benefit(root):
|
|
2181
|
+
"""Report what the process caught, beside what it costs to read.
|
|
2182
|
+
|
|
2183
|
+
ALWAYS exits 0 and emits no warning or advisory. This number is the
|
|
2184
|
+
criterion future units are judged by; a measurement that can fail a build
|
|
2185
|
+
becomes a target, and a targeted measurement stops measuring."""
|
|
2186
|
+
log = root / review_log_rel()
|
|
2187
|
+
print("=== benefit: what the process caught, and what it costs to read ===")
|
|
2188
|
+
if not log.is_file():
|
|
2189
|
+
print("[info] no %s yet: nothing to report. The number appears once "
|
|
2190
|
+
"reviews are logged." % review_log_rel())
|
|
2191
|
+
_print_read_cost()
|
|
2192
|
+
return 0
|
|
2193
|
+
rows, unparsed = parse_review_log(read_text(log))
|
|
2194
|
+
# `tier` carries the MOMENT in Standalone (design / closure) and the
|
|
2195
|
+
# reviewer WEIGHT in Hybrid (deep / light / code / guide / vision). A
|
|
2196
|
+
# Hybrid row is a real review whose moment this column does not state, so
|
|
2197
|
+
# it is counted and named, never folded into a moment by guesswork.
|
|
2198
|
+
moments = {"design": [], "closure": []}
|
|
2199
|
+
unstated = {}
|
|
2200
|
+
unstated_found = unstated_code = unstated_uncounted = 0
|
|
2201
|
+
for r in rows:
|
|
2202
|
+
tier = r["tier"].lower()
|
|
2203
|
+
if tier.startswith("design"):
|
|
2204
|
+
moments["design"].append(r)
|
|
2205
|
+
elif tier.startswith("closure"):
|
|
2206
|
+
moments["closure"].append(r)
|
|
2207
|
+
else:
|
|
2208
|
+
unstated[tier] = unstated.get(tier, 0) + 1
|
|
2209
|
+
n = review_findings(r["findings_real"])
|
|
2210
|
+
if n is None:
|
|
2211
|
+
unstated_uncounted += 1
|
|
2212
|
+
else:
|
|
2213
|
+
unstated_found += n
|
|
2214
|
+
if tier.startswith("code"):
|
|
2215
|
+
unstated_code += n
|
|
2216
|
+
classified = moments["design"] + moments["closure"]
|
|
2217
|
+
print("rows parsed: %d rows unparsed: %d%s"
|
|
2218
|
+
% (len(rows), len(unparsed),
|
|
2219
|
+
(" -> " + "; ".join("line %d: %s" % u for u in unparsed[:5]))
|
|
2220
|
+
if unparsed else ""))
|
|
2221
|
+
print("reviews with a stated moment: %d moment not stated by `tier`: %d%s"
|
|
2222
|
+
% (len(classified), sum(unstated.values()),
|
|
2223
|
+
(" (" + ", ".join("%s x%d" % (t, n) for t, n
|
|
2224
|
+
in sorted(unstated.items())) + ")")
|
|
2225
|
+
if unstated else ""))
|
|
2226
|
+
if not classified:
|
|
2227
|
+
_print_read_cost()
|
|
2228
|
+
return 0
|
|
2229
|
+
dates = sorted(r["date"] for r in classified)
|
|
2230
|
+
print("span of the reviews counted below: %s -> %s" % (dates[0], dates[-1]))
|
|
2231
|
+
|
|
2232
|
+
print()
|
|
2233
|
+
print("%-9s %6s %10s %11s %10s" % ("moment", "rows", "findings",
|
|
2234
|
+
"FAIL verdict", "per review"))
|
|
2235
|
+
totals, uncounted = {}, 0
|
|
2236
|
+
for m in ("design", "closure"):
|
|
2237
|
+
rs = moments[m]
|
|
2238
|
+
counts = [review_findings(r["findings_real"]) for r in rs]
|
|
2239
|
+
uncounted += sum(1 for c in counts if c is None)
|
|
2240
|
+
found = sum(c for c in counts if c is not None)
|
|
2241
|
+
failed = sum(1 for r in rs if _is_fail(r["verdict"]))
|
|
2242
|
+
totals[m] = found
|
|
2243
|
+
print("%-9s %6d %10d %11d %10.1f"
|
|
2244
|
+
% (m, len(rs), found, failed, (found / len(rs)) if rs else 0.0))
|
|
2245
|
+
total = sum(totals.values())
|
|
2246
|
+
if uncounted:
|
|
2247
|
+
print("findings cells stating no number: %d (counted as unknown, "
|
|
2248
|
+
"never as zero)" % uncounted)
|
|
2249
|
+
if total:
|
|
2250
|
+
print()
|
|
2251
|
+
print("Caught BEFORE the code existed: %d / %d findings = %.0f%%"
|
|
2252
|
+
% (totals["design"], total, 100.0 * totals["design"] / total))
|
|
2253
|
+
# The residue, stated as a BOUND rather than as a direction. The
|
|
2254
|
+
# sentence this replaces claimed the share was "a floor, not a
|
|
2255
|
+
# ceiling" because `code` rows are post-implementation -- which is
|
|
2256
|
+
# backwards, since folding them LOWERS the design share. But the fact
|
|
2257
|
+
# was right and only the direction wrong, so `code` findings sit on
|
|
2258
|
+
# the closure side of the upper bound instead of being discarded with
|
|
2259
|
+
# it. No direction is printed at all: the unstated set pushes both
|
|
2260
|
+
# ways, and only an interval is true of all of it.
|
|
2261
|
+
if unstated_found:
|
|
2262
|
+
denom = total + unstated_found
|
|
2263
|
+
print("Bounded by the %d findings in rows whose moment `tier` "
|
|
2264
|
+
"does not state: %.0f%%-%.0f%% (`code` rows are "
|
|
2265
|
+
"post-implementation by definition, so their findings sit "
|
|
2266
|
+
"on the closure side of the upper bound)"
|
|
2267
|
+
% (unstated_found, 100.0 * totals["design"] / denom,
|
|
2268
|
+
100.0 * (totals["design"] + unstated_found - unstated_code)
|
|
2269
|
+
/ denom))
|
|
2270
|
+
if unstated_uncounted:
|
|
2271
|
+
print("%d unstated rows state no findings count (`all`, `VOID`): "
|
|
2272
|
+
"neither zero nor countable, so they lie outside both "
|
|
2273
|
+
"bounds" % unstated_uncounted)
|
|
2274
|
+
failed_all = sum(1 for r in classified if _is_fail(r["verdict"]))
|
|
2275
|
+
inconclusive = sum(1 for r in classified if not _is_fail(r["verdict"])
|
|
2276
|
+
and not _is_pass(r["verdict"]))
|
|
2277
|
+
print("Reviews whose verdict contains FAIL: %d / %d = %.0f%%%s"
|
|
2278
|
+
% (failed_all, len(classified), 100.0 * failed_all / len(classified),
|
|
2279
|
+
(" (+%d inconclusive)" % inconclusive) if inconclusive else ""))
|
|
2280
|
+
print("(both figures are over the %d reviews whose moment `tier` states)"
|
|
2281
|
+
% len(classified))
|
|
2282
|
+
_print_read_cost()
|
|
2283
|
+
return 0
|
|
2284
|
+
|
|
2285
|
+
|
|
2016
2286
|
def cmd_orient(args):
|
|
2017
2287
|
"""SessionStart hook: emit a bounded, repo-sourced ai_docs/ orientation to
|
|
2018
2288
|
stdout and ALWAYS return 0 (fail-open, P-TM T8) -- a session hook must never
|
|
@@ -2229,6 +2499,12 @@ def cmd_migrate(root, args):
|
|
|
2229
2499
|
# --------------------------------------------------------------------- main
|
|
2230
2500
|
|
|
2231
2501
|
def main(argv=None):
|
|
2502
|
+
# `remind` is intercepted BEFORE argparse (kb's F-041 rule, kept): it is a
|
|
2503
|
+
# hook surface fired on every prompt, so a stray flag must cost a usage
|
|
2504
|
+
# message and a non-zero exit on a turn, not the turn itself.
|
|
2505
|
+
if (argv if argv is not None else sys.argv[1:])[:1] == ["remind"]:
|
|
2506
|
+
return cmd_remind(None)
|
|
2507
|
+
|
|
2232
2508
|
common = argparse.ArgumentParser(add_help=False)
|
|
2233
2509
|
common.add_argument("--root", help="project root (default: walk up until a docs root is found)")
|
|
2234
2510
|
common.add_argument("--docs-dir", dest="docs_dir",
|
|
@@ -2270,9 +2546,18 @@ def main(argv=None):
|
|
|
2270
2546
|
gp.add_argument("--file", help="file path to evaluate (alternative to --hook)")
|
|
2271
2547
|
gp.add_argument("--protected", default="", help="protected prefixes separated by ';' (e.g. \"src/auth;src/crypto\")")
|
|
2272
2548
|
|
|
2549
|
+
sub.add_parser("benefit", parents=[common],
|
|
2550
|
+
help="report what the review gates caught (and what the "
|
|
2551
|
+
"doctrine costs to read) -- a report, never a gate: "
|
|
2552
|
+
"always exits 0")
|
|
2553
|
+
|
|
2273
2554
|
sub.add_parser("orient", parents=[common, hybrid_opt],
|
|
2274
2555
|
help="SessionStart hook: emit docs-root orientation to stdout (fail-open, zero-execution)")
|
|
2275
2556
|
|
|
2557
|
+
sub.add_parser("remind", parents=[common],
|
|
2558
|
+
help="UserPromptSubmit hook: print the constant per-turn line "
|
|
2559
|
+
"(fail-open, zero-read, zero-execution)")
|
|
2560
|
+
|
|
2276
2561
|
pp = sub.add_parser("plan", parents=[common],
|
|
2277
2562
|
help="Subagent Execution: validate/brief a PLAN_[feature].md (zero-execution)")
|
|
2278
2563
|
pp_sub = pp.add_subparsers(dest="plan_cmd", required=True)
|
|
@@ -2296,7 +2581,7 @@ def main(argv=None):
|
|
|
2296
2581
|
# nothing is being guessed. Refusing here would make the guard block the
|
|
2297
2582
|
# one command that ends the ambiguity.
|
|
2298
2583
|
discovered, name = None, args.from_dir
|
|
2299
|
-
elif args.cmd
|
|
2584
|
+
elif args.cmd in ("orient", "remind"):
|
|
2300
2585
|
# The SessionStart hook is fail-open by contract: it must never block a
|
|
2301
2586
|
# session, not even on a half-migrated tree. It orients on the default
|
|
2302
2587
|
# and says so rather than exiting non-zero.
|
|
@@ -2311,6 +2596,8 @@ def main(argv=None):
|
|
|
2311
2596
|
return cmd_gate(args)
|
|
2312
2597
|
if args.cmd == "orient":
|
|
2313
2598
|
return cmd_orient(args)
|
|
2599
|
+
if args.cmd == "remind":
|
|
2600
|
+
return cmd_remind(args)
|
|
2314
2601
|
|
|
2315
2602
|
root = Path(args.root).resolve() if args.root else (discovered or find_project_root())
|
|
2316
2603
|
if args.cmd == "check":
|
|
@@ -2323,6 +2610,8 @@ def main(argv=None):
|
|
|
2323
2610
|
return cmd_stale(root, hybrid=args.hybrid)
|
|
2324
2611
|
if args.cmd == "mark":
|
|
2325
2612
|
return cmd_mark(root, args.paths)
|
|
2613
|
+
if args.cmd == "benefit":
|
|
2614
|
+
return cmd_benefit(root)
|
|
2326
2615
|
if args.cmd == "migrate":
|
|
2327
2616
|
return cmd_migrate(root, args)
|
|
2328
2617
|
if args.cmd == "plan":
|
|
@@ -439,16 +439,18 @@ Date: 2026-06-11 (UTC)
|
|
|
439
439
|
|
|
440
440
|
One row per completed review (`review.md` §When a review is due). Append-only; it
|
|
441
441
|
is the record that the gate ran and what it was worth. **One schema for both
|
|
442
|
-
modes** —
|
|
443
|
-
|
|
442
|
+
modes** — one required CORE (`date`, `doc_key`, `tier`, `model`, `findings_raised`,
|
|
443
|
+
`findings_real`, `verdict`, `revise_rounds`) plus each mode's own realization
|
|
444
|
+
columns: Standalone adds `reviewer`, a Hybrid devPNT row adds `instrument` and
|
|
445
|
+
`notes` and carries no `reviewer` at all. Both modes write this same file.
|
|
444
446
|
|
|
445
447
|
```markdown
|
|
446
448
|
# Independent Review Log
|
|
447
449
|
|
|
448
|
-
| date | doc_key | tier | reviewer | findings_raised | findings_real | verdict | revise_rounds |
|
|
449
|
-
|
|
450
|
-
| 2026-06-11 | ANALYSIS_login_sso.md | design | subagent (
|
|
451
|
-
| 2026-06-12 | diff feature/sso-login | closure | self-pass (declared; absent) | 2 | 2 | PASS with findings → corrections re-reviewed, PASS | 2 |
|
|
450
|
+
| date | doc_key | tier | model | reviewer | findings_raised | findings_real | verdict | revise_rounds |
|
|
451
|
+
|---|---|---|---|---|---|---|---|---|
|
|
452
|
+
| 2026-06-11 | ANALYSIS_login_sso.md | design | deep | subagent (fresh ctx) | 4 | 3 | FAIL → PASS | 2 |
|
|
453
|
+
| 2026-06-12 | diff feature/sso-login | closure | deep | self-pass (declared; absent) | 2 | 2 | PASS with findings → corrections re-reviewed, PASS | 2 |
|
|
452
454
|
|
|
453
455
|
## Notes
|
|
454
456
|
<!-- One short paragraph per review that found something worth remembering: what
|
|
@@ -460,9 +462,14 @@ adds values to the existing columns rather than a second table.
|
|
|
460
462
|
|
|
461
463
|
`tier` is the moment plus, in Hybrid, the reviewer weight: `design`, `design (late)`
|
|
462
464
|
and `closure` (Standalone); `deep`, `light`, `code`, `guide`, `vision` (devPNT gates
|
|
463
|
-
and the Vision blind check).
|
|
465
|
+
and the Vision blind check). `model` is a different question about the same review: `tier` says WHICH GATE ran,
|
|
466
|
+
`model` says WHAT CAPABILITY ran it (`deep`, `light`, `economy`,
|
|
467
|
+
`single (client exposes no choice)`, `below floor: <reason>` — the floor and its
|
|
468
|
+
arbitration are `review.md` §The capability floor). The two columns share the words
|
|
469
|
+
`deep`/`light` and mean different things by them, which is why they are read by
|
|
470
|
+
header and never by position. The validator reads this column by its header name, so
|
|
464
471
|
extra or reordered columns are fine — but the header must say `tier`. `reviewer`
|
|
465
|
-
records the realization actually used — fresh subagent, one-shot client run, or a
|
|
472
|
+
also carries the AUTHORING session's own below-floor disclosure where one is owed (the log has no column for the authoring tier, so `review.md` §The capability floor routes it here; Hybrid rows use `notes`). Otherwise it records the realization actually used — fresh subagent, one-shot client run, or a
|
|
466
473
|
**declared** self-pass — and, for any rung below rung 1, WHY the rung(s) above did
|
|
467
474
|
not run, in the ladder's reason words (`review.md`): `absent` — the client has no
|
|
468
475
|
such facility (a claim about the client, never about a policy); `gated, declined` —
|
|
@@ -631,7 +638,8 @@ status: CURRENT
|
|
|
631
638
|
- `qty`: `<value> <unit> <kind>` — effort in person-days (h/d/w/mo/fte-mo; 8h=1d, 1w=5d,
|
|
632
639
|
1mo=21d), duration in calendar days (h/d/w/mo), cost within one currency, count unit-matched.
|
|
633
640
|
- `about`: `<predicate> -> <slug>` for relationship claims; stored once, under the subject.
|
|
634
|
-
- `state`: `OK` | `CONTESTED <id>[,..]` | `SUPERSEDED <id
|
|
641
|
+
- `state`: `OK` | `CONTESTED <id>[,..]` | `SUPERSEDED <id>[,..]` — per claim, never per
|
|
642
|
+
node; a split bundle (`reconciliation.md` §Splitting) names all its successors.
|
|
635
643
|
- A tombstone (merged/renamed topic): `status: SUPERSEDED` + `redirect_to: <slug>`, body empty.
|
|
636
644
|
|
|
637
645
|
## ai_docs/corpus/given/[name].meta.md (source sidecar)
|