Skip to content

fix(hooks): correct Factory Droid hooks file shape#2320

Closed
RaviTharuma wants to merge 1 commit into
dyoshikawa:mainfrom
RaviTharuma:fix/factory-hooks-2317
Closed

fix(hooks): correct Factory Droid hooks file shape#2320
RaviTharuma wants to merge 1 commit into
dyoshikawa:mainfrom
RaviTharuma:fix/factory-hooks-2317

Conversation

@RaviTharuma

Copy link
Copy Markdown

Summary

  • write Factory Droid's dedicated .factory/hooks.json as the native event map instead of nesting it under a settings-style hooks key
  • preserve existing top-level native hook events while replacing generated events
  • retain legacy .factory/settings.json import compatibility
  • add regression coverage for valid JSON shape and command preservation

Test plan

  • pnpm check
  • pnpm exec vitest run src/features/hooks/factorydroid-hooks.test.ts src/features/hooks/hooks-processor.test.ts
  • pnpm test — 323 files, 7,268 tests passed

Closes #2317.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

1 similar comment
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

1 similar comment
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@RaviTharuma
RaviTharuma force-pushed the fix/factory-hooks-2317 branch 2 times, most recently from b11b481 to 4ecdab3 Compare July 20, 2026 00:31
@RaviTharuma

Copy link
Copy Markdown
Author

CI update: the external-contributor policy allows at most two concurrent open PRs. PR #2322 was temporarily closed, leaving only these two open. The branch has also been updated with the CI fixes discovered in the first run.

@dyoshikawa dyoshikawa left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks for the PR, but I don't think we can take this change as-is. The core premise looks incorrect: the official Factory docs (https://docs.factory.ai/reference/hooks-reference, "Structure" section) show .factory/hooks.json wrapping the event map in a top-level "hooks" key — the exact shape main already emits — and the docs also say Droid falls back to the hooks key in settings.json when hooks.json is absent. Even issue #2317's own "Expected output" uses the nested shape. With this PR, Droid would find no hooks key in the generated file and no hook would ever fire. If you have a doc or Droid release note showing the flat shape is now the accepted format, please link it, because everything I can find says otherwise.

Separately, the issue #2317 symptoms (dropped command field, trailing comma producing invalid JSON) can't actually be produced by the current code path — output goes through JSON.stringify, which never emits trailing commas, and existing tests already assert the command field survives overrides. So even if we kept this PR, it wouldn't be what fixes #2317. There are a couple of mechanical issues flagged inline as well (stale-key merge behavior, the weakened e2e assertion, and forDeletion still emitting the nested shape this PR declares wrong).

My suggestion would be to close this one unless there's evidence the flat shape is correct. One genuinely real nearby bug we did confirm while verifying: absolute hook commands like /tmp/notify.sh get mangled to "$FACTORY_PROJECT_DIR"//tmp/notify.sh by the prefix logic in tool-hooks-converter.ts — that's pre-existing on main and would make a good separate fix if you're interested.

logger,
});
const merged = { ...settings, hooks: factorydroidHooks };
const merged = { ...settings, ...factorydroidHooks };

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

This is the crux: the Factory hooks reference documents hooks.json as { "hooks": { "EventName": [...] } } — nested under a top-level hooks key, same as what main emits today. Flattening the events to the top level produces a file Droid won't load. Also, switching from hooks: factorydroidHooks (wholesale replacement of the key we own) to a spread means stale event keys from previous generations are never removed — deleting a hook from .rulesync/hooks.jsonc and regenerating leaves the removed event behind, and a file generated by current rulesync keeps its old nested hooks block forever alongside the new top-level keys.

@@ -139,7 +139,7 @@ export class FactorydroidHooks extends ToolHooks {
);
}
const hooks = toolHooksToCanonical({

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

parsed.hooks ?? parsed prefers a leftover legacy hooks block over the top-level events. Combined with the spread-merge above, regenerating over a v14-format file leaves both shapes in the file, and import then silently canonicalizes the stale nested block instead of the fresh top-level one.

Comment thread src/e2e/e2e-hooks.spec.ts
expect(parsed.hooks).toBeDefined();
const serialized = JSON.stringify(parsed.hooks);
function assertHookCommandsPreserved(parsed: { hooks?: unknown }, rootIsHooks = false): void {
const hooks = rootIsHooks ? parsed : parsed.hooks;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

With rootIsHooks = true this assertion becomes near-vacuous: expect(hooks).toBeDefined() is trivially true for any parsed object, and stringifying the whole file then grepping for the command substrings passes regardless of the file's structure. That's how the shape change sailed through the e2e matrix green. The e2e check should pin the documented shape (e.g. assert parsed.hooks.SessionStart exists) rather than degrade to a whole-file substring match.

});

const parsed = JSON.parse(factorydroidHooks.getFileContent());
expect(parsed).toEqual({

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

This test doesn't reproduce issue #2317: the issue used an absolute command (/tmp/notify.sh), which the prefix logic in tool-hooks-converter.ts mangles into "$FACTORY_PROJECT_DIR"//tmp/notify.sh — a real pre-existing bug this PR doesn't touch. Substituting a relative path here sidesteps the actual repro. Also note forDeletion and the fromFile fallback in factorydroid-hooks.ts still emit {"hooks": {}}, which contradicts the flat shape this PR introduces.

@dyoshikawa

Copy link
Copy Markdown
Owner

Thanks for the contribution, but I'm closing this PR because it changes the generated .factory/hooks.json away from Factory's documented format.

The official Factory hooks reference (https://docs.factory.ai/reference/hooks-reference) documents hooks.json with a top-level "hooks" key wrapping the event map:

{
  "hooks": {
    "UserPromptSubmit": [
      { "hooks": [{ "type": "command", "command": "..." }] }
    ]
  }
}

That is exactly what rulesync currently generates. This PR flattens the event map to the top level of the file ({"UserPromptSubmit": [...]}), which contradicts the documented shape — and also contradicts the "expected output" shown in #2317 itself, which includes the "hooks" wrapper.

Additionally, the underlying issue #2317 does not reproduce on 14.0.1: the current emitter preserves command and produces valid JSON (verified with the issue's own repro script; details in #2317). The test changes in this PR codify the undocumented flattened shape rather than fixing a real defect, and the { ...settings, ...factorydroidHooks } spread would also stop cleaning up previously generated events that were removed from the rulesync config (stale top-level events would persist across regenerations).

Closing along with #2317.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Factory Droid hooks generation drops command and emits invalid JSON (v14.0.1)

2 participants