Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .agents/pm/history/pm-e48i.jsonl

Large diffs are not rendered by default.

39 changes: 39 additions & 0 deletions .agents/pm/tasks/pm-e48i.toon
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
id: pm-e48i
title: Package install fallback hints and extension collision plans
description: "Bundle GitHub issues #133 and #134 under package-platform maturity: improve npm package install recovery for unpublished packages with deterministic fallback hints, and add machine-readable collision planning to extension diagnostics/explore so agents can resolve multi-package overlaps with less context. Duplicate check: pm search for unpublished npm install fallback and collision-aware extension profiles found only closed historical package-platform items, no open implementation item."
type: Task
status: closed
priority: 2
tags[6]: agent-ux,"area:extensions","area:packages","area:sdk",github-issue-133,github-issue-134
created_at: "2026-06-07T05:03:46.374Z"
updated_at: "2026-06-07T05:31:45.133Z"
author: codex-impl-20260607
parent: pm-ugqx
resolution: Delivered deterministic package-install recovery and extension collision planning for agent package workflows.
expected_result: Agents receive compact machine-readable recovery hints for unpublished npm package installs and deterministic collision remediation plans for multi-package extension overlaps.
actual_result: npm 404 JSON errors now preserve fallback_candidates and next_best_command end-to-end; package doctor triage now includes collision_plan with ranked deactivate candidates and feature-loss hints; docs and tests cover both behaviors.
comments[4]{created_at,author,text}:
"2026-06-07T05:03:54.475Z",codex-impl-20260607,"Orientation evidence: no open PRs; open GitHub issues #133/#134 map to this package-platform task; recent main CI/Docs/CodeQL success on f8d9f31a; pnpm audit --audit-level low reports no known vulnerabilities; pnpm outdated shows commander 15 latest but wanted/current 14.0.3, keeping out of scope due Node support history."
"2026-06-07T05:15:08.709Z",codex-impl-20260607,"Implementation evidence: added npm 404 classification for npm:<name> installs with JSON recovery.fallback_candidates and recovery.next_best_command; added structured triage.collision_plan for extension registration collisions with deterministic deactivate candidates and feature-loss hints; preserved command-specific recovery during CLI error enrichment; documented package-source fallback and collision_plan JSON diagnostics. Sub-agent evidence: GitHub/security lane found no open PRs, current main CI/Docs/CodeQL green, no open Dependabot/code-scanning/secret/advisory alerts, pnpm audit clean, commander@15 blocked by Node engine; SDK/package-author lane recommended pm-e48i as the non-duplicate canonical item for #133/#134."
"2026-06-07T05:15:09.041Z",codex-impl-20260607,"Verification evidence: node scripts/run-tests.mjs test -- tests/unit/cli-main-errors.spec.ts tests/unit/extension-command.spec.ts tests/unit/error-guidance.spec.ts passed (3 files, 80 tests); pnpm typecheck passed; pnpm build passed; pnpm contracts:check passed; pnpm quality:static passed; pnpm security:scan passed; pnpm audit --audit-level low reported no known vulnerabilities; node scripts/run-tests.mjs coverage passed at 2531 tests with 100% statements/branches/functions/lines; manual isolated temp smoke at /tmp/pm-e48i-smoke-cQqRP5 passed init, npm 404 JSON fallback assertions, and health --check-only --summary --json. pm validate --check-resolution --check-history-drift reported history_drift ok and pre-existing resolution metadata warnings on older closed items."
"2026-06-07T05:31:45.133Z",codex-impl-20260607,"Review follow-up evidence: addressed CodeRabbit findings by narrowing isNpmNotFoundError to explicit registry 404 signatures, routing broader generic not-found matching only through the npm-pack wrapper, suppressing first-party GitHub fallback candidates for third-party npm package specs, replacing the live npm 404 unit test with synthetic wrapper coverage, and tightening assertions for attempted_command, normalized_args, fallback reason, and next_best_command. Verification after follow-up: focused unit tests passed (3 files, 82 tests); pnpm typecheck passed; docs-skills/static/security gates passed; contracts/changelog checks passed; isolated temp smoke /tmp/pm-e48i-smoke-vVO4xv passed; full coverage passed 2533 tests with 100% statements/branches/functions/lines."
files[9]{path,scope,note}:
src/cli/commands/extension.ts,project,Expose collision_plan in extension triage summary type
src/cli/commands/extension/doctor.ts,project,Build structured extension collision remediation plan
src/cli/commands/extension/install-sources.ts,project,Classify npm 404 package installs and attach deterministic fallback recovery
src/cli/error-guidance.ts,project,Render and preserve fallback recovery metadata in JSON/text errors
src/cli/main.ts,project,Preserve command-specific fallback recovery during CLI error enrichment
src/core/shared/errors.ts,project,Extend recovery payload for structured package fallback hints
tests/unit/cli-main-errors.spec.ts,project,Cover fallback recovery merge in main error path
tests/unit/error-guidance.spec.ts,project,Cover fallback recovery JSON/text plumbing
tests/unit/extension-command.spec.ts,project,Cover npm fallback and collision plan behavior
tests[3]{command,path,scope,timeout_seconds,pm_context_mode,env_set,env_clear,shared_host_safe,assert_stdout_contains,assert_stdout_regex,assert_stderr_contains,assert_stderr_regex,assert_stdout_min_lines,assert_json_field_equals,assert_json_field_gte,note}:
node scripts/run-tests.mjs test -- tests/unit/cli-main-errors.spec.ts tests/unit/extension-command.spec.ts tests/unit/error-guidance.spec.ts,null,project,240,null,null,null,null,null,null,null,null,null,null,null,null
node scripts/run-tests.mjs test -- tests/unit/extension-command.spec.ts tests/unit/error-guidance.spec.ts,null,project,240,null,null,null,null,null,null,null,null,null,null,null,null
pnpm typecheck,null,project,240,null,null,null,null,null,null,null,null,null,null,null,null
test_runs[1]{run_id,kind,status,started_at,finished_at,recorded_at,attempt,resumed_from,passed,failed,skipped,items,linked_tests,fail_on_skipped_triggered}:
test-local-mq3blrxa-0yh706,test,passed,"2026-06-07T05:07:01.993Z","2026-06-07T05:07:21.118Z","2026-06-07T05:07:21.118Z",null,null,2,0,0,null,null,null
docs[1]{path,scope,note}:
docs/EXTENSIONS.md,project,Document npm 404 fallback and collision_plan JSON diagnostics
close_reason: "Delivered package install fallback hints and extension collision plans for GitHub issues #133/#134 with tests, docs, temp smoke, coverage, security, contracts, health, and changelog validation evidence."
body: ""
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

### Other

- Package install fallback hints and extension collision plans ([pm-e48i](https://github.com/unbraind/pm-cli/blob/main/.agents/pm/tasks/pm-e48i.toon))
- Stabilize npx onboarding path for scoped pm package ([pm-pgew](https://github.com/unbraind/pm-cli/blob/main/.agents/pm/tasks/pm-pgew.toon))
- Publish pm-github starter package as a community reference \(credential-requiring pattern\) ([pm-zw0n](https://github.com/unbraind/pm-cli/blob/main/.agents/pm/chores/pm-zw0n.toon))
- Make list --ids repeatable for focused agent working-set refresh ([pm-42tb](https://github.com/unbraind/pm-cli/blob/main/.agents/pm/tasks/pm-42tb.toon))
Expand Down
17 changes: 8 additions & 9 deletions docs/EXTENSIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,7 @@ pm upgrade --dry-run

`pm extension ...` remains supported for compatibility and low-level runtime debugging.

Related docs:

- [SDK](SDK.md)
- [Configuration](CONFIGURATION.md)
- [Testing](TESTING.md)
- [Command Reference](COMMANDS.md)
- [Extension Author Contracts](EXTENSION_AUTHOR_CONTRACTS.md)
Related docs: [SDK](SDK.md), [Configuration](CONFIGURATION.md), [Testing](TESTING.md), [Command Reference](COMMANDS.md), [Extension Author Contracts](EXTENSION_AUTHOR_CONTRACTS.md).

## Package Sources

Expand Down Expand Up @@ -46,7 +40,7 @@ pm install governance-audit --project

`pm install '*'`, `pm install all`, and shell-expanded `pm install *` are normalized to the same bundled install-all request. First-party package aliases come from each package manifest, with a fallback derived from the `packages/pm-*` directory name.

External registry packages are installed by exact package name:
External registry packages are installed by exact package name. If `npm:<name>` returns a registry 404, JSON error output includes `fallback_candidates` and `next_best_command`; unpublished first-party packages fall back to `pm install --project github.com/unbraind/<name>`.

```bash
npm search "pm-cli pm-package"
Expand All @@ -56,6 +50,11 @@ pm package doctor --project --detail deep --trace
pm github validate --repo owner/repo
```

If `npm:<name>` returns a registry 404, JSON error output includes a compact
recovery bundle with `fallback_candidates` and `next_best_command`. For
unpublished first-party packages, the first fallback is the canonical GitHub
source, for example `pm install --project github.com/unbraind/<name>`.

For `pm-github`, run `pm github validate --repo owner/repo` before mutating commands; write paths require `GITHUB_TOKEN`/`GH_TOKEN` or `gh auth login`.

For ecosystem maintenance, use the reusable external package smoke harness after
Expand Down Expand Up @@ -282,7 +281,7 @@ pm package deactivate <conflicting-package> --project
pm package doctor --project --strict-exit
```

For production stacks, keep broad demo/starter packages separate from packages that own real workflow behavior, or constrain registration surfaces through `extensions.policy.extension_overrides`.
Doctor JSON also includes `triage.collision_plan` with grouped surfaces, ranked deactivation candidates, and command/action feature-loss hints. For production stacks, keep broad demo/starter packages separate from packages that own real workflow behavior, or constrain registration surfaces through `extensions.policy.extension_overrides`.

## Runtime APIs

Expand Down
24 changes: 24 additions & 0 deletions src/cli/commands/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,30 @@ export interface ExtensionTriageSummary {
update_check_failed_total: number;
top_warnings: string[];
remediation: string[];
collision_plan?: ExtensionCollisionPlan;
}

export interface ExtensionCollisionPlan {
status: "ok" | "conflicts_detected";
collision_count: number;
extension_count: number;
next_best_command: string;
collisions: Array<{
code: string;
surface: string;
winner: { layer: ExtensionScope; name: string };
displaced: { layer: ExtensionScope; name: string };
}>;
remediation_candidates: Array<{
action: "deactivate";
extension: string;
command: string;
affected_collisions: number;
feature_loss: {
command_paths: string[];
action_paths: string[];
};
}>;
}

export type ExtensionUpdateCheckStatus = "checked" | "skipped_unmanaged" | "skipped_non_github" | "failed" | "not_checked";
Expand Down
73 changes: 73 additions & 0 deletions src/cli/commands/extension/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { EXIT_CODE } from "../../../core/shared/constants.js";
import { PmCliError } from "../../../core/shared/errors.js";
import { normalizeExtensionNameForMatch } from "./shared.js";
import type {
ExtensionCollisionPlan,
ExtensionCommandOptions,
ExtensionDoctorDetailMode,
ExtensionScope,
Expand Down Expand Up @@ -166,6 +167,7 @@ function isExtensionLayer(value: string | undefined): value is "project" | "glob

interface RegistrationCollisionWarningParts {
code: string;
surface: string;
winner: { layer: "project" | "global"; name: string };
displaced: { layer: "project" | "global"; name: string };
}
Expand All @@ -192,6 +194,7 @@ function parseRegistrationCollisionWarning(warning: string): RegistrationCollisi
}
return {
code,
surface: parts.slice(1, -4).join(":") || "global",
winner: { layer: winnerLayer, name: winnerName },
displaced: { layer: displacedLayer, name: displacedName },
};
Expand Down Expand Up @@ -227,6 +230,74 @@ export function buildRegistrationCollisionRemediation(
);
}

function buildRegistrationCollisionPlan(
scope: ExtensionScope,
warnings: string[],
extensions: ManagedExtensionSummary[],
options: ExtensionCommandOptions,
): ExtensionCollisionPlan | undefined {
const collisions = warnings
.map((warning) => parseRegistrationCollisionWarning(warning))
.filter((entry): entry is RegistrationCollisionWarningParts => entry !== null)
.sort((left, right) =>
`${left.code}:${left.surface}:${left.winner.name}:${left.displaced.name}`.localeCompare(
`${right.code}:${right.surface}:${right.winner.name}:${right.displaced.name}`,
),
);
if (collisions.length === 0) {
return undefined;
}
const scopeFlag = scope === "global" ? "--global" : "--project";
const deactivatePrefix = lifecycleFlagCommand(options, "deactivate");
const doctorCommand = `${lifecycleFlagCommand(options, "doctor")} ${scopeFlag} --detail deep --trace`;
const extensionByName = new Map(extensions.map((entry) => [normalizeExtensionNameForMatch(entry.name), entry]));
const affectedByName = new Map<string, number>();
for (const collision of collisions) {
for (const name of [collision.winner.name, collision.displaced.name]) {
const normalizedName = normalizeExtensionNameForMatch(name);
affectedByName.set(normalizedName, (affectedByName.get(normalizedName) ?? 0) + 1);
}
}
const remediationCandidates = [...affectedByName.entries()]
.map(([normalizedName, affectedCollisions]) => {
const extension = extensionByName.get(normalizedName);
const extensionName = extension?.name ?? normalizedName;
const commandPaths = extension?.command_paths ?? [];
const actionPaths = extension?.action_paths ?? [];
return {
action: "deactivate" as const,
extension: extensionName,
command: `${deactivatePrefix} ${extensionName} ${scopeFlag}`,
affected_collisions: affectedCollisions,
feature_loss: {
command_paths: [...commandPaths].sort((left, right) => left.localeCompare(right)),
action_paths: [...actionPaths].sort((left, right) => left.localeCompare(right)),
},
};
})
.sort((left, right) => {
const affectedDelta = right.affected_collisions - left.affected_collisions;
if (affectedDelta !== 0) {
return affectedDelta;
}
const leftSurfaceCount = left.feature_loss.command_paths.length + left.feature_loss.action_paths.length;
const rightSurfaceCount = right.feature_loss.command_paths.length + right.feature_loss.action_paths.length;
if (leftSurfaceCount !== rightSurfaceCount) {
return leftSurfaceCount - rightSurfaceCount;
}
return left.extension.localeCompare(right.extension);
});

return {
status: "conflicts_detected",
collision_count: collisions.length,
extension_count: affectedByName.size,
next_best_command: doctorCommand,
collisions,
remediation_candidates: remediationCandidates,
};
}

export function classifyDoctorLoadFailureWarnings(loadFailures: Array<{ name: string; error: string }>): string[] {
const warnings: string[] = [];
for (const failure of loadFailures) {
Expand Down Expand Up @@ -394,6 +465,7 @@ export function buildExtensionTriageSummary(
);
}
}
const collisionPlan = buildRegistrationCollisionPlan(scope, normalizedWarnings, extensions, options);
if (updateHealthPartial) {
remediation.push(
`Update-check coverage is partial because unmanaged extensions need adoption. Adopt existing installs via ${lifecycleFlagCommand(options, "manage")} ${scopeFlag} --fix-managed-state (or ${lifecycleFlagCommand(options, "adopt-all")} ${scopeFlag}, ${lifecycleFlagCommand(options, "adopt")} <name> ${scopeFlag}, or reinstall via ${lifecycleFlagCommand(options, "install")} ${scopeFlag} <source>).`,
Expand Down Expand Up @@ -439,6 +511,7 @@ export function buildExtensionTriageSummary(
update_check_failed_total: updateCheckFailedTotal,
top_warnings: effectiveWarnings.slice(0, 8),
remediation,
...(collisionPlan ? { collision_plan: collisionPlan } : {}),
};
}

Expand Down
Loading
Loading