Skip to content

feat(#271): add add pack / add feature scaffold subcommand - #485

Merged
apotema merged 2 commits into
mainfrom
packs/add-scaffold
Jul 1, 2026
Merged

feat(#271): add add pack / add feature scaffold subcommand#485
apotema merged 2 commits into
mainfrom
packs/add-scaffold

Conversation

@apotema

@apotema apotema commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Part of the Packs initiative (umbrella labelle-toolkit/labelle-engine#651, RFC-packs §7). This is the assembler half of labelle-cli#271 — it owns the file templating; the CLI PR (labelle-toolkit/labelle-cli#272) is the thin forwarder.

What

A new add subcommand mirroring the init split:

labelle-assembler add pack <name>
labelle-assembler add feature <kind> <name>     # kind: need | role | status

add pack <name>

Creates packs/<name>/ with the convention subdirs (components/ events/ scripts/ prefabs/ hooks/, each with a .gitkeep) and a pack.labelle:

.{
    .name = "<name>",
    .manifest_version = 1,
    .convention_dirs = .copy_and_scan,
}

Refuses if packs/<name>/ already exists. The manifest parses cleanly through plugin_manifest.loadPackFromDir (asserted in a test).

add feature <kind> <name>

Scaffolds a feature-unit in the game root — a components/<name>.zig (Saveable component) + a scripts/playing/xx_<name>.zig stub — per the RFC "feature = events + component + script" shape:

  • needvalue in [0,1] (.saveable) + a decay script that flags yellow/red threshold crossings and shows the exact need_threshold_crossed emit as a TODO (the event lives once in the citizens pack, RFC §6).
  • role — a role marker (.saveable) + a per-frame behavior stub.
  • status — a transient status flag (.transient) + an overlay-driver stub.

<kind> is a hardcoded set behind an explicit arg; unknown kinds are rejected with the valid set. <name> must be a lowercase identifier (derived to a PascalCase type). Both files use exclusive writes — refuses to overwrite.

Templates are deliberately minimal and correct: per RFC §7 they double as the "recipe source" the pack manifest (#442) will surface, and every generated file passes zig ast-check.

Bumps PROTOCOL_VERSION 3 → 4.

Verified

  • zig build + zig build test green (new tests: pack tree + parseable manifest, feature need component+script, status transient policy, name validation, PascalCase derivation, overwrite refusal).
  • Ran the built binary end-to-end: add pack, add feature {need,role,status}, and all error paths (existing dir → 1, unknown kind → 2, bad name → 2, existing file → 1). Every generated .zig passes zig ast-check.

Part of labelle-toolkit/labelle-engine#651.

Summary by CodeRabbit

  • New Features

    • Added a new add CLI command to scaffold packs and feature components.
    • Automatically creates the standard pack folder structure and starter files, including manifest setup.
    • Generates feature scaffolds with the correct templates based on feature kind.
  • Documentation

    • Updated --help output to include add pack <name> and add feature <kind> <name>.
  • Bug Fixes

    • Prevents overwriting existing packs or scaffold targets, with preflight checks to keep operations all-or-nothing.
  • Tests

    • Added coverage for name validation, scaffolding behavior, and exclusive file creation rules.

Scaffolds the two authoring units the Packs RFC (§7) defines:

  labelle-assembler add pack <name>
  labelle-assembler add feature <kind> <name>   (kind: need|role|status)

`add pack` creates packs/<name>/ with the convention subdirs
(components/ events/ scripts/ prefabs/ hooks/, each with a .gitkeep) and a
`pack.labelle` (`.name`, `.manifest_version = 1`, scalar
`.convention_dirs = .copy_and_scan`). Refuses an existing dir.

`add feature <kind> <name>` scaffolds a feature-unit in the game root: a
`components/<name>.zig` (Saveable component) plus a
`scripts/playing/xx_<name>.zig` stub. Per kind:
  need   — value in [0,1] + a decay script that flags threshold crossings
           and shows the standard need_threshold_crossed emit (TODO)
  role   — role marker + per-frame behavior stub
  status — transient status flag + overlay-driver stub
Templates are minimal, AST-check-clean, and double as the recipe source
the pack manifest (#442) will surface. Refuses to overwrite either file.

Follows the `init` split: the CLI parses `add ...` and forwards it to this
subcommand, which owns the templating. Bumps PROTOCOL_VERSION to 4.

Part of #651.
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a new add subcommand with pack and feature scaffolding flows, argument validation, name-to-type conversion, exclusive file creation, and pack auto-registration. src/main.zig now wires the command in, updates help text, and bumps the subprocess protocol version.

Changes

Add Subcommand Implementation

Layer / File(s) Summary
CLI entrypoint and argument parsing
src/add_cmd.zig
Adds usage text, cmdAdd, and argument validation for pack and feature, including name checks, trailing-argument rejection, and dispatch to scaffolding functions.
Name conversion helper
src/add_cmd.zig
Adds toTypeName for converting snake_case feature names into PascalCase Zig type names, including digit-leading prefix handling.
Pack scaffolding
src/add_cmd.zig
Implements scaffoldPack and project registration updates, creating pack directories and manifests, rewriting .plugins when possible, and falling back to a registration hint when needed.
Feature scaffolding and templates
src/add_cmd.zig
Implements scaffoldFeature, preflight checks, template writers, and exclusive-write helpers for generated component and running-script files.
Binary wiring and protocol bump
src/main.zig
Imports add_cmd, bumps PROTOCOL_VERSION to 4, extends the help text, dispatches the new add branch, and includes add_cmd.zig in the test build.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Poem

A rabbit hops with a brand-new add,
Packs and features, neatly clad.
Names stay lowercase, types rise bright,
Files appear with exclusive might.
Hop, hop—PROTOCOL_VERSION grew,
And tests now nibble the whole thing through 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly states the new scaffold subcommand and matches the main change set.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch packs/add-scaffold

Comment @coderabbitai help to get the list of available commands.

@apotema

apotema commented Jul 1, 2026

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces the add subcommand to the labelle-assembler binary, allowing users to scaffold packs and feature-units (needs, roles, and statuses) with pre-defined templates. The protocol version has been bumped to 4, and the new subcommand is integrated into the main entry point. Feedback on the changes suggests improving the robustness of the toTypeName helper by handling digit-prefixed inputs to ensure valid Zig identifiers, and refactoring the writeExclusiveData helper to return errors instead of calling std.process.exit(1) directly to enhance testability.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/add_cmd.zig
Comment on lines +146 to +163
pub fn toTypeName(allocator: std.mem.Allocator, name: []const u8) ![]u8 {
var out: std.ArrayList(u8) = .empty;
errdefer out.deinit(allocator);
var at_word_start = true;
for (name) |c| {
if (c == '_') {
at_word_start = true;
continue;
}
if (at_word_start and c >= 'a' and c <= 'z') {
try out.append(allocator, c - ('a' - 'A'));
} else {
try out.append(allocator, c);
}
at_word_start = false;
}
return out.toOwnedSlice(allocator);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

To ensure robustness and compliance with the general rules, toTypeName should handle cases where the input string starts with a digit by prefixing the resulting Zig identifier with an underscore. Since Zig identifiers cannot start with a digit, this prevents generating invalid code if the function is called with such inputs in other contexts.

pub fn toTypeName(allocator: std.mem.Allocator, name: []const u8) ![]u8 {
    var out: std.ArrayList(u8) = .empty;
    errdefer out.deinit(allocator);
    if (name.len > 0 and name[0] >= '0' and name[0] <= '9') {
        try out.append(allocator, '_');
    }
    var at_word_start = true;
    for (name) |c| {
        if (c == '_') {
            at_word_start = true;
            continue;
        }
        if (at_word_start and c >= 'a' and c <= 'z') {
            try out.append(allocator, c - ('a' - 'A'));
        } else {
            try out.append(allocator, c);
        }
        at_word_start = false;
    }
    return out.toOwnedSlice(allocator);
}
References
  1. Zig identifiers must not start with a digit. When converting strings (such as file paths) to Zig identifiers, prefix the result with an underscore if the input starts with a digit to ensure the generated code is valid.

Comment thread src/add_cmd.zig Outdated
Comment on lines +473 to +488
fn writeExclusiveData(io: std.Io, cwd: std.Io.Dir, path: []const u8, data: []const u8) void {
cwd.writeFile(io, .{
.sub_path = path,
.data = data,
.flags = .{ .exclusive = true },
}) catch |err| switch (err) {
error.PathAlreadyExists => {
std.log.err("labelle-assembler add: '{s}' already exists — refusing to overwrite", .{path});
std.process.exit(1);
},
else => {
std.log.err("labelle-assembler add: could not write '{s}': {s}", .{ path, @errorName(err) });
std.process.exit(1);
},
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Calling std.process.exit(1) directly inside helper functions like writeExclusiveData limits their reusability and testability. Returning standard Zig errors (e.g., propagating error.PathAlreadyExists) allows the caller to handle the exit logic, and enables unit tests to directly assert error conditions without aborting the test runner.

fn writeExclusiveData(io: std.Io, cwd: std.Io.Dir, path: []const u8, data: []const u8) !void {
    cwd.writeFile(io, .{
        .sub_path = path,
        .data = data,
        .flags = .{ .exclusive = true },
    }) catch |err| switch (err) {
        error.PathAlreadyExists => {
            std.log.err("labelle-assembler add: \'{s}\' already exists — refusing to overwrite", .{path});
            return err;
        },
        else => {
            std.log.err("labelle-assembler add: could not write \'{s}\": {s}", .{ path, @errorName(err) });
            return err;
        },
    };
}

@gemini-code-assist

Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/add_cmd.zig`:
- Around line 240-245: scaffoldFeature is swallowing createDirPath failures for
components_dir and playing_dir, which hides the real cause and lets later writes
fail with misleading errors. Update the directory creation flow in
scaffoldFeature to handle these failures the same way scaffoldPack does:
preserve the error from cwd.createDirPath, emit a clear diagnostic with the
actual path/error, and stop execution instead of continuing into
writeExclusiveData.
- Around line 86-92: The unknown FeatureKind branch in cmdAdd currently logs an
error and exits without showing the add_usage hint like the other parse-error
paths. Update the unknown-kind handling in add_cmd.zig around
std.meta.stringToEnum(FeatureKind, kind_str) to also write the add_usage block
to stderr before exiting, matching the behavior used for missing
target/pack/feature and unexpected argument errors.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5da428a3-5c55-44a3-963e-acc742f78723

📥 Commits

Reviewing files that changed from the base of the PR and between b2f0b8a and 9df8b1a.

📒 Files selected for processing (2)
  • src/add_cmd.zig
  • src/main.zig

Comment thread src/add_cmd.zig
Comment thread src/add_cmd.zig Outdated

@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: 9df8b1abcd

ℹ️ 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 thread src/add_cmd.zig Outdated
defer allocator.free(file);
const path = try std.fs.path.join(allocator, &.{ components_dir, file });
defer allocator.free(path);
writeExclusiveData(io, cwd, path, aw.written());

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 Preflight both feature outputs before writing

When scripts/playing/xx_<name>.zig already exists but components/<name>.zig does not, this write succeeds and the later exclusive script write exits, so the command reports failure after leaving a half-created scaffold; rerunning then fails on the newly created component as well. Preflight both target paths before writing, or roll back the component on script failure, so the documented refusal to overwrite either file stays atomic.

Useful? React with 👍 / 👎.

Comment thread src/add_cmd.zig Outdated
\\ defer view.deinit();
\\
\\ while (view.next()) |entity| {{
\\ const need = game.getComponent(entity, {s}) orelse continue;

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 Route feature component lookups through the ECS backend

For any scaffolded feature, this generated script receives the assembled game, whose component API is exposed through game.ecs_backend (the view call just above uses that path, and existing scripts in this repo do the same). game.getComponent is not a Game method, so add feature need/role/status produces a project that fails to compile as soon as the generated script is type-checked; use game.ecs_backend.getComponent(...) here and in the equivalent role/status templates.

Useful? React with 👍 / 👎.

Comment thread src/add_cmd.zig Outdated

// ─── pack ──────────────────────────────────────────────────────────────

const pack_convention_dirs = [_][]const u8{ "components", "events", "scripts", "prefabs", "hooks" };

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 Stop scaffolding pack scripts until they are scanned

The scaffold advertises and creates packs/<name>/scripts/, but the generator's scanPack only copies/scans components, events, prefabs, and hooks (src/root.zig lines 652-660). When a user follows the scaffold and adds a pack script there, it is silently ignored by generate, so either this directory should not be created yet or pack script scanning needs to be wired at the same time.

Useful? React with 👍 / 👎.

Comment thread src/add_cmd.zig Outdated
const components_dir = try std.fs.path.join(allocator, &.{ root, "components" });
defer allocator.free(components_dir);
cwd.createDirPath(io, components_dir) catch {};
const playing_dir = try std.fs.path.join(allocator, &.{ root, "scripts", "playing" });

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 Use an active default state for feature scripts

In projects scaffolded by init, ProjectConfig.states defaults to "running", and ScriptScanner.scanDir ignores first-level script directories that are not in the configured state list. Because add feature always writes the script under scripts/playing/, a default project can successfully scaffold a feature whose script is then silently omitted from generated AllScripts; write it under the default state or update the project states as part of the scaffold.

Useful? React with 👍 / 👎.

Comment thread src/add_cmd.zig
writeExclusiveData(io, cwd, manifest_path, aw.written());
}

std.log.info("labelle-assembler: scaffolded pack '{s}' in {s}/", .{ name, pack_dir });

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 Register scaffolded packs in project.labelle

A light pack only participates in generation when project.labelle declares it in .plugins (for example with repo = "@packs/<name>"), and generate() discovers packs by iterating cfg.plugins. This command only writes packs/<name>/ and reports success, so a default project that runs add pack citizens gets a valid-looking pack that generate completely ignores; update project.labelle or at least emit that required next step.

Useful? React with 👍 / 👎.

…y work

Substantive (codex):
- Auto-register a scaffolded pack in project.labelle .plugins as
  .{ .name, .repo = "@packs/<name>" } so generate() (which discovers packs
  by iterating cfg.plugins) picks it up; falls back to a printed next-step
  when project.labelle is absent/unparseable/already has it. A pack is no
  longer left dead.
- Stop scaffolding packs/<name>/scripts/ — scanPack only scans
  components/events/prefabs/hooks, so pack scripts are silently ignored
  today (follow-up: wire pack-script scanning, then re-add the dir).
- Feature scripts now land under scripts/running/ (the default
  ProjectConfig.states) instead of scripts/playing/, which ScriptScanner
  silently drops in a default project.
- Feature component lookup goes through game.ecs_backend.getComponent,
  consistent with the ecs_backend.view call.
- add feature preflights BOTH target files before writing either, so a
  partial pre-existing scaffold no longer leaves a half-written result.

Quality (CodeRabbit + Gemini):
- Unknown feature kind now prints the usage/valid-kinds hint.
- Feature dir-creation errors are reported + abort instead of swallowed.
- toTypeName prefixes '_' when the name starts with a digit.
- writeExclusiveData / preflightAbsent return errors instead of calling
  std.process.exit, and emit diagnostics to stderr (repo convention) so
  they're unit-testable; callers map the error to the exit code.

Tests: digit-prefix, unknown-kind path, preflight all-or-nothing,
returning write helper, and pack auto-registration (empty + populated
.plugins). Updated existing scaffold tests for scripts/running and the
dropped pack scripts/ dir.
@apotema

apotema commented Jul 1, 2026

Copy link
Copy Markdown
Contributor Author

Review findings addressed (pushed 8467601)

All in src/add_cmd.zig. zig build + zig build test pass (Zig 0.16).

Substantive (codex) — scaffold now produces working output

  1. Register the scaffolded pack (L225). Fixed. generate() discovers packs by iterating cfg.plugins, so a pack that isn't declared is dead. add pack now auto-appends .{ .name = "<name>", .repo = "@packs/<name>" } to project.labelle .plugins (the in-project @-path form the resolver understands, same shape as @libs/...), inserting right after the .plugins = .{ opener (works for empty and populated tuples). When project.labelle is absent, unreadable, has no .plugins, or already lists the pack, it prints the exact next-step instead of guessing. New registerPackInProject / printRegisterHint.
  2. Pack scripts/ scanning (L167). Fixed by removal. Verified scanPack (src/root.zig) only copies/scans components/events/prefabs/hooks — pack scripts are not scanned today, so a scaffolded scripts/ would be dead. Dropped it from pack_convention_dirs and the usage text. Deferred: wiring pack-script scanning needs a separate ticket; re-add the dir once that lands.
  3. Feature default state (L243). Fixed. Verified ProjectConfig.states defaults to &.{"running"} and ScriptScanner.scanDir silently drops first-level dirs that aren't a declared state. Scripts now scaffold under scripts/running/ (was scripts/playing/).
  4. Feature component lookup (L375). Fixed. Now game.ecs_backend.getComponent(...), consistent with the game.ecs_backend.view(...) call two lines up. (Note: Game.getComponent does exist and delegates to the backend, but the ecs_backend form is the consistent/correct one for the object that exposes .ecs_backend.)
  5. Preflight both feature outputs (L257). Fixed. scaffoldFeature now preflightAbsent-checks both target paths before creating dirs or writing anything, so it's all-or-nothing.

Quality

  1. Unknown-kind hint (CR L92). Fixed — the unknown-kind branch now writes add_usage to stderr like the other parse-error paths.
  2. Swallowed dir-creation errors (CR L245). Fixed — both createDirPath calls in scaffoldFeature now report the error and abort, matching scaffoldPack.
  3. toTypeName digit prefix (Gemini L163). Fixed — prefixes _ when the name starts with a digit so the result is always a legal Zig identifier.
  4. std.process.exit in helpers (Gemini L488). Fixed — writeExclusiveData and preflightAbsent now return errors (diagnostics to stderr, per the repo's testable-error convention); callers map to the exit code.

Tests

Added: digit-prefix toTypeName, preflightAbsent detection, returning writeExclusiveData, and pack auto-registration into both empty and populated .plugins (re-parsed as ProjectConfig to confirm the rewrite stays valid ZON). Updated existing scaffold tests for scripts/running/, the ecs_backend lookup, and the dropped pack scripts/ dir.

@apotema

apotema commented Jul 1, 2026

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist

Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (1)
src/add_cmd.zig (1)

285-321: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value

Dedup only checks the @packs/<name> repo string.

registerPackInProject decides "already registered" solely by searching for "@packs/<name>". If project.labelle already declares this pack under any other repo form (e.g. local:packs/<name> after a manual edit), the check misses it and a second entry with the same .name is appended, yielding two plugins with the same name in .plugins. Since the tool always writes @packs/ this is an edge case, but consider also short-circuiting when a plugin .name = "<name>" already appears.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/add_cmd.zig` around lines 285 - 321, The dedup check in
registerPackInProject only looks for the `@packs/`<name> repo string, so it can
miss an existing plugin entry with the same .name under a different repo form
and append a duplicate. Update the registration guard in registerPackInProject
to also detect any existing .name = "<name>" entry in src before building the
new .plugins item, so the function short-circuits on either match and avoids
duplicate plugins.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/add_cmd.zig`:
- Around line 285-321: The dedup check in registerPackInProject only looks for
the `@packs/`<name> repo string, so it can miss an existing plugin entry with the
same .name under a different repo form and append a duplicate. Update the
registration guard in registerPackInProject to also detect any existing .name =
"<name>" entry in src before building the new .plugins item, so the function
short-circuits on either match and avoids duplicate plugins.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4172612f-ea4e-437a-8aff-7ac6fd59a715

📥 Commits

Reviewing files that changed from the base of the PR and between 9df8b1a and 8467601.

📒 Files selected for processing (1)
  • src/add_cmd.zig

@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: 84676019a9

ℹ️ 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 thread src/add_cmd.zig
// aren't a declared state — so a script under `scripts/playing/` would
// never run in a default project (chatgpt-codex review). Target
// `scripts/running/` so the scaffolded script is picked up out of the box.
const script_dir = try std.fs.path.join(allocator, &.{ root, "scripts", "running" });

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 Choose a configured state for feature scripts

This still drops scaffolded scripts for projects that customize .states to omit the default running state (for example .states = .{ "playing" }): add feature writes scripts/running/xx_<name>.zig, but ScriptScanner.scanDir skips first-level script directories that are not in ProjectConfig.states, so the generated feature component exists while its behavior script is silently omitted. Read project.labelle and use a configured state (or emit a root-level/global script) instead of hard-coding running.

Useful? React with 👍 / 👎.

Comment thread src/add_cmd.zig
Comment on lines +288 to +290
// Already registered? Leave the file untouched.
if (std.mem.indexOf(u8, src, repo_ref) != null) {
std.log.info(" pack '{s}' is already registered in project.labelle .plugins", .{name});

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 Reject existing plugin names before inserting pack

When project.labelle already declares a plugin or pack with the same .name but a different repo, this only checks for the "@packs/<name>" repo string and then inserts a second .plugins entry with the same name. That duplicate name can break generation: light-pack filtering is by plugin name, so adding a local pack named like an existing decl-module plugin causes the original plugin to be filtered out as though it were the light pack. Check for an existing .name = "<name>" (preferably by parsing the config) before auto-registering.

Useful? React with 👍 / 👎.

Comment thread src/add_cmd.zig
return;
}

// Find the `.plugins = .{` opener and insert right after the `{`.

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 rewriting comments as plugin fields

Because this searches the raw file for the first .plugins substring, add pack can rewrite a comment or string instead of the actual ZON field. For a valid project that omits .plugins but documents it in a comment such as // .plugins = .{} ..., the insertion adds real ZON text into that comment example and leaves project.labelle malformed while reporting success; parse/validate the config edit or at least ignore comments/strings before writing.

Useful? React with 👍 / 👎.

Comment thread src/add_cmd.zig
if (name.len == 0) return false;
if (name[0] < 'a' or name[0] > 'z') return false;
for (name) |c| {
const ok = (c >= 'a' and c <= 'z') or (c >= '0' and c <= '9') or c == '_';

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 Cap feature names to registry type length

This accepts arbitrarily long feature names, but the generator derives component registry declarations with pathToPascal into a fixed 128-byte buffer. For a name whose PascalCase type exceeds that limit, add feature writes a component exporting the full type name while generated main.zig later imports the truncated type, so the scaffolded project fails to build. Reject names whose derived type would exceed the generator limit, or derive the template name through the same bounded path.

Useful? React with 👍 / 👎.

@apotema
apotema merged commit 4050ab9 into main Jul 1, 2026
4 checks passed
@apotema
apotema deleted the packs/add-scaffold branch July 1, 2026 18:38
apotema added a commit that referenced this pull request Jul 1, 2026
…col 5

Both #485 (add, protocol 4) and this PR (#440/#270 check) added a subcommand +
protocol bump. Resolved: keep both subcommand imports/usage/dispatch/tests;
add is v4, check is now v5. (The naive union left the check/add dispatch ifs
sharing one closing brace — fixed to two separate blocks.)
apotema added a commit that referenced this pull request Jul 1, 2026
…atch (#487) (#496)

A light pack scanned components/events/prefabs/hooks but NOT scripts/, so a
pack's per-frame SYSTEM had to leak into the game root (the #1 gap the
pack-colony-demo eval found). This copies a pack's scripts/<state>/*.zig into
`packs/<name>/scripts/` and registers them into the SAME per-state script
dispatch the game root + plugins use, so a pack script's `pub fn tick(game, dt)`
runs in its declared state.

Key decisions:
- Placement UNDER the pack dir (packs/<name>/scripts/…), NOT scripts/.plugin_<name>/.
  A pack has no importable module, so its script reaches its own components by
  relative import (../../components/foo.zig) exactly as a game-root script does.
  Copying the subtree under the pack — beside the components/ scanPack already
  copies — preserves that relative offset so the import resolves unchanged.
- ScriptEntry.import_base: "scripts/" for game+plugin scripts (unchanged),
  "" for pack scripts whose rel_path is already a full packs/<name>/scripts/…
  target-relative path. AllScripts emits @import(import_base ++ rel_path).
- Pack scripts carry plugin_name = pack name, so they form their own
  numeric-prefix scope, sort into the plugin block (after game scripts) by
  declaration order, and are skipped by the game-script FlowNode walk. Pack
  scripts are therefore lifecycle-only (tick/setup/State/drawGui); FlowNode
  discovery inside pack scripts is a deliberate follow-up.
- Packs are skipped in the plugin-scripts loop to avoid double-scanning.
- Re-add scripts/ to the `add pack` scaffold's convention dirs (#485).

Existing game-root-only projects are unaffected (regression test guards the
"scripts/" prefix). Adds 4 tests. Unblocks assembler#491.

Claude-Session: https://claude.ai/code/session_01P7B7UzgrWEbBYLT3YBrAog
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