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.
- package/.onto/authority/core-lexicon.yaml +1 -1
- package/.onto/authority/supported-models.yaml +153 -0
- package/.onto/domains/accounting-kr/competency_qs.md +153 -153
- package/.onto/processes/reconstruct/obligation-coverage-ledger.yaml +842 -0
- package/.onto/processes/reconstruct/obligation-coverage-recorded.yaml +225 -0
- package/.onto/processes/reconstruct/ontology-seeding-and-maturation-design.md +93 -54
- package/.onto/processes/reconstruct/reconstruct-contract-registry.yaml +202 -49
- package/.onto/processes/reconstruct/reconstruct-execution-ux-contract.md +72 -1
- package/.onto/processes/reconstruct/source-profiles/spreadsheet.md +35 -0
- package/.onto/processes/review/nesting-batch-worker-contract.md +1 -1
- package/.onto/processes/review/productized-live-path.md +9 -5
- package/.onto/processes/review/prompt-execution-runner-contract.md +32 -8
- package/.onto/processes/review/review-target-profile-contract.md +47 -4
- package/.onto/processes/shared/pipeline-execution-ledger-contract.md +11 -0
- package/.onto/processes/shared/target-material-kind-contract.md +1 -1
- package/README.md +64 -1
- package/dist/cli.js +11 -1
- package/dist/core-api/reconstruct-api.js +679 -18
- package/dist/core-api/review-api.js +70 -5
- package/dist/core-api/review-progress.js +7 -0
- package/dist/core-api/runtime-observation.js +7 -0
- package/dist/core-runtime/artifact-io.js +131 -0
- package/dist/core-runtime/cli/claude-code-review-unit-executor.js +21 -9
- package/dist/core-runtime/cli/claude-nesting-batch-worker.js +4 -2
- package/dist/core-runtime/cli/materialize-review-prompt-packets.js +68 -14
- package/dist/core-runtime/cli/prepare-review-session.js +34 -1
- package/dist/core-runtime/cli/render-review-final-output.js +2 -1
- package/dist/core-runtime/cli/run-review-prompt-execution.js +758 -48
- package/dist/core-runtime/cli/run-seats.js +18 -0
- package/dist/core-runtime/cli/runtime-submit-context.js +10 -2
- package/dist/core-runtime/cli/unit-resubmit.js +269 -0
- package/dist/core-runtime/cli/worker-structured-output.js +80 -2
- package/dist/core-runtime/discovery/host-detection.js +71 -22
- package/dist/core-runtime/discovery/review-cert-assemble.js +208 -0
- package/dist/core-runtime/discovery/review-cert-record.js +442 -0
- package/dist/core-runtime/discovery/review-execution-units.js +22 -0
- package/dist/core-runtime/discovery/seat-inventory.js +167 -0
- package/dist/core-runtime/discovery/settings-chain.js +311 -52
- package/dist/core-runtime/discovery/supported-models.js +289 -25
- package/dist/core-runtime/discovery/synthesize-cert-assemble.js +197 -0
- package/dist/core-runtime/discovery/synthesize-cert-capsule.js +399 -0
- package/dist/core-runtime/discovery/synthesize-cert-judge.js +340 -0
- package/dist/core-runtime/discovery/synthesize-cert-loop.js +234 -0
- package/dist/core-runtime/discovery/synthesize-cert-mutation.js +117 -0
- package/dist/core-runtime/discovery/synthesize-cert-packet.js +182 -0
- package/dist/core-runtime/discovery/synthesize-cert-record.js +831 -0
- package/dist/core-runtime/discovery/synthesize-cert-sampler.js +336 -0
- package/dist/core-runtime/effort-calibration-ingest.js +326 -0
- package/dist/core-runtime/effort-calibration-reconstruct.js +47 -0
- package/dist/core-runtime/effort-calibration-review.js +22 -0
- package/dist/core-runtime/effort-calibration-sweep.js +110 -0
- package/dist/core-runtime/effort-frontier.js +134 -0
- package/dist/core-runtime/llm/claude-bin.js +77 -0
- package/dist/core-runtime/llm/dispatch-breaker.js +412 -0
- package/dist/core-runtime/llm/dispatch-fallback-adapter-capabilities.js +33 -0
- package/dist/core-runtime/llm/llm-caller.js +316 -35
- package/dist/core-runtime/llm/mock-llm-realization.js +35 -4
- package/dist/core-runtime/llm/model-switcher.js +1 -1
- package/dist/core-runtime/llm/openai-responses-incomplete-error.js +46 -0
- package/dist/core-runtime/llm/sealed-dispatch-capability.js +359 -0
- package/dist/core-runtime/llm/structured-dispatch-error.js +15 -0
- package/dist/core-runtime/onboard/claude-profile-scan.js +65 -0
- package/dist/core-runtime/onboard/cli-host.js +5 -2
- package/dist/core-runtime/onboard/configure-provider.js +33 -1
- package/dist/core-runtime/onboard/host-target.js +28 -3
- package/dist/core-runtime/onboard/register.js +54 -12
- package/dist/core-runtime/pipeline-execution-ledger.js +10 -0
- package/dist/core-runtime/reconstruct/artifact-types.js +37 -0
- package/dist/core-runtime/reconstruct/benchmark-evidence.js +16 -0
- package/dist/core-runtime/reconstruct/claim-projection-validation.js +19 -5
- package/dist/core-runtime/reconstruct/competency-projection-contract.js +63 -0
- package/dist/core-runtime/reconstruct/comprehension-artifact.js +321 -0
- package/dist/core-runtime/reconstruct/comprehension-reduce.js +367 -0
- package/dist/core-runtime/reconstruct/comprehension-semantic-map.js +771 -0
- package/dist/core-runtime/reconstruct/contract-registry.js +20 -0
- package/dist/core-runtime/reconstruct/directive-validation.js +4 -3
- package/dist/core-runtime/reconstruct/dispatch-fallback-artifacts.js +640 -0
- package/dist/core-runtime/reconstruct/execution-telemetry.js +84 -9
- package/dist/core-runtime/reconstruct/final-output-sections.js +89 -0
- package/dist/core-runtime/reconstruct/governing-snapshot.js +36 -1
- package/dist/core-runtime/reconstruct/leaf-reader.js +305 -0
- package/dist/core-runtime/reconstruct/llm-dispatch-failure.js +270 -0
- package/dist/core-runtime/reconstruct/llm-touch-fingerprint.js +94 -0
- package/dist/core-runtime/reconstruct/material-admission-validation.js +20 -5
- package/dist/core-runtime/reconstruct/material-profile-validation.js +24 -6
- package/dist/core-runtime/reconstruct/materialize-preparation.js +380 -23
- package/dist/core-runtime/reconstruct/maturation-validation.js +1522 -67
- package/dist/core-runtime/reconstruct/mock-llm-realization.js +120 -1
- package/dist/core-runtime/reconstruct/obligation-assertion.js +18 -0
- package/dist/core-runtime/reconstruct/ontology-seed-validation.js +39 -7
- package/dist/core-runtime/reconstruct/output-budget.js +6 -0
- package/dist/core-runtime/reconstruct/pipeline-execution-ledger.js +112 -5
- package/dist/core-runtime/reconstruct/post-seed-validation.js +122 -11
- package/dist/core-runtime/reconstruct/proof-authority-validation.js +2 -6
- package/dist/core-runtime/reconstruct/purpose-authority-validation.js +119 -7
- package/dist/core-runtime/reconstruct/record.js +49 -3
- package/dist/core-runtime/reconstruct/registry-verification-validation.js +41 -5
- package/dist/core-runtime/reconstruct/run-control-validation.js +1019 -37
- package/dist/core-runtime/reconstruct/run.js +5409 -517
- package/dist/core-runtime/reconstruct/seed-authoring-readiness-validation.js +56 -8
- package/dist/core-runtime/reconstruct/semantic-quality-gate.js +112 -7
- package/dist/core-runtime/reconstruct/source-observation-delta-validation.js +53 -5
- package/dist/core-runtime/reconstruct/source-observations.js +166 -0
- package/dist/core-runtime/reconstruct/source-safety-validation.js +77 -188
- package/dist/core-runtime/reconstruct/source-scout-pack-validation.js +50 -6
- package/dist/core-runtime/reconstruct/terminal-validation.js +145 -13
- package/dist/core-runtime/review/continuation-plan.js +12 -5
- package/dist/core-runtime/review/issue-artifact-runtime.js +4 -1
- package/dist/core-runtime/review/materializers.js +100 -11
- package/dist/core-runtime/review/nesting-batch.js +6 -2
- package/dist/core-runtime/review/obligation-shardability.js +138 -0
- package/dist/core-runtime/review/pipeline-execution-ledger.js +42 -2
- package/dist/core-runtime/review/review-artifact-utils.js +219 -7
- package/dist/core-runtime/review/review-execution-steps.js +32 -8
- package/dist/core-runtime/review/review-progress-contract.js +8 -0
- package/dist/core-runtime/review/review-prompt-budget.js +97 -0
- package/dist/core-runtime/review/semantic-quality-gate.js +33 -7
- package/dist/core-runtime/review/spreadsheet-review-disposition.js +94 -0
- package/dist/core-runtime/route-identity.js +182 -0
- package/dist/core-runtime/spreadsheet-header-escalation.js +204 -0
- package/dist/core-runtime/spreadsheet-structure-observer.js +2689 -0
- package/dist/core-runtime/target-material-kind.js +58 -0
- package/dist/mcp/server.js +27 -5
- package/dist/mcp/tool-schemas.js +8 -0
- package/dist/tui/app.js +167 -0
- package/dist/tui/data/event-follower.js +116 -0
- package/dist/tui/data/node-detail.js +42 -0
- package/dist/tui/data/projection-poll.js +26 -0
- package/dist/tui/data/session-discovery.js +65 -0
- package/dist/tui/index.js +108 -0
- package/dist/tui/screens/log.js +25 -0
- package/dist/tui/screens/node-detail.js +17 -0
- package/dist/tui/screens/session-selector.js +26 -0
- package/dist/tui/screens/workflow-tree.js +90 -0
- package/dist/tui/view-model/reconstruct-adapter.js +162 -0
- package/dist/tui/view-model/review-adapter.js +270 -0
- package/dist/tui/view-model/tree-view-model.js +19 -0
- package/package.json +20 -3
- 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
|
+
}
|
package/dist/mcp/server.js
CHANGED
|
@@ -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: "
|
|
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
|
|
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 } : {}),
|
package/dist/mcp/tool-schemas.js
CHANGED
|
@@ -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),
|
package/dist/tui/app.js
ADDED
|
@@ -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
|
+
}
|