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
95 changes: 95 additions & 0 deletions docs/todos/2026-06-18-issue-536-copilot-cli-hint/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Copilot CLI Session-Start Hint Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Add the remaining Copilot hook compatibility behavior from issue #536 by logging a safe server-unreachable hint from `session-start`.

**Architecture:** Preserve the existing hook model: `session-start` reads JSON from stdin, posts to `/agentmemory/session/start`, writes context to stdout only when injection is enabled, and otherwise exits cleanly. Add a small local error formatter/logger used by both fetch rejection paths so rejected fetches write a concise stderr hint without changing stdout or exit behavior.

**Tech Stack:** TypeScript ESM hook sources, tsdown bundled plugin scripts, Vitest hook smoke tests, pnpm.

---

## Files

- Modify: `test/hook-source-smoke.test.ts`
- Modify: `src/hooks/session-start.ts`
- Generated by build if needed: `plugin/scripts/session-start.mjs`
- Update: `docs/todos/2026-06-18-issue-536-copilot-cli-hint/todo.md`

## Task 1: Add Failing Coverage

- [ ] Add assertions to `test/hook-source-smoke.test.ts` in the existing `session-start is fire-and-forget by default and writes context only when opted in` test:

```ts
expect(String(abortedTelemetry.stderrWrite.mock.calls[0]?.[0])).toContain(
"agentmemory server unreachable at http://localhost:3111; start npx @agentmemory/agentmemory",
);

expect(String(abortedInjected.stderrWrite.mock.calls[0]?.[0])).toContain(
"agentmemory server unreachable at http://localhost:3111; start npx @agentmemory/agentmemory",
);
```

- [ ] Run the narrow test to verify it fails for the missing hint:

```bash
corepack pnpm exec vitest run test/hook-source-smoke.test.ts -t "session-start is fire-and-forget by default and writes context only when opted in"
```

Expected: FAIL because `stderrWrite` is not called on rejected fetches.

## Task 2: Implement Minimal Hint

- [ ] Add a local helper in `src/hooks/session-start.ts`:

```ts
function logSessionStartFetchError(err: unknown): void {
const detail = err instanceof Error && err.message ? ` (${err.message})` : "";
process.stderr.write(
`agentmemory server unreachable at ${REST_URL}; start npx @agentmemory/agentmemory${detail}\n`,
);
}
```

- [ ] Replace both `.catch(() => {})` / empty `catch` paths for the session-start fetch with `logSessionStartFetchError`.

- [ ] Run the narrow test again. Expected: PASS.

## Task 3: Regenerate Packaged Hook

- [ ] Run the build if `plugin/scripts/session-start.mjs` is not automatically updated:

```bash
corepack pnpm run build
```

Expected: build exits 0 and updates bundled hook output.

- [ ] Run focused package/hook tests:

```bash
corepack pnpm exec vitest run test/hook-source-smoke.test.ts test/copilot-plugin.test.ts test/build-package-contract.test.ts
```

Expected: all tests pass.

## Task 4: Final Gates And GitHub Workflow

- [ ] Run focused Copilot/connect verification:

```bash
corepack pnpm exec vitest run test/cli-connect.test.ts test/copilot-plugin.test.ts test/onboarding.test.ts test/build-package-contract.test.ts test/hook-source-smoke.test.ts
```

- [ ] Run required security/secret gates for code/tooling changes before commit:

```bash
semgrep scan --config p/default --error --metrics=off .
osv-scanner scan source .
git add <task-owned paths>
gitleaks protect --staged --redact
```

- [ ] Commit task-owned paths only.
- [ ] Push branch to `origin`, create PR against `main`, monitor checks, merge on success, verify final issue status, and close or link issue #536 with evidence.
81 changes: 81 additions & 0 deletions docs/todos/2026-06-18-issue-536-copilot-cli-hint/todo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Issue 536 Copilot CLI Hook Hint

## Scope

- Repository: `/Users/A1538552/.codex/worktrees/d03c/agentmemory`
- Branch: `issue/536-copilot-cli-connect`
- Issue: GitHub #536, upstream PR #652 tracking Copilot CLI connect wiring and hook compatibility.
- Target: `origin/main` only. Do not prepare or open PRs against `rohitg00/agentmemory`.

## Assumptions

- `origin/main` at `4d5fbe20f5cd8273687da459ec1128dfaf4f5226` already contains the main Copilot CLI connect adapter, Copilot plugin manifests, hook manifest, and PostToolUse payload compatibility.
- The remaining valid gap is the upstream note that `session-start` should emit a useful server-unreachable hint while exiting successfully.
- No user configuration should be mutated; tests must use mocks or isolated subprocess fixtures.

## Sprint Contract

- Goal: close the remaining Copilot hook compatibility gap by surfacing a safe server-unreachable hint from `session-start`.
- Scope: `src/hooks/session-start.ts`, generated packaged hook output if required by existing package-contract tests, focused tests, and this task record.
- Non-goals: changing MCP/connect behavior, adding new tools or endpoints, changing auth, schema, persistence, remote service calls, or wiring real user Copilot configuration.
- Acceptance criteria:
- `session-start` still exits successfully when the server is unreachable.
- Both fire-and-forget and context-injection paths log a concise hint to stderr on rejected fetches.
- The hint does not write to stdout or block hook startup.
- Packaged hook output stays aligned with source.
- Intended verification:
- Red/green targeted hook test.
- Focused Copilot/connect/package test set.
- Build when packaged hook output must be regenerated.
- Required security/secret gates before commit if changes remain.
- Known boundaries:
- No real Copilot or home-directory config mutation.
- No PRs to upstream `rohitg00/agentmemory`.
- Remote push/PR/merge/issue-close only within the explicit delegated GitHub workflow.
- Stop conditions:
- Tests show a wider behavior contract conflict.
- Fix requires changing auth, API, persistence, schema, or external service boundaries.
- Required security gates fail and cannot be fixed within scope.

## Feature / Verification Matrix

| Change | Verification method | Status | Evidence |
| --- | --- | --- | --- |
| Validate existing Copilot wiring is present | Read-only file inspection plus focused tests | Done | `corepack pnpm exec vitest run test/cli-connect.test.ts test/copilot-plugin.test.ts test/onboarding.test.ts test/build-package-contract.test.ts` passed 70 tests |
| Add `session-start` unreachable hint | TDD in `test/hook-source-smoke.test.ts` | Done | Red: targeted test failed because `stderrWrite` was undefined. Green: targeted test passed after `logSessionStartFetchError`. |
| Keep packaged hook aligned | Build/package contract check | Done | `corepack pnpm run build` exited 0; `corepack pnpm exec vitest run test/hook-source-smoke.test.ts test/copilot-plugin.test.ts test/build-package-contract.test.ts` passed 46 tests |
| Final GitHub workflow | Git status, commit, push, PR, checks, merge, issue close | In progress | Local commit and base integration complete; remote workflow pending |

## Subagent Ledger

| Workstream | Scope | Edits allowed | Expected output | Result | Residual risk |
| --- | --- | --- | --- | --- | --- |
| Read-only validation | Copilot connect/hooks/manifests/tests | No | Evidence whether #536 is valid/stale/already fixed | Reported main wiring is already fixed and identified missing `session-start` ECONNREFUSED hint | Subagent did not perform remote writes |
| Implementation review | Task-owned diff only | No | ACCEPT or evidence-backed findings | ACCEPT; no Critical or Important actionable issues | Residual risk limited to failures after the 500ms fire-and-forget exit window |

## Progress And Review Notes

- Confirmed worktree started clean on detached `origin/main` commit `4d5fbe20f5cd8273687da459ec1128dfaf4f5226`, then created local branch `issue/536-copilot-cli-connect`.
- Confirmed `origin` is `https://github.com/wbugitlab1/agentmemory.git`; `upstream` is the forbidden `rohitg00/agentmemory` remote and was not used as a PR target.
- Issue #536 is mostly stale: Copilot CLI connect adapter, plugin manifest, hook manifest, Copilot payload compatibility, and focused tests already exist on `origin/main`.
- Remaining valid gap: `session-start` swallowed rejected fetches in both telemetry and injection paths, so Copilot users did not see the upstream-described server-unreachable hint.
- Passive security-best-practices review: no new credentials are logged, stdout remains reserved for context injection, stderr message contains only configured base URL plus generic error text, no new network target is introduced, and no user config mutation is added.

## Verification Evidence

- `corepack pnpm install --frozen-lockfile --ignore-scripts` exited 0 after initial pnpm ignored-build hardening blocked dependency materialization.
- `corepack pnpm exec vitest run test/hook-source-smoke.test.ts -t "session-start is fire-and-forget by default and writes context only when opted in"` failed before implementation with `stderrWrite` undefined.
- Same targeted test passed after implementation: 1 passed, 22 skipped.
- `corepack pnpm run build` exited 0 and regenerated the tracked packaged hook script.
- `corepack pnpm exec vitest run test/hook-source-smoke.test.ts test/copilot-plugin.test.ts test/build-package-contract.test.ts` passed 46 tests.
- `corepack pnpm exec vitest run test/cli-connect.test.ts test/copilot-plugin.test.ts test/onboarding.test.ts test/build-package-contract.test.ts test/hook-source-smoke.test.ts` passed 93 tests.
- `corepack pnpm run lint` exited 0.
- `corepack pnpm test` passed 194 test files and 2728 tests.
- `semgrep scan --config p/default --error --metrics=off .` completed with 0 findings.
- `osv-scanner scan source .` completed with no unfiltered issues; one existing transitive OpenTelemetry finding was filtered by the repo waiver.
- `git diff --cached --check` exited 0 after removing one trailing blank line from `plan.md`.
- `gitleaks protect --staged --redact` scanned the staged diff and found no leaks.
- Post-base merge: fetched `origin/main` at `667f19f542b3959c29a553d17c8571d6cd673ad3`; merged that captured base commit into `issue/536-copilot-cli-connect` with no conflicts.
- Post-base merge verification: `corepack pnpm exec vitest run test/cli-connect.test.ts test/copilot-plugin.test.ts test/onboarding.test.ts test/build-package-contract.test.ts test/hook-source-smoke.test.ts` passed 93 tests.
- Post-base merge verification: `corepack pnpm run lint` exited 0.
- Post-base merge verification: `corepack pnpm test` passed 194 test files and 2728 tests.
10 changes: 8 additions & 2 deletions plugin/scripts/session-start.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,10 @@ function optionalString(value) {
function logSessionStartHttpError(res) {
if (!res.ok) process.stderr.write(`agentmemory: /agentmemory/session/start returned HTTP ${res.status}\n`);
}
function logSessionStartFetchError(err) {
const detail = err instanceof Error && err.message ? ` (${err.message})` : "";
process.stderr.write(`agentmemory server unreachable at ${REST_URL}; start npx @agentmemory/agentmemory${detail}\n`);
}
async function main() {
let input = "";
for await (const chunk of process.stdin) input += chunk;
Expand Down Expand Up @@ -222,7 +226,7 @@ async function main() {
guardedFetch(REST_URL, "/agentmemory/session/start", SECRET, {
...init,
signal: AbortSignal.timeout(REGISTER_TIMEOUT_MS)
})?.then(logSessionStartHttpError).catch(() => {});
})?.then(logSessionStartHttpError).catch(logSessionStartFetchError);
setTimeout(() => process.exit(0), 500).unref();
return;
}
Expand All @@ -237,7 +241,9 @@ async function main() {
const result = await res.json();
if (result.context) process.stdout.write(result.context);
}
} catch {}
} catch (err) {
logSessionStartFetchError(err);
}
}
main();
//#endregion
Expand Down
12 changes: 10 additions & 2 deletions src/hooks/session-start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ function logSessionStartHttpError(res: Response): void {
}
}

function logSessionStartFetchError(err: unknown): void {
const detail = err instanceof Error && err.message ? ` (${err.message})` : "";
process.stderr.write(
`agentmemory server unreachable at ${REST_URL}; start npx @agentmemory/agentmemory${detail}\n`,
);
}

async function main() {
let input = "";
for await (const chunk of process.stdin) {
Expand Down Expand Up @@ -103,7 +110,7 @@ async function main() {
signal: AbortSignal.timeout(REGISTER_TIMEOUT_MS),
})
?.then(logSessionStartHttpError)
.catch(() => {});
.catch(logSessionStartFetchError);
setTimeout(() => process.exit(0), 500).unref();
return;
}
Expand All @@ -121,8 +128,9 @@ async function main() {
process.stdout.write(result.context);
}
}
} catch {
} catch (err) {
// silently fail -- don't block Claude Code startup
logSessionStartFetchError(err);
}
}

Expand Down
16 changes: 12 additions & 4 deletions test/hook-source-smoke.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,9 @@ describe("source hook entrypoints", () => {
{ fetchMode: "rejected" },
);
expect(abortedTelemetry.stdoutWrite).not.toHaveBeenCalled();
expect(abortedTelemetry.stderrWrite).not.toHaveBeenCalled();
expect(String(abortedTelemetry.stderrWrite.mock.calls[0]?.[0])).toContain(
"agentmemory server unreachable at http://localhost:3111; start npx @agentmemory/agentmemory",
);
expect(abortedTelemetry.setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 500);

const abortedInjected = await importHook(
Expand All @@ -497,7 +499,9 @@ describe("source hook entrypoints", () => {
{ fetchMode: "rejected" },
);
expect(abortedInjected.stdoutWrite).not.toHaveBeenCalled();
expect(abortedInjected.stderrWrite).not.toHaveBeenCalled();
expect(String(abortedInjected.stderrWrite.mock.calls[0]?.[0])).toContain(
"agentmemory server unreachable at http://localhost:3111; start npx @agentmemory/agentmemory",
);
expect(abortedInjected.setTimeoutSpy).not.toHaveBeenCalled();
});

Expand Down Expand Up @@ -584,7 +588,9 @@ describe("source hook entrypoints", () => {
"packaged",
);
expect(abortedSessionStart.stdoutWrite).not.toHaveBeenCalled();
expect(abortedSessionStart.stderrWrite).not.toHaveBeenCalled();
expect(String(abortedSessionStart.stderrWrite.mock.calls[0]?.[0])).toContain(
"agentmemory server unreachable at http://localhost:3111; start npx @agentmemory/agentmemory",
);
expect(abortedSessionStart.setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 500);

const abortedInjectedSessionStart = await importHook(
Expand All @@ -598,7 +604,9 @@ describe("source hook entrypoints", () => {
"packaged",
);
expect(abortedInjectedSessionStart.stdoutWrite).not.toHaveBeenCalled();
expect(abortedInjectedSessionStart.stderrWrite).not.toHaveBeenCalled();
expect(String(abortedInjectedSessionStart.stderrWrite.mock.calls[0]?.[0])).toContain(
"agentmemory server unreachable at http://localhost:3111; start npx @agentmemory/agentmemory",
);
expect(abortedInjectedSessionStart.setTimeoutSpy).not.toHaveBeenCalled();

const subagentStart = await importHook(
Expand Down
Loading