Skip to content

inkeep pull, split introspect-generator.ts in individual generator task.collect functions#2654

Draft
dimaMachina wants to merge 5 commits intomainfrom
prd-6192-333
Draft

inkeep pull, split introspect-generator.ts in individual generator task.collect functions#2654
dimaMachina wants to merge 5 commits intomainfrom
prd-6192-333

Conversation

@dimaMachina
Copy link
Collaborator

No description provided.

@changeset-bot
Copy link

changeset-bot bot commented Mar 11, 2026

⚠️ No Changeset found

Latest commit: e4d9c94

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel
Copy link

vercel bot commented Mar 11, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agents-api Ready Ready Preview, Comment Mar 20, 2026 10:54pm
agents-docs Ready Ready Preview, Comment Mar 20, 2026 10:54pm
agents-manage-ui Ready Ready Preview, Comment Mar 20, 2026 10:54pm

Request Review

@dimaMachina
Copy link
Collaborator Author

@claude --review

@pullfrog
Copy link
Contributor

pullfrog bot commented Mar 11, 2026

TL;DR — Decomposes the monolithic introspect-generator.ts (~2,700 lines) into a modular architecture where each generator file owns its own task.collect function, reference resolution is centralized in a new GenerationResolver class, and shared utilities are split into focused submodules under utils/.

Key changes

Summary | 52 files | 1 commit | base: mainprd-6192-333

  • introspect-generator.ts — Reduced from ~2,700 lines to ~70. All collect* functions, type definitions, and inline helpers moved out. Now simply iterates generationTasks and delegates to writeTypeScriptFile.
  • generators/index.ts — New barrel that auto-discovers *-generator.ts files via Vite's import.meta.glob with eager task named exports.
  • GenerationResolver — New class that pre-builds reference name/path lookup maps at construction time, replacing scattered ad-hoc resolution throughout collect* functions.
  • generation-types.ts — New file housing all shared interfaces (GenerationContext, GenerationTask, ProjectPaths, *ReferenceOverrides, etc.) previously inlined in introspect-generator.ts.
  • collector-common.ts / collector-reference-helpers.ts — Extracted shared collection utilities (record helpers, reference ID extraction, credential/sub-agent reference override builders).
  • reference-resolution.ts — New module for import binding resolution with collision strategies (descriptive, numeric, numeric-for-duplicates).
  • simple-factory-generator.ts — Reusable scaffold for generators that follow the validate → create factory → populate config object pattern.
  • typescript-file-writer.ts — Extracted writeTypeScriptFile, merge logic, object shorthand normalization, and variable declaration reordering out of introspect-generator.ts.
  • utils/ — Monolithic utils.ts (576 lines, deleted) split into code-values.ts, factory-writer.ts, naming.ts, schema-rendering.ts, shared.ts, templates.ts with a barrel index.ts.
  • Each *-generator.ts — Now exports a task object with colocated collect and generate functions instead of only exporting generate.
  • docs/pull.md — New internal documentation describing the pull-v4 pipeline, generation task order, merge behavior, and module structure.

introspect-generator.ts → orchestrator-only

Before: Single 2,700-line file containing all type definitions, collect* functions, generate* re-exports, reference resolution logic, file writing, and the main loop.


After: ~70-line orchestrator that validates the project, builds a GenerationResolver, iterates generationTasks, and calls writeTypeScriptFile for each record.

The GenerationContext interface gains a resolver: GenerationResolver field, giving every collect function access to centralized reference lookups without ad-hoc map construction.

introspect-generator.ts · generation-types.ts


GenerationResolver — centralized reference name/path resolution

Before: Each collect*Record function manually built referenceOverrides and referencePathOverrides maps by calling standalone build* helpers and checking the component registry inline.


After: GenerationResolver pre-computes all reference maps (tools, credentials, external agents, sub-agents, agents) at construction and exposes typed getters like getToolReferenceName(id) / getToolReferencePath(id) that prefer existing component registry entries over generated names.

How does the resolver prefer existing names? Each getter calls getPreferredReferenceName(), which first checks the parsed component registry (from the on-disk project in merge mode) for a matching component name. Only if no existing component is found does it fall back to the pre-built generated name map. This preserves user-edited variable names across pulls.

generation-resolver.ts


generators/index.ts — glob-based task auto-discovery

Before: createGenerationTasks() in introspect-generator.ts returned a hand-maintained array of { type, collect, generate } objects.


After: import.meta.glob('./*-generator.ts', { import: 'task', eager: true }) auto-collects every export const task from the generators/ directory. Adding a new generator only requires creating a file with the right naming convention.

generators/index.ts


collect functions colocated with generators

Before: All collect*Records functions lived in introspect-generator.ts, separated from their corresponding generate* functions in generators/.


After: Each generator (e.g. credential-generator.ts, environment-generator.ts, agent-generator.ts) exports const task: GenerationTask<T> with a colocated collect and generate. Generators also adopt generateSimpleFactoryDefinition / generateFactorySourceFile from the new simple-factory-generator.ts to reduce boilerplate.

credential-generator.ts · agent-generator.ts · sub-agent-generator.ts · project-generator.ts


utils.tsutils/ submodules

Before: Single 576-line utils.ts mixing code-generation helpers, naming conventions, factory writers, template formatting, and schema rendering.


After: Six focused modules behind a barrel re-export:

Module Responsibility
code-values.ts CodeValue union type, codeReference(), codeExpression(), codePropertyAccess(), formatInlineLiteral()
factory-writer.ts createFactoryDefinition(), addFactoryConfigVariable(), addValueToObject()
naming.ts toCamelCase(), toKebabCase(), buildComponentFileName(), naming collision helpers
schema-rendering.ts JSON Schema → Zod string conversion
shared.ts isPlainObject(), isHumanReadableId()
templates.ts formatTemplate(), formatStringLiteral(), formatPropertyName()

utils/index.ts · utils/code-values.ts · utils/factory-writer.ts · utils/naming.ts


reference-resolution.ts — import binding deduplication

Before: Each generator manually resolved import aliases and handled name collisions inline with different strategies.


After: resolveReferenceBindings() accepts a list of ReferenceResolutionInput entries and produces ResolvedReferenceBinding objects with pre-resolved localName, namedImport (with optional alias), and modulePath. Supports three collision strategies: descriptive (appends a suffix), numeric (appends index), and numeric-for-duplicates (numeric only when the same importName appears multiple times).

reference-resolution.ts


docs/pull.md — internal architecture documentation

Before: No written documentation for the pull-v4 pipeline.


After: 263-line document covering entry points, inkeep pull lifecycle, target directory resolution, remote normalization, generated layout, generator pipeline order, merge behavior, file-scope merging, and the module structure introduced in this PR.

docs/pull.md

Pullfrog  | View workflow run | Using Claude Code | Triggered by Pullfrogpullfrog.com𝕏

Copy link
Contributor

@pullfrog pullfrog bot left a comment

Choose a reason for hiding this comment

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

Well-structured refactoring that breaks a ~2700-line monolith into focused, cohesive modules. The GenerationResolver class cleanly centralizes reference resolution, and the import.meta.glob auto-discovery is a nice pattern for keeping the task registry maintenance-free. Three items to address: the documentation lists an explicit task order that no longer matches reality, asRecord/stripExtension are duplicated across three files, and there's a helper function duplication between collector-common.ts and generators/helpers/agent.ts.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow runpullfrog.com𝕏

Comment on lines +76 to +94
The current task order is:

1. credentials
2. environment settings
3. environment index
4. artifact components
5. data components
6. function tools
7. MCP tools
8. external agents
9. context configs
10. triggers
11. scheduled triggers
12. sub-agents
13. status components
14. agents
15. project index

Skills are not part of `generationTasks`. They are written separately by `generateSkills()` because they emit `SKILL.md` files instead of TypeScript source files.
Copy link
Contributor

Choose a reason for hiding this comment

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

This numbered list documents a specific "current task order" that no longer exists. With the switch to import.meta.glob('./*-generator.ts'), execution order is now alphabetical by filename (agent → artifact-component → context-config → credential → ...). Either update this list to reflect alphabetical order or document that order is intentionally unspecified since generators read from an immutable context and write to non-overlapping paths.

@@ -0,0 +1,8 @@
import type { GenerationTask } from '../generation-types';

export const generationTasks: Record<`./${string}-generator.ts`, GenerationTask<any>> = import.meta
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor: GenerationTask<any> — since each generator file already uses satisfies GenerationTask<SpecificType> for internal type safety, the any here is acceptable. Consider unknown instead of any to prevent accidental direct payload access on the record values without going through a specific generator's typed collect/generate contract.

Comment on lines +587 to +592
function asRecord(value: unknown): Record<string, unknown> | undefined {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return;
}
return value as Record<string, unknown>;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

asRecord is declared identically in three files: here (private), generation-types.ts (private), and collector-common.ts (exported). stripExtension is duplicated here and in collector-common.ts. Import from collector-common.ts to reduce maintenance surface.

Comment on lines +121 to +127
if (
!asRecord(data.subAgents) ||
// @ts-expect-error -- existing runtime behavior
!Object.keys(data.subAgents).length
) {
return { complete: false, reason: 'no sub-agents defined' };
}
Copy link
Contributor

Choose a reason for hiding this comment

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

The @ts-expect-error can be eliminated by using the asRecord return value directly:

Suggested change
if (
!asRecord(data.subAgents) ||
// @ts-expect-error -- existing runtime behavior
!Object.keys(data.subAgents).length
) {
return { complete: false, reason: 'no sub-agents defined' };
}
const subAgents = asRecord(data.subAgents);
if (
!subAgents ||
!Object.keys(subAgents).length
) {
return { complete: false, reason: 'no sub-agents defined' };
}

This avoids suppressing a real type error and matches the pattern used everywhere else in this PR.

Comment on lines +20 to +31
export function collectTemplateVariableNamesFromFields(
values: Array<string | undefined>
): string[] {
const variables: string[] = [];
for (const value of values) {
if (typeof value !== 'string') {
continue;
}
variables.push(...collectTemplateVariableNames(value));
}
return variables;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

collectTemplateVariableNamesFromFields is functionally identical to collectTemplateVariablesFromValues exported from collector-common.ts — same loop, same filter, same output. Import the shared version to avoid divergence.


- the output is not TypeScript,
- generation is async and writes files directly,
- the new output behaves more like the existing `skill-generator`.
Copy link
Contributor

Choose a reason for hiding this comment

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

References skill-generator but the actual module is skill.ts (not inside generators/, and not suffixed -generator). Update to reference skill.ts or say "the existing skill generator (skill.ts)" to avoid confusion when someone searches the codebase for a file called skill-generator.

@itoqa
Copy link

itoqa bot commented Mar 11, 2026

Ito Test Report ❌

19 test cases ran. 18 passed, 1 failed.

✅ Most pull-v4 routing, batch generation, logic integrity, adversarial hardening, and docs viewport checks behaved as expected. 🔍 One path-based pull flow exposed a reproducible local project-loading defect; code inspection supports a real application bug in the TypeScript module loading path used by --project <local-path>.

✅ Passed (18)
Test Case Summary Timestamp Screenshot
ROUTE-1 Health check succeeded and pull --project support-project generated files including skills/general-gameplan/SKILL.md. 16:33 ROUTE-1_16-33.png
ROUTE-2 pull --all discovered two remote projects and generated both project directories in a clean workspace, including skills output for each pulled project. 25:17 ROUTE-2_25-17.png
ROUTE-3 pull --json returned normalized support-project payload with skills data and no generated file output observed for this run target. 16:39 ROUTE-3_16-39.png
ROUTE-5 With one intentionally broken target path, batch pull continued processing and completed the other project, reporting one success and one failure in summary output. 25:17 ROUTE-5_25-17.png
LOGIC-1 Context-config regression suite passed and health checkpoint was successful. 27:47 LOGIC-1_27-47.png
LOGIC-2 Reference-resolution suite passed with stable alias/import behavior. 27:47 LOGIC-2_27-47.png
LOGIC-3 Write-mode introspection suite passed with merge-preservation expectations met. 27:47 LOGIC-3_27-47.png
LOGIC-4 Environment-generator suite passed with conditional generation behavior verified. 27:47 LOGIC-4_27-47.png
LOGIC-5 Trigger-generator suite passed and scheduled-trigger generation behavior is validated. 27:47 LOGIC-5_27-47.png
EDGE-1 CLI returned non-zero with explicit guidance to run from a directory containing index.ts or use --project. 0:00 EDGE-1_0-00.png
EDGE-2 CLI detected local project id support-project and rejected --project different-id with mismatch guard output before pull execution. 0:00 EDGE-2_0-00.png
EDGE-3 inkeep pull rejected malformed skills input with validation errors and did not create a skills/ directory for the target project. 36:38 EDGE-3_36-38.png
EDGE-4 Three back-to-back pull executions completed cleanly and regenerated files without command-level syntax/runtime failures. 25:17 EDGE-4_25-17.png
ADV-1 Traversal-like IDs were rejected during validation and no escaped filesystem writes were detected outside the target project directory. 36:38 ADV-1_36-38.png
ADV-2 Adversarial template content remained inside a string context, no top-level injected import statement was emitted, and the generated context config compiled successfully. 36:38 ADV-2_36-38.png
ADV-3 Collision-oriented fixture generation completed, duplicate import alias scan returned none, and focused TypeScript compilation of generated entrypoint succeeded. 36:38 ADV-3_36-38.png
ADV-4 After forced browser refresh and reopen, pull reruns completed successfully and generated project state remained intact with a valid non-empty index.ts. 25:17 ADV-4_25-17.png
EDGE-5 At 390x844 viewport, pull.md content remained readable and scrolling through long sections worked using local browser-accessible markdown file fallback. 38:53 EDGE-5_38-53.png
❌ Failed (1)
Test Case Summary Timestamp Screenshot
ROUTE-4 Path-based --project execution failed to load local index.ts due require(esm) cycle, so directory-targeted pull behavior could not be validated successfully. 17:13 ROUTE-4_17-13.png
Project resolution via --project as local path works and targets that directory – Failed
  • Where: inkeep pull project resolution flow when --project points to a local directory containing index.ts.

  • Steps to reproduce: Run inkeep pull --project <path-to-local-project-with-index-ts> from a directory without local index.ts.

  • What failed: CLI fails while loading the local project module with Cannot require() ES Module ... in a cycle, instead of resolving project ID from that directory and continuing pull.

  • Code analysis: I reviewed the path-based branch in pull routing plus shared project/module loading utilities. The command always executes loadProject(projectDir) for path-based resolution, and that loader dynamically registers TSX ESM hooks and immediately imports index.ts. This execution path can trigger the ESM require-cycle failure seen in runtime evidence and lacks a fallback path for extracting project ID without full module execution.

  • Relevant code:

    agents-cli/src/commands/pull-v4/introspect/index.ts (lines 251–267)

    const projectPath = resolve(currentDir, options.project);
    const hasIndexInPath = existsSync(join(projectPath, 'index.ts'));
    
    if (hasIndexInPath) {
      projectDir = projectPath;
      s.start('Loading project from specified path...');
      try {
        localProjectForId = await loadProject(projectDir);
        projectId = localProjectForId.getId();
      } catch (error) {
        throw new Error(`Could not load project from ${projectPath}: ${error instanceof Error ? error.message : String(error)}`);
      }
    }

    agents-cli/src/utils/project-loader.ts (lines 23–31)

    // Import the module with TypeScript support
    const module = await importWithTypeScriptSupport(indexPath);
    
    // Find the first export with __type = "project"
    const exports = Object.keys(module);
    for (const exportKey of exports) {
      const value = module[exportKey];
      if (value && typeof value === 'object' && value.__type === 'project') {
        return value as Project;
      }
    }

    agents-cli/src/utils/tsx-loader.ts (lines 13–25)

    if (ext === '.ts') {
      const unregister = register();
      try {
        const fileUrl = pathToFileURL(filePath).href;
        const module = await import(fileUrl);
        return module;
      } finally {
        unregister();
      }
    }
  • Why this is likely a bug: The local-path mode is expected to resolve and use a valid project directory, but the current implementation hard-fails on a module-loading cycle in a core supported path with no recovery strategy, preventing intended command behavior.

  • Introduced by this PR: No – pre-existing bug (the failing loader path is in utils/project-loader.ts and utils/tsx-loader.ts, which were not modified in this PR, and the PR changes in introspect/index.ts are outside this branch).

  • Timestamp: 17:13

📋 View Recording

Screen Recording

Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this a transient spec document or are we keeping this documentation indefinitely? If indefinite, it should be put into the agents-docs

@itoqa
Copy link

itoqa bot commented Mar 17, 2026

Ito Test Report ❌

12 test cases ran. 10 passed, 2 failed.

🔍 Verification confirmed broad pull/generation coverage is working, and identified two actionable security defects in skill generation and template literal handling. These two issues have clear production-code defect paths and should be prioritized.

✅ Passed (10)
Test Case Summary Timestamp Screenshot
ROUTE-1 pull --debug from test-agents completed and synced files at the same project location. 18:26 ROUTE-1_18-26.png
LOGIC-1 Targeted introspect generation suite passed with 2/2 tests, validating supported generation pipeline outputs. 27:35 LOGIC-1_27-35.png
LOGIC-2 Merge-focused context-config suite passed with 2/2 tests, confirming preservation of local header references and valid merged output. 27:35 LOGIC-2_27-35.png
LOGIC-4 Regression suite passed with 2/2 tests, confirming template/header normalization behavior and no normalized-field leakage. 27:35 LOGIC-4_27-35.png
EDGE-1 Executed duplicate import-name collision test twice and observed consistent successful alias-suffix resolution across reruns. 36:14 EDGE-1_36-14.png
EDGE-2 Reference-resolution collision suite passed, confirming reserved-name conflicts are resolved via safe alias generation. 36:14 EDGE-2_36-14.png
EDGE-4 Project introspect test passed and validated expected environment import/path behavior in generated project output. 27:35 EDGE-4_27-35.png
EDGE-5 CLI consistently rejects mismatched --project values inside an existing project directory, preventing overwrite flow before remote fetch. 0:00 EDGE-5_0-00.png
ADV-4 With one project intentionally broken, batch pull continued to the remaining project and produced accurate success/failure summary reporting. 22:07 ADV-4_22-07.png
ADV-5 After interrupting an in-progress batch pull, a subsequent rerun completed successfully and restored a clean executable workflow state. 22:07 ADV-5_22-07.png
❌ Failed (2)
Test Case Summary Timestamp Screenshot
ADV-1 Direct skills-generation probe with a traversal-like skill ID created SKILL.md outside the intended skills boundary, demonstrating a filesystem traversal vulnerability. 49:16 ADV-1_49-16.png
ADV-2 Template formatting probe rendered hostile ${...} payload as an executable template literal segment, indicating unsafe interpolation handling in generated output. 49:16 ADV-2_49-16.png
Skill path traversal attempt – Failed
  • Where: Skill file generation path handling in pull-v4 skill writer

  • Steps to reproduce: Run pull/generation with a skill ID containing traversal segments (for example ../outside), then inspect filesystem writes relative to <project>/skills

  • What failed: A crafted skill ID is joined directly into the output path, allowing SKILL.md creation outside the intended skills directory boundary

  • Code analysis: Reviewed the pull-v4 skill generation implementation and found that skillId is directly passed to path.join and written without canonicalization or boundary checks

  • Relevant code:

    agents-cli/src/commands/pull-v4/skill.ts (lines 38-52)

    for (const [skillId, skill] of Object.entries(validatedSkills)) {
      const parts: string[] = ['---', `name: ${JSON.stringify(skill.name)}`];
      parts.push(`description: ${JSON.stringify(skill.description ?? '')}`);
    
      if (skill.metadata && Object.keys(skill.metadata).length > 0) {
        parts.push(formatMetadata(skill.metadata));
      }
    
      parts.push('---', '', skill.content || '');
    
      const skillDir = join(validatedSkillsDir, skillId);
      await mkdir(skillDir, { recursive: true });
    
      const filePath = join(skillDir, 'SKILL.md');
      await writeFile(filePath, parts.join('\n'), 'utf8');
    }
  • Why this is likely a bug: The write path is derived from untrusted skillId without restricting resolved output to the skills root, enabling out-of-bound filesystem writes.

  • Introduced by this PR: Yes - this PR modified the relevant code

  • Timestamp: 49:16

Template/code injection strings in context data – Failed
  • Where: String literal rendering for generated TypeScript config values

  • Steps to reproduce: Provide a context/template value containing ${...} payload text, run generation, and inspect emitted TS string literal in generated output

  • What failed: Payload text containing ${...} is emitted inside template-literal quotes and can execute interpolation semantics instead of being preserved as inert data

  • Code analysis: Reviewed template/string formatting and recursive literal rendering. String formatting intentionally selects backticks when ${ exists, but escaping does not neutralize ${, and the generated value is consumed as executable TS literal output

  • Relevant code:

    agents-cli/src/commands/pull-v4/utils/templates.ts (lines 18-27)

    export function formatStringLiteral(value: string): string {
      const hasSingleQuote = value.includes(QUOTE.single);
      const hasDoubleQuote = value.includes(QUOTE.double);
      const quote =
        value.includes('\n') || value.includes('${') || (hasSingleQuote && hasDoubleQuote)
          ? QUOTE.template
          : hasSingleQuote
            ? QUOTE.double
            : QUOTE.single;
      return escapeStringLiteral(value, quote);
    }

    agents-cli/src/commands/pull-v4/utils/templates.ts (lines 88-90)

    function escapeStringLiteral(value: string, quote: Quote): string {
      return [quote, value.replaceAll('\\', '\\\\').replaceAll(quote, `\\${quote}`), quote].join('');
    }

    agents-cli/src/commands/pull-v4/utils/code-values.ts (lines 78-80)

    if (typeof value === 'string') {
      return formatStringLiteral(value);
    }
  • Why this is likely a bug: The code path emits attacker-controlled ${...} inside backtick literals without escaping interpolation markers, creating executable interpolation in generated code.

  • Introduced by this PR: Yes - this PR modified the relevant code

  • Timestamp: 49:16

📋 View Recording

Screen Recording

@itoqa
Copy link

itoqa bot commented Mar 19, 2026

Ito Test Report ❌

19 test cases ran. 17 passed, 2 failed.

✅ Most pull-v4 flows validated successfully, including single-project pull, batch pull, merge behavior, safety checks, and docs/workflow coverage. 🔍 Two failures remain as likely real product bugs after code inspection: JSON mode does not terminate cleanly, and overwrite behavior is not wired through the CLI path, allowing stale generated fragments to persist.

✅ Passed (17)
Test Case Summary Timestamp Screenshot
ROUTE-1 Pull completed successfully in a clean folder and generated the expected support-project baseline files. 15:39 ROUTE-1_15-39.png
ROUTE-2 Batch pull succeeded in a clean folder, processed two projects, and generated per-project directories including a skills-enabled project. 20:48 ROUTE-2_20-48.png
ROUTE-3 Running pull with a different project ID from inside an existing local project produced a clear mismatch error and did not create unrelated project output. 28:05 ROUTE-3_28-05.png
LOGIC-1 Generated SKILL.md contains valid frontmatter with name/description/metadata and a correctly separated markdown body. 20:48 LOGIC-1_20-48.png
LOGIC-2 Generated files were present and all checked relative import targets resolved to existing files. 15:39 LOGIC-2_15-39.png
LOGIC-3 Merge pull completed in the customized support-project fixture and preserved renamed agent/tool/credential references and custom file paths while syncing remote data. 39:26 LOGIC-3_39-26.png
EDGE-1 Support project completed successfully in batch pull and did not create unexpected skill artifacts. 20:48 EDGE-1_20-48.png
EDGE-2 Validated collision handling with a targeted pull-v4 reference-resolution test that confirms deterministic unique alias assignment for duplicate import names. 50:07 EDGE-2_50-07.png
EDGE-3 Validated non-fatal handling in pull-v4 completeness filtering: complete agents continue through generation while incomplete entries are excluded with explicit reason metadata. 50:07 EDGE-3_50-07.png
EDGE-4 Context config retained template references and did not include checked internal normalization artifacts. 15:39 EDGE-4_15-39.png
EDGE-6 A deterministic mid-run interrupt created a partial state (index.ts absent), and an immediate rerun restored a complete valid project tree (index.ts present, file generation successful). 47:33 EDGE-6_47-33.png
ADV-1 Traversal-like input did not mutate the tested parent boundary and pull terminated with project-not-found behavior, indicating containment within allowed workspace handling. 24:30 ADV-1_24-30.png
ADV-2 Script-like and template-like tokens were serialized into SKILL.md as plain text without execution side effects or markdown corruption. 20:48 ADV-2_20-48.png
ADV-3 Validated malformed payload behavior with targeted simple-factory validation test confirming explicit labeled error signaling for invalid component structure. 50:07 ADV-3_50-07.png
ADV-4 Invalid bearer token request returned 401 Unauthorized while the response remained sanitized with no secret/token leakage markers. 55:46 ADV-4_55-46.png
MOBILE-1 The pull documentation remained readable and navigable at 390x844 with preserved headings/code fences and vertical scrolling; desktop viewport retained equivalent content semantics. 1:00:56 MOBILE-1_1-00-56.png
WORKFLOW-1 During pull attempts, pane navigation plus browser back/forward and refresh did not produce fake success; command failures remained visible and filesystem state stayed consistent with those failures. 1:00:06 WORKFLOW-1_1-00-06.png
❌ Failed (2)
Test Case Summary Timestamp Screenshot
ROUTE-4 JSON payload was emitted and no generated project directory remained, but the CLI process did not terminate within the execution timeout. 15:39 ROUTE-4_15-39.png
LOGIC-4 Two overwrite-mode fallback runs produced identical hashes for representative generated files, but a stale local fragment remained after overwrite, so canonical cleanup behavior failed. 39:26 LOGIC-4_39-26.png
JSON mode outputs normalized payload without generation – Failed
  • Where: CLI inkeep pull --json flow in pull-v4 command handling.

  • Steps to reproduce: Run inkeep pull --project <id> --json and wait for output completion.

  • What failed: JSON payload prints correctly, but process does not terminate as expected and times out.

  • Code analysis: The command starts and updates a spinner, then returns early in JSON mode without stopping that spinner. This leaves active spinner state on the success path.

  • Relevant code:

    agents-cli/src/commands/pull-v4/introspect/index.ts (lines 368-374)

    s.message('Project data fetched');
    
    if (options.json) {
      console.log(JSON.stringify(remoteProject, null, 2));
      restoreLogLevel();
      return;
    }

    agents-cli/src/commands/pull-v4/introspect/index.ts (lines 172-173)

    const s = p.spinner();
  • Why this is likely a bug: The JSON success path exits without a matching s.stop(...), which plausibly leaves spinner internals active and aligns with the observed non-terminating behavior.

  • Introduced by this PR: Yes – this PR modified the relevant code.

  • Timestamp: 15:39

Overwrite mode regenerates deterministic canonical files – Failed
  • Where: Pull-v4 generation mode selection and file writing path.

  • Steps to reproduce: Run pull expecting overwrite semantics (full canonical regeneration), then verify whether stale local fragments are removed.

  • What failed: Deterministic reruns occurred, but stale local fragments persisted, indicating merge-like preservation instead of overwrite cleanup.

  • Code analysis: CLI parses a --force flag but does not pass any overwrite mode into generation; generation defaults to merge mode and writes with merge semantics unless explicitly overridden.

  • Relevant code:

    agents-cli/src/index.ts (lines 120-136)

    program
      .command('pull')
      .description('Pull project configuration with clean, efficient code generation')
      // ...
      .option('--force', 'Force regeneration even if no changes detected')
      .option('--all', 'Pull all projects for current tenant')

    agents-cli/src/commands/pull-v4/introspect/index.ts (lines 381-387)

    s.start('Starting generating files...');
    await introspectGenerate({
      // @ts-expect-error -- ignore Types of property 'models' are incompatible.
      project: remoteProject,
      paths,
      debug: options.debug,
    });

    agents-cli/src/commands/pull-v4/introspect-generator.ts (lines 23-27)

    export async function introspectGenerate({
      project,
      paths,
      writeMode = 'merge',
      debug = false,
    }: IntrospectOptions): Promise<void> {
  • Why this is likely a bug: Overwrite expectations cannot be honored when generation is always invoked without writeMode: 'overwrite', so stale merged fragments can remain.

  • Introduced by this PR: Yes – this PR modified the relevant code.

  • Timestamp: 39:26

📋 View Recording

Screen Recording

@itoqa
Copy link

itoqa bot commented Mar 20, 2026

Ito Test Report ❌

18 test cases ran. 3 failed, 15 passed.

Across 18 test cases, 16 passed and 2 failed, indicating generally stable pull-v4 behavior across batch generation, merge determinism, edge recovery, mobile usability, and adversarial/security scenarios. The key findings are two confirmed production defects: a pre-existing medium-severity issue where inkeep pull --project support-project --json can print valid JSON but hang due to skipping the explicit success exit path, and a high-severity PR-introduced merge bug where reruns can retain malformed existing TypeScript and append regenerated code instead of safely replacing invalid files.

❌ Failed (3)
Category Summary Screenshot
Edge ⚠️ Merge-mode rerun can preserve pre-existing malformed TypeScript and append regenerated code, leaving invalid output instead of recovering cleanly. N/A
Happy-path 🟠 pull --json can hang because JSON mode returns without explicit process exit. ROUTE-1
Happy-path 🟠 JSON mode prints output but can remain running due to missing explicit exit path. ROUTE-3
⚠️ Malformed-file merge fallback fails to repair invalid generated TypeScript
  • What failed: The rerun should recover by replacing invalid generated code with valid regenerated output, but malformed declarations can remain and regenerated exports are appended, leaving parse-invalid TypeScript.
  • Impact: A routine recovery rerun can leave generated source in a broken state that fails parsing/build checks. Teams may ship or commit corrupted generated files unless they manually inspect and repair them.
  • Introduced by this PR: Yes – this PR modified the relevant code
  • Steps to reproduce:
    1. Run inkeep pull --project <project-id> once to generate baseline files.
    2. Manually corrupt a generated TypeScript file (for example export const broken = ;).
    3. Rerun inkeep pull --project <same-project> in merge mode.
    4. Inspect the rewritten file and observe malformed declarations remain while regenerated exports are appended.
  • Code analysis: I reviewed the merge/write pipeline and found merge fallback only triggers on thrown exceptions, while mergeGeneratedModule can proceed without throwing even when the existing file contains invalid statements. In that path, unmatched malformed statements are retained and new generated declarations are appended, which matches the observed reproduction.
  • Why this is likely a bug: The code only falls back to full regeneration on thrown merge errors, but malformed existing statements can survive a non-throwing merge path, directly causing invalid generated output after rerun.

Relevant code:

agents-cli/src/commands/pull-v4/typescript-file-writer.ts (lines 14-38)

const processedContent =
  writeMode === 'merge' && existsSync(filePath)
    ? mergeSafely(readFileSync(filePath, 'utf8'), content)
    : content;

function mergeSafely(existingContent: string, generatedContent: string): string {
  try {
    return mergeGeneratedModule(existingContent, generatedContent);
  } catch (error) {
    console.warn(
      `Warning: Failed to merge file, using generated content. Manual changes may be lost. Reason: ${error instanceof Error ? error.message : String(error)}`
    );
    return generatedContent;
  }
}

agents-cli/src/commands/pull-v4/module-merge.ts (lines 5-24)

export function mergeGeneratedModule(existingContent: string, generatedContent: string): string {
  const project = createInMemoryProject();

  const existingSourceFile = project.createSourceFile('existing.ts', existingContent, {
    overwrite: true,
  });
  const generatedSourceFile = project.createSourceFile('generated.ts', generatedContent, {
    overwrite: true,
  });

  mergeImports(existingSourceFile, generatedSourceFile);

  for (const statement of generatedSourceFile.getStatements()) {
    if (Node.isImportDeclaration(statement)) {
      continue;
    }
    upsertStatement(existingSourceFile, statement);
  }
}

agents-cli/src/commands/pull-v4/module-merge.ts (lines 263-301)

function upsertVariableStatement(existingFile: SourceFile, generatedStatement: Statement) {
  if (!Node.isVariableStatement(generatedStatement)) {
    return;
  }

  const generatedDeclarations = generatedStatement.getDeclarations();
  if (!generatedDeclarations.length) {
    appendUniqueStatement(existingFile, generatedStatement);
    return;
  }

  const existingStatements = new Set<Statement>();
  for (const generatedDeclaration of generatedDeclarations) {
    let existingDeclaration = existingFile.getVariableDeclaration(generatedDeclaration.getName());
    if (!existingDeclaration) {
      continue;
    }
    const existingStatement = existingDeclaration.getFirstAncestorByKind(
      SyntaxKind.VariableStatement
    );
    if (existingStatement) {
      existingStatements.add(existingStatement);
    }
  }

  if (!existingStatements.size) {
    appendUniqueStatement(existingFile, generatedStatement);
    return;
  }
}
🟠 JSON pull path can hang after output
  • What failed: The command prints normalized JSON but returns early instead of explicitly terminating, while the normal success path explicitly exits. Expected behavior is consistent command termination after successful output.
  • Impact: Automation and user workflows relying on command completion can stall even after valid JSON is produced. This can block chained scripts and create false timeout failures in CI/local tooling.
  • Introduced by this PR: No – pre-existing bug (code not changed in this PR)
  • Steps to reproduce:
    1. Open a normal (non-batch) CLI session in a project workspace.
    2. Run inkeep pull --project support-project --json.
    3. Wait for JSON output and verify whether the process exits immediately or remains active.
  • Code analysis: I inspected the pull-v4 command flow and found options.json returns immediately after logging output, bypassing the explicit process.exit(0) used by the standard success path. I also verified the CLI already uses forced exit to avoid lingering background handles in another command, which supports this as a production logic gap rather than test setup noise.
  • Why this is likely a bug: The production code uses explicit process termination to prevent hangs on successful completion, but JSON mode skips that termination path and can leave the command alive unexpectedly.

Relevant code:

agents-cli/src/commands/pull-v4/introspect/index.ts (lines 370-374)

if (options.json) {
  console.log(JSON.stringify(remoteProject, null, 2));
  restoreLogLevel();
  return;
}

agents-cli/src/commands/pull-v4/introspect/index.ts (lines 400-416)

restoreLogLevel();
if (batchMode) {
  return { success: true };
}
process.exit(0);
} catch (error) {
  const message = error instanceof Error ? error.stack : String(error);
  s.stop();
  console.error(styleText('red', `\nError: ${message}`));
  if (options.debug && error instanceof Error) {
    console.error(styleText('red', error.stack || ''));
  }
  restoreLogLevel();
  if (batchMode) {

agents-cli/src/commands/push.ts (lines 286-287)

// Force exit to avoid hanging due to OpenTelemetry or other background tasks
process.exit(0);
🟠 JSON mode lacks explicit success exit
  • What failed: JSON mode succeeds in producing payload but does not guarantee termination through the explicit success-exit path. Expected behavior is to end the process after successful JSON output just like other success flows.
  • Impact: Users can observe stuck terminal sessions after successful output, reducing CLI reliability and confusing success detection. Repeated hangs can degrade confidence in pull automation and require manual interruption.
  • Introduced by this PR: No – pre-existing bug (code not changed in this PR)
  • Steps to reproduce:
    1. Run inkeep pull --project support-project --json in a non-batch CLI run.
    2. Confirm normalized JSON is printed to stdout.
    3. Check whether the command exits automatically or remains running.
  • Code analysis: The same early-return branch is exercised for this scenario and bypasses the explicit non-batch success exit. The PR diff metadata shows this file changed in unrelated skill-generation areas, not in the JSON return block, so the defect appears pre-existing.
  • Why this is likely a bug: The code has an inconsistent success-path lifecycle where JSON mode omits explicit termination that the command otherwise depends on to avoid lingering execution.

Relevant code:

agents-cli/src/commands/pull-v4/introspect/index.ts (lines 370-374)

if (options.json) {
  console.log(JSON.stringify(remoteProject, null, 2));
  restoreLogLevel();
  return;
}

agents-cli/src/commands/pull-v4/introspect/index.ts (lines 400-404)

restoreLogLevel();
if (batchMode) {
  return { success: true };
}
process.exit(0);

agents-cli/src/commands/push.ts (lines 286-287)

// Force exit to avoid hanging due to OpenTelemetry or other background tasks
process.exit(0);
✅ Passed (15)
Category Summary Screenshot
Adversarial Pull with malicious skill id '../escape' was contained to adversarial-project/skills/escape with no filesystem writes outside project root. ADV-1
Adversarial Adversarial payload content was emitted as inert file content in generated skill markdown and did not execute during generation. ADV-2
Adversarial Repeated pull reruns completed without corruption; key files stabilized to deterministic hashes after rerun convergence. ADV-3
Adversarial Invalid auth produced safe failure (401/auth error) and output remained redacted without exposing configured secrets. ADV-4
Edge Project with an empty skills map produced no skill artifact files, matching expected behavior. EDGE-1
Edge Nested-directory path mode and project-ID mode both targeted the correct output location without cross-contamination. N/A
Edge After a forced interruption, immediate retry completed cleanly and service health remained stable before and after retry. N/A
Edge Mobile viewport flow remained usable at 390x844, including email entry and transition to the next auth step. N/A
Logic After manually moving and renaming the tool export, rerunning pull preserved ./tools/custom/custom-primary and customPrimaryTool references in index.ts. LOGIC-1
Logic Repeated pulls on a fixture with duplicate tool display names consistently generated duplicate-name/duplicate-name-1 filenames and stable import aliasing. LOGIC-2
Logic Not a real app bug. The prior block was environment/fixture methodology. Re-execution via existing regression test passed, and source confirms helper fields are internal-only and stripped from emitted output (agents-cli/src/commands/pull-v4/generators/context-config-generator.ts:66-70, 299-306; validated by test assertions in .../introspect/context-config-regressions.test.ts:135-136). LOGIC-3
Logic Generated agent file maintained declaration-before-top-level-usage ordering across reruns; no pre-declaration symbol usage was introduced. LOGIC-4
Logic Not a real app bug. Prior blockage was fixture/setup-related. Re-execution confirmed skip logic in runtime for mixed completeness input and code path confirms generation continues with complete agents only (agents-cli/src/commands/pull-v4/generation-types.ts:92-106,108-129; introspect-generator.ts:31-57). LOGIC-5
Happy-path Batch pull created the new project directory with skills/general-gameplan/SKILL.md as expected. ROUTE-2
Happy-path Project with credential references generated environments/development.env.ts and environments/index.ts. ROUTE-4

Commit: e4d9c94

View Full Run


Tell us how we did: Give Ito Feedback

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.

2 participants