Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "orgx-claude-code-plugin",
"version": "0.1.1",
"description": "OrgX MCP tools and runtime telemetry hooks for Claude Code.",
"version": "0.1.2",
"description": "OrgX MCP tools, runtime telemetry hooks, and Work Graph reconciliation for Claude Code.",
"author": {
"name": "OrgX Team",
"url": "https://useorgx.com"
Expand Down
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

Claude Code plugin package for OrgX:
- OrgX MCP server wiring (`mcp.useorgx.com`)
- Runtime hooks that post activity/progress back to OrgX
- Runtime hooks that post activity/progress back to OrgX and spool compact Work
Graph events for reconciliation
- Browser pairing login (`/orgx-login`) with macOS keychain storage
- Session env hydration from keychain (`hooks/scripts/load-orgx-env.mjs`)
- Skill-pack sync from OrgX to local `SKILL.md` files (`/orgx-sync-skills`)
Expand Down Expand Up @@ -45,6 +46,8 @@ skills/**/SKILL.md # Reusable guidance
- `ORGX_SKILLS_DIR` (optional skills root override; default `.claude/orgx-skills`)
- `ORGX_SKILL_PACK_NAME` (optional; default `orgx-agent-suite`)
- `ORGX_RUNTIME_HOOK_URL` and `ORGX_HOOK_TOKEN` (optional local runtime relay)
- `ORGX_WIZARD_HOOK_OUTBOX` (optional local JSONL outbox; default
`~/.config/useorgx/wizard/hooks/events.jsonl`)

## Login + Autopilot

Expand Down Expand Up @@ -100,8 +103,12 @@ claude --plugin-dir . --permission-mode bypassPermissions -p "Use the orgx_statu
- activity events -> `/api/client/live/activity`
- optional completion changeset -> `/api/client/live/changesets/apply`
- optional local runtime relay -> `ORGX_RUNTIME_HOOK_URL`
- compact, redacted Work Graph hook events -> local wizard outbox

The script is best-effort and exits cleanly on failures to avoid interrupting Claude sessions.
It never writes raw transcripts or full hook payloads; the reconciler should keep
raw client history local and promote only redacted summaries, evidence refs,
Work Graph fingerprints, and approved OrgX activity.

## Next Steps

Expand Down
132 changes: 129 additions & 3 deletions hooks/scripts/post-reporting-event.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
#!/usr/bin/env node

import { appendFileSync, mkdirSync } from "node:fs";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
import process from "node:process";
import { pathToFileURL } from "node:url";

Expand Down Expand Up @@ -36,6 +39,37 @@ export function normalizeSourceClient(value, fallback = "claude-code") {
return normalized;
}

async function readStdin() {
const chunks = [];
for await (const chunk of process.stdin) {
chunks.push(Buffer.from(chunk));
}
return Buffer.concat(chunks).toString("utf8");
}

export function parseJsonRecord(value) {
try {
const parsed = JSON.parse(value || "{}");
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
? parsed
: {};
} catch {
return {};
}
}

export function sanitizeArgs(args) {
const redacted = {};
for (const [key, value] of Object.entries(args ?? {})) {
if (/token|api[_-]?key|authorization|cookie|secret/i.test(key)) {
redacted[key] = "[redacted]";
} else {
redacted[key] = value;
}
}
return redacted;
}

async function postJson(url, payload, headers, fetchImpl = fetch) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 8000);
Expand Down Expand Up @@ -89,7 +123,7 @@ export function buildRuntimePayload({
message,
metadata: {
source: "hook_runtime_relay",
raw_args: args,
raw_args: sanitizeArgs(args),
},
timestamp: new Date().toISOString(),
};
Expand All @@ -116,7 +150,7 @@ export function buildActivityPayload({
metadata: {
source: "hook_backstop",
hook_event: event,
raw_args: args,
raw_args: sanitizeArgs(args),
},
};
}
Expand Down Expand Up @@ -145,13 +179,82 @@ export function buildCompletionChangesetPayload({
};
}

export function buildWorkGraphHookRecord({
args,
payload,
sourceClient,
event,
cwd = process.cwd(),
timestamp = new Date().toISOString(),
}) {
const toolName = pickString(
payload.tool_name,
payload.toolName,
payload.tool?.name,
payload.name
);
const prompt = pickString(payload.prompt, payload.user_prompt, payload.userPrompt);
const sessionId = pickString(
payload.session_id,
payload.sessionId,
payload.conversation_id,
payload.conversationId,
args.session_id,
args.sessionId
);

return {
schema_version: "2026-05-07",
source: "orgx_claude_code_plugin_runtime_hook",
source_client: sourceClient,
event,
session_id: sessionId,
turn_id: pickString(payload.turn_id, payload.turnId, args.turn_id, args.turnId),
cwd: pickString(
payload.cwd,
payload.working_directory,
payload.workspace,
args.cwd,
cwd
),
transcript_path: pickString(payload.transcript_path, payload.transcriptPath),
timestamp,
summary: {
tool_name: toolName,
prompt_chars: prompt ? prompt.length : undefined,
payload_keys: Object.keys(payload).slice(0, 40),
initiative_id: pickString(args.initiative, args.initiative_id),
workstream_id: pickString(args.workstream_id),
task_id: pickString(args.task_id),
run_id: pickString(args.run_id),
correlation_id: pickString(args.correlation_id),
},
};
}

export function appendWorkGraphHookRecord(record, outbox) {
try {
mkdirSync(dirname(outbox), { recursive: true, mode: 0o700 });
appendFileSync(outbox, `${JSON.stringify(record)}\n`, {
encoding: "utf8",
mode: 0o600,
});
return true;
} catch {
return false;
}
}

export async function main({
argv = process.argv.slice(2),
env = process.env,
fetchImpl = fetch,
now = () => Date.now(),
stdinText = "",
cwd = process.cwd(),
} = {}) {
const args = parseArgs(argv);
const stdinPayload = parseJsonRecord(stdinText);

const runtimeHookUrl = pickString(
args.runtime_hook_url,
Expand Down Expand Up @@ -186,6 +289,22 @@ export async function main({
const progressPctRaw = pickString(args.progress_pct, env.ORGX_PROGRESS_PCT);
const progressPct = progressPctRaw ? Number(progressPctRaw) : undefined;
const message = pickString(args.message, `Hook event: ${event}`);
const outbox = pickString(
args.outbox,
env.ORGX_WIZARD_HOOK_OUTBOX,
join(homedir(), ".config", "useorgx", "wizard", "hooks", "events.jsonl")
);
const workGraphSpooled = appendWorkGraphHookRecord(
buildWorkGraphHookRecord({
args,
payload: stdinPayload,
sourceClient,
event,
cwd,
timestamp: new Date(now()).toISOString(),
}),
outbox
);

let runtimePosted = false;
let runtimePostFailed = false;
Expand Down Expand Up @@ -222,6 +341,7 @@ export async function main({
return {
ok: true,
runtime_posted: runtimePosted,
work_graph_spooled: workGraphSpooled,
skipped: "missing_api_key",
...(runtimePostFailed ? { runtime_skipped: "runtime_post_failed" } : {}),
};
Expand All @@ -230,6 +350,7 @@ export async function main({
return {
ok: true,
runtime_posted: runtimePosted,
work_graph_spooled: workGraphSpooled,
skipped: "missing_initiative_id",
...(runtimePostFailed ? { runtime_skipped: "runtime_post_failed" } : {}),
};
Expand All @@ -256,6 +377,7 @@ export async function main({
return {
ok: true,
runtime_posted: runtimePosted,
work_graph_spooled: workGraphSpooled,
skipped: "activity_post_failed",
};
}
Expand All @@ -265,6 +387,7 @@ export async function main({
return {
ok: true,
runtime_posted: runtimePosted,
work_graph_spooled: workGraphSpooled,
activity_posted: true,
changeset_posted: false,
};
Expand All @@ -285,6 +408,7 @@ export async function main({
return {
ok: true,
runtime_posted: runtimePosted,
work_graph_spooled: workGraphSpooled,
activity_posted: true,
changeset_posted: false,
skipped: "changeset_post_failed",
Expand All @@ -294,13 +418,15 @@ export async function main({
return {
ok: true,
runtime_posted: runtimePosted,
work_graph_spooled: workGraphSpooled,
activity_posted: true,
changeset_posted: true,
};
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
main()
readStdin()
.then((stdinText) => main({ stdinText }))
.then(() => {
process.exit(0);
})
Expand Down
15 changes: 13 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@useorgx/claude-code-plugin",
"version": "0.1.1",
"description": "OrgX Claude Code plugin with MCP + runtime telemetry hooks + Sovereign Execution peer sidecar",
"version": "0.1.2",
"description": "OrgX Claude Code plugin with MCP, runtime telemetry hooks, Work Graph reconciliation, and Sovereign Execution peer sidecar",
"type": "module",
"private": true,
"bin": {
Expand Down
2 changes: 1 addition & 1 deletion plugin.manifest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"plugin_name": "@useorgx/claude-code-plugin",
"version": "0.1.1",
"version": "0.1.2",
"manifest_fingerprint": "",
"signature": "",
"capabilities": ["gateway:drive", "plugin:heartbeat"],
Expand Down
11 changes: 11 additions & 0 deletions scripts/verify-plugin.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,17 @@ for (const eventName of ["SessionStart", "PostToolUse", "SubagentStop", "Stop"])
if (!Array.isArray(hooks.hooks[eventName])) fail(`hooks.${eventName} must be an array`);
}

const hookScript = readFileSync(hookScriptPath, "utf8");
if (!hookScript.includes("orgx_claude_code_plugin_runtime_hook")) {
fail("hook script must emit orgx_claude_code_plugin_runtime_hook records");
}
if (!hookScript.includes("ORGX_WIZARD_HOOK_OUTBOX")) {
fail("hook script must support ORGX_WIZARD_HOOK_OUTBOX");
}
if (hookScript.includes("appendFileSync(outbox, stdinText")) {
fail("hook script must not persist raw hook stdin");
}

console.log("verify-plugin: ok");
console.log(`manifest: ${manifest.name}@${manifest.version}`);
console.log(`mcp server: ${orgxServer.url}`);
30 changes: 30 additions & 0 deletions skills/orgx-runtime-reporting/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@

Use this skill when a Claude Code session must report execution state to OrgX.

## Reporting contract

There are two reporting paths:

- **Active path:** call OrgX MCP tools or client APIs during the work when you
know the initiative, task, decision, blocker, or artifact context.
- **Passive backstop:** Claude Code runtime hooks record compact session events
into the local OrgX wizard outbox for later Work Graph reconciliation.

Do not treat hook presence as a substitute for intentional OrgX writes. Hooks
answer whether OrgX was used; MCP/API calls make the work durable while the
session is still fresh.

## Workflow

1. Resolve context IDs from env/args:
Expand All @@ -23,8 +36,25 @@ Use this skill when a Claude Code session must report execution state to OrgX.
5. On completion:
- mark task done using a changeset when task id is known

6. If no OrgX IDs are available:
- Continue the work, but make the final response easy for the hook reconciler to
classify: name decisions, artifacts, blockers, next actions, and verification.
- Do not claim OrgX was updated unless an MCP tool or API call actually
succeeded.

7. Preserve Work Graph continuity:
- When a Work Graph report is generated, include its `work_graph_fingerprint`
and `signup_hydration.hydration_key` in summaries or artifacts that are safe
to store.
- Treat the fingerprint as the durable claim key that lets OrgX hydrate
pre-signup audit value into a user's future workspace.
- Never derive the fingerprint from secrets or raw transcripts that would need
to leave the local machine.

## Quality Bar

- Never post empty or generic updates.
- Include IDs whenever available.
- Use `source_client=claude-code`.
- Preserve secrets: never emit tokens, cookies, API keys, or storage state into
activity, retro, hook summaries, or final reports.
Loading
Loading