@basou/cli 0.46.0 → 0.48.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/dist/program.js CHANGED
@@ -1250,7 +1250,8 @@ Input format (a JSON array; one object per decision):
1250
1250
  "rationale": "Workspace protocol and a content-addressed store fit our layout.",
1251
1251
  "alternatives": ["npm workspaces", "yarn"],
1252
1252
  "rejected_reason": "npm hoisting caused phantom-dependency bugs",
1253
- "linked_files": ["pnpm-workspace.yaml"]
1253
+ "linked_files": ["pnpm-workspace.yaml"],
1254
+ "kind": "decision"
1254
1255
  },
1255
1256
  {
1256
1257
  "title": "Form-based admin editing is the next track (only 6/19 sections done)",
@@ -1259,10 +1260,16 @@ Input format (a JSON array; one object per decision):
1259
1260
  }
1260
1261
  ]
1261
1262
 
1262
- Only "title" is required; every other field is optional. Set "kind": "track" to
1263
- record a strategic, UNFINISHED direction (+ why): orientation/handoff resurface
1264
- open tracks every session until you close one with 'basou decision void <id>'.
1265
- Absent / "decision" is a point-in-time decision (surfaced only as the latest).
1263
+ "title" and "kind" are both required. "kind" names the vessel: "track" records a
1264
+ strategic, UNFINISHED direction (+ why) and orientation/handoff resurface it
1265
+ every session until you close it with 'basou decision void <id>', while
1266
+ "decision" records a point-in-time call (surfaced as the latest). Every other
1267
+ field is optional.
1268
+
1269
+ "kind" has no default because getting it wrong fails silently: a track filed as
1270
+ a decision never enters the open-track list, so nothing holds it there -- the
1271
+ next decision simply takes its place. Omitting it on ANY item refuses the whole
1272
+ batch and writes nothing, so a partial write can never be reported as a success.
1266
1273
  All decisions are written into one ad-hoc session timestamped now, so
1267
1274
  orientation surfaces them as the latest decisions. Run from a workspace-view
1268
1275
  directory and it resolves to the planning repo, like 'basou orient' /
@@ -1270,7 +1277,7 @@ directory and it resolves to the planning repo, like 'basou orient' /
1270
1277
 
1271
1278
  Example (heredoc on stdin):
1272
1279
  basou decision capture <<'JSON'
1273
- [{ "title": "Ship the capture command", "rationale": "Close the why-capture gap" }]
1280
+ [{ "title": "Ship the capture command", "rationale": "Close the why-capture gap", "kind": "decision" }]
1274
1281
  JSON
1275
1282
  `;
1276
1283
  async function runDecisionRecord(options, ctx = {}) {
@@ -1306,15 +1313,6 @@ async function warnLinkedFilesOutsideRoots(input) {
1306
1313
  } catch {
1307
1314
  }
1308
1315
  }
1309
- var TRACK_MARKER_IN_TITLE = /^\s*track\s*[:\uff1a]|[[(\uff08\uff3b\u3010]\s*track\s*[\])\uff09\uff3d\u3011]/i;
1310
- function warnTrackMarkerWithoutKind(decisions, markerWithoutKind) {
1311
- for (const index of markerWithoutKind) {
1312
- const title = (decisions[index]?.title ?? "").trim();
1313
- console.error(
1314
- `basou: decision[${index}] title carries a track marker (${title.slice(0, 40)}) but "kind" is absent \u2014 it is recorded as a point-in-time decision and will NOT resurface in orient. Set "kind": "track" to open a track, or "kind": "decision" to say the marker is part of the title.`
1315
- );
1316
- }
1317
- }
1318
1316
  async function doRunDecisionRecord(options, ctx) {
1319
1317
  const cwd = ctx.cwd ?? process.cwd();
1320
1318
  const repositoryRoot = await resolveRepositoryRootForDecision(cwd);
@@ -1412,8 +1410,7 @@ async function doRunDecisionCapture(options, ctx) {
1412
1410
  const paths = basouPaths4(repositoryRoot);
1413
1411
  await assertWorkspaceInitialized2(paths.root);
1414
1412
  const raw = await readCaptureInput(options, ctx);
1415
- const { decisions, markerWithoutKind } = parseCaptureInput(raw);
1416
- warnTrackMarkerWithoutKind(decisions, markerWithoutKind);
1413
+ const { decisions } = parseCaptureInput(raw);
1417
1414
  await warnLinkedFilesOutsideRoots({
1418
1415
  linkedFiles: decisions.flatMap((d) => d.linked_files ?? []),
1419
1416
  cwd,
@@ -1675,13 +1672,31 @@ function parseCaptureInput(raw) {
1675
1672
  throw new Error("Input array must contain at least one decision.");
1676
1673
  }
1677
1674
  const decisions = [];
1678
- const markerWithoutKind = [];
1679
- parsed.forEach((item, index) => {
1680
- const { input, kindWasPresent } = validateCaptureItem(item, index);
1681
- decisions.push(input);
1682
- if (!kindWasPresent && TRACK_MARKER_IN_TITLE.test(input.title)) markerWithoutKind.push(index);
1683
- });
1684
- return { decisions, markerWithoutKind };
1675
+ const missingKind = [];
1676
+ try {
1677
+ parsed.forEach((item, index) => {
1678
+ const input = validateCaptureItem(item, index);
1679
+ decisions.push(input);
1680
+ if (input.kind === void 0) missingKind.push(index);
1681
+ });
1682
+ } catch (error) {
1683
+ const detail = error instanceof Error ? error.message : String(error);
1684
+ throw new Error(`${detail} ${batchDisposition(parsed.length)}`, { cause: error });
1685
+ }
1686
+ if (missingKind.length > 0) {
1687
+ throw new Error(`${missingKindError(missingKind)} ${batchDisposition(parsed.length)}`);
1688
+ }
1689
+ return { decisions };
1690
+ }
1691
+ var MISSING_KIND_SAMPLE = 10;
1692
+ function batchDisposition(total) {
1693
+ return `Nothing was written: all ${total} item(s) in this batch were refused.`;
1694
+ }
1695
+ function missingKindError(indices) {
1696
+ const shown = indices.slice(0, MISSING_KIND_SAMPLE).map((i) => `decision[${i}]`);
1697
+ const overflow = indices.length - shown.length;
1698
+ const list = overflow > 0 ? `${shown.join(", ")} (... +${overflow} more)` : shown.join(", ");
1699
+ return `${list}: "kind" is required. It names the vessel: "track" for an unfinished direction, which orient keeps in its open-track list until you close it with 'basou decision void', or "decision" for a settled point-in-time call, which the next decision replaces. Add "kind" to every item and re-run.`;
1685
1700
  }
1686
1701
  function validateCaptureItem(item, index) {
1687
1702
  if (typeof item !== "object" || item === null || Array.isArray(item)) {
@@ -1698,12 +1713,12 @@ function validateCaptureItem(item, index) {
1698
1713
  if (typeof obj.title !== "string" || isBlank(obj.title)) {
1699
1714
  throw new Error(`decision[${index}].title must be a non-empty string.`);
1700
1715
  }
1716
+ if (obj.kind !== void 0 && obj.kind !== "decision" && obj.kind !== "track") {
1717
+ throw new Error(`decision[${index}].kind must be "decision" or "track", got '${obj.kind}'.`);
1718
+ }
1701
1719
  const out = { title: obj.title };
1702
1720
  if (obj.kind !== void 0) {
1703
- if (obj.kind !== "decision" && obj.kind !== "track") {
1704
- throw new Error(`decision[${index}].kind must be "decision" or "track", got '${obj.kind}'.`);
1705
- }
1706
- if (obj.kind === "track") out.kind = "track";
1721
+ out.kind = obj.kind;
1707
1722
  }
1708
1723
  if (obj.rationale !== void 0) {
1709
1724
  out.rationale = requireNonEmptyString(obj.rationale, index, "rationale");
@@ -1742,7 +1757,7 @@ function validateCaptureItem(item, index) {
1742
1757
  }
1743
1758
  });
1744
1759
  }
1745
- return { input: out, kindWasPresent: obj.kind !== void 0 };
1760
+ return out;
1746
1761
  }
1747
1762
  function requireNonEmptyString(value, index, field) {
1748
1763
  if (typeof value !== "string" || isBlank(value)) {
@@ -1793,9 +1808,12 @@ function captureItemToPayload(item) {
1793
1808
  if (item.input.kind !== void 0) payload.kind = item.input.kind;
1794
1809
  return payload;
1795
1810
  }
1796
- function kindMarker(kind) {
1811
+ function previewKindMarker(kind) {
1797
1812
  return kind === "track" ? " [TRACK]" : " [DECISION]";
1798
1813
  }
1814
+ function recordedKindMarker(kind) {
1815
+ return kind === "track" ? " [TRACK]" : "";
1816
+ }
1799
1817
  function printCapturePreview(options, decisions) {
1800
1818
  if (options.json === true) {
1801
1819
  console.log(JSON.stringify({ dry_run: true, count: decisions.length, decisions }));
@@ -1805,7 +1823,7 @@ function printCapturePreview(options, decisions) {
1805
1823
  `Would capture ${decisions.length} decision${decisions.length === 1 ? "" : "s"} (dry run; nothing written):`
1806
1824
  );
1807
1825
  for (const decision of decisions) {
1808
- console.log(`- ${decision.title}${kindMarker(decision.kind)}`);
1826
+ console.log(`- ${decision.title}${previewKindMarker(decision.kind)}`);
1809
1827
  }
1810
1828
  }
1811
1829
  function printCaptureResult(options, result) {
@@ -1826,7 +1844,7 @@ function printCaptureResult(options, result) {
1826
1844
  `Captured ${result.items.length} decision${result.items.length === 1 ? "" : "s"} in ad-hoc session ${sid}:`
1827
1845
  );
1828
1846
  for (const item of result.items) {
1829
- console.log(`- ${item.decisionId}: ${item.input.title}${kindMarker(item.input.kind)}`);
1847
+ console.log(`- ${item.decisionId}: ${item.input.title}${recordedKindMarker(item.input.kind)}`);
1830
1848
  }
1831
1849
  }
1832
1850
  function pickRichFields(options) {
@@ -1933,13 +1951,14 @@ function printDecisionResult(options, result) {
1933
1951
  }
1934
1952
  const trackPrefix = result.rich.kind === "track" ? "track " : "";
1935
1953
  const rationaleSuffix = result.rich.rationale !== void 0 ? ` (rationale: ${result.rich.rationale})` : "";
1954
+ const vesselSuffix = result.rich.kind === "track" ? "" : " \u2014 a point-in-time decision, not an open track (use --track for a direction that should keep coming back until you close it)";
1936
1955
  if (result.mode === "ad-hoc") {
1937
1956
  console.log(
1938
- `Recorded ${trackPrefix}${result.decisionId} in ad-hoc session ${sid}${rationaleSuffix}`
1957
+ `Recorded ${trackPrefix}${result.decisionId} in ad-hoc session ${sid}${rationaleSuffix}${vesselSuffix}`
1939
1958
  );
1940
1959
  } else {
1941
1960
  console.log(
1942
- `Recorded ${trackPrefix}${result.decisionId} in session ${sid} (${result.sessionStatus})${rationaleSuffix}`
1961
+ `Recorded ${trackPrefix}${result.decisionId} in session ${sid} (${result.sessionStatus})${rationaleSuffix}${vesselSuffix}`
1943
1962
  );
1944
1963
  }
1945
1964
  }
@@ -12705,6 +12724,18 @@ var VIEW_HTML = `<!doctype html>
12705
12724
  return el('span', { class: 'badge ok', text: 'up to date' });
12706
12725
  }
12707
12726
 
12727
+ // Three states hide behind a bare 'in-flight 0': nothing ever recorded,
12728
+ // everything finished, and a task store that cannot be read -- where the
12729
+ // count is not zero but UNKNOWN. Say which one it is, as orient does.
12730
+ // Kept as a named top-level function with no free variables so the test suite
12731
+ // can lift it out of this template and run it; inlined in the card it was
12732
+ // unreachable from any test.
12733
+ function taskFlightLabel(w) {
12734
+ if (w.unreadableTaskCount > 0) return 'in-flight unknown (' + w.unreadableTaskCount + ' unreadable)';
12735
+ if (w.anyTaskEverRecorded === false) return 'no tasks recorded';
12736
+ return 'in-flight ' + w.inFlightCount;
12737
+ }
12738
+
12708
12739
  function portfolioCard(w, generatedAt) {
12709
12740
  if (!w.initialized) {
12710
12741
  return el('div', { class: 'card pcard muted' }, [
@@ -12720,6 +12751,7 @@ var VIEW_HTML = `<!doctype html>
12720
12751
  }
12721
12752
  var pend = w.pendingApprovals || [];
12722
12753
  var pendText = 'pending ' + pend.length + (pend.length ? ' (' + highestRisk(pend) + ')' : '');
12754
+ var flightText = taskFlightLabel(w);
12723
12755
  var now = w.latestSession ? ((w.latestSession.label || '(session)') + ' [' + w.latestSession.status + ']') : '(no live sessions)';
12724
12756
  var dec = w.latestDecision ? w.latestDecision.title : '(no decisions yet)';
12725
12757
  var newest = (w.freshness && w.freshness.newestStartedAt) ? w.freshness.newestStartedAt : null;
@@ -12732,7 +12764,7 @@ var VIEW_HTML = `<!doctype html>
12732
12764
  ]),
12733
12765
  el('div', { class: 'f', text: 'now: ' + now }),
12734
12766
  el('div', { class: 'f', text: 'latest: ' + dec }),
12735
- el('div', { class: 'f', text: 'in-flight ' + w.inFlightCount + ' | ' + pendText + ' | suspect ' + w.suspectCount }),
12767
+ el('div', { class: 'f', text: flightText + ' | ' + pendText + ' | suspect ' + w.suspectCount }),
12736
12768
  el('div', { class: 'f muted', text: 'sessions ' + w.sessionCount + ' | newest ' + relAge(newest, generatedAt) })
12737
12769
  ]);
12738
12770
  }
@@ -13309,6 +13341,13 @@ async function portfolioCard(ws, nowIso) {
13309
13341
  sessionCount: s.sessionCount,
13310
13342
  suspectCount: s.suspects.length,
13311
13343
  inFlightCount: s.inFlightTasks.length,
13344
+ // Three states hide behind `inFlightCount: 0`: nothing ever recorded,
13345
+ // everything finished, and a store whose task files cannot be read (where
13346
+ // the count is not 0 but UNKNOWN). `orient` branches all three ways; the
13347
+ // card carries the two extra facts so it can too, instead of reporting a
13348
+ // loader blind spot as an empty record.
13349
+ anyTaskEverRecorded: s.anyTaskEverRecorded,
13350
+ unreadableTaskCount: s.unreadableTaskCount,
13312
13351
  pendingApprovals: s.pendingApprovals.map((a) => ({
13313
13352
  risk: a.risk,
13314
13353
  kind: a.kind,
@@ -13776,7 +13815,7 @@ async function assertWorkspaceInitialized15(basouRoot) {
13776
13815
  function readBuildStamp() {
13777
13816
  if (false) return void 0;
13778
13817
  try {
13779
- return JSON.parse('{"version":"0.46.0","commit":"dbc9c57","committedAt":"2026-09-19T15:43:35+09:00"}');
13818
+ return JSON.parse('{"version":"0.48.0","commit":"2e8fee7","committedAt":"2026-09-21T11:35:12+09:00"}');
13780
13819
  } catch {
13781
13820
  return void 0;
13782
13821
  }