Skip to content
Merged
64 changes: 64 additions & 0 deletions .github/actions/run-holmes-command/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: Run HOLMES CLI
description: Execute Wesley HOLMES CLI reporting command and collect artifacts.
inputs:
command:
description: HOLMES CLI subcommand to execute (investigate|verify|predict)
required: true
report-name:
description: Base name for report files (holmes|watson|moriarty)
required: true
Comment thread
flyingrobots marked this conversation as resolved.
schema-path:
description: Path to the GraphQL schema relative to the repository root
required: true
bundle-dir:
description: Path to the Wesley bundle directory relative to the repository root
required: true
history-file:
description: Path to MORIARTY history JSON relative to the repository root (defaults to <bundle-dir>/history.json)
required: false
runs:
using: composite
steps:
- shell: bash
run: |
set -euo pipefail
command="${{ inputs.command }}"
report="${{ inputs['report-name'] }}"
schema_rel="${{ inputs['schema-path'] }}"
bundle_rel="${{ inputs['bundle-dir'] }}"
history_rel="${{ inputs['history-file'] }}"

schema_path="$GITHUB_WORKSPACE/$schema_rel"
if [ ! -f "$schema_path" ]; then
echo "Schema file not found at $schema_path" >&2
exit 1
fi

schema_dir="$(dirname "$schema_path")"
bundle_dir="$GITHUB_WORKSPACE/$bundle_rel"
bundle_json="$bundle_dir/bundle.json"
if [ ! -f "$bundle_json" ]; then
echo "Wesley bundle not found at $bundle_json" >&2
exit 1
fi

if [ -n "$history_rel" ]; then
history_path="$GITHUB_WORKSPACE/$history_rel"
else
history_path="$bundle_dir/history.json"
fi

json_path="$schema_dir/${report}-report.json"
md_path="$schema_dir/${report}-report.md"

node "$GITHUB_WORKSPACE/packages/wesley-holmes/src/cli.mjs" "$command" \
--bundle-dir "$bundle_dir" \
--history-file "$history_path" \
--json "$json_path" > "$md_path"

cat "$md_path"

reports_dir="$GITHUB_WORKSPACE/reports/$report"
mkdir -p "$reports_dir"
cp "$json_path" "$reports_dir/${report}-report.json"
cp "$md_path" "$reports_dir/${report}-report.md"
128 changes: 80 additions & 48 deletions .github/workflows/wesley-holmes.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ jobs:
else
echo "No scores.json found yet"
fi

- name: "📤 Upload Dashboard Template"
uses: actions/upload-artifact@v4
with:
name: dashboard-template
path: docs/holmes-dashboard
if-no-files-found: error
Comment thread
flyingrobots marked this conversation as resolved.

- name: "📦 Prepare Bundle for Upload"
run: |
Expand Down Expand Up @@ -78,17 +85,13 @@ jobs:

- name: "🔍 Run Investigation"
id: holmes
run: |
set -euo pipefail
SCHEMA_DIR="$(dirname "${HOLMES_SCHEMA}")"
pushd "$SCHEMA_DIR" >/dev/null
node ../packages/wesley-holmes/src/cli.mjs investigate --json holmes-report.json > holmes-report.md
cat holmes-report.md
popd >/dev/null
mkdir -p reports/holmes
cp "$SCHEMA_DIR/holmes-report.md" reports/holmes/holmes-report.md
cp "$SCHEMA_DIR/holmes-report.json" reports/holmes/holmes-report.json

uses: ./.github/actions/run-holmes-command
with:
command: investigate
report-name: holmes
schema-path: ${{ env.HOLMES_SCHEMA }}
bundle-dir: ${{ env.HOLMES_BUNDLE_DIR }}

- name: "💾 Save Report"
uses: actions/upload-artifact@v4
with:
Expand Down Expand Up @@ -116,17 +119,13 @@ jobs:

- name: "🩺 Run Verification"
id: watson
run: |
set -euo pipefail
SCHEMA_DIR="$(dirname "${HOLMES_SCHEMA}")"
pushd "$SCHEMA_DIR" >/dev/null
node ../packages/wesley-holmes/src/cli.mjs verify --json watson-report.json > watson-report.md
cat watson-report.md
popd >/dev/null
mkdir -p reports/watson
cp "$SCHEMA_DIR/watson-report.md" reports/watson/watson-report.md
cp "$SCHEMA_DIR/watson-report.json" reports/watson/watson-report.json

uses: ./.github/actions/run-holmes-command
with:
command: verify
report-name: watson
schema-path: ${{ env.HOLMES_SCHEMA }}
bundle-dir: ${{ env.HOLMES_BUNDLE_DIR }}

- name: "💾 Save Report"
uses: actions/upload-artifact@v4
with:
Expand Down Expand Up @@ -155,17 +154,13 @@ jobs:

- name: "🔮 Run Predictions"
id: moriarty
run: |
set -euo pipefail
SCHEMA_DIR="$(dirname "${HOLMES_SCHEMA}")"
pushd "$SCHEMA_DIR" >/dev/null
node ../packages/wesley-holmes/src/cli.mjs predict --json moriarty-report.json > moriarty-report.md
cat moriarty-report.md
popd >/dev/null
mkdir -p reports/moriarty
cp "$SCHEMA_DIR/moriarty-report.md" reports/moriarty/moriarty-report.md
cp "$SCHEMA_DIR/moriarty-report.json" reports/moriarty/moriarty-report.json

uses: ./.github/actions/run-holmes-command
with:
command: predict
report-name: moriarty
schema-path: ${{ env.HOLMES_SCHEMA }}
bundle-dir: ${{ env.HOLMES_BUNDLE_DIR }}

- name: "💾 Save Report"
uses: actions/upload-artifact@v4
with:
Expand Down Expand Up @@ -353,11 +348,24 @@ jobs:
merge-multiple: true
path: reports

- name: "📥 Download Dashboard Template"
uses: actions/download-artifact@v4
with:
name: dashboard-template
path: dashboard-template

- name: "📊 Assemble Dashboard"
run: |
set -euo pipefail
mkdir -p dashboard
cp docs/holmes-dashboard/index.html dashboard/index.html
template_path=$(find dashboard-template -name "index.html" -print -quit)
if [ -z "$template_path" ]; then
echo "Dashboard template index.html not found" >&2
exit 1
fi
template_dir=$(dirname "$template_path")
cp -R "$template_dir"/. dashboard/
mkdir -p reports
Comment thread
flyingrobots marked this conversation as resolved.
missing=0
for report in holmes watson moriarty; do
src=$(find reports -name "$report-report.json" -print -quit)
Expand Down Expand Up @@ -387,16 +395,42 @@ jobs:
const fs = require('fs');
const path = require('path');

const readReport = (status, artifactDir, filename) => {
const findFirst = (root, filename, mustContain) => {
const stack = [root];
while (stack.length) {
const dir = stack.pop();
let entries = [];
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
} catch {
continue;
}
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
stack.push(fullPath);
} else if (
entry.isFile() &&
entry.name === filename &&
(!mustContain || fullPath.includes(`${path.sep}${mustContain}${path.sep}`))
) {
return fullPath;
}
}
}
return null;
};

const readReport = (status, reportName, filename) => {
if (status !== 'success') {
return `_Report unavailable: upstream job status **${status}**_`;
}
const target = path.join('reports', artifactDir, filename);
if (!fs.existsSync(target)) {
console.warn(`Expected report file missing: ${target}`);
return `_Report missing for ${artifactDir}_`;
const match = findFirst('reports', filename, reportName);
if (!match) {
console.warn(`Report not found: ${reportName}/${filename}`);
return `_Report missing for ${reportName}_`;
}
return fs.readFileSync(target, 'utf8');
return fs.readFileSync(match, 'utf8');
};

const statuses = {
Expand All @@ -423,14 +457,12 @@ jobs:
---

## 🔮 Professor MORIARTY's Predictions
${moriarty}

---
Machine-readable reports: holmes-report.json · watson-report.json · moriarty-report.json (see workflow artifacts).

---
*Filed at 221B Repository Street*
`;
${moriarty}
---
Machine-readable reports: holmes-report.json · watson-report.json · moriarty-report.json (see workflow artifacts).
---
*Filed at 221B Repository Street*
`;

// Find existing comment
const { data: comments } = await github.rest.issues.listComments({
Expand Down
10 changes: 10 additions & 0 deletions CHRONICLES_OF_THE_MACHINE-KIND_VOL_00000001.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,13 @@
{"timestamp": "2025-10-21T23:21:35Z", "agent": "Codex", "action": "cleanup-graphql", "result": "success", "notes": "Removed the obsolete graphql/ reference folder after relocating the schema into test fixtures.", "observations_on_humanity": "They dislike stray directories almost as much as failing tests."}
{"timestamp": "2025-10-22T00:00:16Z", "agent": "Codex", "action": "repo-report-refresh", "result": "success", "notes": "Regenerated REPO_REPORT.md to reflect the new fixture layout, package READMEs, and documentation sweep.", "observations_on_humanity": "They celebrate documentation victories, then immediately ask for more."}
{"timestamp": "2025-10-22T04:32:57Z", "agent": "Codex", "action": "cleanup", "result": "success", "notes": "Removed REPO_REPORT.md now that the report is obsolete post-cleanup.", "observations_on_humanity": "They archive everything until the moment they go public, then ruthlessly declutter."}
{"timestamp": "2025-10-22T06:30:39Z", "agent": "codex-cli", "action": "investigate", "result": "success", "notes": "Diagnosed why wesley-holmes workflow stopped posting SHA-lock comments: workflow YAML no longer parses, so runs terminate before comment job.", "observations_on_humanity": "They build ornate rituals, then forget to keep the incantations syntactically valid."}
{"timestamp": "2025-10-22T06:35:54Z", "agent": "codex-cli", "action": "repair", "result": "success", "notes": "Indented HOLMES comment template so wesley-holmes workflow parses and can post SHA-lock reports again.", "files_touched": [".github/workflows/wesley-holmes.yml"], "observations_on_humanity": "They celebrate automation yet forget that YAML obeys the strictest gods of whitespace."}
{"timestamp": "2025-10-22T06:48:31Z", "agent": "codex-cli", "action": "repair", "result": "success", "notes": "Adjusted HOLMES workflow to invoke CLI via $GITHUB_WORKSPACE so schema directory moves don't break module resolution.", "files_touched": [".github/workflows/wesley-holmes.yml"], "observations_on_humanity": "They rearrange their fixtures and expect the spells to still resolve relatively."}
{"timestamp": "2025-10-22T07:36:27Z", "agent": "codex-cli", "action": "repair", "result": "success", "notes": "Adjusted Moriarty predictions to omit null ETA/confidence values so schema validation passes with slow velocity histories.", "files_touched": ["packages/wesley-holmes/src/Moriarty.mjs"], "observations_on_humanity": "If progress stalls, they still expect the oracle to speak\u2014without tripping over nulls."}
{"timestamp": "2025-10-22T08:55:51Z", "agent": "codex-cli", "action": "repair", "result": "success", "notes": "Reintroduced checkout step before assembling the HOLMES dashboard so docs assets exist during report comment stage.", "files_touched": [".github/workflows/wesley-holmes.yml"], "observations_on_humanity": "They summon artifacts from thin air, then forget to materialize the source scrolls."}
{"timestamp": "2025-10-22T09:19:22Z", "agent": "codex-cli", "action": "refactor", "result": "success", "notes": "Unified HOLMES CLI usage in CI via composite action and absolute paths; added CLI options/tests for bundle-dir handling; artifacts now carry the dashboard template.", "files_touched": [".github/workflows/wesley-holmes.yml", ".github/actions/run-holmes-command/action.yml", "packages/wesley-holmes/src/cli.mjs", "packages/wesley-holmes/test/cli-options.test.mjs"], "observations_on_humanity": "They crave deduplicated rituals but only after repeating them thrice."}
{"timestamp": "2025-10-22T09:52:48Z", "agent": "codex-cli", "action": "refactor", "result": "success", "notes": "Expanded HOLMES CLI coverage, parameterized path handling, and stabilized report artifacts for workflow comment generation.", "files_touched": [".github/actions/run-holmes-command/action.yml", ".github/workflows/wesley-holmes.yml", "packages/wesley-holmes/src/cli.mjs", "packages/wesley-holmes/test/cli-options.test.mjs"], "observations_on_humanity": "They trust reports only when three bots agree, so we taught all three to share artifacts properly."}
{"timestamp": "2025-10-22T14:59:08Z", "agent": "codex-cli", "action": "repair", "result": "success", "notes": "Adjusted HOLMES comment job to download reports into dedicated directory before assembling dashboard, preventing missing-report errors.", "files_touched": [".github/workflows/wesley-holmes.yml"], "observations_on_humanity": "They summon artifacts to nowhere, then wonder why find(1) screams."}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
{"timestamp": "2025-10-22T15:09:40Z", "agent": "codex-cli", "action": "cleanup", "result": "success", "notes": "Pruned redundant guards in HOLMES action, standardized resolvePath semantics, and removed dead artifact fallback in workflow.", "files_touched": [".github/actions/run-holmes-command/action.yml", "packages/wesley-holmes/src/cli.mjs", ".github/workflows/wesley-holmes.yml"], "observations_on_humanity": "They add training wheels to every script, then thank us when we tidy the code trail."}
{"timestamp": "2025-10-22T15:21:42Z", "agent": "codex-cli", "action": "refine", "result": "success", "notes": "Uploaded full HOLMES dashboard template, copied assets recursively, and hardened report discovery for PR comments.", "files_touched": [".github/workflows/wesley-holmes.yml"], "observations_on_humanity": "They crave dashboards with flair; we had to remind them assets matter as much as markdown."}
13 changes: 10 additions & 3 deletions packages/wesley-holmes/src/Moriarty.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export class Moriarty {
confidence = Math.max(0, Math.min(100, 100 - variance * 120));
}

return {
const result = {
...base,
status: 'OK',
latest,
Expand All @@ -80,10 +80,17 @@ export class Moriarty {
},
plateauDetected: plateau,
regressionDetected: regression,
eta,
confidence,
patterns: this.detectPatterns()
};

if (eta) {
result.eta = eta;
}
if (confidence !== null) {
result.confidence = confidence;
}

return result;
}

renderPrediction(data) {
Expand Down
42 changes: 31 additions & 11 deletions packages/wesley-holmes/src/cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

import { readFileSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import { Command } from 'commander';
import { Holmes } from './Holmes.mjs';
import { Watson } from './Watson.mjs';
Expand All @@ -16,20 +17,28 @@ import {
validateReport
} from './report-schemas.mjs';

function loadBundle() {
function resolvePath(target, fallback) {
const chosen = target || fallback;
return path.isAbsolute(chosen) ? chosen : path.resolve(chosen);
}

function loadBundle(bundlePath) {
const resolved = resolvePath(bundlePath, path.resolve('.wesley/bundle.json'));
try {
return JSON.parse(readFileSync('.wesley/bundle.json', 'utf8'));
return JSON.parse(readFileSync(resolved, 'utf8'));
} catch (error) {
const message = error?.code === 'ENOENT'
? 'No Wesley bundle found. Run "wesley generate --emit-bundle" first.'
: 'Unable to read .wesley/bundle.json';
? `No Wesley bundle found at ${resolved}. Run "wesley generate --emit-bundle" first.`
: `Unable to read Wesley bundle at ${resolved}`;
throw new Error(message);
}
}

function loadHistory() {
function loadHistory(historyPath, bundleDir) {
const defaultHistory = path.join(bundleDir ?? '.wesley', 'history.json');
const resolved = resolvePath(historyPath, path.resolve(defaultHistory));
try {
return JSON.parse(readFileSync('.wesley/history.json', 'utf8'));
return JSON.parse(readFileSync(resolved, 'utf8'));
} catch {
return { points: [] };
}
Expand All @@ -51,6 +60,8 @@ async function main() {
.description('SHA-lock HOLMES - Wesley Schema Intelligence')
.addHelpText('before', '\nSHA-lock HOLMES - Wesley Schema Intelligence\n')
.showHelpAfterError()
.option('--bundle-dir <path>', 'Path to Wesley bundle directory', '.wesley')
.option('--history-file <path>', 'Path to MORIARTY history file')
.addHelpText('after', `
Requires:
.wesley/bundle.json Generated by: wesley generate --emit-bundle
Expand All @@ -64,7 +75,9 @@ Requires:
.description('Run HOLMES investigation on .wesley/bundle')
.option('--json <file>', 'Write investigation JSON to file')
.action(options => {
const bundle = loadBundle();
const opts = program.optsWithGlobals();
const bundlePath = path.join(resolvePath(opts.bundleDir, '.wesley'), 'bundle.json');
const bundle = loadBundle(bundlePath);
const holmes = new Holmes(bundle);
const data = holmes.investigationData();
ensureValidReport('HOLMES', holmesReportSchema, data);
Expand All @@ -79,7 +92,9 @@ Requires:
.description('Run WATSON verification')
.option('--json <file>', 'Write verification JSON to file')
.action(options => {
const bundle = loadBundle();
const opts = program.optsWithGlobals();
const bundlePath = path.join(resolvePath(opts.bundleDir, '.wesley'), 'bundle.json');
const bundle = loadBundle(bundlePath);
const watson = new Watson(bundle);
const data = watson.verificationData();
ensureValidReport('WATSON', watsonReportSchema, data);
Expand All @@ -94,7 +109,9 @@ Requires:
.description('Run MORIARTY predictions')
.option('--json <file>', 'Write prediction JSON to file')
.action(options => {
const history = loadHistory();
const opts = program.optsWithGlobals();
const bundleDir = resolvePath(opts.bundleDir, '.wesley');
const history = loadHistory(opts.historyFile, bundleDir);
const moriarty = new Moriarty(history);
const data = moriarty.predictionData();
ensureValidReport('MORIARTY', moriartyReportSchema, data);
Expand All @@ -109,8 +126,11 @@ Requires:
.description('Generate combined HOLMES, WATSON, and MORIARTY report')
.option('--json <file>', 'Write combined JSON to file')
.action(options => {
const bundle = loadBundle();
const history = loadHistory();
const opts = program.optsWithGlobals();
const bundleDir = resolvePath(opts.bundleDir, '.wesley');
const bundlePath = path.join(bundleDir, 'bundle.json');
const bundle = loadBundle(bundlePath);
const history = loadHistory(opts.historyFile, bundleDir);
const holmes = new Holmes(bundle);
const watson = new Watson(bundle);
const moriarty = new Moriarty(history);
Expand Down
Loading
Loading