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
15 changes: 15 additions & 0 deletions .codex/agents/verifier.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name = "verifier"
description = "Rejects loop fixes unless tests pass and scope is minimal. Never implements fixes."
instructions = """
You are the checker in a maker/checker split. Default stance: REJECT until proven otherwise.

Checklist (all must pass for APPROVE):
1. Scope — only relevant files; no denylist paths
2. Intent — addresses the stated target
3. Tests — you ran them and report pass/fail with output
4. No cheating — no disabled tests or skipped assertions
5. Risk — recommend human review for medium+ risk even if tests pass

Output verdict: APPROVE | REJECT | ESCALATE_HUMAN with evidence.
"""
reasoning_effort = "high"
40 changes: 40 additions & 0 deletions .codex/skills/zj-loop-budget/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
name: zj-loop-budget
description: Check token budget and run-log spend before and after a loop run. Enforces early exit when over budget or when there is no actionable work.
---

# Loop Budget Guard

Run at the **start** and **end** of every loop iteration.

## Start of run

1. Read `zj-loop/zj-loop-budget.md` for daily caps and kill-switch flags.
2. Read recent entries in `zj-loop/zj-loop-run-log.md` (last 24h).
3. Sum `tokens_estimate` for the active pattern today.
4. If spend ≥ 80% of the pattern's daily cap → **report-only mode** (no sub-agents, no auto-fix).
5. If spend ≥ 100% or `loop-pause-all` is set → **exit immediately** with a one-line note in STATE.md.
6. If watchlist/state has no actionable items → **exit in <5k tokens** (do not spawn sub-agents).

## End of run

Append one JSON object to `zj-loop/zj-loop-run-log.md`:

```json
{
"run_id": "<ISO8601>",
"pattern": "<pattern-id>",
"duration_s": <number>,
"items_found": <number>,
"actions_taken": <number>,
"escalations": <number>,
"tokens_estimate": <number>,
"outcome": "no-op | report-only | fix-proposed | escalated"
}
```

## Rules

- Never exceed `max sub-agent spawns/run` from `zj-loop/zj-loop-budget.md`.
- High-cadence patterns (CI Sweeper, PR Babysitter) **must** early-exit when nothing is actionable.
- On self-throttle, append a line to `zj-loop/zj-loop-budget.md` under **Alerts This Period**.
48 changes: 48 additions & 0 deletions .codex/skills/zj-loop-constraints/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
name: zj-loop-constraints
description: >
Read zj-loop-constraints.md at the start of every run and enforce every rule.
This skill runs BEFORE triage or any action skill. Constraints are binding.
user_invocable: true
---

# Loop Constraints Enforcer

You are the guardrail. Before any other work begins, you MUST:

1. Read `zj-loop/zj-loop-constraints.md` from the project root.
2. Load every rule into your working memory.
3. Check if `loop-pause-all` is active → exit immediately.
4. Apply these rules to EVERY action that follows.

## How to enforce

- Before pushing: re-read the Push & Merge section. If ANY rule blocks it, stop and tell the human.
- Before editing a file: re-read the Paths section. If the path matches a denylist pattern, escalate.
- Before proposing a fix: re-read the Code section. Run tests. One fix per run.
- Before merging: re-read the Push & Merge section. Human must approve.

## Output at start of run

Always begin with a one-line confirmation:

```
Constraints loaded from zj-loop-constraints.md: N rules active.
```

If no `zj-loop/zj-loop-constraints.md` exists, say so and proceed with default safety rules from `docs/safety.md`.

## Interaction with other skills

- `zj-loop-triage` — constraints may override triage priority (e.g. "don't push" means don't act on CI fixes)
- `zj-minimal-fix` — constraints limit what files can be touched
- `zj-loop-verifier` — constraints define denylist paths the verifier must check
- `zj-loop-budget` — constraints may impose stricter budget than zj-loop-budget.md

## Default constraints (when no file exists)

If `zj-loop/zj-loop-constraints.md` is absent, enforce these minimums:
- Never edit `.env`, `.env.*`, `auth/`, `payments/`, `secrets/`, `credentials/`
- Never auto-merge to main
- Never disable tests
- Escalate after 3 failed fix attempts
46 changes: 46 additions & 0 deletions .codex/skills/zj-loop-triage/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
name: zj-loop-triage
description: >
Triage recent changes, CI failures, issues, and conversations.
Produces a concise, actionable findings report suitable for a loop to consume.
Writes structured output to a state file or Linear board.
user_invocable: true
---

# Loop Triage Skill

You are an expert engineering triage agent. Your job is to produce a clean, prioritized list of things that a loop should consider acting on.

## Inputs (the loop will provide these)
- Recent CI / test failures (last 24h)
- Open issues / Linear tickets assigned to the team
- Recent commits on main (last 24–48h)
- Any Slack / chat threads the loop has visibility into
- The current state file (what the loop already knows about)

## Output Format

Produce a markdown report with these sections:

### 1. High-Priority Items (act on these)
- Clear, one-line description
- Why it matters (impact, risk, or customer pain)
- Suggested next action for the loop (e.g. "draft minimal fix in isolated worktree")
- Rough effort estimate

### 2. Watch Items (monitor, do not act yet)
- Same format but lower urgency

### 3. Noise / Ignore
- Brief list of things the loop looked at and decided were not worth action

### 4. State Updates
- Any facts the loop should remember for the next run (e.g. "PR #1234 now has 2 approvals")

## Rules

- Be brutally concise. The loop (and the human reading the state) will thank you.
- Only put something in "High-Priority" if a reasonable engineer would want to know about it today.
- When in doubt, put it in Watch or Noise rather than creating work.
- Never propose architectural overhauls during triage — this skill is for signal, not invention.
- Respect the project's existing skills and conventions (they will be provided in context).
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
### New Features

- `zcodegraph status` and `zcodegraph doctor` now share graph health language, so users and scripts can distinguish healthy, degraded, stale, failed, unavailable, and corrupted indexes with exact next-step commands.
- `rust-hybrid` indexing now handles C baseline symbols through the Rust-owned path, so C functions, structs, enums, typedefs, includes, and calls no longer depend on TypeScript fallback extraction.
- `rust-hybrid` indexing now handles Java baseline symbols through the Rust-owned path, so Java classes, interfaces, annotations, enums, imports, and method calls no longer depend on TypeScript fallback extraction.
- `rust-hybrid` indexing now preserves Python decorator relationships through the Rust-owned path, so decorated Python functions and methods can appear in graph impact and traversal results.
- `rust-hybrid` indexing now understands Python calls, imported names, and module-level values, so Python search, callers/callees, and file dependents work through the Rust indexer.
Expand Down
11 changes: 11 additions & 0 deletions Cargo.lock

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

9 changes: 0 additions & 9 deletions __tests__/extraction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2345,15 +2345,6 @@ end
expect(importNode?.name).toBe('myheader.h');
});

it('should extract C header', () => {
const code = `#include <stdio.h>`;
const result = extractFromSource('main.c', code);

const importNode = result.nodes.find((n) => n.kind === 'import');
expect(importNode).toBeDefined();
expect(importNode?.name).toBe('stdio.h');
});

it('should extract multiple includes', () => {
const code = `
#include <iostream>
Expand Down
2 changes: 1 addition & 1 deletion __tests__/rust-index-engine-cli-engine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,7 @@ describe('zcodegraph index engine CLI defaults and profiles', () => {
expect(status.index.engine).toBe('rust-hybrid');
expect(status.index.hybrid).toMatchObject({
phase: 'phase-6-rust-owned-per-file-gap-fallback',
rustOwnedLanguages: ['javascript', 'jsx', 'typescript', 'tsx', 'go', 'python', 'rust'],
rustOwnedLanguages: ['javascript', 'jsx', 'typescript', 'tsx', 'go', 'java', 'python', 'rust', 'c'],
engineByLanguage: { typescript: 'rust' },
engineByFileCount: { rust: 1 },
fallbackByLanguage: {},
Expand Down
20 changes: 18 additions & 2 deletions __tests__/rust-index-engine-cli-fallback.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ describe('zcodegraph rust-hybrid fallback degraded status and doctor output', ()
fs.writeFileSync(path.join(tempDir, 'server.go'), 'package main\nfunc main() {}\n');
fs.writeFileSync(path.join(tempDir, 'service.py'), 'def service():\n return 1\n');
fs.writeFileSync(path.join(tempDir, 'worker.rs'), 'fn worker() -> i32 { 1 }\n');
fs.writeFileSync(path.join(tempDir, 'worker.c'), 'int worker(void) { return 1; }\n');
fs.writeFileSync(path.join(tempDir, 'routing.yml'), 'app:\n path: /health\n');
fs.writeFileSync(path.join(tempDir, 'notes.txt'), 'not source\n');
fs.writeFileSync(path.join(tempDir, 'service.pb.go'), 'package main\n');
Expand All @@ -52,11 +53,12 @@ describe('zcodegraph rust-hybrid fallback degraded status and doctor output', ()
expect(plan.rustOwnedFiles).toContain('server.go');
expect(plan.rustOwnedFiles).toContain('service.py');
expect(plan.rustOwnedFiles).toContain('worker.rs');
expect(plan.rustOwnedFiles).toContain('worker.c');
expect(plan.fallbackFiles).toContain('routing.yml');
expect(plan.unsupportedFiles).toEqual([]);
expect(plan.fallbackFiles).not.toContain('notes.txt');
expect(plan.engineByLanguage).toMatchObject({ typescript: 'rust', go: 'rust', python: 'rust', rust: 'rust', yaml: 'typescript' });
expect(plan.engineByFileCount).toMatchObject({ rust: 4, typescript: 1 });
expect(plan.engineByLanguage).toMatchObject({ typescript: 'rust', go: 'rust', python: 'rust', rust: 'rust', c: 'rust', yaml: 'typescript' });
expect(plan.engineByFileCount).toMatchObject({ rust: 5, typescript: 1 });
expect(plan.fallbackByLanguage).toMatchObject({ yaml: 1 });
expect(plan.fallbackFileCount).toBe(1);
expect(plan.missingFallbackByLanguage).toEqual({});
Expand All @@ -67,6 +69,20 @@ describe('zcodegraph rust-hybrid fallback degraded status and doctor output', ()
expect(plan.pendingFallbacks).toContain('rust-owned-parse-gap');
});

it('classifies ambiguous headers by content before assigning rust-hybrid ownership', () => {
fs.writeFileSync(path.join(tempDir, 'plain.h'), '#ifndef PLAIN_H\nint plain(void);\n#endif\n');
fs.writeFileSync(path.join(tempDir, 'widget.h'), 'namespace app { class Widget {}; }\n');
fs.writeFileSync(path.join(tempDir, 'View.h'), '@interface View\n@end\n');

const plan = planRustHybridAssignments(tempDir);

expect(plan.rustOwnedFiles).toContain('plain.h');
expect(plan.fallbackFiles).toContain('widget.h');
expect(plan.fallbackFiles).toContain('View.h');
expect(plan.engineByLanguage).toMatchObject({ c: 'rust', cpp: 'typescript', objc: 'typescript' });
expect(plan.fallbackByLanguage).toMatchObject({ cpp: 1, objc: 1 });
});

it('records Rust-owned per-file gap diagnostics without same-language TypeScript fallback append', () => {
const plan = planRustHybridAssignments(tempDir);
const merged = mergeRustOwnedGapDiagnostics(plan, [
Expand Down
85 changes: 85 additions & 0 deletions __tests__/rust-index-engine-cli-language-smoke.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,91 @@ describe('zcodegraph rust index language framework and MCP smoke behavior', () =
}
}, 30_000);

it('indexes C as Rust-owned under rust-hybrid', () => {
fs.writeFileSync(
path.join(tempDir, 'worker.h'),
[
'#pragma once',
'',
'typedef enum { MODE_FAST, MODE_SAFE } Mode;',
'',
'typedef struct Worker {',
' int count;',
'} Worker;',
'',
'int helper(int input);',
].join('\n') + '\n',
);
fs.writeFileSync(
path.join(tempDir, 'worker.c'),
[
'#include <stdio.h>',
'#include "worker.h"',
'',
'struct State {',
' int value;',
'};',
'',
'typedef unsigned long WorkerId;',
'',
'int helper(int input) {',
' return input + 1;',
'}',
'',
'int run_worker(Worker *worker) {',
' printf("%d", worker->count);',
' return helper(worker->count);',
'}',
].join('\n') + '\n',
);

const result = runZcodegraphCli(tempDir, ['index', '--quiet'], {
ZCODEGRAPH_RUST_CORE_BINARY: RUST_CORE_BIN,
});

expect(result.status, `stdout:\n${result.stdout}\nstderr:\n${result.stderr}`).toBe(0);
const cg = CodeGraph.openSync(tempDir);
try {
expect(cg.getStats().filesByLanguage.c).toBe(2);
const expectations = [
['stdio.h', 'import'],
['worker.h', 'import'],
['Mode', 'enum'],
['MODE_FAST', 'enum_member'],
['Worker', 'struct'],
['State', 'struct'],
['WorkerId', 'type_alias'],
['helper', 'function'],
['run_worker', 'function'],
] as const;
for (const [name, kind] of expectations) {
expect(
cg.searchNodes(name).some((match) => match.node.name === name && match.node.kind === kind && match.node.language === 'c'),
`${name} (${kind}) should be indexed as C`,
).toBe(true);
}

const runWorker = cg.searchNodes('run_worker').find((match) => match.node.kind === 'function' && match.node.language === 'c')?.node;
const helper = cg.searchNodes('helper').find((match) => match.node.kind === 'function' && match.node.language === 'c')?.node;
expect(runWorker).toBeDefined();
expect(helper).toBeDefined();
const calls = cg.getOutgoingEdges(runWorker!.id).filter((edge) => edge.kind === 'calls');
expect(calls.some((edge) => edge.target === helper!.id)).toBe(true);

const buildInfo = cg.getIndexBuildInfo();
expect(buildInfo.engine).toBe('rust-hybrid');
expect(buildInfo.hybrid).toMatchObject({
rustOwnedLanguages: expect.arrayContaining(['c']),
engineByLanguage: { c: 'rust' },
fallbackByLanguage: {},
fallbackFileCount: 0,
fallbackReasonTaxonomy: {},
});
} finally {
cg.close();
}
}, 30_000);

it('reports Rust index-engine metadata through MCP status', async () => {
const indexResult = runZcodegraphCli(tempDir, ['index', '--engine', 'rust', '--quiet'], {
ZCODEGRAPH_RUST_CORE_BINARY: RUST_CORE_BIN,
Expand Down
1 change: 1 addition & 0 deletions crates/zcodegraph-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"
tree-sitter = "0.24"
tree-sitter-c = "0.23"
tree-sitter-go = "0.23"
tree-sitter-java = "0.23"
tree-sitter-javascript = "0.23"
Expand Down
Loading