Skip to content

PR A: Close the sealed Command hierarchy honestly — sealed module Command, abstract BuiltinCommand open edge, delete the sorbet-runtime ivar pokes, un-private CommandRepository - #121

Merged
JPDuchesne merged 3 commits into
mainfrom
ai/117-pr-a-close-the-sealed-command-hierarchy
Aug 18, 2026

Conversation

@d3mlabs-ai-flow

Copy link
Copy Markdown
Contributor

Implements #117.

Requested by @JPDuchesne.

Closes#117

…uiltinBody interface, final BuiltinCommand holding a body, delete the sorbet-runtime ivar pokes, un-private CommandRepository
Co-authored-by: JPDuchesne <2636122+JPDuchesne@users.noreply.github.com>
@codecov

codecovBot commented Aug 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

…abstract open-edge class
A sealed module's included hook fires only for direct includers (include
never transfers singleton methods), so the hierarchy closes without the
BuiltinBody indirection or the final wrapper leaf: builtins subclass
BuiltinCommand directly, the data leaves are final!, and the composition
root wires builtins straight in. BuiltinCommand stays a class because
Sorbet flattens module mixins — a module open edge would re-include the
sealed Command in every builtin and fail the same-file check statically.
Co-authored-by: Cursor <cursoragent@cursor.com>
@JPDuchesneJPDuchesne changed the title PR A: Close the sealed Command hierarchy honestly — BuiltinBody interface, final BuiltinCommand holding a body, delete the sorbet-runtime ivar pokes, un-private CommandRepositoryPR A: Close the sealed Command hierarchy honestly — sealed module Command, abstract BuiltinCommand open edge, delete the sorbet-runtime ivar pokes, un-private CommandRepositoryAug 18, 2026
@JPDuchesne

Copy link
Copy Markdown
Contributor

Reworked per review discussion: the BuiltinBody interface and the final BuiltinCommand wrapper are gone.

  • Command is now a sealed! abstract!module with the trait defaults. A sealed module's runtime included hook fires only for direct includers — include never transfers singleton methods — so the seal holds with no sorbet-runtime ivar pokes and nothing to suppress.
  • BuiltinCommand is an abstract! class that includes Command beside it: the hierarchy's one declared open edge. Builtins subclass it directly again (class CacheCommand < BuiltinCommand), and the composition root wires them straight in — no wrapping.
  • It stays a class rather than a module because Sorbet flattens module mixins: a module open edge would make every builtin a direct includer of sealed Command and fail the same-file check statically. A superclass edge is not flattened, so it typechecks and is invisible to the seal's runtime hooks.
  • ProjectCommand and OverriddenCommand include Command and are final!, so descent is closed everywhere it isn't explicitly declared. CommandExecutor's exhaustive case + T.absurd is unchanged.
  • Tests now assert the real closure properties: direct include Dev::Command raises, subclassing either data leaf raises, and subclassing BuiltinCommand is the declared open edge.

srb tc clean, 887 tests / 0 failures, rubocop clean.

@stamps = stamps
@calls = []
@body = body
super()

@JPDuchesneJPDuchesneAug 18, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

/ask what's with this line change?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

/ask

The removed super() was a no-op, so the diff is dead-code cleanup in the test fake. ExecutorFakeBuiltin subclasses Dev::BuiltinCommand, and that base class (see src/dev/command.rb) defines no initialize of its own — it's an abstract class that only declares the abstract call method, and Command is a plain module with no constructor either. So super() fell through to Object#initialize, which takes no arguments and does nothing; the line initialized nothing and its presence wrongly implied the base class had constructor behavior to run. Removing it also matches how the real builtins under src/dev/builtins/ are written — e.g. CdCommand and CredCommand define initialize without calling super. Behavior of the tests in test/dev/command_executor_test.rb is unchanged. (One caveat: git history access is restricted in this checkout, so I can't confirm whether the line was a leftover from an earlier BuiltinCommand shape — the file's comments mention it previously carried sorbet-runtime workarounds — but as the code stands today the call is provably inert.)

⚙️ workflow run · model: claude-fable-5-high

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Resolved in b8ed631: the super() calls are restored — and the convention is now applied uniformly. The bot's replies were right that the calls are inert today, but that misses the point of the rule: an explicit super() means future base-class constructor state either runs automatically (defaulted params) or fails loudly at first instantiation (required params), instead of silently constructing half-initialized objects. The fakes had actually been the only callers — the 11 real builtins never called super() — so the commit adds it everywhere an initializer derives from the hierarchy: both data leaves, all builtins, and the fakes.

def initialize(desc: "a builtin", hidden: false)
@desc = desc
@hidden = hidden
super()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

/ask Same here, why not keep super?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Restored in b8ed631, along with every other initializer in the hierarchy (the real builtins had never called super() — the fakes were the outliers in the other direction). See the thread on command_executor_test.rb for the full rationale.

@staleness_exempt = staleness_exempt
@stamps = stamps
@calls = []
super()

@JPDuchesneJPDuchesneAug 18, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

/ask Same here, why not keep super?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

/ask

Because there is nothing for super() to reach: it's a pure no-op here. ServiceFakeBuiltin subclasses Dev::BuiltinCommand (src/dev/command.rb), and neither BuiltinCommand nor the Command module it includes defines an initialize — there is no parent state to set up, so super() falls through to Object#initialize, which does nothing. It isn't needed for the abstract guard either: sorbet-runtime's abstract! enforces non-instantiability by wrapping self.new on the abstract class, not initialize, so the guard runs regardless of whether the subclass calls super. Lint doesn't ask for it either — Lint/MissingSuper is disabled in the inherited rubocop-shopify config. Keeping it would be harmless but would imply the base class has initialization the fake must run, which it doesn't; dropping the dead call is the same reasoning as in the other thread. (If BuiltinCommand ever grows a real initializer, its subclasses would need explicit constructor wiring anyway, per the repo's constructor-injection convention.)

⚙️ workflow run · model: claude-fable-5-high

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Restored in b8ed631, along with every other initializer in the hierarchy. The bot is right that abstract! guards new rather than initialize and that Lint/MissingSuper is disabled in rubocop-shopify — but "provably inert today" is exactly the case the always-super convention exists for. See the thread on command_executor_test.rb for the full rationale.

…rchy
Ruby silently skips base initialization when a subclass overrides
initialize without super. The calls are inert today (no ancestor defines
initialize), but they make future base-class constructor state either
run automatically (defaulted params) or fail loudly at first
instantiation (required params) instead of constructing half-initialized
objects. Applied uniformly: the two data leaves, all builtins, and the
test fakes.
Co-authored-by: Cursor <cursoragent@cursor.com>
@JPDuchesne
JPDuchesne merged commit 5d9c57b into mainAug 18, 2026
26 checks passed
@JPDuchesne
JPDuchesne deleted the ai/117-pr-a-close-the-sealed-command-hierarchy branch August 18, 2026 17:47
JPDuchesne added a commit that referenced this pull request Aug 19, 2026
916fe89 Merge pull request #128 from d3mlabs/jpd/skill-installer-ephemeral-guard
b9ea143 Move the ephemeral-source guard to SkillInstaller, the seam all links share
36803f7 Merge pull request #127 from d3mlabs/jpd/capture-learning-root-cause-gate
d6e081e Name the wide-angle goal, not one command: an exact git-log depth invites checkbox compliance
f876dbe capture-learning: gate workaround learnings on root cause, add wide angle
7249198 Merge pull request #123 from d3mlabs/ai/119-pr-b-typed-child-process-failure-taxonom
e71063a Merge pull request #126 from d3mlabs/jpd/hermetic-scrub-guard
f911e38 Make the scrub-list guard hermetic: construct the bundler launch it measures
6c398b8 ai-flow /build: let's resolve conflicts
cb68d60 Merge pull request #122 from d3mlabs/ai/118-pr-d-split-commandexecutor-into-a-dispat
4477c6b Update the manifest-loader contract note for the eager toolchain pass
3ad03c3 Constructor-inject CommandRunner; two messages replace the wait flag
2ac941a Route help through the command path; group and eager-load usage
a78ba14 Add the help builtin
c7ae57a Add Category trait to the Command hierarchy
2e05625 ai-flow /build: let's fix the fake classes, put them within the test class
a29b5e6 Merge main: sealed-module Command hierarchy, super() convention, and bin/test.rb runner
5d9c57b Merge pull request #121 from d3mlabs/ai/117-pr-a-close-the-sealed-command-hierarchy
b8ed631 Call super() in every initializer that derives from the Command hierarchy
5bcc76f Rework the seal: Command becomes a sealed module, BuiltinCommand the abstract open-edge class
5c68c85 Merge pull request #120 from d3mlabs/ai/116-pr-c-bin-test-rb-tee-suite-output-to-a-s
c62efc8 ai-flow /build: PR B: Typed child-process failure taxonomy in CommandRunner (CommandFailedError / CommandKilledError / CommandSpawnError) mapped to exit codes in Runner#exit_for
f09f845 ai-flow /build: PR D: Split CommandExecutor into a dispatching composite with injectable BuiltinExecutor / ProjectExecutor / OverriddenExecutor strategies (exec_into vs run_waiting)
0775515 ai-flow /build: PR A: Close the sealed Command hierarchy honestly — BuiltinBody interface, final BuiltinCommand holding a body, delete the sorbet-runtime ivar pokes, un-private CommandRepository
5a5fb41 ai-flow /build: PR C: bin/test.rb — tee suite output to a stable log artifact and pass file args through to rake TEST
4c89408 Merge pull request #115 from d3mlabs/ai/37-layer-the-dev-runner-application-service
04d461c Add the simplecov-cobertura gem RBI
81677f6 Upload cobertura to codecov instead of SimpleCov JSON
0a741f0 Cover the default factories, image credential providers, and nocov the sealed absurd arm
fe7c94e ai-flow /build: Layer the dev Runner (application service + boundary coercion)
d782b1a Merge pull request #107 from d3mlabs/ai/101-dev-clone-host-global-builtin-cloning-vi
f305ac9 ai-flow /build: codecov coverage missing
fac96ee ai-flow /build: dev clone: host-global builtin cloning via gh auth to the canonical $DEV_CD_ROOT path
d16b757 Merge pull request #100 from d3mlabs/jpd/99-pin-homebrew-installer
2ad614e Pin the Homebrew installer to a commit SHA (dev#99)
53e3616 Merge pull request #90 from d3mlabs/ai/89-gemskilllinker-links-minted-under-a-sand
95ee372 Merge pull request #97 from d3mlabs/ai/learn-promote-rbenv-libruby-rpath-hijack
f7edc33 chore: nudge origin-firing after ai-flow#57 (removal diffs skip green)
646f189 Merge pull request #98 from d3mlabs/jpd/proposal-checks-edited
50e913a proposal-checks: re-verify on PR body edits (ai-flow#54)
59a3146 ai-flow /learn: drop rbenv-libruby-rpath-hijack (promoted to the org tier)
f119987 Merge pull request #96 from d3mlabs/jpd/ai-flow-knowledge-repo
b0e7131 ai-flow config: opt dev into org-tier learning promotion
d17b2ff Merge pull request #95 from d3mlabs/jpd/94-self-defending-entrypoint
29b2e16 Test readability: one aliased scrub list, one property per test
f7197ac Drift guard: the unset list must cover what the running bundler exports
049bbc8 Probe the shim scrub with a stub ruby instead of a full dev command run
88fa953 bin/dev: scrub foreign bundler activation before Ruby boots
9cf868a Merge pull request #92 from d3mlabs/ai/60-plan-pull-mangles-files-with-an-empty-fr
6783469 Merge pull request #93 from d3mlabs/ai/learn-issue-60
991d46d ai-flow /build: capture learnings from the build pass
fe64507 ai-flow /build: Plan pull mangles files with an empty frontmatter block above the real one (double frontmatter)
d8db57d ai-flow /build: GemSkillLinker: links minted under a sandboxed session point into ephemeral sandbox cache paths
b4526ea Merge pull request #88 from d3mlabs/jpd/ast-transform-3.1.1
de9beaf Bump ast_transform to 3.1.1 and drop the heredoc-emission workaround
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant

@JPDuchesne
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
PR A: Close the sealed Command hierarchy honestly — sealed module Command, abstract BuiltinCommand open edge, delete the sorbet-runtime ivar pokes, un-private CommandRepository by d3mlabs-ai-flow[bot] · Pull Request #121 · d3mlabs/dev · GitHub
Skip to content

PR A: Close the sealed Command hierarchy honestly — sealed module Command, abstract BuiltinCommand open edge, delete the sorbet-runtime ivar pokes, un-private CommandRepository - #121

Merged
JPDuchesne merged 3 commits into
mainfrom
ai/117-pr-a-close-the-sealed-command-hierarchy
Aug 18, 2026

Conversation

@d3mlabs-ai-flow

Copy link
Copy Markdown
Contributor

Implements #117.

Requested by @JPDuchesne.

Closes#117

…uiltinBody interface, final BuiltinCommand holding a body, delete the sorbet-runtime ivar pokes, un-private CommandRepository
Co-authored-by: JPDuchesne <2636122+JPDuchesne@users.noreply.github.com>
@codecov

codecovBot commented Aug 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

…abstract open-edge class
A sealed module's included hook fires only for direct includers (include
never transfers singleton methods), so the hierarchy closes without the
BuiltinBody indirection or the final wrapper leaf: builtins subclass
BuiltinCommand directly, the data leaves are final!, and the composition
root wires builtins straight in. BuiltinCommand stays a class because
Sorbet flattens module mixins — a module open edge would re-include the
sealed Command in every builtin and fail the same-file check statically.
Co-authored-by: Cursor <cursoragent@cursor.com>
@JPDuchesneJPDuchesne changed the title PR A: Close the sealed Command hierarchy honestly — BuiltinBody interface, final BuiltinCommand holding a body, delete the sorbet-runtime ivar pokes, un-private CommandRepositoryPR A: Close the sealed Command hierarchy honestly — sealed module Command, abstract BuiltinCommand open edge, delete the sorbet-runtime ivar pokes, un-private CommandRepositoryAug 18, 2026
@JPDuchesne

Copy link
Copy Markdown
Contributor

Reworked per review discussion: the BuiltinBody interface and the final BuiltinCommand wrapper are gone.

  • Command is now a sealed! abstract!module with the trait defaults. A sealed module's runtime included hook fires only for direct includers — include never transfers singleton methods — so the seal holds with no sorbet-runtime ivar pokes and nothing to suppress.
  • BuiltinCommand is an abstract! class that includes Command beside it: the hierarchy's one declared open edge. Builtins subclass it directly again (class CacheCommand < BuiltinCommand), and the composition root wires them straight in — no wrapping.
  • It stays a class rather than a module because Sorbet flattens module mixins: a module open edge would make every builtin a direct includer of sealed Command and fail the same-file check statically. A superclass edge is not flattened, so it typechecks and is invisible to the seal's runtime hooks.
  • ProjectCommand and OverriddenCommand include Command and are final!, so descent is closed everywhere it isn't explicitly declared. CommandExecutor's exhaustive case + T.absurd is unchanged.
  • Tests now assert the real closure properties: direct include Dev::Command raises, subclassing either data leaf raises, and subclassing BuiltinCommand is the declared open edge.

srb tc clean, 887 tests / 0 failures, rubocop clean.

@stamps = stamps
@calls = []
@body = body
super()

@JPDuchesneJPDuchesneAug 18, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

/ask what's with this line change?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

/ask

The removed super() was a no-op, so the diff is dead-code cleanup in the test fake. ExecutorFakeBuiltin subclasses Dev::BuiltinCommand, and that base class (see src/dev/command.rb) defines no initialize of its own — it's an abstract class that only declares the abstract call method, and Command is a plain module with no constructor either. So super() fell through to Object#initialize, which takes no arguments and does nothing; the line initialized nothing and its presence wrongly implied the base class had constructor behavior to run. Removing it also matches how the real builtins under src/dev/builtins/ are written — e.g. CdCommand and CredCommand define initialize without calling super. Behavior of the tests in test/dev/command_executor_test.rb is unchanged. (One caveat: git history access is restricted in this checkout, so I can't confirm whether the line was a leftover from an earlier BuiltinCommand shape — the file's comments mention it previously carried sorbet-runtime workarounds — but as the code stands today the call is provably inert.)

⚙️ workflow run · model: claude-fable-5-high

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Resolved in b8ed631: the super() calls are restored — and the convention is now applied uniformly. The bot's replies were right that the calls are inert today, but that misses the point of the rule: an explicit super() means future base-class constructor state either runs automatically (defaulted params) or fails loudly at first instantiation (required params), instead of silently constructing half-initialized objects. The fakes had actually been the only callers — the 11 real builtins never called super() — so the commit adds it everywhere an initializer derives from the hierarchy: both data leaves, all builtins, and the fakes.

def initialize(desc: "a builtin", hidden: false)
@desc = desc
@hidden = hidden
super()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

/ask Same here, why not keep super?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Restored in b8ed631, along with every other initializer in the hierarchy (the real builtins had never called super() — the fakes were the outliers in the other direction). See the thread on command_executor_test.rb for the full rationale.

@staleness_exempt = staleness_exempt
@stamps = stamps
@calls = []
super()

@JPDuchesneJPDuchesneAug 18, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

/ask Same here, why not keep super?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

/ask

Because there is nothing for super() to reach: it's a pure no-op here. ServiceFakeBuiltin subclasses Dev::BuiltinCommand (src/dev/command.rb), and neither BuiltinCommand nor the Command module it includes defines an initialize — there is no parent state to set up, so super() falls through to Object#initialize, which does nothing. It isn't needed for the abstract guard either: sorbet-runtime's abstract! enforces non-instantiability by wrapping self.new on the abstract class, not initialize, so the guard runs regardless of whether the subclass calls super. Lint doesn't ask for it either — Lint/MissingSuper is disabled in the inherited rubocop-shopify config. Keeping it would be harmless but would imply the base class has initialization the fake must run, which it doesn't; dropping the dead call is the same reasoning as in the other thread. (If BuiltinCommand ever grows a real initializer, its subclasses would need explicit constructor wiring anyway, per the repo's constructor-injection convention.)

⚙️ workflow run · model: claude-fable-5-high

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Restored in b8ed631, along with every other initializer in the hierarchy. The bot is right that abstract! guards new rather than initialize and that Lint/MissingSuper is disabled in rubocop-shopify — but "provably inert today" is exactly the case the always-super convention exists for. See the thread on command_executor_test.rb for the full rationale.

…rchy
Ruby silently skips base initialization when a subclass overrides
initialize without super. The calls are inert today (no ancestor defines
initialize), but they make future base-class constructor state either
run automatically (defaulted params) or fail loudly at first
instantiation (required params) instead of constructing half-initialized
objects. Applied uniformly: the two data leaves, all builtins, and the
test fakes.
Co-authored-by: Cursor <cursoragent@cursor.com>
@JPDuchesne
JPDuchesne merged commit 5d9c57b into mainAug 18, 2026
26 checks passed
@JPDuchesne
JPDuchesne deleted the ai/117-pr-a-close-the-sealed-command-hierarchy branch August 18, 2026 17:47
JPDuchesne added a commit that referenced this pull request Aug 19, 2026
916fe89 Merge pull request #128 from d3mlabs/jpd/skill-installer-ephemeral-guard
b9ea143 Move the ephemeral-source guard to SkillInstaller, the seam all links share
36803f7 Merge pull request #127 from d3mlabs/jpd/capture-learning-root-cause-gate
d6e081e Name the wide-angle goal, not one command: an exact git-log depth invites checkbox compliance
f876dbe capture-learning: gate workaround learnings on root cause, add wide angle
7249198 Merge pull request #123 from d3mlabs/ai/119-pr-b-typed-child-process-failure-taxonom
e71063a Merge pull request #126 from d3mlabs/jpd/hermetic-scrub-guard
f911e38 Make the scrub-list guard hermetic: construct the bundler launch it measures
6c398b8 ai-flow /build: let's resolve conflicts
cb68d60 Merge pull request #122 from d3mlabs/ai/118-pr-d-split-commandexecutor-into-a-dispat
4477c6b Update the manifest-loader contract note for the eager toolchain pass
3ad03c3 Constructor-inject CommandRunner; two messages replace the wait flag
2ac941a Route help through the command path; group and eager-load usage
a78ba14 Add the help builtin
c7ae57a Add Category trait to the Command hierarchy
2e05625 ai-flow /build: let's fix the fake classes, put them within the test class
a29b5e6 Merge main: sealed-module Command hierarchy, super() convention, and bin/test.rb runner
5d9c57b Merge pull request #121 from d3mlabs/ai/117-pr-a-close-the-sealed-command-hierarchy
b8ed631 Call super() in every initializer that derives from the Command hierarchy
5bcc76f Rework the seal: Command becomes a sealed module, BuiltinCommand the abstract open-edge class
5c68c85 Merge pull request #120 from d3mlabs/ai/116-pr-c-bin-test-rb-tee-suite-output-to-a-s
c62efc8 ai-flow /build: PR B: Typed child-process failure taxonomy in CommandRunner (CommandFailedError / CommandKilledError / CommandSpawnError) mapped to exit codes in Runner#exit_for
f09f845 ai-flow /build: PR D: Split CommandExecutor into a dispatching composite with injectable BuiltinExecutor / ProjectExecutor / OverriddenExecutor strategies (exec_into vs run_waiting)
0775515 ai-flow /build: PR A: Close the sealed Command hierarchy honestly — BuiltinBody interface, final BuiltinCommand holding a body, delete the sorbet-runtime ivar pokes, un-private CommandRepository
5a5fb41 ai-flow /build: PR C: bin/test.rb — tee suite output to a stable log artifact and pass file args through to rake TEST
4c89408 Merge pull request #115 from d3mlabs/ai/37-layer-the-dev-runner-application-service
04d461c Add the simplecov-cobertura gem RBI
81677f6 Upload cobertura to codecov instead of SimpleCov JSON
0a741f0 Cover the default factories, image credential providers, and nocov the sealed absurd arm
fe7c94e ai-flow /build: Layer the dev Runner (application service + boundary coercion)
d782b1a Merge pull request #107 from d3mlabs/ai/101-dev-clone-host-global-builtin-cloning-vi
f305ac9 ai-flow /build: codecov coverage missing
fac96ee ai-flow /build: dev clone: host-global builtin cloning via gh auth to the canonical $DEV_CD_ROOT path
d16b757 Merge pull request #100 from d3mlabs/jpd/99-pin-homebrew-installer
2ad614e Pin the Homebrew installer to a commit SHA (dev#99)
53e3616 Merge pull request #90 from d3mlabs/ai/89-gemskilllinker-links-minted-under-a-sand
95ee372 Merge pull request #97 from d3mlabs/ai/learn-promote-rbenv-libruby-rpath-hijack
f7edc33 chore: nudge origin-firing after ai-flow#57 (removal diffs skip green)
646f189 Merge pull request #98 from d3mlabs/jpd/proposal-checks-edited
50e913a proposal-checks: re-verify on PR body edits (ai-flow#54)
59a3146 ai-flow /learn: drop rbenv-libruby-rpath-hijack (promoted to the org tier)
f119987 Merge pull request #96 from d3mlabs/jpd/ai-flow-knowledge-repo
b0e7131 ai-flow config: opt dev into org-tier learning promotion
d17b2ff Merge pull request #95 from d3mlabs/jpd/94-self-defending-entrypoint
29b2e16 Test readability: one aliased scrub list, one property per test
f7197ac Drift guard: the unset list must cover what the running bundler exports
049bbc8 Probe the shim scrub with a stub ruby instead of a full dev command run
88fa953 bin/dev: scrub foreign bundler activation before Ruby boots
9cf868a Merge pull request #92 from d3mlabs/ai/60-plan-pull-mangles-files-with-an-empty-fr
6783469 Merge pull request #93 from d3mlabs/ai/learn-issue-60
991d46d ai-flow /build: capture learnings from the build pass
fe64507 ai-flow /build: Plan pull mangles files with an empty frontmatter block above the real one (double frontmatter)
d8db57d ai-flow /build: GemSkillLinker: links minted under a sandboxed session point into ephemeral sandbox cache paths
b4526ea Merge pull request #88 from d3mlabs/jpd/ast-transform-3.1.1
de9beaf Bump ast_transform to 3.1.1 and drop the heredoc-emission workaround
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant

@JPDuchesne
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' PR A: Close the sealed Command hierarchy honestly — sealed module Command, abstract BuiltinCommand open edge, delete the sorbet-runtime ivar pokes, un-private CommandRepository by d3mlabs-ai-flow[bot] · Pull Request #121 · d3mlabs/dev · GitHub
Skip to content

PR A: Close the sealed Command hierarchy honestly — sealed module Command, abstract BuiltinCommand open edge, delete the sorbet-runtime ivar pokes, un-private CommandRepository - #121

Merged
JPDuchesne merged 3 commits into
mainfrom
ai/117-pr-a-close-the-sealed-command-hierarchy
Aug 18, 2026

Conversation

@d3mlabs-ai-flow

Copy link
Copy Markdown
Contributor

Implements #117.

Requested by @JPDuchesne.

Closes#117

…uiltinBody interface, final BuiltinCommand holding a body, delete the sorbet-runtime ivar pokes, un-private CommandRepository
Co-authored-by: JPDuchesne <2636122+JPDuchesne@users.noreply.github.com>
@codecov

codecovBot commented Aug 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

…abstract open-edge class
A sealed module's included hook fires only for direct includers (include
never transfers singleton methods), so the hierarchy closes without the
BuiltinBody indirection or the final wrapper leaf: builtins subclass
BuiltinCommand directly, the data leaves are final!, and the composition
root wires builtins straight in. BuiltinCommand stays a class because
Sorbet flattens module mixins — a module open edge would re-include the
sealed Command in every builtin and fail the same-file check statically.
Co-authored-by: Cursor <cursoragent@cursor.com>
@JPDuchesneJPDuchesne changed the title PR A: Close the sealed Command hierarchy honestly — BuiltinBody interface, final BuiltinCommand holding a body, delete the sorbet-runtime ivar pokes, un-private CommandRepositoryPR A: Close the sealed Command hierarchy honestly — sealed module Command, abstract BuiltinCommand open edge, delete the sorbet-runtime ivar pokes, un-private CommandRepositoryAug 18, 2026
@JPDuchesne

Copy link
Copy Markdown
Contributor

Reworked per review discussion: the BuiltinBody interface and the final BuiltinCommand wrapper are gone.

  • Command is now a sealed! abstract!module with the trait defaults. A sealed module's runtime included hook fires only for direct includers — include never transfers singleton methods — so the seal holds with no sorbet-runtime ivar pokes and nothing to suppress.
  • BuiltinCommand is an abstract! class that includes Command beside it: the hierarchy's one declared open edge. Builtins subclass it directly again (class CacheCommand < BuiltinCommand), and the composition root wires them straight in — no wrapping.
  • It stays a class rather than a module because Sorbet flattens module mixins: a module open edge would make every builtin a direct includer of sealed Command and fail the same-file check statically. A superclass edge is not flattened, so it typechecks and is invisible to the seal's runtime hooks.
  • ProjectCommand and OverriddenCommand include Command and are final!, so descent is closed everywhere it isn't explicitly declared. CommandExecutor's exhaustive case + T.absurd is unchanged.
  • Tests now assert the real closure properties: direct include Dev::Command raises, subclassing either data leaf raises, and subclassing BuiltinCommand is the declared open edge.

srb tc clean, 887 tests / 0 failures, rubocop clean.

@stamps = stamps
@calls = []
@body = body
super()

@JPDuchesneJPDuchesneAug 18, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

/ask what's with this line change?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

/ask

The removed super() was a no-op, so the diff is dead-code cleanup in the test fake. ExecutorFakeBuiltin subclasses Dev::BuiltinCommand, and that base class (see src/dev/command.rb) defines no initialize of its own — it's an abstract class that only declares the abstract call method, and Command is a plain module with no constructor either. So super() fell through to Object#initialize, which takes no arguments and does nothing; the line initialized nothing and its presence wrongly implied the base class had constructor behavior to run. Removing it also matches how the real builtins under src/dev/builtins/ are written — e.g. CdCommand and CredCommand define initialize without calling super. Behavior of the tests in test/dev/command_executor_test.rb is unchanged. (One caveat: git history access is restricted in this checkout, so I can't confirm whether the line was a leftover from an earlier BuiltinCommand shape — the file's comments mention it previously carried sorbet-runtime workarounds — but as the code stands today the call is provably inert.)

⚙️ workflow run · model: claude-fable-5-high

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Resolved in b8ed631: the super() calls are restored — and the convention is now applied uniformly. The bot's replies were right that the calls are inert today, but that misses the point of the rule: an explicit super() means future base-class constructor state either runs automatically (defaulted params) or fails loudly at first instantiation (required params), instead of silently constructing half-initialized objects. The fakes had actually been the only callers — the 11 real builtins never called super() — so the commit adds it everywhere an initializer derives from the hierarchy: both data leaves, all builtins, and the fakes.

def initialize(desc: "a builtin", hidden: false)
@desc = desc
@hidden = hidden
super()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

/ask Same here, why not keep super?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Restored in b8ed631, along with every other initializer in the hierarchy (the real builtins had never called super() — the fakes were the outliers in the other direction). See the thread on command_executor_test.rb for the full rationale.

@staleness_exempt = staleness_exempt
@stamps = stamps
@calls = []
super()

@JPDuchesneJPDuchesneAug 18, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

/ask Same here, why not keep super?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

/ask

Because there is nothing for super() to reach: it's a pure no-op here. ServiceFakeBuiltin subclasses Dev::BuiltinCommand (src/dev/command.rb), and neither BuiltinCommand nor the Command module it includes defines an initialize — there is no parent state to set up, so super() falls through to Object#initialize, which does nothing. It isn't needed for the abstract guard either: sorbet-runtime's abstract! enforces non-instantiability by wrapping self.new on the abstract class, not initialize, so the guard runs regardless of whether the subclass calls super. Lint doesn't ask for it either — Lint/MissingSuper is disabled in the inherited rubocop-shopify config. Keeping it would be harmless but would imply the base class has initialization the fake must run, which it doesn't; dropping the dead call is the same reasoning as in the other thread. (If BuiltinCommand ever grows a real initializer, its subclasses would need explicit constructor wiring anyway, per the repo's constructor-injection convention.)

⚙️ workflow run · model: claude-fable-5-high

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Restored in b8ed631, along with every other initializer in the hierarchy. The bot is right that abstract! guards new rather than initialize and that Lint/MissingSuper is disabled in rubocop-shopify — but "provably inert today" is exactly the case the always-super convention exists for. See the thread on command_executor_test.rb for the full rationale.

…rchy
Ruby silently skips base initialization when a subclass overrides
initialize without super. The calls are inert today (no ancestor defines
initialize), but they make future base-class constructor state either
run automatically (defaulted params) or fail loudly at first
instantiation (required params) instead of constructing half-initialized
objects. Applied uniformly: the two data leaves, all builtins, and the
test fakes.
Co-authored-by: Cursor <cursoragent@cursor.com>
@JPDuchesne
JPDuchesne merged commit 5d9c57b into mainAug 18, 2026
26 checks passed
@JPDuchesne
JPDuchesne deleted the ai/117-pr-a-close-the-sealed-command-hierarchy branch August 18, 2026 17:47
JPDuchesne added a commit that referenced this pull request Aug 19, 2026
916fe89 Merge pull request #128 from d3mlabs/jpd/skill-installer-ephemeral-guard
b9ea143 Move the ephemeral-source guard to SkillInstaller, the seam all links share
36803f7 Merge pull request #127 from d3mlabs/jpd/capture-learning-root-cause-gate
d6e081e Name the wide-angle goal, not one command: an exact git-log depth invites checkbox compliance
f876dbe capture-learning: gate workaround learnings on root cause, add wide angle
7249198 Merge pull request #123 from d3mlabs/ai/119-pr-b-typed-child-process-failure-taxonom
e71063a Merge pull request #126 from d3mlabs/jpd/hermetic-scrub-guard
f911e38 Make the scrub-list guard hermetic: construct the bundler launch it measures
6c398b8 ai-flow /build: let's resolve conflicts
cb68d60 Merge pull request #122 from d3mlabs/ai/118-pr-d-split-commandexecutor-into-a-dispat
4477c6b Update the manifest-loader contract note for the eager toolchain pass
3ad03c3 Constructor-inject CommandRunner; two messages replace the wait flag
2ac941a Route help through the command path; group and eager-load usage
a78ba14 Add the help builtin
c7ae57a Add Category trait to the Command hierarchy
2e05625 ai-flow /build: let's fix the fake classes, put them within the test class
a29b5e6 Merge main: sealed-module Command hierarchy, super() convention, and bin/test.rb runner
5d9c57b Merge pull request #121 from d3mlabs/ai/117-pr-a-close-the-sealed-command-hierarchy
b8ed631 Call super() in every initializer that derives from the Command hierarchy
5bcc76f Rework the seal: Command becomes a sealed module, BuiltinCommand the abstract open-edge class
5c68c85 Merge pull request #120 from d3mlabs/ai/116-pr-c-bin-test-rb-tee-suite-output-to-a-s
c62efc8 ai-flow /build: PR B: Typed child-process failure taxonomy in CommandRunner (CommandFailedError / CommandKilledError / CommandSpawnError) mapped to exit codes in Runner#exit_for
f09f845 ai-flow /build: PR D: Split CommandExecutor into a dispatching composite with injectable BuiltinExecutor / ProjectExecutor / OverriddenExecutor strategies (exec_into vs run_waiting)
0775515 ai-flow /build: PR A: Close the sealed Command hierarchy honestly — BuiltinBody interface, final BuiltinCommand holding a body, delete the sorbet-runtime ivar pokes, un-private CommandRepository
5a5fb41 ai-flow /build: PR C: bin/test.rb — tee suite output to a stable log artifact and pass file args through to rake TEST
4c89408 Merge pull request #115 from d3mlabs/ai/37-layer-the-dev-runner-application-service
04d461c Add the simplecov-cobertura gem RBI
81677f6 Upload cobertura to codecov instead of SimpleCov JSON
0a741f0 Cover the default factories, image credential providers, and nocov the sealed absurd arm
fe7c94e ai-flow /build: Layer the dev Runner (application service + boundary coercion)
d782b1a Merge pull request #107 from d3mlabs/ai/101-dev-clone-host-global-builtin-cloning-vi
f305ac9 ai-flow /build: codecov coverage missing
fac96ee ai-flow /build: dev clone: host-global builtin cloning via gh auth to the canonical $DEV_CD_ROOT path
d16b757 Merge pull request #100 from d3mlabs/jpd/99-pin-homebrew-installer
2ad614e Pin the Homebrew installer to a commit SHA (dev#99)
53e3616 Merge pull request #90 from d3mlabs/ai/89-gemskilllinker-links-minted-under-a-sand
95ee372 Merge pull request #97 from d3mlabs/ai/learn-promote-rbenv-libruby-rpath-hijack
f7edc33 chore: nudge origin-firing after ai-flow#57 (removal diffs skip green)
646f189 Merge pull request #98 from d3mlabs/jpd/proposal-checks-edited
50e913a proposal-checks: re-verify on PR body edits (ai-flow#54)
59a3146 ai-flow /learn: drop rbenv-libruby-rpath-hijack (promoted to the org tier)
f119987 Merge pull request #96 from d3mlabs/jpd/ai-flow-knowledge-repo
b0e7131 ai-flow config: opt dev into org-tier learning promotion
d17b2ff Merge pull request #95 from d3mlabs/jpd/94-self-defending-entrypoint
29b2e16 Test readability: one aliased scrub list, one property per test
f7197ac Drift guard: the unset list must cover what the running bundler exports
049bbc8 Probe the shim scrub with a stub ruby instead of a full dev command run
88fa953 bin/dev: scrub foreign bundler activation before Ruby boots
9cf868a Merge pull request #92 from d3mlabs/ai/60-plan-pull-mangles-files-with-an-empty-fr
6783469 Merge pull request #93 from d3mlabs/ai/learn-issue-60
991d46d ai-flow /build: capture learnings from the build pass
fe64507 ai-flow /build: Plan pull mangles files with an empty frontmatter block above the real one (double frontmatter)
d8db57d ai-flow /build: GemSkillLinker: links minted under a sandboxed session point into ephemeral sandbox cache paths
b4526ea Merge pull request #88 from d3mlabs/jpd/ast-transform-3.1.1
de9beaf Bump ast_transform to 3.1.1 and drop the heredoc-emission workaround
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant

@JPDuchesne
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' PR A: Close the sealed Command hierarchy honestly — sealed module Command, abstract BuiltinCommand open edge, delete the sorbet-runtime ivar pokes, un-private CommandRepository by d3mlabs-ai-flow[bot] · Pull Request #121 · d3mlabs/dev · GitHub
Skip to content

PR A: Close the sealed Command hierarchy honestly — sealed module Command, abstract BuiltinCommand open edge, delete the sorbet-runtime ivar pokes, un-private CommandRepository - #121

Merged
JPDuchesne merged 3 commits into
mainfrom
ai/117-pr-a-close-the-sealed-command-hierarchy
Aug 18, 2026

Conversation

@d3mlabs-ai-flow

Copy link
Copy Markdown
Contributor

Implements #117.

Requested by @JPDuchesne.

Closes#117

…uiltinBody interface, final BuiltinCommand holding a body, delete the sorbet-runtime ivar pokes, un-private CommandRepository
Co-authored-by: JPDuchesne <2636122+JPDuchesne@users.noreply.github.com>
@codecov

codecovBot commented Aug 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

…abstract open-edge class
A sealed module's included hook fires only for direct includers (include
never transfers singleton methods), so the hierarchy closes without the
BuiltinBody indirection or the final wrapper leaf: builtins subclass
BuiltinCommand directly, the data leaves are final!, and the composition
root wires builtins straight in. BuiltinCommand stays a class because
Sorbet flattens module mixins — a module open edge would re-include the
sealed Command in every builtin and fail the same-file check statically.
Co-authored-by: Cursor <cursoragent@cursor.com>
@JPDuchesneJPDuchesne changed the title PR A: Close the sealed Command hierarchy honestly — BuiltinBody interface, final BuiltinCommand holding a body, delete the sorbet-runtime ivar pokes, un-private CommandRepositoryPR A: Close the sealed Command hierarchy honestly — sealed module Command, abstract BuiltinCommand open edge, delete the sorbet-runtime ivar pokes, un-private CommandRepositoryAug 18, 2026
@JPDuchesne

Copy link
Copy Markdown
Contributor

Reworked per review discussion: the BuiltinBody interface and the final BuiltinCommand wrapper are gone.

  • Command is now a sealed! abstract!module with the trait defaults. A sealed module's runtime included hook fires only for direct includers — include never transfers singleton methods — so the seal holds with no sorbet-runtime ivar pokes and nothing to suppress.
  • BuiltinCommand is an abstract! class that includes Command beside it: the hierarchy's one declared open edge. Builtins subclass it directly again (class CacheCommand < BuiltinCommand), and the composition root wires them straight in — no wrapping.
  • It stays a class rather than a module because Sorbet flattens module mixins: a module open edge would make every builtin a direct includer of sealed Command and fail the same-file check statically. A superclass edge is not flattened, so it typechecks and is invisible to the seal's runtime hooks.
  • ProjectCommand and OverriddenCommand include Command and are final!, so descent is closed everywhere it isn't explicitly declared. CommandExecutor's exhaustive case + T.absurd is unchanged.
  • Tests now assert the real closure properties: direct include Dev::Command raises, subclassing either data leaf raises, and subclassing BuiltinCommand is the declared open edge.

srb tc clean, 887 tests / 0 failures, rubocop clean.

@stamps = stamps
@calls = []
@body = body
super()

@JPDuchesneJPDuchesneAug 18, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

/ask what's with this line change?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

/ask

The removed super() was a no-op, so the diff is dead-code cleanup in the test fake. ExecutorFakeBuiltin subclasses Dev::BuiltinCommand, and that base class (see src/dev/command.rb) defines no initialize of its own — it's an abstract class that only declares the abstract call method, and Command is a plain module with no constructor either. So super() fell through to Object#initialize, which takes no arguments and does nothing; the line initialized nothing and its presence wrongly implied the base class had constructor behavior to run. Removing it also matches how the real builtins under src/dev/builtins/ are written — e.g. CdCommand and CredCommand define initialize without calling super. Behavior of the tests in test/dev/command_executor_test.rb is unchanged. (One caveat: git history access is restricted in this checkout, so I can't confirm whether the line was a leftover from an earlier BuiltinCommand shape — the file's comments mention it previously carried sorbet-runtime workarounds — but as the code stands today the call is provably inert.)

⚙️ workflow run · model: claude-fable-5-high

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Resolved in b8ed631: the super() calls are restored — and the convention is now applied uniformly. The bot's replies were right that the calls are inert today, but that misses the point of the rule: an explicit super() means future base-class constructor state either runs automatically (defaulted params) or fails loudly at first instantiation (required params), instead of silently constructing half-initialized objects. The fakes had actually been the only callers — the 11 real builtins never called super() — so the commit adds it everywhere an initializer derives from the hierarchy: both data leaves, all builtins, and the fakes.

def initialize(desc: "a builtin", hidden: false)
@desc = desc
@hidden = hidden
super()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

/ask Same here, why not keep super?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Restored in b8ed631, along with every other initializer in the hierarchy (the real builtins had never called super() — the fakes were the outliers in the other direction). See the thread on command_executor_test.rb for the full rationale.

@staleness_exempt = staleness_exempt
@stamps = stamps
@calls = []
super()

@JPDuchesneJPDuchesneAug 18, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

/ask Same here, why not keep super?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

/ask

Because there is nothing for super() to reach: it's a pure no-op here. ServiceFakeBuiltin subclasses Dev::BuiltinCommand (src/dev/command.rb), and neither BuiltinCommand nor the Command module it includes defines an initialize — there is no parent state to set up, so super() falls through to Object#initialize, which does nothing. It isn't needed for the abstract guard either: sorbet-runtime's abstract! enforces non-instantiability by wrapping self.new on the abstract class, not initialize, so the guard runs regardless of whether the subclass calls super. Lint doesn't ask for it either — Lint/MissingSuper is disabled in the inherited rubocop-shopify config. Keeping it would be harmless but would imply the base class has initialization the fake must run, which it doesn't; dropping the dead call is the same reasoning as in the other thread. (If BuiltinCommand ever grows a real initializer, its subclasses would need explicit constructor wiring anyway, per the repo's constructor-injection convention.)

⚙️ workflow run · model: claude-fable-5-high

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Restored in b8ed631, along with every other initializer in the hierarchy. The bot is right that abstract! guards new rather than initialize and that Lint/MissingSuper is disabled in rubocop-shopify — but "provably inert today" is exactly the case the always-super convention exists for. See the thread on command_executor_test.rb for the full rationale.

…rchy
Ruby silently skips base initialization when a subclass overrides
initialize without super. The calls are inert today (no ancestor defines
initialize), but they make future base-class constructor state either
run automatically (defaulted params) or fail loudly at first
instantiation (required params) instead of constructing half-initialized
objects. Applied uniformly: the two data leaves, all builtins, and the
test fakes.
Co-authored-by: Cursor <cursoragent@cursor.com>
@JPDuchesne
JPDuchesne merged commit 5d9c57b into mainAug 18, 2026
26 checks passed
@JPDuchesne
JPDuchesne deleted the ai/117-pr-a-close-the-sealed-command-hierarchy branch August 18, 2026 17:47
JPDuchesne added a commit that referenced this pull request Aug 19, 2026
916fe89 Merge pull request #128 from d3mlabs/jpd/skill-installer-ephemeral-guard
b9ea143 Move the ephemeral-source guard to SkillInstaller, the seam all links share
36803f7 Merge pull request #127 from d3mlabs/jpd/capture-learning-root-cause-gate
d6e081e Name the wide-angle goal, not one command: an exact git-log depth invites checkbox compliance
f876dbe capture-learning: gate workaround learnings on root cause, add wide angle
7249198 Merge pull request #123 from d3mlabs/ai/119-pr-b-typed-child-process-failure-taxonom
e71063a Merge pull request #126 from d3mlabs/jpd/hermetic-scrub-guard
f911e38 Make the scrub-list guard hermetic: construct the bundler launch it measures
6c398b8 ai-flow /build: let's resolve conflicts
cb68d60 Merge pull request #122 from d3mlabs/ai/118-pr-d-split-commandexecutor-into-a-dispat
4477c6b Update the manifest-loader contract note for the eager toolchain pass
3ad03c3 Constructor-inject CommandRunner; two messages replace the wait flag
2ac941a Route help through the command path; group and eager-load usage
a78ba14 Add the help builtin
c7ae57a Add Category trait to the Command hierarchy
2e05625 ai-flow /build: let's fix the fake classes, put them within the test class
a29b5e6 Merge main: sealed-module Command hierarchy, super() convention, and bin/test.rb runner
5d9c57b Merge pull request #121 from d3mlabs/ai/117-pr-a-close-the-sealed-command-hierarchy
b8ed631 Call super() in every initializer that derives from the Command hierarchy
5bcc76f Rework the seal: Command becomes a sealed module, BuiltinCommand the abstract open-edge class
5c68c85 Merge pull request #120 from d3mlabs/ai/116-pr-c-bin-test-rb-tee-suite-output-to-a-s
c62efc8 ai-flow /build: PR B: Typed child-process failure taxonomy in CommandRunner (CommandFailedError / CommandKilledError / CommandSpawnError) mapped to exit codes in Runner#exit_for
f09f845 ai-flow /build: PR D: Split CommandExecutor into a dispatching composite with injectable BuiltinExecutor / ProjectExecutor / OverriddenExecutor strategies (exec_into vs run_waiting)
0775515 ai-flow /build: PR A: Close the sealed Command hierarchy honestly — BuiltinBody interface, final BuiltinCommand holding a body, delete the sorbet-runtime ivar pokes, un-private CommandRepository
5a5fb41 ai-flow /build: PR C: bin/test.rb — tee suite output to a stable log artifact and pass file args through to rake TEST
4c89408 Merge pull request #115 from d3mlabs/ai/37-layer-the-dev-runner-application-service
04d461c Add the simplecov-cobertura gem RBI
81677f6 Upload cobertura to codecov instead of SimpleCov JSON
0a741f0 Cover the default factories, image credential providers, and nocov the sealed absurd arm
fe7c94e ai-flow /build: Layer the dev Runner (application service + boundary coercion)
d782b1a Merge pull request #107 from d3mlabs/ai/101-dev-clone-host-global-builtin-cloning-vi
f305ac9 ai-flow /build: codecov coverage missing
fac96ee ai-flow /build: dev clone: host-global builtin cloning via gh auth to the canonical $DEV_CD_ROOT path
d16b757 Merge pull request #100 from d3mlabs/jpd/99-pin-homebrew-installer
2ad614e Pin the Homebrew installer to a commit SHA (dev#99)
53e3616 Merge pull request #90 from d3mlabs/ai/89-gemskilllinker-links-minted-under-a-sand
95ee372 Merge pull request #97 from d3mlabs/ai/learn-promote-rbenv-libruby-rpath-hijack
f7edc33 chore: nudge origin-firing after ai-flow#57 (removal diffs skip green)
646f189 Merge pull request #98 from d3mlabs/jpd/proposal-checks-edited
50e913a proposal-checks: re-verify on PR body edits (ai-flow#54)
59a3146 ai-flow /learn: drop rbenv-libruby-rpath-hijack (promoted to the org tier)
f119987 Merge pull request #96 from d3mlabs/jpd/ai-flow-knowledge-repo
b0e7131 ai-flow config: opt dev into org-tier learning promotion
d17b2ff Merge pull request #95 from d3mlabs/jpd/94-self-defending-entrypoint
29b2e16 Test readability: one aliased scrub list, one property per test
f7197ac Drift guard: the unset list must cover what the running bundler exports
049bbc8 Probe the shim scrub with a stub ruby instead of a full dev command run
88fa953 bin/dev: scrub foreign bundler activation before Ruby boots
9cf868a Merge pull request #92 from d3mlabs/ai/60-plan-pull-mangles-files-with-an-empty-fr
6783469 Merge pull request #93 from d3mlabs/ai/learn-issue-60
991d46d ai-flow /build: capture learnings from the build pass
fe64507 ai-flow /build: Plan pull mangles files with an empty frontmatter block above the real one (double frontmatter)
d8db57d ai-flow /build: GemSkillLinker: links minted under a sandboxed session point into ephemeral sandbox cache paths
b4526ea Merge pull request #88 from d3mlabs/jpd/ast-transform-3.1.1
de9beaf Bump ast_transform to 3.1.1 and drop the heredoc-emission workaround
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant

@JPDuchesne
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' PR A: Close the sealed Command hierarchy honestly — sealed module Command, abstract BuiltinCommand open edge, delete the sorbet-runtime ivar pokes, un-private CommandRepository by d3mlabs-ai-flow[bot] · Pull Request #121 · d3mlabs/dev · GitHub
Skip to content

PR A: Close the sealed Command hierarchy honestly — sealed module Command, abstract BuiltinCommand open edge, delete the sorbet-runtime ivar pokes, un-private CommandRepository - #121

Merged
JPDuchesne merged 3 commits into
mainfrom
ai/117-pr-a-close-the-sealed-command-hierarchy
Aug 18, 2026

Conversation

@d3mlabs-ai-flow

Copy link
Copy Markdown
Contributor

Implements #117.

Requested by @JPDuchesne.

Closes#117

…uiltinBody interface, final BuiltinCommand holding a body, delete the sorbet-runtime ivar pokes, un-private CommandRepository
Co-authored-by: JPDuchesne <2636122+JPDuchesne@users.noreply.github.com>
@codecov

codecovBot commented Aug 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

…abstract open-edge class
A sealed module's included hook fires only for direct includers (include
never transfers singleton methods), so the hierarchy closes without the
BuiltinBody indirection or the final wrapper leaf: builtins subclass
BuiltinCommand directly, the data leaves are final!, and the composition
root wires builtins straight in. BuiltinCommand stays a class because
Sorbet flattens module mixins — a module open edge would re-include the
sealed Command in every builtin and fail the same-file check statically.
Co-authored-by: Cursor <cursoragent@cursor.com>
@JPDuchesneJPDuchesne changed the title PR A: Close the sealed Command hierarchy honestly — BuiltinBody interface, final BuiltinCommand holding a body, delete the sorbet-runtime ivar pokes, un-private CommandRepositoryPR A: Close the sealed Command hierarchy honestly — sealed module Command, abstract BuiltinCommand open edge, delete the sorbet-runtime ivar pokes, un-private CommandRepositoryAug 18, 2026
@JPDuchesne

Copy link
Copy Markdown
Contributor

Reworked per review discussion: the BuiltinBody interface and the final BuiltinCommand wrapper are gone.

  • Command is now a sealed! abstract!module with the trait defaults. A sealed module's runtime included hook fires only for direct includers — include never transfers singleton methods — so the seal holds with no sorbet-runtime ivar pokes and nothing to suppress.
  • BuiltinCommand is an abstract! class that includes Command beside it: the hierarchy's one declared open edge. Builtins subclass it directly again (class CacheCommand < BuiltinCommand), and the composition root wires them straight in — no wrapping.
  • It stays a class rather than a module because Sorbet flattens module mixins: a module open edge would make every builtin a direct includer of sealed Command and fail the same-file check statically. A superclass edge is not flattened, so it typechecks and is invisible to the seal's runtime hooks.
  • ProjectCommand and OverriddenCommand include Command and are final!, so descent is closed everywhere it isn't explicitly declared. CommandExecutor's exhaustive case + T.absurd is unchanged.
  • Tests now assert the real closure properties: direct include Dev::Command raises, subclassing either data leaf raises, and subclassing BuiltinCommand is the declared open edge.

srb tc clean, 887 tests / 0 failures, rubocop clean.

@stamps = stamps
@calls = []
@body = body
super()

@JPDuchesneJPDuchesneAug 18, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

/ask what's with this line change?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

/ask

The removed super() was a no-op, so the diff is dead-code cleanup in the test fake. ExecutorFakeBuiltin subclasses Dev::BuiltinCommand, and that base class (see src/dev/command.rb) defines no initialize of its own — it's an abstract class that only declares the abstract call method, and Command is a plain module with no constructor either. So super() fell through to Object#initialize, which takes no arguments and does nothing; the line initialized nothing and its presence wrongly implied the base class had constructor behavior to run. Removing it also matches how the real builtins under src/dev/builtins/ are written — e.g. CdCommand and CredCommand define initialize without calling super. Behavior of the tests in test/dev/command_executor_test.rb is unchanged. (One caveat: git history access is restricted in this checkout, so I can't confirm whether the line was a leftover from an earlier BuiltinCommand shape — the file's comments mention it previously carried sorbet-runtime workarounds — but as the code stands today the call is provably inert.)

⚙️ workflow run · model: claude-fable-5-high

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Resolved in b8ed631: the super() calls are restored — and the convention is now applied uniformly. The bot's replies were right that the calls are inert today, but that misses the point of the rule: an explicit super() means future base-class constructor state either runs automatically (defaulted params) or fails loudly at first instantiation (required params), instead of silently constructing half-initialized objects. The fakes had actually been the only callers — the 11 real builtins never called super() — so the commit adds it everywhere an initializer derives from the hierarchy: both data leaves, all builtins, and the fakes.

def initialize(desc: "a builtin", hidden: false)
@desc = desc
@hidden = hidden
super()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

/ask Same here, why not keep super?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Restored in b8ed631, along with every other initializer in the hierarchy (the real builtins had never called super() — the fakes were the outliers in the other direction). See the thread on command_executor_test.rb for the full rationale.

@staleness_exempt = staleness_exempt
@stamps = stamps
@calls = []
super()

@JPDuchesneJPDuchesneAug 18, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

/ask Same here, why not keep super?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

/ask

Because there is nothing for super() to reach: it's a pure no-op here. ServiceFakeBuiltin subclasses Dev::BuiltinCommand (src/dev/command.rb), and neither BuiltinCommand nor the Command module it includes defines an initialize — there is no parent state to set up, so super() falls through to Object#initialize, which does nothing. It isn't needed for the abstract guard either: sorbet-runtime's abstract! enforces non-instantiability by wrapping self.new on the abstract class, not initialize, so the guard runs regardless of whether the subclass calls super. Lint doesn't ask for it either — Lint/MissingSuper is disabled in the inherited rubocop-shopify config. Keeping it would be harmless but would imply the base class has initialization the fake must run, which it doesn't; dropping the dead call is the same reasoning as in the other thread. (If BuiltinCommand ever grows a real initializer, its subclasses would need explicit constructor wiring anyway, per the repo's constructor-injection convention.)

⚙️ workflow run · model: claude-fable-5-high

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Restored in b8ed631, along with every other initializer in the hierarchy. The bot is right that abstract! guards new rather than initialize and that Lint/MissingSuper is disabled in rubocop-shopify — but "provably inert today" is exactly the case the always-super convention exists for. See the thread on command_executor_test.rb for the full rationale.

…rchy
Ruby silently skips base initialization when a subclass overrides
initialize without super. The calls are inert today (no ancestor defines
initialize), but they make future base-class constructor state either
run automatically (defaulted params) or fail loudly at first
instantiation (required params) instead of constructing half-initialized
objects. Applied uniformly: the two data leaves, all builtins, and the
test fakes.
Co-authored-by: Cursor <cursoragent@cursor.com>
@JPDuchesne
JPDuchesne merged commit 5d9c57b into mainAug 18, 2026
26 checks passed
@JPDuchesne
JPDuchesne deleted the ai/117-pr-a-close-the-sealed-command-hierarchy branch August 18, 2026 17:47
JPDuchesne added a commit that referenced this pull request Aug 19, 2026
916fe89 Merge pull request #128 from d3mlabs/jpd/skill-installer-ephemeral-guard
b9ea143 Move the ephemeral-source guard to SkillInstaller, the seam all links share
36803f7 Merge pull request #127 from d3mlabs/jpd/capture-learning-root-cause-gate
d6e081e Name the wide-angle goal, not one command: an exact git-log depth invites checkbox compliance
f876dbe capture-learning: gate workaround learnings on root cause, add wide angle
7249198 Merge pull request #123 from d3mlabs/ai/119-pr-b-typed-child-process-failure-taxonom
e71063a Merge pull request #126 from d3mlabs/jpd/hermetic-scrub-guard
f911e38 Make the scrub-list guard hermetic: construct the bundler launch it measures
6c398b8 ai-flow /build: let's resolve conflicts
cb68d60 Merge pull request #122 from d3mlabs/ai/118-pr-d-split-commandexecutor-into-a-dispat
4477c6b Update the manifest-loader contract note for the eager toolchain pass
3ad03c3 Constructor-inject CommandRunner; two messages replace the wait flag
2ac941a Route help through the command path; group and eager-load usage
a78ba14 Add the help builtin
c7ae57a Add Category trait to the Command hierarchy
2e05625 ai-flow /build: let's fix the fake classes, put them within the test class
a29b5e6 Merge main: sealed-module Command hierarchy, super() convention, and bin/test.rb runner
5d9c57b Merge pull request #121 from d3mlabs/ai/117-pr-a-close-the-sealed-command-hierarchy
b8ed631 Call super() in every initializer that derives from the Command hierarchy
5bcc76f Rework the seal: Command becomes a sealed module, BuiltinCommand the abstract open-edge class
5c68c85 Merge pull request #120 from d3mlabs/ai/116-pr-c-bin-test-rb-tee-suite-output-to-a-s
c62efc8 ai-flow /build: PR B: Typed child-process failure taxonomy in CommandRunner (CommandFailedError / CommandKilledError / CommandSpawnError) mapped to exit codes in Runner#exit_for
f09f845 ai-flow /build: PR D: Split CommandExecutor into a dispatching composite with injectable BuiltinExecutor / ProjectExecutor / OverriddenExecutor strategies (exec_into vs run_waiting)
0775515 ai-flow /build: PR A: Close the sealed Command hierarchy honestly — BuiltinBody interface, final BuiltinCommand holding a body, delete the sorbet-runtime ivar pokes, un-private CommandRepository
5a5fb41 ai-flow /build: PR C: bin/test.rb — tee suite output to a stable log artifact and pass file args through to rake TEST
4c89408 Merge pull request #115 from d3mlabs/ai/37-layer-the-dev-runner-application-service
04d461c Add the simplecov-cobertura gem RBI
81677f6 Upload cobertura to codecov instead of SimpleCov JSON
0a741f0 Cover the default factories, image credential providers, and nocov the sealed absurd arm
fe7c94e ai-flow /build: Layer the dev Runner (application service + boundary coercion)
d782b1a Merge pull request #107 from d3mlabs/ai/101-dev-clone-host-global-builtin-cloning-vi
f305ac9 ai-flow /build: codecov coverage missing
fac96ee ai-flow /build: dev clone: host-global builtin cloning via gh auth to the canonical $DEV_CD_ROOT path
d16b757 Merge pull request #100 from d3mlabs/jpd/99-pin-homebrew-installer
2ad614e Pin the Homebrew installer to a commit SHA (dev#99)
53e3616 Merge pull request #90 from d3mlabs/ai/89-gemskilllinker-links-minted-under-a-sand
95ee372 Merge pull request #97 from d3mlabs/ai/learn-promote-rbenv-libruby-rpath-hijack
f7edc33 chore: nudge origin-firing after ai-flow#57 (removal diffs skip green)
646f189 Merge pull request #98 from d3mlabs/jpd/proposal-checks-edited
50e913a proposal-checks: re-verify on PR body edits (ai-flow#54)
59a3146 ai-flow /learn: drop rbenv-libruby-rpath-hijack (promoted to the org tier)
f119987 Merge pull request #96 from d3mlabs/jpd/ai-flow-knowledge-repo
b0e7131 ai-flow config: opt dev into org-tier learning promotion
d17b2ff Merge pull request #95 from d3mlabs/jpd/94-self-defending-entrypoint
29b2e16 Test readability: one aliased scrub list, one property per test
f7197ac Drift guard: the unset list must cover what the running bundler exports
049bbc8 Probe the shim scrub with a stub ruby instead of a full dev command run
88fa953 bin/dev: scrub foreign bundler activation before Ruby boots
9cf868a Merge pull request #92 from d3mlabs/ai/60-plan-pull-mangles-files-with-an-empty-fr
6783469 Merge pull request #93 from d3mlabs/ai/learn-issue-60
991d46d ai-flow /build: capture learnings from the build pass
fe64507 ai-flow /build: Plan pull mangles files with an empty frontmatter block above the real one (double frontmatter)
d8db57d ai-flow /build: GemSkillLinker: links minted under a sandboxed session point into ephemeral sandbox cache paths
b4526ea Merge pull request #88 from d3mlabs/jpd/ast-transform-3.1.1
de9beaf Bump ast_transform to 3.1.1 and drop the heredoc-emission workaround
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant

@JPDuchesne
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' PR A: Close the sealed Command hierarchy honestly — sealed module Command, abstract BuiltinCommand open edge, delete the sorbet-runtime ivar pokes, un-private CommandRepository by d3mlabs-ai-flow[bot] · Pull Request #121 · d3mlabs/dev · GitHub
Skip to content

PR A: Close the sealed Command hierarchy honestly — sealed module Command, abstract BuiltinCommand open edge, delete the sorbet-runtime ivar pokes, un-private CommandRepository - #121

Merged
JPDuchesne merged 3 commits into
mainfrom
ai/117-pr-a-close-the-sealed-command-hierarchy
Aug 18, 2026

Conversation

@d3mlabs-ai-flow

Copy link
Copy Markdown
Contributor

Implements #117.

Requested by @JPDuchesne.

Closes#117

…uiltinBody interface, final BuiltinCommand holding a body, delete the sorbet-runtime ivar pokes, un-private CommandRepository
Co-authored-by: JPDuchesne <2636122+JPDuchesne@users.noreply.github.com>
@codecov

codecovBot commented Aug 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

…abstract open-edge class
A sealed module's included hook fires only for direct includers (include
never transfers singleton methods), so the hierarchy closes without the
BuiltinBody indirection or the final wrapper leaf: builtins subclass
BuiltinCommand directly, the data leaves are final!, and the composition
root wires builtins straight in. BuiltinCommand stays a class because
Sorbet flattens module mixins — a module open edge would re-include the
sealed Command in every builtin and fail the same-file check statically.
Co-authored-by: Cursor <cursoragent@cursor.com>
@JPDuchesneJPDuchesne changed the title PR A: Close the sealed Command hierarchy honestly — BuiltinBody interface, final BuiltinCommand holding a body, delete the sorbet-runtime ivar pokes, un-private CommandRepositoryPR A: Close the sealed Command hierarchy honestly — sealed module Command, abstract BuiltinCommand open edge, delete the sorbet-runtime ivar pokes, un-private CommandRepositoryAug 18, 2026
@JPDuchesne

Copy link
Copy Markdown
Contributor

Reworked per review discussion: the BuiltinBody interface and the final BuiltinCommand wrapper are gone.

  • Command is now a sealed! abstract!module with the trait defaults. A sealed module's runtime included hook fires only for direct includers — include never transfers singleton methods — so the seal holds with no sorbet-runtime ivar pokes and nothing to suppress.
  • BuiltinCommand is an abstract! class that includes Command beside it: the hierarchy's one declared open edge. Builtins subclass it directly again (class CacheCommand < BuiltinCommand), and the composition root wires them straight in — no wrapping.
  • It stays a class rather than a module because Sorbet flattens module mixins: a module open edge would make every builtin a direct includer of sealed Command and fail the same-file check statically. A superclass edge is not flattened, so it typechecks and is invisible to the seal's runtime hooks.
  • ProjectCommand and OverriddenCommand include Command and are final!, so descent is closed everywhere it isn't explicitly declared. CommandExecutor's exhaustive case + T.absurd is unchanged.
  • Tests now assert the real closure properties: direct include Dev::Command raises, subclassing either data leaf raises, and subclassing BuiltinCommand is the declared open edge.

srb tc clean, 887 tests / 0 failures, rubocop clean.

@stamps = stamps
@calls = []
@body = body
super()

@JPDuchesneJPDuchesneAug 18, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

/ask what's with this line change?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

/ask

The removed super() was a no-op, so the diff is dead-code cleanup in the test fake. ExecutorFakeBuiltin subclasses Dev::BuiltinCommand, and that base class (see src/dev/command.rb) defines no initialize of its own — it's an abstract class that only declares the abstract call method, and Command is a plain module with no constructor either. So super() fell through to Object#initialize, which takes no arguments and does nothing; the line initialized nothing and its presence wrongly implied the base class had constructor behavior to run. Removing it also matches how the real builtins under src/dev/builtins/ are written — e.g. CdCommand and CredCommand define initialize without calling super. Behavior of the tests in test/dev/command_executor_test.rb is unchanged. (One caveat: git history access is restricted in this checkout, so I can't confirm whether the line was a leftover from an earlier BuiltinCommand shape — the file's comments mention it previously carried sorbet-runtime workarounds — but as the code stands today the call is provably inert.)

⚙️ workflow run · model: claude-fable-5-high

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Resolved in b8ed631: the super() calls are restored — and the convention is now applied uniformly. The bot's replies were right that the calls are inert today, but that misses the point of the rule: an explicit super() means future base-class constructor state either runs automatically (defaulted params) or fails loudly at first instantiation (required params), instead of silently constructing half-initialized objects. The fakes had actually been the only callers — the 11 real builtins never called super() — so the commit adds it everywhere an initializer derives from the hierarchy: both data leaves, all builtins, and the fakes.

def initialize(desc: "a builtin", hidden: false)
@desc = desc
@hidden = hidden
super()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

/ask Same here, why not keep super?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Restored in b8ed631, along with every other initializer in the hierarchy (the real builtins had never called super() — the fakes were the outliers in the other direction). See the thread on command_executor_test.rb for the full rationale.

@staleness_exempt = staleness_exempt
@stamps = stamps
@calls = []
super()

@JPDuchesneJPDuchesneAug 18, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

/ask Same here, why not keep super?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

/ask

Because there is nothing for super() to reach: it's a pure no-op here. ServiceFakeBuiltin subclasses Dev::BuiltinCommand (src/dev/command.rb), and neither BuiltinCommand nor the Command module it includes defines an initialize — there is no parent state to set up, so super() falls through to Object#initialize, which does nothing. It isn't needed for the abstract guard either: sorbet-runtime's abstract! enforces non-instantiability by wrapping self.new on the abstract class, not initialize, so the guard runs regardless of whether the subclass calls super. Lint doesn't ask for it either — Lint/MissingSuper is disabled in the inherited rubocop-shopify config. Keeping it would be harmless but would imply the base class has initialization the fake must run, which it doesn't; dropping the dead call is the same reasoning as in the other thread. (If BuiltinCommand ever grows a real initializer, its subclasses would need explicit constructor wiring anyway, per the repo's constructor-injection convention.)

⚙️ workflow run · model: claude-fable-5-high

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Restored in b8ed631, along with every other initializer in the hierarchy. The bot is right that abstract! guards new rather than initialize and that Lint/MissingSuper is disabled in rubocop-shopify — but "provably inert today" is exactly the case the always-super convention exists for. See the thread on command_executor_test.rb for the full rationale.

…rchy
Ruby silently skips base initialization when a subclass overrides
initialize without super. The calls are inert today (no ancestor defines
initialize), but they make future base-class constructor state either
run automatically (defaulted params) or fail loudly at first
instantiation (required params) instead of constructing half-initialized
objects. Applied uniformly: the two data leaves, all builtins, and the
test fakes.
Co-authored-by: Cursor <cursoragent@cursor.com>
@JPDuchesne
JPDuchesne merged commit 5d9c57b into mainAug 18, 2026
26 checks passed
@JPDuchesne
JPDuchesne deleted the ai/117-pr-a-close-the-sealed-command-hierarchy branch August 18, 2026 17:47
JPDuchesne added a commit that referenced this pull request Aug 19, 2026
916fe89 Merge pull request #128 from d3mlabs/jpd/skill-installer-ephemeral-guard
b9ea143 Move the ephemeral-source guard to SkillInstaller, the seam all links share
36803f7 Merge pull request #127 from d3mlabs/jpd/capture-learning-root-cause-gate
d6e081e Name the wide-angle goal, not one command: an exact git-log depth invites checkbox compliance
f876dbe capture-learning: gate workaround learnings on root cause, add wide angle
7249198 Merge pull request #123 from d3mlabs/ai/119-pr-b-typed-child-process-failure-taxonom
e71063a Merge pull request #126 from d3mlabs/jpd/hermetic-scrub-guard
f911e38 Make the scrub-list guard hermetic: construct the bundler launch it measures
6c398b8 ai-flow /build: let's resolve conflicts
cb68d60 Merge pull request #122 from d3mlabs/ai/118-pr-d-split-commandexecutor-into-a-dispat
4477c6b Update the manifest-loader contract note for the eager toolchain pass
3ad03c3 Constructor-inject CommandRunner; two messages replace the wait flag
2ac941a Route help through the command path; group and eager-load usage
a78ba14 Add the help builtin
c7ae57a Add Category trait to the Command hierarchy
2e05625 ai-flow /build: let's fix the fake classes, put them within the test class
a29b5e6 Merge main: sealed-module Command hierarchy, super() convention, and bin/test.rb runner
5d9c57b Merge pull request #121 from d3mlabs/ai/117-pr-a-close-the-sealed-command-hierarchy
b8ed631 Call super() in every initializer that derives from the Command hierarchy
5bcc76f Rework the seal: Command becomes a sealed module, BuiltinCommand the abstract open-edge class
5c68c85 Merge pull request #120 from d3mlabs/ai/116-pr-c-bin-test-rb-tee-suite-output-to-a-s
c62efc8 ai-flow /build: PR B: Typed child-process failure taxonomy in CommandRunner (CommandFailedError / CommandKilledError / CommandSpawnError) mapped to exit codes in Runner#exit_for
f09f845 ai-flow /build: PR D: Split CommandExecutor into a dispatching composite with injectable BuiltinExecutor / ProjectExecutor / OverriddenExecutor strategies (exec_into vs run_waiting)
0775515 ai-flow /build: PR A: Close the sealed Command hierarchy honestly — BuiltinBody interface, final BuiltinCommand holding a body, delete the sorbet-runtime ivar pokes, un-private CommandRepository
5a5fb41 ai-flow /build: PR C: bin/test.rb — tee suite output to a stable log artifact and pass file args through to rake TEST
4c89408 Merge pull request #115 from d3mlabs/ai/37-layer-the-dev-runner-application-service
04d461c Add the simplecov-cobertura gem RBI
81677f6 Upload cobertura to codecov instead of SimpleCov JSON
0a741f0 Cover the default factories, image credential providers, and nocov the sealed absurd arm
fe7c94e ai-flow /build: Layer the dev Runner (application service + boundary coercion)
d782b1a Merge pull request #107 from d3mlabs/ai/101-dev-clone-host-global-builtin-cloning-vi
f305ac9 ai-flow /build: codecov coverage missing
fac96ee ai-flow /build: dev clone: host-global builtin cloning via gh auth to the canonical $DEV_CD_ROOT path
d16b757 Merge pull request #100 from d3mlabs/jpd/99-pin-homebrew-installer
2ad614e Pin the Homebrew installer to a commit SHA (dev#99)
53e3616 Merge pull request #90 from d3mlabs/ai/89-gemskilllinker-links-minted-under-a-sand
95ee372 Merge pull request #97 from d3mlabs/ai/learn-promote-rbenv-libruby-rpath-hijack
f7edc33 chore: nudge origin-firing after ai-flow#57 (removal diffs skip green)
646f189 Merge pull request #98 from d3mlabs/jpd/proposal-checks-edited
50e913a proposal-checks: re-verify on PR body edits (ai-flow#54)
59a3146 ai-flow /learn: drop rbenv-libruby-rpath-hijack (promoted to the org tier)
f119987 Merge pull request #96 from d3mlabs/jpd/ai-flow-knowledge-repo
b0e7131 ai-flow config: opt dev into org-tier learning promotion
d17b2ff Merge pull request #95 from d3mlabs/jpd/94-self-defending-entrypoint
29b2e16 Test readability: one aliased scrub list, one property per test
f7197ac Drift guard: the unset list must cover what the running bundler exports
049bbc8 Probe the shim scrub with a stub ruby instead of a full dev command run
88fa953 bin/dev: scrub foreign bundler activation before Ruby boots
9cf868a Merge pull request #92 from d3mlabs/ai/60-plan-pull-mangles-files-with-an-empty-fr
6783469 Merge pull request #93 from d3mlabs/ai/learn-issue-60
991d46d ai-flow /build: capture learnings from the build pass
fe64507 ai-flow /build: Plan pull mangles files with an empty frontmatter block above the real one (double frontmatter)
d8db57d ai-flow /build: GemSkillLinker: links minted under a sandboxed session point into ephemeral sandbox cache paths
b4526ea Merge pull request #88 from d3mlabs/jpd/ast-transform-3.1.1
de9beaf Bump ast_transform to 3.1.1 and drop the heredoc-emission workaround
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant

@JPDuchesne
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' PR A: Close the sealed Command hierarchy honestly — sealed module Command, abstract BuiltinCommand open edge, delete the sorbet-runtime ivar pokes, un-private CommandRepository by d3mlabs-ai-flow[bot] · Pull Request #121 · d3mlabs/dev · GitHub
Skip to content

PR A: Close the sealed Command hierarchy honestly — sealed module Command, abstract BuiltinCommand open edge, delete the sorbet-runtime ivar pokes, un-private CommandRepository - #121

Merged
JPDuchesne merged 3 commits into
mainfrom
ai/117-pr-a-close-the-sealed-command-hierarchy
Aug 18, 2026

Conversation

@d3mlabs-ai-flow

Copy link
Copy Markdown
Contributor

Implements #117.

Requested by @JPDuchesne.

Closes#117

…uiltinBody interface, final BuiltinCommand holding a body, delete the sorbet-runtime ivar pokes, un-private CommandRepository
Co-authored-by: JPDuchesne <2636122+JPDuchesne@users.noreply.github.com>
@codecov

codecovBot commented Aug 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

…abstract open-edge class
A sealed module's included hook fires only for direct includers (include
never transfers singleton methods), so the hierarchy closes without the
BuiltinBody indirection or the final wrapper leaf: builtins subclass
BuiltinCommand directly, the data leaves are final!, and the composition
root wires builtins straight in. BuiltinCommand stays a class because
Sorbet flattens module mixins — a module open edge would re-include the
sealed Command in every builtin and fail the same-file check statically.
Co-authored-by: Cursor <cursoragent@cursor.com>
@JPDuchesneJPDuchesne changed the title PR A: Close the sealed Command hierarchy honestly — BuiltinBody interface, final BuiltinCommand holding a body, delete the sorbet-runtime ivar pokes, un-private CommandRepositoryPR A: Close the sealed Command hierarchy honestly — sealed module Command, abstract BuiltinCommand open edge, delete the sorbet-runtime ivar pokes, un-private CommandRepositoryAug 18, 2026
@JPDuchesne

Copy link
Copy Markdown
Contributor

Reworked per review discussion: the BuiltinBody interface and the final BuiltinCommand wrapper are gone.

  • Command is now a sealed! abstract!module with the trait defaults. A sealed module's runtime included hook fires only for direct includers — include never transfers singleton methods — so the seal holds with no sorbet-runtime ivar pokes and nothing to suppress.
  • BuiltinCommand is an abstract! class that includes Command beside it: the hierarchy's one declared open edge. Builtins subclass it directly again (class CacheCommand < BuiltinCommand), and the composition root wires them straight in — no wrapping.
  • It stays a class rather than a module because Sorbet flattens module mixins: a module open edge would make every builtin a direct includer of sealed Command and fail the same-file check statically. A superclass edge is not flattened, so it typechecks and is invisible to the seal's runtime hooks.
  • ProjectCommand and OverriddenCommand include Command and are final!, so descent is closed everywhere it isn't explicitly declared. CommandExecutor's exhaustive case + T.absurd is unchanged.
  • Tests now assert the real closure properties: direct include Dev::Command raises, subclassing either data leaf raises, and subclassing BuiltinCommand is the declared open edge.

srb tc clean, 887 tests / 0 failures, rubocop clean.

@stamps = stamps
@calls = []
@body = body
super()

@JPDuchesneJPDuchesneAug 18, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

/ask what's with this line change?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

/ask

The removed super() was a no-op, so the diff is dead-code cleanup in the test fake. ExecutorFakeBuiltin subclasses Dev::BuiltinCommand, and that base class (see src/dev/command.rb) defines no initialize of its own — it's an abstract class that only declares the abstract call method, and Command is a plain module with no constructor either. So super() fell through to Object#initialize, which takes no arguments and does nothing; the line initialized nothing and its presence wrongly implied the base class had constructor behavior to run. Removing it also matches how the real builtins under src/dev/builtins/ are written — e.g. CdCommand and CredCommand define initialize without calling super. Behavior of the tests in test/dev/command_executor_test.rb is unchanged. (One caveat: git history access is restricted in this checkout, so I can't confirm whether the line was a leftover from an earlier BuiltinCommand shape — the file's comments mention it previously carried sorbet-runtime workarounds — but as the code stands today the call is provably inert.)

⚙️ workflow run · model: claude-fable-5-high

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Resolved in b8ed631: the super() calls are restored — and the convention is now applied uniformly. The bot's replies were right that the calls are inert today, but that misses the point of the rule: an explicit super() means future base-class constructor state either runs automatically (defaulted params) or fails loudly at first instantiation (required params), instead of silently constructing half-initialized objects. The fakes had actually been the only callers — the 11 real builtins never called super() — so the commit adds it everywhere an initializer derives from the hierarchy: both data leaves, all builtins, and the fakes.

def initialize(desc: "a builtin", hidden: false)
@desc = desc
@hidden = hidden
super()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

/ask Same here, why not keep super?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Restored in b8ed631, along with every other initializer in the hierarchy (the real builtins had never called super() — the fakes were the outliers in the other direction). See the thread on command_executor_test.rb for the full rationale.

@staleness_exempt = staleness_exempt
@stamps = stamps
@calls = []
super()

@JPDuchesneJPDuchesneAug 18, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

/ask Same here, why not keep super?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

/ask

Because there is nothing for super() to reach: it's a pure no-op here. ServiceFakeBuiltin subclasses Dev::BuiltinCommand (src/dev/command.rb), and neither BuiltinCommand nor the Command module it includes defines an initialize — there is no parent state to set up, so super() falls through to Object#initialize, which does nothing. It isn't needed for the abstract guard either: sorbet-runtime's abstract! enforces non-instantiability by wrapping self.new on the abstract class, not initialize, so the guard runs regardless of whether the subclass calls super. Lint doesn't ask for it either — Lint/MissingSuper is disabled in the inherited rubocop-shopify config. Keeping it would be harmless but would imply the base class has initialization the fake must run, which it doesn't; dropping the dead call is the same reasoning as in the other thread. (If BuiltinCommand ever grows a real initializer, its subclasses would need explicit constructor wiring anyway, per the repo's constructor-injection convention.)

⚙️ workflow run · model: claude-fable-5-high

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Restored in b8ed631, along with every other initializer in the hierarchy. The bot is right that abstract! guards new rather than initialize and that Lint/MissingSuper is disabled in rubocop-shopify — but "provably inert today" is exactly the case the always-super convention exists for. See the thread on command_executor_test.rb for the full rationale.

…rchy
Ruby silently skips base initialization when a subclass overrides
initialize without super. The calls are inert today (no ancestor defines
initialize), but they make future base-class constructor state either
run automatically (defaulted params) or fail loudly at first
instantiation (required params) instead of constructing half-initialized
objects. Applied uniformly: the two data leaves, all builtins, and the
test fakes.
Co-authored-by: Cursor <cursoragent@cursor.com>
@JPDuchesne
JPDuchesne merged commit 5d9c57b into mainAug 18, 2026
26 checks passed
@JPDuchesne
JPDuchesne deleted the ai/117-pr-a-close-the-sealed-command-hierarchy branch August 18, 2026 17:47
JPDuchesne added a commit that referenced this pull request Aug 19, 2026
916fe89 Merge pull request #128 from d3mlabs/jpd/skill-installer-ephemeral-guard
b9ea143 Move the ephemeral-source guard to SkillInstaller, the seam all links share
36803f7 Merge pull request #127 from d3mlabs/jpd/capture-learning-root-cause-gate
d6e081e Name the wide-angle goal, not one command: an exact git-log depth invites checkbox compliance
f876dbe capture-learning: gate workaround learnings on root cause, add wide angle
7249198 Merge pull request #123 from d3mlabs/ai/119-pr-b-typed-child-process-failure-taxonom
e71063a Merge pull request #126 from d3mlabs/jpd/hermetic-scrub-guard
f911e38 Make the scrub-list guard hermetic: construct the bundler launch it measures
6c398b8 ai-flow /build: let's resolve conflicts
cb68d60 Merge pull request #122 from d3mlabs/ai/118-pr-d-split-commandexecutor-into-a-dispat
4477c6b Update the manifest-loader contract note for the eager toolchain pass
3ad03c3 Constructor-inject CommandRunner; two messages replace the wait flag
2ac941a Route help through the command path; group and eager-load usage
a78ba14 Add the help builtin
c7ae57a Add Category trait to the Command hierarchy
2e05625 ai-flow /build: let's fix the fake classes, put them within the test class
a29b5e6 Merge main: sealed-module Command hierarchy, super() convention, and bin/test.rb runner
5d9c57b Merge pull request #121 from d3mlabs/ai/117-pr-a-close-the-sealed-command-hierarchy
b8ed631 Call super() in every initializer that derives from the Command hierarchy
5bcc76f Rework the seal: Command becomes a sealed module, BuiltinCommand the abstract open-edge class
5c68c85 Merge pull request #120 from d3mlabs/ai/116-pr-c-bin-test-rb-tee-suite-output-to-a-s
c62efc8 ai-flow /build: PR B: Typed child-process failure taxonomy in CommandRunner (CommandFailedError / CommandKilledError / CommandSpawnError) mapped to exit codes in Runner#exit_for
f09f845 ai-flow /build: PR D: Split CommandExecutor into a dispatching composite with injectable BuiltinExecutor / ProjectExecutor / OverriddenExecutor strategies (exec_into vs run_waiting)
0775515 ai-flow /build: PR A: Close the sealed Command hierarchy honestly — BuiltinBody interface, final BuiltinCommand holding a body, delete the sorbet-runtime ivar pokes, un-private CommandRepository
5a5fb41 ai-flow /build: PR C: bin/test.rb — tee suite output to a stable log artifact and pass file args through to rake TEST
4c89408 Merge pull request #115 from d3mlabs/ai/37-layer-the-dev-runner-application-service
04d461c Add the simplecov-cobertura gem RBI
81677f6 Upload cobertura to codecov instead of SimpleCov JSON
0a741f0 Cover the default factories, image credential providers, and nocov the sealed absurd arm
fe7c94e ai-flow /build: Layer the dev Runner (application service + boundary coercion)
d782b1a Merge pull request #107 from d3mlabs/ai/101-dev-clone-host-global-builtin-cloning-vi
f305ac9 ai-flow /build: codecov coverage missing
fac96ee ai-flow /build: dev clone: host-global builtin cloning via gh auth to the canonical $DEV_CD_ROOT path
d16b757 Merge pull request #100 from d3mlabs/jpd/99-pin-homebrew-installer
2ad614e Pin the Homebrew installer to a commit SHA (dev#99)
53e3616 Merge pull request #90 from d3mlabs/ai/89-gemskilllinker-links-minted-under-a-sand
95ee372 Merge pull request #97 from d3mlabs/ai/learn-promote-rbenv-libruby-rpath-hijack
f7edc33 chore: nudge origin-firing after ai-flow#57 (removal diffs skip green)
646f189 Merge pull request #98 from d3mlabs/jpd/proposal-checks-edited
50e913a proposal-checks: re-verify on PR body edits (ai-flow#54)
59a3146 ai-flow /learn: drop rbenv-libruby-rpath-hijack (promoted to the org tier)
f119987 Merge pull request #96 from d3mlabs/jpd/ai-flow-knowledge-repo
b0e7131 ai-flow config: opt dev into org-tier learning promotion
d17b2ff Merge pull request #95 from d3mlabs/jpd/94-self-defending-entrypoint
29b2e16 Test readability: one aliased scrub list, one property per test
f7197ac Drift guard: the unset list must cover what the running bundler exports
049bbc8 Probe the shim scrub with a stub ruby instead of a full dev command run
88fa953 bin/dev: scrub foreign bundler activation before Ruby boots
9cf868a Merge pull request #92 from d3mlabs/ai/60-plan-pull-mangles-files-with-an-empty-fr
6783469 Merge pull request #93 from d3mlabs/ai/learn-issue-60
991d46d ai-flow /build: capture learnings from the build pass
fe64507 ai-flow /build: Plan pull mangles files with an empty frontmatter block above the real one (double frontmatter)
d8db57d ai-flow /build: GemSkillLinker: links minted under a sandboxed session point into ephemeral sandbox cache paths
b4526ea Merge pull request #88 from d3mlabs/jpd/ast-transform-3.1.1
de9beaf Bump ast_transform to 3.1.1 and drop the heredoc-emission workaround
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant

@JPDuchesne
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); PR A: Close the sealed Command hierarchy honestly — sealed module Command, abstract BuiltinCommand open edge, delete the sorbet-runtime ivar pokes, un-private CommandRepository by d3mlabs-ai-flow[bot] · Pull Request #121 · d3mlabs/dev · GitHub
Skip to content

PR A: Close the sealed Command hierarchy honestly — sealed module Command, abstract BuiltinCommand open edge, delete the sorbet-runtime ivar pokes, un-private CommandRepository - #121

Merged
JPDuchesne merged 3 commits into
mainfrom
ai/117-pr-a-close-the-sealed-command-hierarchy
Aug 18, 2026

Conversation

@d3mlabs-ai-flow

Copy link
Copy Markdown
Contributor

Implements #117.

Requested by @JPDuchesne.

Closes#117

…uiltinBody interface, final BuiltinCommand holding a body, delete the sorbet-runtime ivar pokes, un-private CommandRepository
Co-authored-by: JPDuchesne <2636122+JPDuchesne@users.noreply.github.com>
@codecov

codecovBot commented Aug 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

…abstract open-edge class
A sealed module's included hook fires only for direct includers (include
never transfers singleton methods), so the hierarchy closes without the
BuiltinBody indirection or the final wrapper leaf: builtins subclass
BuiltinCommand directly, the data leaves are final!, and the composition
root wires builtins straight in. BuiltinCommand stays a class because
Sorbet flattens module mixins — a module open edge would re-include the
sealed Command in every builtin and fail the same-file check statically.
Co-authored-by: Cursor <cursoragent@cursor.com>
@JPDuchesneJPDuchesne changed the title PR A: Close the sealed Command hierarchy honestly — BuiltinBody interface, final BuiltinCommand holding a body, delete the sorbet-runtime ivar pokes, un-private CommandRepositoryPR A: Close the sealed Command hierarchy honestly — sealed module Command, abstract BuiltinCommand open edge, delete the sorbet-runtime ivar pokes, un-private CommandRepositoryAug 18, 2026
@JPDuchesne

Copy link
Copy Markdown
Contributor

Reworked per review discussion: the BuiltinBody interface and the final BuiltinCommand wrapper are gone.

  • Command is now a sealed! abstract!module with the trait defaults. A sealed module's runtime included hook fires only for direct includers — include never transfers singleton methods — so the seal holds with no sorbet-runtime ivar pokes and nothing to suppress.
  • BuiltinCommand is an abstract! class that includes Command beside it: the hierarchy's one declared open edge. Builtins subclass it directly again (class CacheCommand < BuiltinCommand), and the composition root wires them straight in — no wrapping.
  • It stays a class rather than a module because Sorbet flattens module mixins: a module open edge would make every builtin a direct includer of sealed Command and fail the same-file check statically. A superclass edge is not flattened, so it typechecks and is invisible to the seal's runtime hooks.
  • ProjectCommand and OverriddenCommand include Command and are final!, so descent is closed everywhere it isn't explicitly declared. CommandExecutor's exhaustive case + T.absurd is unchanged.
  • Tests now assert the real closure properties: direct include Dev::Command raises, subclassing either data leaf raises, and subclassing BuiltinCommand is the declared open edge.

srb tc clean, 887 tests / 0 failures, rubocop clean.

@stamps = stamps
@calls = []
@body = body
super()

@JPDuchesneJPDuchesneAug 18, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

/ask what's with this line change?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

/ask

The removed super() was a no-op, so the diff is dead-code cleanup in the test fake. ExecutorFakeBuiltin subclasses Dev::BuiltinCommand, and that base class (see src/dev/command.rb) defines no initialize of its own — it's an abstract class that only declares the abstract call method, and Command is a plain module with no constructor either. So super() fell through to Object#initialize, which takes no arguments and does nothing; the line initialized nothing and its presence wrongly implied the base class had constructor behavior to run. Removing it also matches how the real builtins under src/dev/builtins/ are written — e.g. CdCommand and CredCommand define initialize without calling super. Behavior of the tests in test/dev/command_executor_test.rb is unchanged. (One caveat: git history access is restricted in this checkout, so I can't confirm whether the line was a leftover from an earlier BuiltinCommand shape — the file's comments mention it previously carried sorbet-runtime workarounds — but as the code stands today the call is provably inert.)

⚙️ workflow run · model: claude-fable-5-high

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Resolved in b8ed631: the super() calls are restored — and the convention is now applied uniformly. The bot's replies were right that the calls are inert today, but that misses the point of the rule: an explicit super() means future base-class constructor state either runs automatically (defaulted params) or fails loudly at first instantiation (required params), instead of silently constructing half-initialized objects. The fakes had actually been the only callers — the 11 real builtins never called super() — so the commit adds it everywhere an initializer derives from the hierarchy: both data leaves, all builtins, and the fakes.

def initialize(desc: "a builtin", hidden: false)
@desc = desc
@hidden = hidden
super()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

/ask Same here, why not keep super?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Restored in b8ed631, along with every other initializer in the hierarchy (the real builtins had never called super() — the fakes were the outliers in the other direction). See the thread on command_executor_test.rb for the full rationale.

@staleness_exempt = staleness_exempt
@stamps = stamps
@calls = []
super()

@JPDuchesneJPDuchesneAug 18, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

/ask Same here, why not keep super?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

/ask

Because there is nothing for super() to reach: it's a pure no-op here. ServiceFakeBuiltin subclasses Dev::BuiltinCommand (src/dev/command.rb), and neither BuiltinCommand nor the Command module it includes defines an initialize — there is no parent state to set up, so super() falls through to Object#initialize, which does nothing. It isn't needed for the abstract guard either: sorbet-runtime's abstract! enforces non-instantiability by wrapping self.new on the abstract class, not initialize, so the guard runs regardless of whether the subclass calls super. Lint doesn't ask for it either — Lint/MissingSuper is disabled in the inherited rubocop-shopify config. Keeping it would be harmless but would imply the base class has initialization the fake must run, which it doesn't; dropping the dead call is the same reasoning as in the other thread. (If BuiltinCommand ever grows a real initializer, its subclasses would need explicit constructor wiring anyway, per the repo's constructor-injection convention.)

⚙️ workflow run · model: claude-fable-5-high

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Restored in b8ed631, along with every other initializer in the hierarchy. The bot is right that abstract! guards new rather than initialize and that Lint/MissingSuper is disabled in rubocop-shopify — but "provably inert today" is exactly the case the always-super convention exists for. See the thread on command_executor_test.rb for the full rationale.

…rchy
Ruby silently skips base initialization when a subclass overrides
initialize without super. The calls are inert today (no ancestor defines
initialize), but they make future base-class constructor state either
run automatically (defaulted params) or fail loudly at first
instantiation (required params) instead of constructing half-initialized
objects. Applied uniformly: the two data leaves, all builtins, and the
test fakes.
Co-authored-by: Cursor <cursoragent@cursor.com>
@JPDuchesne
JPDuchesne merged commit 5d9c57b into mainAug 18, 2026
26 checks passed
@JPDuchesne
JPDuchesne deleted the ai/117-pr-a-close-the-sealed-command-hierarchy branch August 18, 2026 17:47
JPDuchesne added a commit that referenced this pull request Aug 19, 2026
916fe89 Merge pull request #128 from d3mlabs/jpd/skill-installer-ephemeral-guard
b9ea143 Move the ephemeral-source guard to SkillInstaller, the seam all links share
36803f7 Merge pull request #127 from d3mlabs/jpd/capture-learning-root-cause-gate
d6e081e Name the wide-angle goal, not one command: an exact git-log depth invites checkbox compliance
f876dbe capture-learning: gate workaround learnings on root cause, add wide angle
7249198 Merge pull request #123 from d3mlabs/ai/119-pr-b-typed-child-process-failure-taxonom
e71063a Merge pull request #126 from d3mlabs/jpd/hermetic-scrub-guard
f911e38 Make the scrub-list guard hermetic: construct the bundler launch it measures
6c398b8 ai-flow /build: let's resolve conflicts
cb68d60 Merge pull request #122 from d3mlabs/ai/118-pr-d-split-commandexecutor-into-a-dispat
4477c6b Update the manifest-loader contract note for the eager toolchain pass
3ad03c3 Constructor-inject CommandRunner; two messages replace the wait flag
2ac941a Route help through the command path; group and eager-load usage
a78ba14 Add the help builtin
c7ae57a Add Category trait to the Command hierarchy
2e05625 ai-flow /build: let's fix the fake classes, put them within the test class
a29b5e6 Merge main: sealed-module Command hierarchy, super() convention, and bin/test.rb runner
5d9c57b Merge pull request #121 from d3mlabs/ai/117-pr-a-close-the-sealed-command-hierarchy
b8ed631 Call super() in every initializer that derives from the Command hierarchy
5bcc76f Rework the seal: Command becomes a sealed module, BuiltinCommand the abstract open-edge class
5c68c85 Merge pull request #120 from d3mlabs/ai/116-pr-c-bin-test-rb-tee-suite-output-to-a-s
c62efc8 ai-flow /build: PR B: Typed child-process failure taxonomy in CommandRunner (CommandFailedError / CommandKilledError / CommandSpawnError) mapped to exit codes in Runner#exit_for
f09f845 ai-flow /build: PR D: Split CommandExecutor into a dispatching composite with injectable BuiltinExecutor / ProjectExecutor / OverriddenExecutor strategies (exec_into vs run_waiting)
0775515 ai-flow /build: PR A: Close the sealed Command hierarchy honestly — BuiltinBody interface, final BuiltinCommand holding a body, delete the sorbet-runtime ivar pokes, un-private CommandRepository
5a5fb41 ai-flow /build: PR C: bin/test.rb — tee suite output to a stable log artifact and pass file args through to rake TEST
4c89408 Merge pull request #115 from d3mlabs/ai/37-layer-the-dev-runner-application-service
04d461c Add the simplecov-cobertura gem RBI
81677f6 Upload cobertura to codecov instead of SimpleCov JSON
0a741f0 Cover the default factories, image credential providers, and nocov the sealed absurd arm
fe7c94e ai-flow /build: Layer the dev Runner (application service + boundary coercion)
d782b1a Merge pull request #107 from d3mlabs/ai/101-dev-clone-host-global-builtin-cloning-vi
f305ac9 ai-flow /build: codecov coverage missing
fac96ee ai-flow /build: dev clone: host-global builtin cloning via gh auth to the canonical $DEV_CD_ROOT path
d16b757 Merge pull request #100 from d3mlabs/jpd/99-pin-homebrew-installer
2ad614e Pin the Homebrew installer to a commit SHA (dev#99)
53e3616 Merge pull request #90 from d3mlabs/ai/89-gemskilllinker-links-minted-under-a-sand
95ee372 Merge pull request #97 from d3mlabs/ai/learn-promote-rbenv-libruby-rpath-hijack
f7edc33 chore: nudge origin-firing after ai-flow#57 (removal diffs skip green)
646f189 Merge pull request #98 from d3mlabs/jpd/proposal-checks-edited
50e913a proposal-checks: re-verify on PR body edits (ai-flow#54)
59a3146 ai-flow /learn: drop rbenv-libruby-rpath-hijack (promoted to the org tier)
f119987 Merge pull request #96 from d3mlabs/jpd/ai-flow-knowledge-repo
b0e7131 ai-flow config: opt dev into org-tier learning promotion
d17b2ff Merge pull request #95 from d3mlabs/jpd/94-self-defending-entrypoint
29b2e16 Test readability: one aliased scrub list, one property per test
f7197ac Drift guard: the unset list must cover what the running bundler exports
049bbc8 Probe the shim scrub with a stub ruby instead of a full dev command run
88fa953 bin/dev: scrub foreign bundler activation before Ruby boots
9cf868a Merge pull request #92 from d3mlabs/ai/60-plan-pull-mangles-files-with-an-empty-fr
6783469 Merge pull request #93 from d3mlabs/ai/learn-issue-60
991d46d ai-flow /build: capture learnings from the build pass
fe64507 ai-flow /build: Plan pull mangles files with an empty frontmatter block above the real one (double frontmatter)
d8db57d ai-flow /build: GemSkillLinker: links minted under a sandboxed session point into ephemeral sandbox cache paths
b4526ea Merge pull request #88 from d3mlabs/jpd/ast-transform-3.1.1
de9beaf Bump ast_transform to 3.1.1 and drop the heredoc-emission workaround
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant

@JPDuchesne