@bompus/house-rules 0.15.0 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bompus/house-rules",
3
- "version": "0.15.0",
3
+ "version": "0.15.1",
4
4
  "description": "Composable working rules and skills for AI coding agents",
5
5
  "homepage": "https://github.com/bompus/house-rules#readme",
6
6
  "license": "MIT",
@@ -23,7 +23,10 @@ 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. Take a copy of the plan as it stood at 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. Take a copy of the plan as it stood at the handoff,
27
30
  because later edits renumber the lines. Each open or deferred line must map to
28
31
  an item; a context-only reason such as "the rest, reopen only if asked" is
29
32
  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
- for (let i = start + 1; i < lines.length; i++) {
66
- if (new RegExp(`^#{1,${level}}\\s`).test(lines[i])) break;
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) records.push({ id: `L${i + 1}`, text: match[2], mark: match[1] });
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, accepted and ignored with a note on stderr, and removed in a later release:
30
- # --idle-primary <path> and --preflight. Landing no longer waits on any local checkout.
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({ line: i + 1, text: line.trim() });
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
- if (report.errors.length) return report;
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
- for (const entry of plan.narrative.slice(0, 5))
624
- report.warnings.push(`L${entry.line} outside the ledger: ${clip(entry.text, 70)}`);
625
- if (plan.narrative.length > 5)
626
- report.warnings.push(`${plan.narrative.length - 5} more narrative marker line(s)`);
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
- if (code !== 1) console.log(`unanswered offers: ${report.unansweredOffers.length}`);
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
- ? "result: invalid input"
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
  }