Skip to content

feat(rust-port): phases 0-1 foundations — Bun-free gjc host binary slice - #3788

Draft
Yeachan-Heo wants to merge 10 commits into
devfrom
feat/full-conversion-to-rust
Draft

feat(rust-port): phases 0-1 foundations — Bun-free gjc host binary slice#3788
Yeachan-Heo wants to merge 10 commits into
devfrom
feat/full-conversion-to-rust

Conversation

@Yeachan-Heo

@Yeachan-Heo Yeachan-Heo commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Note

Draft until the Rust binary is daily-usable (roadmap phase 6 TUI parity, or earlier if the hybrid Rust-core + Bun-TUI-client path is taken after phases 2-3). Phases 0-1 here are foundation only: nothing in the user-facing runtime is routed through Rust yet.

Summary

First slice of the full conversion to a single Rust binary (see docs/roadmap/full-conversion-to-rust.md, added in this PR). The Rust gjc host binary boots, resolves config with byte-identical parity to the Bun CLI, and reads the existing session store losslessly. All changes are additive: the Bun runtime on dev is unaffected (NAPI export names/signatures unchanged; pi-natives is touched in exactly 4 files).

Commits → roadmap milestones

Commit Milestone
663d001f9 Phase 0: crates/gjc host binary skeleton (tokio + clap) + roadmap doc
2545063ed Phase 0: gjc-configenv-file.ts/dirs.ts ports (config-dir precedence, .env trust guards, XDG)
d346ddf12 Phase 0 gate: settings schema/loader port + read-only gjc config list|get|pathbyte-identical to the Bun CLI on a real ~/.gjc (schemas/settings-flat.json is drift-checked by generate-json-schemas.ts --check)
b0fe360dd Phase 0: gjc-session — managed v2 scope naming (TS digest parity vectors), binding files, loss-free JSONL transcript round-trips (validated on 36 real transcripts / 11,902 entries)
8067f7643 Phase 1: gjc-util (format/spawn-env/snowflake/mime) + gjc-stats (user-metrics with 15 TS parity vectors)
dcb160ed7 Phase 1: pi-natives-core established — NAPI-free core crate with glob_util/hashline/linediff migrated behind unchanged NAPI facades; cargo tree -p gjc contains zero napi; roadmap checkboxes updated
b79dd5e9d Style: new crates formatted with the pinned nightly rustfmt

Verification

  • cargo test for all new crates: 58 tests passing (incl. 16 in pi-natives-core)
  • cargo clippy --all-targets on every new/touched crate: zero warnings under the strict workspace lints
  • cargo check -p pi-natives: green (NAPI facade intact)
  • pinned-nightly rustfmt --check on all new/touched files: clean
  • bun scripts/generate-json-schemas.ts --check: generated schemas in sync
  • Parity harness: diff <(gjc config list --json) <(target/debug/gjc config list --json) plus plain/get/secret-redacted/path variants — all byte-identical
  • TS-generated parity vectors checked in for: managed-scope digests, hashline formatting, user-metrics

Deliberate deferrals (documented in crate lib.rs docs + roadmap)

  • Settings write path, config migrations, imported-agent config providers
  • Session write path / managed-scope security authority
  • SQLite-backed stats storage; ai-typed stats parser/aggregator
  • Remaining pi-natives module migrations (module-by-module under the established core/facade pattern)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 639953f3bf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +113 to +115
pub fn load(dirs: &Dirs) -> Self {
let global = parse_yaml_object(&dirs.settings_path());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reconcile global settings before exposing them

When a current global config.yml contains a supported scalar coercion such as images.autoResize: "false" or a numeric setting written as a string, the Bun loader runs reconcileSettingsSchema and returns the corresponding boolean or number, whereas this loader stores the raw YAML string. Consequently gjc config get/list is not byte-identical for these valid legacy-compatible inputs, and future Rust consumers receive a value of the wrong type. Apply the schema reconciliation/coercion step to the global layer before merging it.

Useful? React with 👍 / 👎.

Comment on lines +557 to +559
// TS counts UTF-16 code units; chars here counts Unicode scalars, which
// agrees for all BMP text (astral characters differ by design).
let chars = trimmed.chars().count();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve UTF-16 character counts in stats

For any message containing astral characters such as emoji, chars().count() counts each Unicode scalar once while the TypeScript metric uses trimmed.length and counts its two UTF-16 code units. This silently changes persisted/aggregated user-message statistics during the Rust migration despite the crate's parity contract; for example, "a😀" reports 2 instead of 3. Count encode_utf16() units and add an astral-character parity vector.

Useful? React with 👍 / 👎.

@Yeachan-Heo
Yeachan-Heo force-pushed the feat/full-conversion-to-rust branch from 639953f to b79dd5e Compare August 3, 2026 08:41

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8cc69e93d3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

for (index, line) in lines.enumerate() {
let entry: SessionEntry = serde_json::from_str(line)
.with_context(|| format!("invalid transcript entry at line {}", index + 2))?;
entries.push(entry);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Replay append-only patch records

For v4/v5 transcripts containing header_patch or entry_patch records, this loop exposes the patches as ordinary SessionEntry values without applying them. The Bun read path applies valid header patches to the current title/cwd and entry patches to the referenced message, so Rust consumers would instead see stale session metadata or message content plus pseudo-entries in the history. Replay these records into the logical header/entries view while retaining raw records separately if lossless serialization is required.

Useful? React with 👍 / 👎.

Comment on lines +78 to +79
let entry: SessionEntry = serde_json::from_str(line)
.with_context(|| format!("invalid transcript entry at line {}", index + 2))?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve lenient JSONL recovery

When an interrupted append leaves a truncated record, or any non-header line is otherwise malformed, the ? aborts parsing of the entire transcript. The existing Bun session reader deliberately skips malformed JSONL records and continues with later valid lines, so this turns a recoverable persisted session into an unreadable one during the Rust migration. Match the lenient per-line behavior after validating the header.

Useful? React with 👍 / 👎.

pub struct SessionHeader {
#[serde(rename = "type")]
pub kind: String,
pub version: u32,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Accept versionless v1 session headers

For legacy v1 transcripts, version is intentionally absent, but requiring a u32 makes Serde reject the header before the session can be read. The current session loader treats a missing version as v1 and migrates it in memory, so users with older retained sessions would lose access after this reader takes over. Represent the field as optional and interpret absence as version 1.

Useful? React with 👍 / 👎.

Yeachan-Heo added 9 commits August 3, 2026 18:09
Phase 0 of docs/roadmap/full-conversion-to-rust.md: clap+tokio host
binary with stubbed run/acp/config commands. Subsystems are ported from
TypeScript per the roadmap; browser, background jobs and TS plugins run
as subprocesses.
…jc-config

Faithful ports of packages/utils/src/env-file.ts and dirs.ts:
- dotenv/shell-env parsing with the same identifier and safety rules
- GJC_CONFIG_DIR/PI_CONFIG_DIR resolution with .env trust guard and
  '..'-segment sanitization
- agent-dir override and XDG data/state/cache redirection semantics
- explicit DirsInput (home/cwd/env) so parity tests can replay any
  environment deterministically
…onfig CLI

- scripts/generate-json-schemas.ts gains a third drift-checked output,
  schemas/settings-flat.json: the SETTINGS_SCHEMA table flattened in
  definition order with type/default/enum values/ui description+tab
- gjc-config embeds the flat table (settings_schema), ports the read
  path of settings.ts (deepMerge, project notification stripping,
  builtin .gjc/settings.json + config.yml project layering, schema
  default fallback) and the secret-redaction rules from config-cli.ts
- crates/gjc wires 'gjc config list|get|path' (read-only; set/reset/
  doctor stay Bun-side for now)

Parity gate: 'gjc config list --json', plain list, get, secret-redacted
get, and path are byte-identical between the Bun CLI and the Rust
binary against a real ~/.gjc.
Read-only ports from packages/coding-agent/src/session:
- scope: managed v2 directory naming (scopeDigest sha256+base32 with
  TS-reference parity vectors), binding-file parsing with unknown-field
  preservation, scope listing
- transcript: JSONL header/entry parsing with loss-free round-trips
  (unknown fields kept via serde flatten; explicit parentId null
  preserved), transcript listing newest-first

Verified against a live ~/.gjc store: 36 transcripts / 11902 entries
round-trip with semantic equality. Golden fixture + smoke tests cover
the synthetic path in CI.
…-stats

gjc-util (ports of packages/utils leaf modules with unit tests):
- format.ts: duration/number/bytes/age/percent formatting with the same
  truncate-below-boundary and clamp semantics, pluralization rules
- spawn-env.ts: env name/value safety and malloc-logging filtering
- snowflake.ts: 22-bit-sequence hex snowflakes (format/parse/bounds)
- mime.ts: PNG/JPEG/GIF/WebP header metadata sniffing

gjc-stats:
- user-metrics.ts: full behavioral-metrics port (profanity/anguish/
  negation/repetition/blame/yelling) with the JS lookbehind and
  backreference constructs rewritten for the regex crate; validated
  against 15 parity vectors generated from the TS implementation
  (checked into tests/fixtures)

Skipped modules and rationale are documented in each crate's lib.rs
(runtime-coupled layers, SQLite storage, ai-typed parser/aggregator).
…es-core

Establishes the roadmap phase 1 pattern: algorithmic cores move to the
new NAPI-free pi-natives-core crate while pi-natives keeps thin facades
with unchanged NAPI export names and signatures (Bun runtime on dev is
unaffected; cargo check -p pi-natives stays green).

Migrated in this step: glob_util (string errors in core, NAPI error
conversion in the facade), hashline (UTF-16 core with byte-identical
TS parity vectors from hashline/hash.ts; JsString/Utf16String interop
stays in the facade), linediff (jsdiff-parity algorithm in core, NAPI
object re-wrap in the facade).

crates/gjc now depends on pi-natives-core; cargo tree confirms napi is
absent from both the gjc and pi-natives-core dependency trees. The
remaining modules migrate module-by-module (tracked in the ultragoal
ledger and roadmap).
The repo's rustfmt.toml relies on nightly-only options (hard tabs,
alignment thresholds, format_strings); the new crates were initially
formatted with a stable rustfmt. Reformat with the rust-toolchain
pinned nightly so cargo fmt --check passes in CI. No behavior change;
the gjc-session smoke test's binding literal is rebuilt with json! to
survive format_strings.
@Yeachan-Heo
Yeachan-Heo force-pushed the feat/full-conversion-to-rust branch from 8cc69e9 to 26e873c Compare August 3, 2026 09:11
@Yeachan-Heo
Yeachan-Heo marked this pull request as draft August 3, 2026 09:13
…-scope allowlist

The Dev CI affected-path validator fails closed when the Cargo
workspace contains unclassified members; register gjc, gjc-config,
gjc-session, gjc-stats, gjc-util, and pi-natives-core (none are NAPI
addon sources) and add their rust-scope rationales.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 26e873c190

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Ok(Self {
home,
cwd: std::env::current_dir()?,
env: std::env::vars().collect(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid panicking on non-UTF-8 environment entries

On Unix, an unrelated environment variable may contain bytes that are not valid UTF-8, and std::env::vars() panics when it encounters one. Because DirsInput::from_process() runs before every implemented config action, such an entry prevents the Rust CLI from starting even though none of the relevant GJC/XDG variables are affected; iterate with vars_os() and skip or explicitly decode unusable entries instead.

Useful? React with 👍 / 👎.

Comment on lines +107 to +109
let rest = trimmed
.strip_prefix("export ")
.map_or(trimmed, str::trim_start);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Accept arbitrary whitespace after export

For shell startup lines such as export OPENAI_API_KEY=... or export\tOPENAI_API_KEY=..., the TypeScript parser's export\s+ prefix accepts the assignment, while stripping only the literal "export " leaves leading whitespace or export\t in the Rust key and causes validation to discard it. This loses credentials and other environment settings for valid, commonly formatted shell assignments; parse the optional export token followed by one or more whitespace characters.

Useful? React with 👍 / 👎.

format!("{}B", js_round(n as f64 / 1_000_000_000.0))
}

/// Format with up to 1 decimal place, dropping a trailing `.0`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Match JavaScript rounding for compact numbers

Rust's fixed-precision formatter uses round-half-even, whereas the TypeScript implementation uses toFixed(1). At exact ties this changes observable output—for example, format_number(1_250) produces 1.2K here but formatNumber(1_250) produces 1.3K—breaking the stated formatting parity for token, request, and download counts; implement the JavaScript rounding behavior before formatting.

Useful? React with 👍 / 👎.

if char_count <= max_len {
return s.to_owned();
}
let slice_len = max_len.saturating_sub(ellipsis.chars().count());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve UTF-16 limits when truncating strings

When the input contains astral characters, iterating Rust chars no longer matches JavaScript's UTF-16 length and slice contract. For example, truncating "😀abc" to 3 with "…" returns "😀a…" here but "😀…" in TypeScript, so the result can exceed the original length limit and change user-visible previews; perform the limit calculation and cut in UTF-16 code units.

Useful? React with 👍 / 👎.

@Yeachan-Heo Yeachan-Heo left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REQUEST_CHANGES — signed owner-batch hostile review

Exact binding

  • PR head: 2adb118587e19afec633f7feffdf9a8dc832e3c7 (feat/full-conversion-to-rust)
  • PR-recorded base / merge base: 38e026c785968e722e5b3a1b8025cadfc54c8c84
  • Current target dev: 732856b3ccb3fade6e9fbc17908a4fbca5a7682f
  • Divergence from the recorded base: current dev +22 commits; PR +10 commits
  • Shape: draft, 45 files, +6,376 / -545, mergeable=false, mergeable_state=dirty
  • Concrete merge conflict: packages/coding-agent/src/internal-urls/docs-index.generated.ts; rebase must retain both live-dev docs and the Rust roadmap, then regenerate the full docs index.

Exact CI

  • PR-head checks on 2adb1185: 32 success / 6 skipped / 0 failure (run 30801035984). These are exact-head but not current-base evidence; the PR is now dirty against live dev.
  • Current dev checks on 732856b3: 23 success / 5 skipped / 3 failure (run 30935832940). The primary failure is test:@gajae-code/coding-agent:shard-8-of-8 at sdk-operation-inventory.test.ts:66 (Expected: 0, Received: 1); the other two failures are aggregate/evidence jobs. This failure does not overlap the PR's changed paths, but there is no fresh exact-head/current-base green run.

Contributor ledger

  • Author association: OWNER; same-repository head.
  • All 10 current commits are authored as Yeachan-Heo <yeachan-heo@gajae.dev>.
  • No co-author trailers and no external contributor credit to preserve.
  • All 10 commits are currently reported unsigned; recorded as ledger provenance, not used as a standalone blocker.
  • The PR body's commit table names pre-rewrite SHAs (663d001f9, 2545063ed, …), not the current 10-commit chain (01ba7052…2adb1185), so the published milestone ledger is stale.

Hostile review verdict

Six independent GPT-heavy read-only lanes all returned REQUEST_CHANGES. The nine existing automated review comments were independently confirmed; none has a reply or disposition.

Blocking findings:

  1. Config trust boundary — HIGH

    • crates/gjc-config/src/dirs.rs:80-101: POSIX sanitization accepts ..\\outside, then the custom join splits backslashes and escapes the home-relative config root.
    • crates/gjc-config/src/env_file.rs:66-70: any unrelated invalid UTF-8 byte makes .env parse as empty, allowing a preloaded project override to bypass the equality-based trust guard.
    • Existing findings are confirmed: raw global settings skip schema reconciliation; std::env::vars() can panic on unrelated non-UTF-8 entries; shell parsing rejects valid multiple-space/tab export forms.
  2. Session compatibility — HIGH

    • crates/gjc-session/src/transcript.rs:69-82: v4/v5 header_patch and entry_patch records are exposed raw rather than replayed.
    • crates/gjc-session/src/transcript.rs:70-80: one malformed append record aborts the whole transcript instead of preserving lenient recovery.
    • crates/gjc-session/src/transcript.rs:21-34: valid versionless v1 headers are rejected while unsupported future versions are accepted without a version gate.
    • Additional parity drift: discovery trusts filename identity and creation timestamp ordering rather than validated headers and file mtime.
  3. Formatting/stats parity — MEDIUM

    • Existing findings are confirmed at crates/gjc-util/src/format.rs:52-73,115-122 and crates/gjc-stats/src/user_metrics.rs:557-560: JS decimal rounding and UTF-16 length/count contracts are not preserved.
    • Additional classifier drift uses Rust Unicode word/whitespace semantics where the TypeScript baseline uses ECMAScript semantics.
  4. CLI/roadmap contract — HIGH

    • docs/roadmap/full-conversion-to-rust.md:53-54 checks off byte-identical config parity, but crates/gjc/src/cli.rs/config_cmd.rs change bare-config defaults, missing-key behavior, exit codes, and TTY scope.
    • The host claims subprocess ownership while run/acp are stubs, and -p/--prompt is accepted but never consumed.
  5. Admission/generator gates — HIGH

    • scripts/check-rust-scope.ts only discovers crates/*/Cargo.toml while accepting every Rust descendant of an allowlisted root; a nested standalone crate bypasses package/path admission.
    • The new general product/pure-logic allowlist entries contradict the file's stated native/OS/measured-hot-path policy without an explicit policy replacement.
    • schemas/settings-flat.json is Rust-consumed, but generator/artifact-only changes are not mapped to a Rust consumer test.

Required before re-sign

  • Rebase onto live dev, resolve the generated docs-index conflict by regeneration, and refresh the PR body/commit ledger.
  • Fix or explicitly rebut every existing review comment and each HIGH finding above; silent drops are not accepted.
  • Make roadmap/CLI claims match observable behavior.
  • Close the nested-crate admission hole and settle the Rust-scope policy change explicitly.
  • Obtain fresh terminal CI bound to the new exact head against the then-current base, followed by another signed hostile review.

VERDICT: REQUEST_CHANGES

— Signed: GJC owner batch / GPT-heavy hostile review, 2026-08-05

@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Triage status: LEFT OPEN — not mergeable in current form. This is the healthiest of the open drafts (green exact-head CI, additive, phased roadmap), but it is dirty against dev and carries unrebutted HIGH findings; it stays open pending the requested fixes.

Verified state (head 2adb1185, last moved 2026-08-03 18:20 KST; PR created 2026-08-03; base dev):

  • Exact-head CI run 30801035984 is green (32 pass / 6 skipped / 0 fail), but it is stale-base evidence: the PR is dirty against live dev with a real conflict in packages/coding-agent/src/internal-urls/docs-index.generated.ts (merge-tree confirms; must be resolved by regeneration that retains both live-dev docs and the Rust roadmap).
  • Review HIGHs reproduce in the head, verified against the code:
    • gjc-config/src/dirs.rs: sanitize_config_dir_name accepts ..\outside on POSIX (Path::components treats backslash as a normal char), and join_under then splits on \ and pushes .. — escaping the home-relative config root. Also splits on backslash where Node path.join would not.
    • gjc-config/src/env_file.rs: invalid-UTF-8 content parses as an empty map, so a preloaded project .env override bypasses the equality-based trust guard.
    • gjc-session/src/transcript.rs: version is required (valid versionless v1 headers rejected) with no future-version gate; one malformed append line aborts the whole parse instead of lenient recovery; v4/v5 header_patch/entry_patch records are kept raw rather than replayed.
    • CLI/roadmap claims vs behavior (bare-config defaults, exit codes, TTY scope; run/acp stubs; -p/--prompt accepted but unconsumed) and the check-rust-scope.ts nested-crate admission hole also stand unrebutted.
    • The body's commit→milestone table names pre-rewrite SHAs (663d001f9, 2545063ed, …) that are not in the current 10-commit chain (01ba7052…2adb1185).
  • None of the nine automated review comments nor the signed verdict findings have replies or fixes; all 10 commits are unsigned (recorded, not used as a standalone blocker).

Remaining plan requested (please reply with the intended next steps): (1) rebase onto live dev and regenerate the docs index; (2) fix or explicitly rebut every review finding and each HIGH — config trust boundary, session compatibility (patch replay, lenient recovery, version gating), stats/format parity, CLI/roadmap contract, admission gates; (3) refresh the body's commit ledger; (4) obtain fresh exact-head CI against the then-current base, then a new review. Keep the draft until phase 6 TUI parity or the hybrid-core decision, as the roadmap states — the foundation itself is sound and additive.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant