@bompus/house-rules 0.15.0 → 0.15.2
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 +1 -1
- package/skills/hr-handoff/references/reconciliation.md +6 -1
- package/skills/hr-handoff/references/successor-thread.md +3 -1
- package/skills/hr-handoff/scripts/check-reconciliation.mjs +24 -4
- package/skills/hr-pr-followup/scripts/merge-pr.sh +2 -0
- package/skills/hr-pr-followup/scripts/verify-landed.sh +5 -2
- package/skills/hr-what-next/scripts/check-open-work.mjs +28 -13
package/package.json
CHANGED
|
@@ -23,7 +23,12 @@ A predecessor plan needs no conversion. Name its `plan.md` as the source
|
|
|
23
23
|
`file`; the checker reads each item line of its `## Ledger` section (`- [ ]`,
|
|
24
24
|
`- [~]`, `- [x]`, `- [-]`) as a record named `L<line>` (`L12`). Other text in
|
|
25
25
|
the section is not read, so turn any follow-up written there into an item line
|
|
26
|
-
before the handoff.
|
|
26
|
+
before the handoff. Item lines elsewhere in the file are not read either. The
|
|
27
|
+
checker counts them and reports a gap that names the first five lines (the open
|
|
28
|
+
or deferred ones, when there are any), so the receipt is partial and cannot claim complete
|
|
29
|
+
coverage until they move into the ledger section.
|
|
30
|
+
|
|
31
|
+
Take a copy of the plan as it stood at the handoff,
|
|
27
32
|
because later edits renumber the lines. Each open or deferred line must map to
|
|
28
33
|
an item; a context-only reason such as "the rest, reopen only if asked" is
|
|
29
34
|
rejected. When one item absorbs several open or deferred lines, give the item
|
|
@@ -57,7 +57,9 @@ registry row, the project and the thread id.
|
|
|
57
57
|
ledger or source record still shows it open. An offer answered, completed,
|
|
58
58
|
deferred or superseded since the handoff was written authorizes nothing. A
|
|
59
59
|
bare code or `go` in the successor selects an offer only when one listed offer
|
|
60
|
-
is open. With several, it asks which one, as § Offers requires.
|
|
60
|
+
is open. With several, it asks which one, as § Offers requires. A thread that
|
|
61
|
+
holds no offer, such as a project Orchestrator, writes the list as "none"; an
|
|
62
|
+
empty list is not left out.
|
|
61
63
|
- The inventory and read-backs from the skill's thread-move step. The
|
|
62
64
|
receiving thread re-points to itself each item the host lets it bind:
|
|
63
65
|
registry rows, scheduled tasks, pull request watches and the messaging name
|
|
@@ -56,18 +56,36 @@ function unique(entries, label) {
|
|
|
56
56
|
// A plan file (`.md`) is read as its `## Ledger` item lines, one record per item, named `L<line>`;
|
|
57
57
|
// other text in the section, wrapped continuation lines included, is not read.
|
|
58
58
|
// Open (`- [ ]`) and deferred (`- [~]`) lines are remembered: they must carry over as items.
|
|
59
|
+
// Item lines elsewhere in the file are not records either, but they are counted and reported as a gap.
|
|
59
60
|
function ledgerRecords(raw) {
|
|
60
61
|
const lines = raw.split(/\r?\n/);
|
|
61
62
|
const start = lines.findIndex((line) => /^#{1,6}\s+Ledger\s*$/i.test(line));
|
|
62
63
|
if (start < 0) return null;
|
|
63
64
|
const level = lines[start].match(/^#+/)[0].length;
|
|
64
65
|
const records = [];
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
const outside = [];
|
|
67
|
+
let inside = false;
|
|
68
|
+
for (let i = 0; i < lines.length; i++) {
|
|
69
|
+
if (i === start) inside = true;
|
|
70
|
+
else if (inside && new RegExp(`^#{1,${level}}\\s`).test(lines[i])) inside = false;
|
|
67
71
|
const match = lines[i].match(/^\s*[-*]\s+\[([ x~-])\]\s+(.*)$/i);
|
|
68
|
-
if (match)
|
|
72
|
+
if (!match) continue;
|
|
73
|
+
if (inside) records.push({ id: `L${i + 1}`, text: match[2], mark: match[1] });
|
|
74
|
+
else outside.push({ id: `L${i + 1}`, mark: match[1] });
|
|
69
75
|
}
|
|
70
|
-
return { records };
|
|
76
|
+
return { records, outside };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function outsideGap(label, outside) {
|
|
80
|
+
const open = outside.filter((line) => /[ ~]/.test(line.mark));
|
|
81
|
+
const named = open.length > 0 ? open : outside;
|
|
82
|
+
const shown = named
|
|
83
|
+
.slice(0, 5)
|
|
84
|
+
.map((line) => line.id)
|
|
85
|
+
.join(", ");
|
|
86
|
+
const more = named.length > 5 ? ` and ${named.length - 5} more` : "";
|
|
87
|
+
const count = open.length > 0 ? `, ${open.length} of them open or deferred` : "";
|
|
88
|
+
return `${label}: ${outside.length} item line(s) outside the \`## Ledger\` section were not read${count} (${shown}${more}); move them into the ledger section`;
|
|
71
89
|
}
|
|
72
90
|
|
|
73
91
|
function check(receipt, directory) {
|
|
@@ -125,6 +143,8 @@ function check(receipt, directory) {
|
|
|
125
143
|
errors.push(`${label}: snapshot must be an object`);
|
|
126
144
|
continue;
|
|
127
145
|
}
|
|
146
|
+
if (source.file.endsWith(".md") && data.outside.length > 0)
|
|
147
|
+
gaps.push(outsideGap(label, data.outside));
|
|
128
148
|
const rows = unique(entries(data.records, `${label}.records`), `${label} record`);
|
|
129
149
|
if (Number.isSafeInteger(source.expectedRecords) && source.expectedRecords !== rows.size) {
|
|
130
150
|
gaps.push(`${label}: expected ${source.expectedRecords} records, read ${rows.size}`);
|
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
#
|
|
29
29
|
# Exit status: 0 landed and every local default current; 1 refused or failed; 2 usage; 3 landed, local sync pending.
|
|
30
30
|
# --idle-primary <path> is deprecated: accepted and ignored with a note on stderr, removed in a later release.
|
|
31
|
+
# It takes the next argument as its path unless that argument is missing or starts with "-".
|
|
32
|
+
# --preflight is not accepted here (unknown argument, exit 2); only verify-landed.sh still takes it.
|
|
31
33
|
#
|
|
32
34
|
# Usage: bash scripts/merge-pr.sh <pr> [--merge] [--remote <name>]
|
|
33
35
|
set -euo pipefail
|
|
@@ -26,8 +26,11 @@
|
|
|
26
26
|
# verified, but a local checkout was skipped (local sync pending). After a failure, rerun this
|
|
27
27
|
# script, not merge-pr.sh: the PR is already merged.
|
|
28
28
|
#
|
|
29
|
-
# Deprecated,
|
|
30
|
-
# --idle-primary <path> and
|
|
29
|
+
# Deprecated, removed in a later release; landing no longer waits on any local checkout.
|
|
30
|
+
# --idle-primary <path> is accepted and ignored with a note on stderr. It takes the next argument as its
|
|
31
|
+
# path unless that argument is missing or starts with "-".
|
|
32
|
+
# --preflight prints a note and "preflight skipped: it no longer gates landing" and exits 0 without
|
|
33
|
+
# checking anything; with --commit or --branch it exits 2.
|
|
31
34
|
# Usage:
|
|
32
35
|
# bash scripts/verify-landed.sh --commit <sha> [--default <branch>]
|
|
33
36
|
# [--branch <merged-head>] [--remote <name>] [--ff]
|
|
@@ -209,7 +209,11 @@ function parsePlan(text) {
|
|
|
209
209
|
i > 0 &&
|
|
210
210
|
(NARRATIVE.test(line) || PROSE_MARKER.test(line) || /^\s*[-*]\s+\[[ ~]\]/.test(line))
|
|
211
211
|
)
|
|
212
|
-
narrative.push({
|
|
212
|
+
narrative.push({
|
|
213
|
+
line: i + 1,
|
|
214
|
+
text: line.trim(),
|
|
215
|
+
box: /^\s*[-*]\s+\[[ ~]\]/.test(line),
|
|
216
|
+
});
|
|
213
217
|
});
|
|
214
218
|
return { status, owner, purpose, project, items, errors, narrative };
|
|
215
219
|
}
|
|
@@ -580,7 +584,15 @@ async function run(options) {
|
|
|
580
584
|
};
|
|
581
585
|
const open = plan.items.filter((item) => item.mark === " " || item.mark === "~");
|
|
582
586
|
report.open = open.map(row);
|
|
583
|
-
|
|
587
|
+
// A plan that fails its lint still gets every check that reads only its text. The checks that read the
|
|
588
|
+
// host (scratch, checkouts, the audit record, GitHub) wait until the plan is valid.
|
|
589
|
+
const invalid = report.errors.length > 0;
|
|
590
|
+
if (invalid) {
|
|
591
|
+
report.invalidPlan = true;
|
|
592
|
+
report.skipped.push(
|
|
593
|
+
"scratch, checkout, audit record and GitHub checks: they wait until the plan passes its lint",
|
|
594
|
+
);
|
|
595
|
+
}
|
|
584
596
|
report.awaitingUser = open.filter(awaitsUser).map(row);
|
|
585
597
|
if (TERMINAL_STATUS.test(plan.status) && open.length)
|
|
586
598
|
report.findings.push(
|
|
@@ -588,7 +600,7 @@ async function run(options) {
|
|
|
588
600
|
);
|
|
589
601
|
const roots = notesRoot(planPath);
|
|
590
602
|
const scratch = options.scratch ?? (roots ? join(roots.notes, "tmp") : null);
|
|
591
|
-
if (scratch) {
|
|
603
|
+
if (scratch && !invalid) {
|
|
592
604
|
const { owned, unreadable } = ownedScratch(scratch, plan.owner);
|
|
593
605
|
for (const manifest of unreadable)
|
|
594
606
|
report.errors.push(
|
|
@@ -596,7 +608,7 @@ async function run(options) {
|
|
|
596
608
|
);
|
|
597
609
|
for (const name of owned) accountFor(`scratch ${name}`, [name], plan.items, report.findings);
|
|
598
610
|
}
|
|
599
|
-
for (const path of options.checkouts) {
|
|
611
|
+
for (const path of invalid ? [] : options.checkouts) {
|
|
600
612
|
const state = checkoutState(path);
|
|
601
613
|
if (state.error) report.errors.push(state.error);
|
|
602
614
|
else if (state.dirty || state.unpushed)
|
|
@@ -608,7 +620,7 @@ async function run(options) {
|
|
|
608
620
|
);
|
|
609
621
|
}
|
|
610
622
|
// An app retires its own checkouts, so their state is reported but is not a finding.
|
|
611
|
-
for (const path of options.appManaged) {
|
|
623
|
+
for (const path of invalid ? [] : options.appManaged) {
|
|
612
624
|
const state = checkoutState(path);
|
|
613
625
|
if (state.error) report.errors.push(state.error);
|
|
614
626
|
else if (state.dirty || state.unpushed)
|
|
@@ -620,20 +632,23 @@ async function run(options) {
|
|
|
620
632
|
report.warnings.push(
|
|
621
633
|
"no `Purpose:` line in the first 40 lines (the session's purpose, one sentence)",
|
|
622
634
|
);
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
635
|
+
// Every unchecked box outside the ledger is a possible lost item, so each is listed. Prose labels such
|
|
636
|
+
// as "Deferred:" are listed up to five, with a count of the rest. Lines stay in file order.
|
|
637
|
+
let prose = 0;
|
|
638
|
+
for (const entry of plan.narrative)
|
|
639
|
+
if (entry.box || ++prose <= 5)
|
|
640
|
+
report.warnings.push(`L${entry.line} outside the ledger: ${clip(entry.text, 70)}`);
|
|
641
|
+
if (prose > 5) report.warnings.push(`${prose - 5} more narrative marker line(s)`);
|
|
627
642
|
offerWarnings(plan.items, report.warnings);
|
|
628
643
|
report.unansweredOffers = unansweredOffers(plan.items);
|
|
629
644
|
if (report.unansweredOffers.length >= OFFER_LIMIT)
|
|
630
645
|
report.warnings.push(
|
|
631
646
|
`${report.unansweredOffers.length} offers are open and unanswered; the offers rule says to present no further offer at ${OFFER_LIMIT} or more. Queue the new decision in a walk-through (hr-what-next, Walk-through) instead`,
|
|
632
647
|
);
|
|
633
|
-
const audit = plan.project && auditWarning(plan.project, roots, options.today);
|
|
648
|
+
const audit = !invalid && plan.project && auditWarning(plan.project, roots, options.today);
|
|
634
649
|
if (audit) report.warnings.push(audit);
|
|
635
650
|
report.recheckDue = rechecks(open, options.today, report.warnings);
|
|
636
|
-
report.stale = await possiblyFinished(open, options, report);
|
|
651
|
+
if (!invalid) report.stale = await possiblyFinished(open, options, report);
|
|
637
652
|
if (options.archive && !report.errors.length) {
|
|
638
653
|
if (!COMPLETED_STATUS.test(plan.status))
|
|
639
654
|
report.findings.push(`archiving refused: Status is "${plan.status}", not completed`);
|
|
@@ -678,7 +693,7 @@ async function main() {
|
|
|
678
693
|
console.log(
|
|
679
694
|
`plan: ${report.plan}\nstatus: ${report.status ?? "?"} | owner: ${report.owner ?? "?"}`,
|
|
680
695
|
);
|
|
681
|
-
|
|
696
|
+
console.log(`unanswered offers: ${report.unansweredOffers.length}`);
|
|
682
697
|
for (const [label, rows] of [
|
|
683
698
|
["error", report.errors],
|
|
684
699
|
["open", report.open],
|
|
@@ -696,7 +711,7 @@ async function main() {
|
|
|
696
711
|
code === 0
|
|
697
712
|
? "result: nothing open"
|
|
698
713
|
: code === 1
|
|
699
|
-
?
|
|
714
|
+
? `result: invalid input${report.invalidPlan ? " (the checks that read only the plan text still ran)" : ""}`
|
|
700
715
|
: `result: ${report.open.length} open item(s) (${report.awaitingUser.length} awaiting your yes/no), ${report.findings.length} finding(s)`,
|
|
701
716
|
);
|
|
702
717
|
}
|