onto-mcp 0.4.11 → 0.4.13

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.
Files changed (139) hide show
  1. package/.onto/authority/core-lexicon.yaml +1 -1
  2. package/.onto/authority/supported-models.yaml +153 -0
  3. package/.onto/domains/accounting-kr/competency_qs.md +153 -153
  4. package/.onto/processes/reconstruct/obligation-coverage-ledger.yaml +842 -0
  5. package/.onto/processes/reconstruct/obligation-coverage-recorded.yaml +225 -0
  6. package/.onto/processes/reconstruct/ontology-seeding-and-maturation-design.md +93 -54
  7. package/.onto/processes/reconstruct/reconstruct-contract-registry.yaml +202 -49
  8. package/.onto/processes/reconstruct/reconstruct-execution-ux-contract.md +72 -1
  9. package/.onto/processes/reconstruct/source-profiles/spreadsheet.md +35 -0
  10. package/.onto/processes/review/nesting-batch-worker-contract.md +1 -1
  11. package/.onto/processes/review/productized-live-path.md +9 -5
  12. package/.onto/processes/review/prompt-execution-runner-contract.md +32 -8
  13. package/.onto/processes/review/review-target-profile-contract.md +47 -4
  14. package/.onto/processes/shared/pipeline-execution-ledger-contract.md +11 -0
  15. package/.onto/processes/shared/target-material-kind-contract.md +1 -1
  16. package/README.md +64 -1
  17. package/dist/cli.js +11 -1
  18. package/dist/core-api/reconstruct-api.js +679 -18
  19. package/dist/core-api/review-api.js +70 -5
  20. package/dist/core-api/review-progress.js +7 -0
  21. package/dist/core-api/runtime-observation.js +7 -0
  22. package/dist/core-runtime/artifact-io.js +131 -0
  23. package/dist/core-runtime/cli/claude-code-review-unit-executor.js +21 -9
  24. package/dist/core-runtime/cli/claude-nesting-batch-worker.js +4 -2
  25. package/dist/core-runtime/cli/materialize-review-prompt-packets.js +68 -14
  26. package/dist/core-runtime/cli/prepare-review-session.js +34 -1
  27. package/dist/core-runtime/cli/render-review-final-output.js +2 -1
  28. package/dist/core-runtime/cli/run-review-prompt-execution.js +758 -48
  29. package/dist/core-runtime/cli/run-seats.js +18 -0
  30. package/dist/core-runtime/cli/runtime-submit-context.js +10 -2
  31. package/dist/core-runtime/cli/unit-resubmit.js +269 -0
  32. package/dist/core-runtime/cli/worker-structured-output.js +80 -2
  33. package/dist/core-runtime/discovery/host-detection.js +71 -22
  34. package/dist/core-runtime/discovery/review-cert-assemble.js +208 -0
  35. package/dist/core-runtime/discovery/review-cert-record.js +442 -0
  36. package/dist/core-runtime/discovery/review-execution-units.js +22 -0
  37. package/dist/core-runtime/discovery/seat-inventory.js +167 -0
  38. package/dist/core-runtime/discovery/settings-chain.js +311 -52
  39. package/dist/core-runtime/discovery/supported-models.js +289 -25
  40. package/dist/core-runtime/discovery/synthesize-cert-assemble.js +197 -0
  41. package/dist/core-runtime/discovery/synthesize-cert-capsule.js +399 -0
  42. package/dist/core-runtime/discovery/synthesize-cert-judge.js +340 -0
  43. package/dist/core-runtime/discovery/synthesize-cert-loop.js +234 -0
  44. package/dist/core-runtime/discovery/synthesize-cert-mutation.js +117 -0
  45. package/dist/core-runtime/discovery/synthesize-cert-packet.js +182 -0
  46. package/dist/core-runtime/discovery/synthesize-cert-record.js +831 -0
  47. package/dist/core-runtime/discovery/synthesize-cert-sampler.js +336 -0
  48. package/dist/core-runtime/effort-calibration-ingest.js +326 -0
  49. package/dist/core-runtime/effort-calibration-reconstruct.js +47 -0
  50. package/dist/core-runtime/effort-calibration-review.js +22 -0
  51. package/dist/core-runtime/effort-calibration-sweep.js +110 -0
  52. package/dist/core-runtime/effort-frontier.js +134 -0
  53. package/dist/core-runtime/llm/claude-bin.js +77 -0
  54. package/dist/core-runtime/llm/dispatch-breaker.js +412 -0
  55. package/dist/core-runtime/llm/dispatch-fallback-adapter-capabilities.js +33 -0
  56. package/dist/core-runtime/llm/llm-caller.js +316 -35
  57. package/dist/core-runtime/llm/mock-llm-realization.js +35 -4
  58. package/dist/core-runtime/llm/model-switcher.js +1 -1
  59. package/dist/core-runtime/llm/openai-responses-incomplete-error.js +46 -0
  60. package/dist/core-runtime/llm/sealed-dispatch-capability.js +359 -0
  61. package/dist/core-runtime/llm/structured-dispatch-error.js +15 -0
  62. package/dist/core-runtime/onboard/claude-profile-scan.js +65 -0
  63. package/dist/core-runtime/onboard/cli-host.js +5 -2
  64. package/dist/core-runtime/onboard/configure-provider.js +33 -1
  65. package/dist/core-runtime/onboard/host-target.js +28 -3
  66. package/dist/core-runtime/onboard/register.js +54 -12
  67. package/dist/core-runtime/pipeline-execution-ledger.js +10 -0
  68. package/dist/core-runtime/reconstruct/artifact-types.js +37 -0
  69. package/dist/core-runtime/reconstruct/benchmark-evidence.js +16 -0
  70. package/dist/core-runtime/reconstruct/claim-projection-validation.js +19 -5
  71. package/dist/core-runtime/reconstruct/competency-projection-contract.js +63 -0
  72. package/dist/core-runtime/reconstruct/comprehension-artifact.js +321 -0
  73. package/dist/core-runtime/reconstruct/comprehension-reduce.js +367 -0
  74. package/dist/core-runtime/reconstruct/comprehension-semantic-map.js +771 -0
  75. package/dist/core-runtime/reconstruct/contract-registry.js +20 -0
  76. package/dist/core-runtime/reconstruct/directive-validation.js +4 -3
  77. package/dist/core-runtime/reconstruct/dispatch-fallback-artifacts.js +640 -0
  78. package/dist/core-runtime/reconstruct/execution-telemetry.js +84 -9
  79. package/dist/core-runtime/reconstruct/final-output-sections.js +89 -0
  80. package/dist/core-runtime/reconstruct/governing-snapshot.js +36 -1
  81. package/dist/core-runtime/reconstruct/leaf-reader.js +305 -0
  82. package/dist/core-runtime/reconstruct/llm-dispatch-failure.js +270 -0
  83. package/dist/core-runtime/reconstruct/llm-touch-fingerprint.js +94 -0
  84. package/dist/core-runtime/reconstruct/material-admission-validation.js +20 -5
  85. package/dist/core-runtime/reconstruct/material-profile-validation.js +24 -6
  86. package/dist/core-runtime/reconstruct/materialize-preparation.js +380 -23
  87. package/dist/core-runtime/reconstruct/maturation-validation.js +1522 -67
  88. package/dist/core-runtime/reconstruct/mock-llm-realization.js +120 -1
  89. package/dist/core-runtime/reconstruct/obligation-assertion.js +18 -0
  90. package/dist/core-runtime/reconstruct/ontology-seed-validation.js +39 -7
  91. package/dist/core-runtime/reconstruct/output-budget.js +6 -0
  92. package/dist/core-runtime/reconstruct/pipeline-execution-ledger.js +112 -5
  93. package/dist/core-runtime/reconstruct/post-seed-validation.js +122 -11
  94. package/dist/core-runtime/reconstruct/proof-authority-validation.js +2 -6
  95. package/dist/core-runtime/reconstruct/purpose-authority-validation.js +119 -7
  96. package/dist/core-runtime/reconstruct/record.js +49 -3
  97. package/dist/core-runtime/reconstruct/registry-verification-validation.js +41 -5
  98. package/dist/core-runtime/reconstruct/run-control-validation.js +1019 -37
  99. package/dist/core-runtime/reconstruct/run.js +5409 -517
  100. package/dist/core-runtime/reconstruct/seed-authoring-readiness-validation.js +56 -8
  101. package/dist/core-runtime/reconstruct/semantic-quality-gate.js +112 -7
  102. package/dist/core-runtime/reconstruct/source-observation-delta-validation.js +53 -5
  103. package/dist/core-runtime/reconstruct/source-observations.js +166 -0
  104. package/dist/core-runtime/reconstruct/source-safety-validation.js +77 -188
  105. package/dist/core-runtime/reconstruct/source-scout-pack-validation.js +50 -6
  106. package/dist/core-runtime/reconstruct/terminal-validation.js +145 -13
  107. package/dist/core-runtime/review/continuation-plan.js +12 -5
  108. package/dist/core-runtime/review/issue-artifact-runtime.js +4 -1
  109. package/dist/core-runtime/review/materializers.js +100 -11
  110. package/dist/core-runtime/review/nesting-batch.js +6 -2
  111. package/dist/core-runtime/review/obligation-shardability.js +138 -0
  112. package/dist/core-runtime/review/pipeline-execution-ledger.js +42 -2
  113. package/dist/core-runtime/review/review-artifact-utils.js +219 -7
  114. package/dist/core-runtime/review/review-execution-steps.js +32 -8
  115. package/dist/core-runtime/review/review-progress-contract.js +8 -0
  116. package/dist/core-runtime/review/review-prompt-budget.js +97 -0
  117. package/dist/core-runtime/review/semantic-quality-gate.js +33 -7
  118. package/dist/core-runtime/review/spreadsheet-review-disposition.js +94 -0
  119. package/dist/core-runtime/route-identity.js +182 -0
  120. package/dist/core-runtime/spreadsheet-header-escalation.js +204 -0
  121. package/dist/core-runtime/spreadsheet-structure-observer.js +2689 -0
  122. package/dist/core-runtime/target-material-kind.js +58 -0
  123. package/dist/mcp/server.js +27 -5
  124. package/dist/mcp/tool-schemas.js +8 -0
  125. package/dist/tui/app.js +167 -0
  126. package/dist/tui/data/event-follower.js +116 -0
  127. package/dist/tui/data/node-detail.js +42 -0
  128. package/dist/tui/data/projection-poll.js +26 -0
  129. package/dist/tui/data/session-discovery.js +65 -0
  130. package/dist/tui/index.js +108 -0
  131. package/dist/tui/screens/log.js +25 -0
  132. package/dist/tui/screens/node-detail.js +17 -0
  133. package/dist/tui/screens/session-selector.js +26 -0
  134. package/dist/tui/screens/workflow-tree.js +90 -0
  135. package/dist/tui/view-model/reconstruct-adapter.js +162 -0
  136. package/dist/tui/view-model/review-adapter.js +270 -0
  137. package/dist/tui/view-model/tree-view-model.js +19 -0
  138. package/package.json +20 -3
  139. package/settings.example.json +77 -16
@@ -66,6 +66,11 @@ const SPREADSHEET_EXTENSIONS = new Set([
66
66
  ".xlsm",
67
67
  ".xlsx",
68
68
  ]);
69
+ /** True when the ref's name denotes spreadsheet/workbook material by extension
70
+ * (the canonical SPREADSHEET_EXTENSIONS set). Pure — no IO. */
71
+ export function isSpreadsheetRef(ref) {
72
+ return SPREADSHEET_EXTENSIONS.has(path.extname(ref).toLowerCase());
73
+ }
69
74
  const DOCUMENT_EXTENSIONS = new Set([
70
75
  ".adoc",
71
76
  ".doc",
@@ -275,6 +280,20 @@ export function reviewMaterialSupportStatus(kind) {
275
280
  reason: null,
276
281
  };
277
282
  }
283
+ if (kind === "spreadsheet") {
284
+ // KIND-level claim: review has a per-material spreadsheet adapter — kind-derived
285
+ // review obligations (reviewMaterialGoals) backed by a structural inventory that is
286
+ // rendered into materialized-input (structure inspected only; formula results are
287
+ // NOT recalculated). The PER-TARGET-FORMAT gate lives in the review materializer:
288
+ // when a specific ref uses an unsupported workbook format (.xls/.xlsb/.ods) or is
289
+ // unreadable, the materializer downgrades the recorded support_status from this
290
+ // kind-level supported to partial, so a `supported`/`null` profile is never emitted
291
+ // for a workbook the inventory could not actually read.
292
+ return {
293
+ status: "supported",
294
+ reason: null,
295
+ };
296
+ }
278
297
  if (kind === "unknown") {
279
298
  return {
280
299
  status: "unknown",
@@ -292,3 +311,42 @@ export function reviewMaterialSupportStatus(kind) {
292
311
  reason: "review records target material kind, but material-specific validation is not implemented yet",
293
312
  };
294
313
  }
314
+ /**
315
+ * Kind-derived review obligations for the review target profile's `review_goal`.
316
+ * These are the persisted, downstream-projected (problem-framing) review dimensions a
317
+ * per-material review adapter adds on top of the artifact-role and domain goals — a
318
+ * distinct surface from the ephemeral prompt-render `material_kind_obligations` prose.
319
+ *
320
+ * Spreadsheet obligations are distilled from the structural-audit checklist (read/observe
321
+ * scope only — no authoring, no recalculation, no business interpretation) and every goal
322
+ * is backed by a `WorkbookStructuralInventory` field that review renders into
323
+ * materialized-input (see review-artifact-utils `renderSpreadsheetStructuralView`):
324
+ *
325
+ * - formula_integrity ← formula_cells_total (>0) + formula_patterns (formula text) + error_cells
326
+ * - cross_sheet_reference_integrity ← formula_patterns[].cross_sheet_refs + cross_sheet_key_overlap
327
+ * - named_range_hygiene ← named_ranges (name/scope/refers_to)
328
+ * - data_validation_coverage ← data_validations (range/rule_summary)
329
+ * - access_and_protection_hygiene ← sheets[].hidden/protected + macro_present
330
+ * - structural_risk_signals ← risk_signals + external_links + error_cells
331
+ *
332
+ * The structure-inspected-only honesty is carried by the invariant render header and the
333
+ * review-target-profile contract, NOT by a review_goal string (uniqueStrings may dedupe or
334
+ * reorder it, and it would collide with the inventory's `inspection_method` literal).
335
+ *
336
+ * Other kinds return `[]` until their per-material review adapters land. `mixed` returns
337
+ * `[]` (a spreadsheet inside a mixed bundle does not yet receive spreadsheet obligations —
338
+ * a known C-review limitation, consistent with the mixed support state).
339
+ */
340
+ export function reviewMaterialGoals(kind) {
341
+ if (kind === "spreadsheet") {
342
+ return [
343
+ "formula_integrity",
344
+ "cross_sheet_reference_integrity",
345
+ "named_range_hygiene",
346
+ "data_validation_coverage",
347
+ "access_and_protection_hygiene",
348
+ "structural_risk_signals",
349
+ ];
350
+ }
351
+ return [];
352
+ }
@@ -345,7 +345,7 @@ const RECONSTRUCT_INPUT_SCHEMA = {
345
345
  resumeMode: {
346
346
  type: "string",
347
347
  enum: ["fresh", "reuse_existing_authored_artifacts"],
348
- description: "Optional promoted resume mode. fresh rejects same-session duplicate starts; reuse_existing_authored_artifacts admits a same-request resume attempt only when authored-artifact provenance can prove a current match.",
348
+ description: "Optional promoted resume mode. fresh rejects same-session duplicate starts; reuse_existing_authored_artifacts admits a same-request resume attempt only when authored-artifact provenance can prove a current match, and lets reconstruct semantic-map recover a validated same-batch dispatch-incomplete frontier.",
349
349
  },
350
350
  semanticAuthorRealization: {
351
351
  type: "string",
@@ -357,6 +357,18 @@ const RECONSTRUCT_INPUT_SCHEMA = {
357
357
  enum: ["direct_call"],
358
358
  description: "Explicit confirmation provider realization. direct_call uses configured llm provider.",
359
359
  },
360
+ llmEffort: {
361
+ type: "string",
362
+ description: "Optional reasoning-effort pin applied to both reconstruct actors (live only; mock ignores it).",
363
+ },
364
+ judgeLlmEffort: {
365
+ type: "string",
366
+ description: "Opt-in: run the answer-support judge at a different reasoning effort than the semantic author (live only). Reduces same-model rubber-stamping.",
367
+ },
368
+ judgeModel: {
369
+ type: "string",
370
+ description: "Opt-in: swap the answer-support judge MODEL (on the semantic author's provider; live only). An unsupported model degrades to the author model (INV-MODEL-1); a degrade is recorded as a runtime status note.",
371
+ },
360
372
  },
361
373
  };
362
374
  const RECONSTRUCT_SESSION_INPUT_SCHEMA = {
@@ -455,7 +467,7 @@ const TOOL_DEFINITIONS = [
455
467
  },
456
468
  {
457
469
  name: "onto_review_continue",
458
- description: "Continue a review session when runControl.continuationAvailable is true by reusing trusted PipelineExecutionLedger units and rerunning only the continuation frontier and downstream units.",
470
+ description: "The default way to resume a review that halted, timed out, or is prepared (runControl.continuationAvailable=true): reuses trusted PipelineExecutionLedger units and reruns only the continuation frontier and downstream units. Prefer this over starting a new onto_review — it keeps already-completed work. This is the operational resume of a session; it is not the continue_review finding action-candidate (which means the review should expand its evidence boundary).",
459
471
  inputSchema: REVIEW_CONTINUE_INPUT_SCHEMA,
460
472
  },
461
473
  {
@@ -495,12 +507,12 @@ const TOOL_DEFINITIONS = [
495
507
  },
496
508
  {
497
509
  name: "onto_reconstruct",
498
- description: "Run the material-aware reconstruct path with live semantic authoring, runtime validation gates, final-output.md, and reconstruct-record.yaml refs.",
510
+ description: "Run the material-aware reconstruct path with live semantic authoring and runtime validation gates. Returns final-output.md and reconstruct-record.yaml refs on completion, or a bounded record-less failed projection for a trusted provider output-ceiling terminal.",
499
511
  inputSchema: RECONSTRUCT_INPUT_SCHEMA,
500
512
  },
501
513
  {
502
514
  name: "onto_reconstruct_read",
503
- description: "Read a reconstruct session — stage progress, liveness, and count summary at projectionLevel compact/standard; the full record, run manifest, and final output at projectionLevel=full. Replaces onto_reconstruct_status and onto_reconstruct_result.",
515
+ description: "Read a reconstruct session — stage progress, liveness, and count summary at projectionLevel compact/standard; the full record, run manifest, and final output at projectionLevel=full. A trusted provider failure before record assembly returns the bounded record-less failed projection at every level. Replaces onto_reconstruct_status and onto_reconstruct_result.",
504
516
  inputSchema: RECONSTRUCT_READ_INPUT_SCHEMA,
505
517
  },
506
518
  ];
@@ -521,7 +533,7 @@ export function advertisedToolDefinitions() {
521
533
  const simple = new Set(OntoSimpleProfileToolNames);
522
534
  return TOOL_DEFINITIONS.filter((tool) => simple.has(tool.name));
523
535
  }
524
- const USAGE_GUIDE = `# Using onto via MCP
536
+ export const USAGE_GUIDE = `# Using onto via MCP
525
537
 
526
538
  onto is an ontology-as-code review runtime. The host LLM drives it through MCP
527
539
  tools; the runtime owns artifacts and validation. Two product paths exist:
@@ -566,6 +578,13 @@ reported through canonical route visibility. Listing tools needs no provider.
566
578
  result (full adds the ReviewRecord and final output text); a halted or failed
567
579
  session returns the status/failure projection.
568
580
  Present using the result's llmPresentation prompts.
581
+ 4. If the review halted (status "halted") and runControl.continuationAvailable is
582
+ true, the default next action is \`onto_review_continue\` with the same
583
+ sessionRoot: it resumes the session, reuses already-completed units, and
584
+ reruns only the failed/missing frontier and its downstream. Prefer it over
585
+ starting a fresh \`onto_review\`. (This operational resume is distinct from the
586
+ \`continue_review\` finding action-candidate, which means "expand the review's
587
+ evidence boundary because it was insufficient".)
569
588
 
570
589
  Other review tools: \`onto_prepare_review\` (materialize without executing) then
571
590
  \`onto_review_continue\`; \`onto_review_cancel\` to stop a running session.
@@ -1518,6 +1537,9 @@ export async function callTool(name, args, options = {}) {
1518
1537
  ...(parsed.resumeMode !== undefined ? { resumeMode: parsed.resumeMode } : {}),
1519
1538
  semanticAuthorRealization: parsed.semanticAuthorRealization,
1520
1539
  confirmationProviderRealization: parsed.confirmationProviderRealization,
1540
+ ...(parsed.llmEffort !== undefined ? { llmEffort: parsed.llmEffort } : {}),
1541
+ ...(parsed.judgeLlmEffort !== undefined ? { judgeLlmEffort: parsed.judgeLlmEffort } : {}),
1542
+ ...(parsed.judgeModel !== undefined ? { judgeModel: parsed.judgeModel } : {}),
1521
1543
  ...(sessionRoot ? { sessionRoot } : {}),
1522
1544
  ...(profilesRoot ? { profilesRoot } : {}),
1523
1545
  ...(filesystemAllowedRoots ? { filesystemAllowedRoots } : {}),
@@ -90,6 +90,14 @@ export const OntoReconstructToolInputSchema = OntoObserveSourceToolInputSchema.e
90
90
  resumeMode: z.enum(["fresh", "reuse_existing_authored_artifacts"]).optional(),
91
91
  semanticAuthorRealization: z.enum(["direct_call"]).default("direct_call"),
92
92
  confirmationProviderRealization: z.enum(["direct_call"]).default("direct_call"),
93
+ // Optional reasoning-effort pin applied to both reconstruct actors (live only).
94
+ llmEffort: z.string().min(1).optional(),
95
+ // Opt-in per-stage answer-support JUDGE overrides (live only). judgeLlmEffort
96
+ // runs the judge at a different effort; judgeModel swaps the judge MODEL on the
97
+ // semantic author's provider. An unsupported judgeModel degrades to the author
98
+ // model (INV-MODEL-1); the judge otherwise inherits the semantic-author config.
99
+ judgeLlmEffort: z.string().min(1).optional(),
100
+ judgeModel: z.string().min(1).optional(),
93
101
  }).strict();
94
102
  export const OntoReconstructSessionInputSchema = z.object({
95
103
  sessionRoot: z.string().min(1),
@@ -0,0 +1,167 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * `onto watch` Ink application — a small screen router over read-only data:
4
+ * selector → pick a session; tree → live WorkflowTree HUD; log → event tail.
5
+ * Keys: ↑↓/enter (selector), tab (tree↔log), s (→selector), r (refresh tree),
6
+ * q (quit). It only reads projections + tails the event stream — never mutates
7
+ * run state.
8
+ */
9
+ import { Box, Text, useApp, useInput } from "ink";
10
+ import { useEffect, useState } from "react";
11
+ import { followRuntimeEvents } from "./data/event-follower.js";
12
+ import { loadTreeViewModel } from "./data/projection-poll.js";
13
+ import { readOutputTail } from "./data/node-detail.js";
14
+ import { LogScreen } from "./screens/log.js";
15
+ import { NodeDetail } from "./screens/node-detail.js";
16
+ import { SessionSelector } from "./screens/session-selector.js";
17
+ import { WorkflowTree } from "./screens/workflow-tree.js";
18
+ import { isTerminalStatus, } from "./view-model/tree-view-model.js";
19
+ export function WatchApp({ sessions, initialSession, ontoHome, defaultPollMs = 1000, nowMs, }) {
20
+ const { exit } = useApp();
21
+ const [screen, setScreen] = useState(initialSession ? "tree" : "selector");
22
+ const [selectedIndex, setSelectedIndex] = useState(0);
23
+ const [session, setSession] = useState(initialSession ?? null);
24
+ const [vm, setVm] = useState(null);
25
+ const [treeError, setTreeError] = useState(null);
26
+ const [events, setEvents] = useState([]);
27
+ const [nonce, setNonce] = useState(0);
28
+ const [nodeCursor, setNodeCursor] = useState(0);
29
+ const [tail, setTail] = useState([]);
30
+ const flatNodes = vm ? vm.phases.flatMap((p) => p.nodes) : [];
31
+ const selectedNode = flatNodes.length > 0
32
+ ? flatNodes[Math.min(nodeCursor, flatNodes.length - 1)]
33
+ : null;
34
+ useInput((input, key) => {
35
+ if (input === "q") {
36
+ exit();
37
+ return;
38
+ }
39
+ if (screen === "selector") {
40
+ if (key.upArrow)
41
+ setSelectedIndex((i) => Math.max(0, i - 1));
42
+ else if (key.downArrow) {
43
+ setSelectedIndex((i) => Math.min(sessions.length - 1, i + 1));
44
+ }
45
+ else if (key.return && sessions[selectedIndex]) {
46
+ setSession(sessions[selectedIndex]);
47
+ setVm(null);
48
+ setTreeError(null);
49
+ setEvents([]);
50
+ setNodeCursor(0);
51
+ // Bump the poll nonce so the tree effect re-runs even when the chosen
52
+ // session is the one already selected (same ref → deps unchanged);
53
+ // otherwise setVm(null) would leave the HUD stuck on "loading…".
54
+ setNonce((n) => n + 1);
55
+ setScreen("tree");
56
+ }
57
+ return;
58
+ }
59
+ if (input === "s")
60
+ setScreen("selector");
61
+ else if (key.tab)
62
+ setScreen((s) => (s === "tree" ? "log" : "tree"));
63
+ else if (screen === "tree" && input === "r")
64
+ setNonce((n) => n + 1);
65
+ else if (screen === "tree" && key.upArrow) {
66
+ setNodeCursor((c) => Math.max(0, c - 1));
67
+ }
68
+ else if (screen === "tree" && key.downArrow) {
69
+ setNodeCursor((c) => Math.min(Math.max(0, flatNodes.length - 1), c + 1));
70
+ }
71
+ });
72
+ // Tree projection poll — respects the projection interval, stops at terminal status.
73
+ useEffect(() => {
74
+ if (!session)
75
+ return;
76
+ let cancelled = false;
77
+ let timer;
78
+ async function tick() {
79
+ try {
80
+ const next = await loadTreeViewModel(session, ontoHome ? { ontoHome } : {});
81
+ if (cancelled)
82
+ return;
83
+ setVm(next);
84
+ setTreeError(null);
85
+ if (isTerminalStatus(next.status))
86
+ return;
87
+ timer = setTimeout(() => void tick(), Math.max(500, next.liveness.pollMs ?? defaultPollMs));
88
+ }
89
+ catch (caught) {
90
+ if (!cancelled) {
91
+ setTreeError(caught instanceof Error ? caught.message : String(caught));
92
+ // Keep the live loop alive: a transient read/parse error (e.g. a file
93
+ // mid-rewrite) self-recovers on the next tick. The error stays visible
94
+ // until a successful poll clears it (setTreeError(null) above).
95
+ timer = setTimeout(() => void tick(), Math.max(500, defaultPollMs));
96
+ }
97
+ }
98
+ }
99
+ void tick();
100
+ return () => {
101
+ cancelled = true;
102
+ if (timer)
103
+ clearTimeout(timer);
104
+ };
105
+ }, [session, ontoHome, defaultPollMs, nonce]);
106
+ // Live event tail for the Log screen.
107
+ useEffect(() => {
108
+ if (!session)
109
+ return;
110
+ const controller = new AbortController();
111
+ let cancelled = false;
112
+ void (async () => {
113
+ for await (const event of followRuntimeEvents(session.sessionRoot, {
114
+ signal: controller.signal,
115
+ })) {
116
+ // Skip a late event from the aborted (previous-session) follower so it
117
+ // never lands in the new session's tail.
118
+ if (cancelled)
119
+ break;
120
+ setEvents((prev) => [...prev, event].slice(-500));
121
+ }
122
+ })();
123
+ return () => {
124
+ cancelled = true;
125
+ controller.abort();
126
+ };
127
+ }, [session]);
128
+ // Drill-down: read the selected node's output tail (read-only).
129
+ const selectedOutputPath = selectedNode?.outputPath ?? null;
130
+ // A running node's `.running.log` keeps growing, so re-read its tail on every
131
+ // poll tick (keyed on the vm the poll replaces). A terminal node's tail is
132
+ // stable, so for it we key only on the path and skip the per-tick re-read.
133
+ const tailIsStable = selectedNode != null
134
+ && (selectedNode.status === "completed"
135
+ || selectedNode.status === "failed"
136
+ || selectedNode.status === "skipped");
137
+ const tailRefreshKey = tailIsStable ? null : vm;
138
+ useEffect(() => {
139
+ let cancelled = false;
140
+ if (!selectedOutputPath) {
141
+ setTail([]);
142
+ return;
143
+ }
144
+ void readOutputTail(selectedOutputPath).then((lines) => {
145
+ if (!cancelled)
146
+ setTail(lines);
147
+ });
148
+ return () => {
149
+ cancelled = true;
150
+ };
151
+ }, [selectedOutputPath, tailRefreshKey]);
152
+ if (screen === "selector") {
153
+ return (_jsx(SessionSelector, { sessions: sessions, selectedIndex: selectedIndex, ...(nowMs !== undefined ? { nowMs } : {}) }));
154
+ }
155
+ if (!session)
156
+ return _jsx(Text, { children: "no session selected" });
157
+ if (screen === "log") {
158
+ return _jsx(LogScreen, { sessionId: session.sessionId, events: events });
159
+ }
160
+ if (treeError) {
161
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "red", children: `onto watch error: ${treeError}` }), _jsx(Text, { dimColor: true, children: "[s]essions [r]etry [q]uit" })] }));
162
+ }
163
+ if (!vm) {
164
+ return _jsx(Text, { children: `loading ${session.pipeline} session ${session.sessionId}…` });
165
+ }
166
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(WorkflowTree, { vm: vm, ...(selectedNode ? { selectedNodeId: selectedNode.id } : {}) }), selectedNode ? _jsx(NodeDetail, { node: selectedNode, tail: tail }) : null] }));
167
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Tails a session's append-only `runtime-events.ndjson` for the `onto watch`
3
+ * TUI. Read-only: it never writes the stream. Reads existing lines, then polls
4
+ * for appended lines (offset-tracked) and yields parsed events until aborted.
5
+ */
6
+ import fs from "node:fs/promises";
7
+ import { runtimeStreamEventLogPath, } from "../../core-api/runtime-observation.js";
8
+ function parseEventLine(line) {
9
+ const trimmed = line.trim();
10
+ if (!trimmed)
11
+ return null;
12
+ try {
13
+ const parsed = JSON.parse(trimmed);
14
+ // Defensive: only accept well-formed runtime_stream events.
15
+ if (parsed && parsed.event_kind === "runtime_stream")
16
+ return parsed;
17
+ return null;
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ /** One-shot read of all events currently in the session's stream. */
24
+ export async function readRuntimeEvents(sessionRoot) {
25
+ const logPath = runtimeStreamEventLogPath(sessionRoot);
26
+ let raw;
27
+ try {
28
+ raw = await fs.readFile(logPath, "utf8");
29
+ }
30
+ catch {
31
+ return [];
32
+ }
33
+ return raw.split("\n").map(parseEventLine).filter((event) => event !== null);
34
+ }
35
+ /** Bytes of historical backlog read on start — the UI only keeps the last 500. */
36
+ const DEFAULT_BACKLOG_BYTES_CAP = 256 * 1024;
37
+ /**
38
+ * Async iterator over a session's runtime events: emits a bounded tail of the
39
+ * existing backlog (unless disabled), then appended events as they land, until
40
+ * `signal` aborts. Tracks a byte offset and only re-parses the trailing partial
41
+ * line on each tick.
42
+ */
43
+ export async function* followRuntimeEvents(sessionRoot, options = {}) {
44
+ const { pollMs = 500, signal, emitBacklog = true, backlogBytesCap = DEFAULT_BACKLOG_BYTES_CAP, } = options;
45
+ const logPath = runtimeStreamEventLogPath(sessionRoot);
46
+ let offset = 0;
47
+ let pending = "";
48
+ let dropPartialLine = false;
49
+ // Bound the initial backlog so a large historical log never loads whole: with
50
+ // emitBacklog, keep only the last backlogBytesCap bytes (dropping the partial
51
+ // leading line); without it, start at EOF and only follow appended lines.
52
+ try {
53
+ const { size } = await fs.stat(logPath);
54
+ if (!emitBacklog) {
55
+ offset = size;
56
+ }
57
+ else if (size > backlogBytesCap) {
58
+ offset = size - backlogBytesCap;
59
+ dropPartialLine = true;
60
+ }
61
+ }
62
+ catch {
63
+ // Stream not created yet — start from 0 and pick it up when it appears.
64
+ }
65
+ while (!signal?.aborted) {
66
+ let chunk = "";
67
+ try {
68
+ const handle = await fs.open(logPath, "r");
69
+ try {
70
+ const stat = await handle.stat();
71
+ if (stat.size < offset) {
72
+ // File shrank/rotated — restart from the top.
73
+ offset = 0;
74
+ pending = "";
75
+ dropPartialLine = false;
76
+ }
77
+ if (stat.size > offset) {
78
+ const length = stat.size - offset;
79
+ const buffer = Buffer.alloc(length);
80
+ // Advance only by bytes actually read; a short read must not skip the
81
+ // unread tail or decode the zero-filled remainder of the buffer.
82
+ const { bytesRead } = await handle.read(buffer, 0, length, offset);
83
+ chunk = buffer.toString("utf8", 0, bytesRead);
84
+ offset += bytesRead;
85
+ }
86
+ }
87
+ finally {
88
+ await handle.close();
89
+ }
90
+ }
91
+ catch {
92
+ // Stream not created yet — wait and retry.
93
+ }
94
+ // Abort may have landed during the fs read; bail before yielding so a late
95
+ // event from an aborted follower never reaches the consumer.
96
+ if (signal?.aborted)
97
+ return;
98
+ if (chunk) {
99
+ pending += chunk;
100
+ const lines = pending.split("\n");
101
+ // Keep the trailing (possibly partial) line for the next tick.
102
+ pending = lines.pop() ?? "";
103
+ if (dropPartialLine) {
104
+ // The first bounded read started mid-file; drop the partial leading line.
105
+ lines.shift();
106
+ dropPartialLine = false;
107
+ }
108
+ for (const line of lines) {
109
+ const event = parseEventLine(line);
110
+ if (event)
111
+ yield event;
112
+ }
113
+ }
114
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
115
+ }
116
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Reads the tail of a tree node's authored output / running log (the node's
3
+ * `outputPath`) for the drill-down detail pane. Read-only; returns [] when the
4
+ * path is absent or unreadable.
5
+ *
6
+ * The detail pane re-reads on every poll tick for a running node whose
7
+ * `.running.log` keeps growing, so this reads only a bounded suffix from the end
8
+ * of the file (not the whole file) to keep cost independent of file size.
9
+ */
10
+ import fs from "node:fs/promises";
11
+ /** Bytes read from the end of the file — ample for `maxLines` of tail. */
12
+ const TAIL_BYTES_CAP = 64 * 1024;
13
+ export async function readOutputTail(outputPath, maxLines = 12) {
14
+ if (!outputPath)
15
+ return [];
16
+ let handle;
17
+ try {
18
+ handle = await fs.open(outputPath, "r");
19
+ const { size } = await handle.stat();
20
+ const start = size > TAIL_BYTES_CAP ? size - TAIL_BYTES_CAP : 0;
21
+ const length = size - start;
22
+ if (length <= 0)
23
+ return [];
24
+ const buffer = Buffer.alloc(length);
25
+ // Decode only the bytes actually read (a short read must not leave the
26
+ // zero-filled remainder of the buffer in the decoded text).
27
+ const { bytesRead } = await handle.read(buffer, 0, length, start);
28
+ let text = buffer.toString("utf8", 0, bytesRead);
29
+ // When we started mid-file, drop the leading partial line.
30
+ if (start > 0) {
31
+ const newline = text.indexOf("\n");
32
+ text = newline >= 0 ? text.slice(newline + 1) : "";
33
+ }
34
+ return text.split("\n").filter((line) => line.length > 0).slice(-maxLines);
35
+ }
36
+ catch {
37
+ return [];
38
+ }
39
+ finally {
40
+ await handle?.close();
41
+ }
42
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Loads a session's {@link TreeViewModel} by calling the same core-api read
3
+ * projection the MCP read tools use, then routing it through the pipeline's
4
+ * adapter. Read-only. The poll LOOP (interval, abort) lives in the Ink app; this
5
+ * module is the single-shot fetch so it stays trivially testable.
6
+ */
7
+ import { createOntoReviewCoreApi } from "../../core-api/review-api.js";
8
+ import { createOntoReconstructCoreApi } from "../../core-api/reconstruct-api.js";
9
+ import { reviewStatusToTreeViewModel } from "../view-model/review-adapter.js";
10
+ import { reconstructStatusToTreeViewModel } from "../view-model/reconstruct-adapter.js";
11
+ /** Single-shot read of the current TreeViewModel for a session. */
12
+ export async function loadTreeViewModel(ref, deps = {}) {
13
+ const options = deps.ontoHome ? { ontoHome: deps.ontoHome } : {};
14
+ if (ref.pipeline === "review") {
15
+ const api = createOntoReviewCoreApi(options);
16
+ // Request the full projection: the standard projection compacts
17
+ // unit_progress `outputPath`/`runningLogRef` with an ellipsis, and the TUI
18
+ // does local file I/O (drill-down tail) on those paths — they must be
19
+ // untruncated.
20
+ const status = await api.getReviewStatus(ref.sessionRoot, { projectionLevel: "full" });
21
+ return reviewStatusToTreeViewModel(status, ref.sessionRoot);
22
+ }
23
+ const api = createOntoReconstructCoreApi(options);
24
+ const status = await api.getRunStatus(ref.sessionRoot);
25
+ return reconstructStatusToTreeViewModel(status, ref.sessionRoot);
26
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Enumerates review/reconstruct session directories under a project's `.onto/`
3
+ * for the `onto watch` SessionSelector. Read-only listing; status enrichment is
4
+ * left to the caller (which polls getReviewStatus / getRunStatus per session).
5
+ */
6
+ import fs from "node:fs/promises";
7
+ import path from "node:path";
8
+ import { runtimeStreamEventLogPath } from "../../core-api/runtime-observation.js";
9
+ const PIPELINE_DIRS = {
10
+ review: "review",
11
+ reconstruct: "reconstruct",
12
+ };
13
+ async function listPipelineSessions(projectRoot, pipeline) {
14
+ const root = path.join(path.resolve(projectRoot), ".onto", PIPELINE_DIRS[pipeline]);
15
+ let entries;
16
+ try {
17
+ entries = await fs.readdir(root, { withFileTypes: true });
18
+ }
19
+ catch {
20
+ return [];
21
+ }
22
+ const sessions = [];
23
+ for (const entry of entries) {
24
+ if (!entry.isDirectory())
25
+ continue;
26
+ const sessionRoot = path.join(root, entry.name);
27
+ let modifiedMs = 0;
28
+ try {
29
+ modifiedMs = (await fs.stat(sessionRoot)).mtimeMs;
30
+ }
31
+ catch {
32
+ continue;
33
+ }
34
+ // The root dir mtime misses content appends (a running session keeps
35
+ // appending to runtime-events.ndjson and nested running logs without
36
+ // touching the dir entry), so fold in the event log's mtime as the primary
37
+ // activity signal — otherwise an active session can sort below older ones.
38
+ try {
39
+ const eventMs = (await fs.stat(runtimeStreamEventLogPath(sessionRoot))).mtimeMs;
40
+ if (eventMs > modifiedMs)
41
+ modifiedMs = eventMs;
42
+ }
43
+ catch {
44
+ // No event log yet — fall back to the dir mtime.
45
+ }
46
+ sessions.push({
47
+ pipeline,
48
+ sessionId: entry.name,
49
+ sessionRoot,
50
+ modifiedMs,
51
+ });
52
+ }
53
+ return sessions;
54
+ }
55
+ /**
56
+ * Lists all review + reconstruct sessions under `projectRoot/.onto/`, most
57
+ * recently modified first.
58
+ */
59
+ export async function discoverSessions(projectRoot) {
60
+ const [review, reconstruct] = await Promise.all([
61
+ listPipelineSessions(projectRoot, "review"),
62
+ listPipelineSessions(projectRoot, "reconstruct"),
63
+ ]);
64
+ return [...review, ...reconstruct].sort((a, b) => b.modifiedMs - a.modifiedMs);
65
+ }