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
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,17 @@ Build mode is the default unless explicitly switched to release mode.
- Re-check `git status -sb` only before commits, branch switches, or destructive operations.
- Batch related tasks (FE + BE + tests) when they share the same outcome.
- Use requested tools when possible; if blocked, use a reliable fallback and note it briefly.
- Solve-first behavior: attempt practical fixes and verification before asking for more user input.
- Prefer small, progressive commits for long-running work.
- Run targeted verification for touched files/surfaces only.
- Do not block on full-suite checks during iterative development.

### Problem-Solving-First Policy
- Default posture is persistence: try to solve the issue end-to-end in the current turn.
- When blocked, attempt at least one concrete fallback path before escalating.
- Escalate early only for high-risk actions or true external blockers (permissions, missing credentials, production-risk decisions).
- Do not stop at analysis if implementation is feasible; implement, verify, then report.

### Build-mode Verification
- Backend changes: run targeted tests for changed modules.
- Frontend changes: run focused desktop/mobile smoke checks for changed flows.
Expand Down
17 changes: 16 additions & 1 deletion hooks/scripts/post-reporting-event.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@ export function pickString(...values) {
return undefined;
}

export function normalizeSourceClient(value, fallback = "claude-code") {
const fallbackClient = pickString(fallback, "claude-code")?.toLowerCase() ?? "claude-code";
const raw = pickString(value);
if (!raw) return fallbackClient;

const normalized = raw.toLowerCase();
if (!/^[a-z][a-z0-9._-]{1,63}$/.test(normalized)) {
return fallbackClient;
}
return normalized;
}

async function postJson(url, payload, headers, fetchImpl = fetch) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 8000);
Expand Down Expand Up @@ -156,7 +168,10 @@ export async function main({
const initiativeId = pickString(args.initiative, env.ORGX_INITIATIVE_ID);
const apiKey = pickString(env.ORGX_API_KEY);

const sourceClient = pickString(args.source_client, env.ORGX_SOURCE_CLIENT, "claude-code");
const sourceClient = normalizeSourceClient(
pickString(args.source_client, env.ORGX_SOURCE_CLIENT),
"claude-code"
);
const runId = pickString(args.run_id, env.ORGX_RUN_ID);
const correlationId = runId
? undefined
Expand Down
41 changes: 41 additions & 0 deletions tests/post-reporting-event.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import assert from "node:assert/strict";
import {
parseArgs,
pickString,
normalizeSourceClient,
buildRuntimePayload,
buildActivityPayload,
buildCompletionChangesetPayload,
Expand All @@ -22,6 +23,13 @@ test("pickString returns first non-empty string", () => {
assert.equal(pickString("", " "), undefined);
});

test("normalizeSourceClient falls back for invalid values", () => {
assert.equal(normalizeSourceClient("claude-code"), "claude-code");
assert.equal(normalizeSourceClient("ORGX.CLI"), "orgx.cli");
assert.equal(normalizeSourceClient("5"), "claude-code");
assert.equal(normalizeSourceClient("bad source"), "claude-code");
});

test("payload builders shape expected OrgX fields", () => {
const runtime = buildRuntimePayload({
initiativeId: "init-1",
Expand Down Expand Up @@ -118,3 +126,36 @@ test("main posts activity and completion changeset", async () => {
assert.equal(calls[1].url, "https://www.useorgx.com/api/client/live/changesets/apply");
assert.equal(calls[0].init.headers.Authorization, "Bearer oxk_test");
});

test("main normalizes invalid ORGX_SOURCE_CLIENT env value", async () => {
const calls = [];
const fetchImpl = async (url, init) => {
calls.push({ url, init });
return {
ok: true,
status: 200,
statusText: "OK",
async json() {
return { ok: true };
},
async text() {
return "";
},
};
};

const result = await main({
argv: ["--event=post_tool_use", "--message=hello"],
env: {
ORGX_API_KEY: "oxk_test",
ORGX_INITIATIVE_ID: "init-1",
ORGX_SOURCE_CLIENT: "5",
ORGX_BASE_URL: "https://www.useorgx.com",
},
fetchImpl,
});

assert.equal(result.ok, true);
const payload = JSON.parse(calls[0].init.body);
assert.equal(payload.source_client, "claude-code");
});
52 changes: 51 additions & 1 deletion tests/run-claude-dispatch-job.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import { mkdirSync, mkdtempSync, utimesSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";

import { findMostRecentStateFile } from "../scripts/run-claude-dispatch-job.mjs";
import {
createReporter,
findMostRecentStateFile,
normalizeSourceClient,
} from "../scripts/run-claude-dispatch-job.mjs";

test("findMostRecentStateFile returns null when logs root has no state files", () => {
const root = mkdtempSync(join(tmpdir(), "orgx-dispatch-empty-"));
Expand All @@ -30,3 +34,49 @@ test("findMostRecentStateFile returns newest job-state.json by mtime", () => {

assert.equal(findMostRecentStateFile(root), newerState);
});

test("normalizeSourceClient falls back for invalid identifiers", () => {
assert.equal(normalizeSourceClient("claude-code"), "claude-code");
assert.equal(normalizeSourceClient("ORGX.Dispatch"), "orgx.dispatch");
assert.equal(normalizeSourceClient("5"), "claude-code");
assert.equal(normalizeSourceClient("bad source"), "claude-code");
});

test("createReporter keeps source_client after run_id is established", async () => {
const activityPayloads = [];
const client = {
async emitActivity(payload) {
activityPayloads.push(payload);
if (activityPayloads.length === 1) {
return { ok: true, run_id: "run-123" };
}
return { ok: true };
},
async applyChangeset() {
return { ok: true };
},
async updateEntity() {
return { ok: true };
},
};

const reporter = createReporter({
client,
initiativeId: "init-1",
sourceClient: "claude-code",
correlationId: "corr-1",
planPath: null,
planHash: "abc123",
jobId: "job-1",
dryRun: false,
});

await reporter.emit({ message: "first" });
await reporter.emit({ message: "second" });

assert.equal(activityPayloads.length, 2);
assert.equal(activityPayloads[0].source_client, "claude-code");
assert.equal(activityPayloads[0].correlation_id, "corr-1");
assert.equal(activityPayloads[1].source_client, "claude-code");
assert.equal(activityPayloads[1].run_id, "run-123");
});
Loading