oh-my-copilot 5.6.2 → 5.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +2 -0
- package/bridge/__pycache__/gyoshu_bridge.cpython-312.pyc +0 -0
- package/bridge/claude-md-coordinator.cjs +2 -2
- package/bridge/cli.cjs +81405 -76559
- package/bridge/mcp-server.cjs +40048 -32187
- package/bridge/runtime-cli.cjs +943 -199
- package/bridge/team-mcp.cjs +716 -125
- package/bridge/team.js +1066 -290
- package/dist/__tests__/cli-win32-warning.test.js +7 -0
- package/dist/__tests__/cli-win32-warning.test.js.map +1 -1
- package/dist/__tests__/run-cjs-copilot-worker-routing.test.d.ts +2 -0
- package/dist/__tests__/run-cjs-copilot-worker-routing.test.d.ts.map +1 -0
- package/dist/__tests__/run-cjs-copilot-worker-routing.test.js +294 -0
- package/dist/__tests__/run-cjs-copilot-worker-routing.test.js.map +1 -0
- package/dist/__tests__/run-cjs-fail-closed-timeout.test.d.ts +2 -0
- package/dist/__tests__/run-cjs-fail-closed-timeout.test.d.ts.map +1 -0
- package/dist/__tests__/run-cjs-fail-closed-timeout.test.js +68 -0
- package/dist/__tests__/run-cjs-fail-closed-timeout.test.js.map +1 -0
- package/dist/__tests__/run-cjs-session-end-budget.test.d.ts +2 -0
- package/dist/__tests__/run-cjs-session-end-budget.test.d.ts.map +1 -0
- package/dist/__tests__/run-cjs-session-end-budget.test.js +171 -0
- package/dist/__tests__/run-cjs-session-end-budget.test.js.map +1 -0
- package/dist/__tests__/session-end-process-exit.test.js +70 -2
- package/dist/__tests__/session-end-process-exit.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-conflicts-json.test.d.ts +2 -0
- package/dist/cli/__tests__/doctor-conflicts-json.test.d.ts.map +1 -0
- package/dist/cli/__tests__/doctor-conflicts-json.test.js +46 -0
- package/dist/cli/__tests__/doctor-conflicts-json.test.js.map +1 -0
- package/dist/cli/__tests__/team.test.js +67 -0
- package/dist/cli/__tests__/team.test.js.map +1 -1
- package/dist/cli/__tests__/wait-teleport-json.test.d.ts +2 -0
- package/dist/cli/__tests__/wait-teleport-json.test.d.ts.map +1 -0
- package/dist/cli/__tests__/wait-teleport-json.test.js +64 -0
- package/dist/cli/__tests__/wait-teleport-json.test.js.map +1 -0
- package/dist/cli/commands/__tests__/smoke-tier2.test.d.ts +2 -0
- package/dist/cli/commands/__tests__/smoke-tier2.test.d.ts.map +1 -0
- package/dist/cli/commands/__tests__/smoke-tier2.test.js +110 -0
- package/dist/cli/commands/__tests__/smoke-tier2.test.js.map +1 -0
- package/dist/cli/commands/__tests__/team-status-json.test.d.ts +2 -0
- package/dist/cli/commands/__tests__/team-status-json.test.d.ts.map +1 -0
- package/dist/cli/commands/__tests__/team-status-json.test.js +141 -0
- package/dist/cli/commands/__tests__/team-status-json.test.js.map +1 -0
- package/dist/cli/commands/factory.d.ts +2 -0
- package/dist/cli/commands/factory.d.ts.map +1 -1
- package/dist/cli/commands/factory.js +8 -1
- package/dist/cli/commands/factory.js.map +1 -1
- package/dist/cli/commands/smoke.d.ts +36 -0
- package/dist/cli/commands/smoke.d.ts.map +1 -0
- package/dist/cli/commands/smoke.js +169 -0
- package/dist/cli/commands/smoke.js.map +1 -0
- package/dist/cli/commands/team.d.ts +2 -0
- package/dist/cli/commands/team.d.ts.map +1 -1
- package/dist/cli/commands/team.js +124 -10
- package/dist/cli/commands/team.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +70 -10
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/win32-warning.d.ts +7 -1
- package/dist/cli/win32-warning.d.ts.map +1 -1
- package/dist/cli/win32-warning.js +9 -1
- package/dist/cli/win32-warning.js.map +1 -1
- package/dist/constants/names.d.ts +1 -0
- package/dist/constants/names.d.ts.map +1 -1
- package/dist/constants/names.js +1 -0
- package/dist/constants/names.js.map +1 -1
- package/dist/factory/__tests__/check-run.test.js +2 -2
- package/dist/factory/__tests__/check-run.test.js.map +1 -1
- package/dist/factory/__tests__/listener.test.js +1 -1
- package/dist/factory/__tests__/listener.test.js.map +1 -1
- package/dist/factory/__tests__/status.test.js +42 -0
- package/dist/factory/__tests__/status.test.js.map +1 -1
- package/dist/factory/listener.d.ts.map +1 -1
- package/dist/factory/listener.js +7 -8
- package/dist/factory/listener.js.map +1 -1
- package/dist/factory/status.d.ts +19 -1
- package/dist/factory/status.d.ts.map +1 -1
- package/dist/factory/status.js +49 -8
- package/dist/factory/status.js.map +1 -1
- package/dist/factory/watchdog.d.ts.map +1 -1
- package/dist/factory/watchdog.js +14 -0
- package/dist/factory/watchdog.js.map +1 -1
- package/dist/factory/watchdog.test.js +45 -0
- package/dist/factory/watchdog.test.js.map +1 -1
- package/dist/hooks/session-end/__tests__/action-runner.test.js +5 -0
- package/dist/hooks/session-end/__tests__/action-runner.test.js.map +1 -1
- package/dist/hooks/session-end/__tests__/chain-enqueuer.test.js +186 -1
- package/dist/hooks/session-end/__tests__/chain-enqueuer.test.js.map +1 -1
- package/dist/hooks/session-end/__tests__/chain-link-env-passthrough.test.d.ts +2 -0
- package/dist/hooks/session-end/__tests__/chain-link-env-passthrough.test.d.ts.map +1 -0
- package/dist/hooks/session-end/__tests__/chain-link-env-passthrough.test.js +49 -0
- package/dist/hooks/session-end/__tests__/chain-link-env-passthrough.test.js.map +1 -0
- package/dist/hooks/session-end/__tests__/foreground-bootstrap.test.js +30 -1
- package/dist/hooks/session-end/__tests__/foreground-bootstrap.test.js.map +1 -1
- package/dist/hooks/session-end/__tests__/spawn-next.test.js +104 -3
- package/dist/hooks/session-end/__tests__/spawn-next.test.js.map +1 -1
- package/dist/hooks/session-end/__tests__/worker.test.js +6 -0
- package/dist/hooks/session-end/__tests__/worker.test.js.map +1 -1
- package/dist/hooks/session-end/action-runner.d.ts.map +1 -1
- package/dist/hooks/session-end/action-runner.js +2 -1
- package/dist/hooks/session-end/action-runner.js.map +1 -1
- package/dist/hooks/session-end/chain-enqueuer.d.ts +70 -7
- package/dist/hooks/session-end/chain-enqueuer.d.ts.map +1 -1
- package/dist/hooks/session-end/chain-enqueuer.js +354 -82
- package/dist/hooks/session-end/chain-enqueuer.js.map +1 -1
- package/dist/hooks/session-end/foreground-bootstrap.d.ts.map +1 -1
- package/dist/hooks/session-end/foreground-bootstrap.js +24 -16
- package/dist/hooks/session-end/foreground-bootstrap.js.map +1 -1
- package/dist/hooks/session-end/index.d.ts +2 -3
- package/dist/hooks/session-end/index.d.ts.map +1 -1
- package/dist/hooks/session-end/index.js +11 -13
- package/dist/hooks/session-end/index.js.map +1 -1
- package/dist/hooks/session-end/spawn-next.d.ts +81 -4
- package/dist/hooks/session-end/spawn-next.d.ts.map +1 -1
- package/dist/hooks/session-end/spawn-next.js +141 -14
- package/dist/hooks/session-end/spawn-next.js.map +1 -1
- package/dist/hooks/session-end/wiki-foreground-bootstrap.d.ts +17 -0
- package/dist/hooks/session-end/wiki-foreground-bootstrap.d.ts.map +1 -0
- package/dist/hooks/session-end/wiki-foreground-bootstrap.js +20 -0
- package/dist/hooks/session-end/wiki-foreground-bootstrap.js.map +1 -0
- package/dist/hooks/session-end/worker.d.ts.map +1 -1
- package/dist/hooks/session-end/worker.js +11 -6
- package/dist/hooks/session-end/worker.js.map +1 -1
- package/dist/lib/__tests__/atomic-write.test.js +32 -1
- package/dist/lib/__tests__/atomic-write.test.js.map +1 -1
- package/dist/lib/__tests__/mode-state-lock.test.js +121 -12
- package/dist/lib/__tests__/mode-state-lock.test.js.map +1 -1
- package/dist/lib/atomic-write.d.ts +13 -3
- package/dist/lib/atomic-write.d.ts.map +1 -1
- package/dist/lib/atomic-write.js +29 -15
- package/dist/lib/atomic-write.js.map +1 -1
- package/dist/lib/mode-state-io.d.ts +2 -2
- package/dist/lib/mode-state-io.d.ts.map +1 -1
- package/dist/lib/mode-state-io.js +16 -10
- package/dist/lib/mode-state-io.js.map +1 -1
- package/dist/mcp/__tests__/host-smoke-tool.test.d.ts +2 -0
- package/dist/mcp/__tests__/host-smoke-tool.test.d.ts.map +1 -0
- package/dist/mcp/__tests__/host-smoke-tool.test.js +174 -0
- package/dist/mcp/__tests__/host-smoke-tool.test.js.map +1 -0
- package/dist/mcp/disable-tools.d.ts.map +1 -1
- package/dist/mcp/disable-tools.js +1 -0
- package/dist/mcp/disable-tools.js.map +1 -1
- package/dist/mcp/tool-registry.d.ts.map +1 -1
- package/dist/mcp/tool-registry.js +2 -0
- package/dist/mcp/tool-registry.js.map +1 -1
- package/dist/shared/types.d.ts +11 -0
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/smoke/__tests__/copilot-chain-scenario.test.d.ts +2 -0
- package/dist/smoke/__tests__/copilot-chain-scenario.test.d.ts.map +1 -0
- package/dist/smoke/__tests__/copilot-chain-scenario.test.js +154 -0
- package/dist/smoke/__tests__/copilot-chain-scenario.test.js.map +1 -0
- package/dist/smoke/__tests__/copilot-sdk-driver.test.d.ts +2 -0
- package/dist/smoke/__tests__/copilot-sdk-driver.test.d.ts.map +1 -0
- package/dist/smoke/__tests__/copilot-sdk-driver.test.js +591 -0
- package/dist/smoke/__tests__/copilot-sdk-driver.test.js.map +1 -0
- package/dist/smoke/__tests__/copilot-sdk-scenarios.test.d.ts +2 -0
- package/dist/smoke/__tests__/copilot-sdk-scenarios.test.d.ts.map +1 -0
- package/dist/smoke/__tests__/copilot-sdk-scenarios.test.js +362 -0
- package/dist/smoke/__tests__/copilot-sdk-scenarios.test.js.map +1 -0
- package/dist/smoke/__tests__/copilot-session-eval.test.d.ts +2 -0
- package/dist/smoke/__tests__/copilot-session-eval.test.d.ts.map +1 -0
- package/dist/smoke/__tests__/copilot-session-eval.test.js +264 -0
- package/dist/smoke/__tests__/copilot-session-eval.test.js.map +1 -0
- package/dist/smoke/__tests__/copilot-smoke.test.d.ts +2 -0
- package/dist/smoke/__tests__/copilot-smoke.test.d.ts.map +1 -0
- package/dist/smoke/__tests__/copilot-smoke.test.js +482 -0
- package/dist/smoke/__tests__/copilot-smoke.test.js.map +1 -0
- package/dist/smoke/copilot-binary.d.ts +21 -0
- package/dist/smoke/copilot-binary.d.ts.map +1 -0
- package/dist/smoke/copilot-binary.js +46 -0
- package/dist/smoke/copilot-binary.js.map +1 -0
- package/dist/smoke/copilot-chain-scenario.d.ts +61 -0
- package/dist/smoke/copilot-chain-scenario.d.ts.map +1 -0
- package/dist/smoke/copilot-chain-scenario.js +253 -0
- package/dist/smoke/copilot-chain-scenario.js.map +1 -0
- package/dist/smoke/copilot-sdk-driver.d.ts +189 -0
- package/dist/smoke/copilot-sdk-driver.d.ts.map +1 -0
- package/dist/smoke/copilot-sdk-driver.js +637 -0
- package/dist/smoke/copilot-sdk-driver.js.map +1 -0
- package/dist/smoke/copilot-sdk-scenarios.d.ts +200 -0
- package/dist/smoke/copilot-sdk-scenarios.d.ts.map +1 -0
- package/dist/smoke/copilot-sdk-scenarios.js +620 -0
- package/dist/smoke/copilot-sdk-scenarios.js.map +1 -0
- package/dist/smoke/copilot-session-env.d.ts +75 -0
- package/dist/smoke/copilot-session-env.d.ts.map +1 -0
- package/dist/smoke/copilot-session-env.js +154 -0
- package/dist/smoke/copilot-session-env.js.map +1 -0
- package/dist/smoke/copilot-session-eval.d.ts +90 -0
- package/dist/smoke/copilot-session-eval.d.ts.map +1 -0
- package/dist/smoke/copilot-session-eval.js +323 -0
- package/dist/smoke/copilot-session-eval.js.map +1 -0
- package/dist/smoke/copilot-smoke.d.ts +166 -0
- package/dist/smoke/copilot-smoke.d.ts.map +1 -0
- package/dist/smoke/copilot-smoke.js +688 -0
- package/dist/smoke/copilot-smoke.js.map +1 -0
- package/dist/smoke/process-utils.d.ts +47 -0
- package/dist/smoke/process-utils.d.ts.map +1 -0
- package/dist/smoke/process-utils.js +99 -0
- package/dist/smoke/process-utils.js.map +1 -0
- package/dist/team/__tests__/runtime-v2.monitor.test.js +35 -0
- package/dist/team/__tests__/runtime-v2.monitor.test.js.map +1 -1
- package/dist/team/__tests__/scaling.test.js +13 -0
- package/dist/team/__tests__/scaling.test.js.map +1 -1
- package/dist/team/__tests__/sdk-transport.runtime.test.d.ts +2 -0
- package/dist/team/__tests__/sdk-transport.runtime.test.d.ts.map +1 -0
- package/dist/team/__tests__/sdk-transport.runtime.test.js +96 -0
- package/dist/team/__tests__/sdk-transport.runtime.test.js.map +1 -0
- package/dist/team/__tests__/sdk-transport.test.d.ts +2 -0
- package/dist/team/__tests__/sdk-transport.test.d.ts.map +1 -0
- package/dist/team/__tests__/sdk-transport.test.js +650 -0
- package/dist/team/__tests__/sdk-transport.test.js.map +1 -0
- package/dist/team/__tests__/tmux-session.pane-env.test.d.ts +2 -0
- package/dist/team/__tests__/tmux-session.pane-env.test.d.ts.map +1 -0
- package/dist/team/__tests__/tmux-session.pane-env.test.js +52 -0
- package/dist/team/__tests__/tmux-session.pane-env.test.js.map +1 -0
- package/dist/team/api-interop.d.ts.map +1 -1
- package/dist/team/api-interop.js +8 -0
- package/dist/team/api-interop.js.map +1 -1
- package/dist/team/runtime-v2.d.ts +33 -0
- package/dist/team/runtime-v2.d.ts.map +1 -1
- package/dist/team/runtime-v2.js +338 -24
- package/dist/team/runtime-v2.js.map +1 -1
- package/dist/team/scaling.d.ts.map +1 -1
- package/dist/team/scaling.js +5 -0
- package/dist/team/scaling.js.map +1 -1
- package/dist/team/sdk-host.d.ts +48 -0
- package/dist/team/sdk-host.d.ts.map +1 -0
- package/dist/team/sdk-host.js +604 -0
- package/dist/team/sdk-host.js.map +1 -0
- package/dist/team/sdk-policy.d.ts +79 -0
- package/dist/team/sdk-policy.d.ts.map +1 -0
- package/dist/team/sdk-policy.js +186 -0
- package/dist/team/sdk-policy.js.map +1 -0
- package/dist/team/sdk-transport.d.ts +164 -0
- package/dist/team/sdk-transport.d.ts.map +1 -0
- package/dist/team/sdk-transport.js +266 -0
- package/dist/team/sdk-transport.js.map +1 -0
- package/dist/team/tmux-session.d.ts +7 -0
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +80 -18
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/team/types.d.ts +2 -0
- package/dist/team/types.d.ts.map +1 -1
- package/dist/team/types.js.map +1 -1
- package/dist/tools/host-smoke-tool.d.ts +34 -0
- package/dist/tools/host-smoke-tool.d.ts.map +1 -0
- package/dist/tools/host-smoke-tool.js +133 -0
- package/dist/tools/host-smoke-tool.js.map +1 -0
- package/docs/CLAUDE.md +1 -1
- package/docs/DEVELOPERS.md +122 -0
- package/docs/HOOKS.md +34 -2
- package/docs/MIGRATION.md +194 -3
- package/docs/REFERENCE.md +95 -1
- package/docs/TOOLS.md +32 -1
- package/package.json +11 -2
- package/plugin.json +1 -1
- package/scripts/dev/repro-state-lock.mjs +1 -1
- package/scripts/lib/atomic-write.mjs +4 -2
- package/scripts/lib/state-lock.mjs +21 -12
- package/scripts/run.cjs +141 -18
- package/scripts/session-start.mjs +13 -0
- package/scripts/wiki-session-end.mjs +2 -1
- package/templates/hooks/lib/atomic-write.mjs +4 -2
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `omg smoke copilot` — load-check the oh-my-copilot plugin inside the real
|
|
3
|
+
* GitHub Copilot CLI.
|
|
4
|
+
*
|
|
5
|
+
* Tier 0 makes no model call: manifest/version, generated hooks/agents drift,
|
|
6
|
+
* `plugin list`/`skill list` against `--plugin-dir`, and the standalone MCP
|
|
7
|
+
* server's tools/list. Tier 1 adds one cheap non-interactive session in a
|
|
8
|
+
* throwaway COPILOT_HOME and asserts on its persisted events, debug log and
|
|
9
|
+
* the `.omg/` state our hooks write. Tier 2 replaces that session with
|
|
10
|
+
* `@github/copilot-sdk` static checks and scripted scenarios
|
|
11
|
+
* (src/smoke/copilot-sdk-driver.ts).
|
|
12
|
+
*/
|
|
13
|
+
import { spawn, spawnSync } from 'child_process';
|
|
14
|
+
import { randomUUID } from 'crypto';
|
|
15
|
+
import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from 'fs';
|
|
16
|
+
import { tmpdir } from 'os';
|
|
17
|
+
import { join, resolve, sep } from 'path';
|
|
18
|
+
import { buildHostBinarySpawn } from '../cli/tmux-utils.js';
|
|
19
|
+
import { getCopilotConfigDir } from '../utils/config-dir.js';
|
|
20
|
+
import { parseCopilotVersion, resolveCopilotBinary } from './copilot-binary.js';
|
|
21
|
+
import { EVIDENCE_MAX, evaluateAdapterErrors, evaluateEventsLog, evaluateHooks, evaluateMcpLoaded, evaluatePluginsLoaded, evaluateSessionExit, evaluateStateWritten, evaluateSubagent, excerpt, parseJsonl, REQUIRED_HOOK_TYPES, TOOL_HOOK_TYPES, } from './copilot-session-eval.js';
|
|
22
|
+
import { hasExited, killProcessTree, runAsync, useProcessGroup } from './process-utils.js';
|
|
23
|
+
import { DEFAULT_SCENARIO_TIMEOUT_MS, loadCopilotSdk, runSdkTier } from './copilot-sdk-driver.js';
|
|
24
|
+
import { runChainScenario } from './copilot-chain-scenario.js';
|
|
25
|
+
import { DEFAULT_SCENARIOS, failedTier2Checks, KNOWN_SCENARIOS, RUNTIME_MIN_MAX_CREDITS } from './copilot-sdk-scenarios.js';
|
|
26
|
+
import { buildSessionEnv, loginIdentity, resolveDefaultPluginRoot } from './copilot-session-env.js';
|
|
27
|
+
export { buildSessionEnv, isStrippedEnvKey, LOGIN_SHADOWING_TOKENS, loginIdentity, resolveDefaultPluginRoot, resolvePackageRoot, SESSION_SET_ENV, SMOKE_SET_ENV, STRIPPED_ENV_EXACT, STRIPPED_ENV_PREFIXES, } from './copilot-session-env.js';
|
|
28
|
+
export { ALL_SCENARIOS, DEFAULT_SCENARIOS, KNOWN_SCENARIOS, OPT_IN_SCENARIOS } from './copilot-sdk-scenarios.js';
|
|
29
|
+
/**
|
|
30
|
+
* No default model id: on Copilot CLI 1.0.91 every explicit `--model` tried
|
|
31
|
+
* (gpt-5-mini, gpt-6-luna, claude-sonnet-5) failed with `Model "<id>" from
|
|
32
|
+
* --model flag is not available` before any model call, while omitting the
|
|
33
|
+
* flag lets the CLI auto-pick (it chose mai-code-1.1-flash, a low-price flash
|
|
34
|
+
* model). Cost stays bounded by --max-ai-credits. Pass --model to pin one.
|
|
35
|
+
*/
|
|
36
|
+
export const DEFAULT_SMOKE_MODEL = undefined;
|
|
37
|
+
export const DEFAULT_MAX_CREDITS = 30;
|
|
38
|
+
/** Copilot CLI rejects `--max-ai-credits` below this. */
|
|
39
|
+
export const MIN_MAX_CREDITS = RUNTIME_MIN_MAX_CREDITS;
|
|
40
|
+
export const DEFAULT_TIER0_TIMEOUT_MS = 60_000;
|
|
41
|
+
export const DEFAULT_TIER1_TIMEOUT_MS = 180_000;
|
|
42
|
+
export const MCP_LIST_TIMEOUT_MS = 10_000;
|
|
43
|
+
export const PLUGIN_NAME = 'oh-my-copilot';
|
|
44
|
+
export const DELEGATE_AGENT = 'oh-my-copilot:architect';
|
|
45
|
+
export const DEFAULT_SMOKE_PROMPT = 'Reply with exactly: SMOKE_OK. Do not use tools.';
|
|
46
|
+
export const DELEGATE_SMOKE_PROMPT = `Delegate one task to the ${DELEGATE_AGENT} agent: ask it to reply with one line. `
|
|
47
|
+
+ 'Then reply with exactly: SMOKE_OK. Do not edit any files.';
|
|
48
|
+
/**
|
|
49
|
+
* Copy of the non-interactive permission flags from COPILOT_WORKER_BASE_FLAGS
|
|
50
|
+
* (src/team/model-contract.ts), minus the path/url grants the smoke does not
|
|
51
|
+
* need. Copied rather than imported so the smoke does not pull in team code.
|
|
52
|
+
*/
|
|
53
|
+
const SMOKE_PERMISSION_FLAGS = ['--allow-all-tools', '--no-ask-user'];
|
|
54
|
+
/**
|
|
55
|
+
* Tier 1 session sandboxing (spellings verified against `copilot --help` and
|
|
56
|
+
* `copilot help permissions`, CLI 1.0.91; deny rules beat --allow-all-tools):
|
|
57
|
+
* - never let the model call our own `host_smoke` tool (server `t`) recursively;
|
|
58
|
+
* - no built-in MCP servers (github-mcp-server, githubiq) in the smoke;
|
|
59
|
+
* - the default prompt forbids tools, so also deny shell and file writes.
|
|
60
|
+
* `--delegate` keeps them, because the subagent hand-off is a tool call.
|
|
61
|
+
*/
|
|
62
|
+
export function sessionSandboxFlags(delegate) {
|
|
63
|
+
return [
|
|
64
|
+
'--deny-tool', 't(host_smoke)',
|
|
65
|
+
'--disable-builtin-mcps',
|
|
66
|
+
...(delegate ? [] : ['--deny-tool', 'shell', '--deny-tool', 'write']),
|
|
67
|
+
];
|
|
68
|
+
}
|
|
69
|
+
const TIER0_COPILOT_IDS = ['copilot.binary', 'copilot.plugin_list', 'copilot.skill_list'];
|
|
70
|
+
function tier1Ids(delegate) {
|
|
71
|
+
return [
|
|
72
|
+
'session.exit',
|
|
73
|
+
'session.events',
|
|
74
|
+
...REQUIRED_HOOK_TYPES.map((t) => `hooks.${t}`),
|
|
75
|
+
...TOOL_HOOK_TYPES.map((t) => `hooks.${t}`),
|
|
76
|
+
'hooks.adapter_errors',
|
|
77
|
+
'plugins.loaded',
|
|
78
|
+
'mcp.server_loaded',
|
|
79
|
+
'state.written',
|
|
80
|
+
...(delegate ? ['subagent.selected'] : []),
|
|
81
|
+
];
|
|
82
|
+
}
|
|
83
|
+
function readJson(path) {
|
|
84
|
+
try {
|
|
85
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function countDirsWith(dir, file) {
|
|
92
|
+
try {
|
|
93
|
+
return readdirSync(dir).filter((name) => existsSync(join(dir, name, file))).sort();
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function countFiles(dir, ext) {
|
|
100
|
+
try {
|
|
101
|
+
return readdirSync(dir).filter((name) => name.endsWith(ext)).length;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return -1;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** Extract the first JSON array from CLI output that may carry warnings around it. */
|
|
108
|
+
export function extractJsonArray(text) {
|
|
109
|
+
const start = text.indexOf('[');
|
|
110
|
+
const end = text.lastIndexOf(']');
|
|
111
|
+
if (start === -1 || end < start)
|
|
112
|
+
return null;
|
|
113
|
+
try {
|
|
114
|
+
const parsed = JSON.parse(text.slice(start, end + 1));
|
|
115
|
+
return Array.isArray(parsed) ? parsed : null;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function makeTempDir(prefix) {
|
|
122
|
+
return mkdtempSync(join(tmpdir(), prefix));
|
|
123
|
+
}
|
|
124
|
+
function removeDir(path) {
|
|
125
|
+
if (!path)
|
|
126
|
+
return;
|
|
127
|
+
try {
|
|
128
|
+
rmSync(path, { recursive: true, force: true, maxRetries: 3, retryDelay: 200 });
|
|
129
|
+
}
|
|
130
|
+
catch { /* best effort */ }
|
|
131
|
+
}
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// Tier 0
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
function checkManifest(root) {
|
|
136
|
+
const manifest = readJson(join(root, 'plugin.json'));
|
|
137
|
+
const pkg = readJson(join(root, 'package.json'));
|
|
138
|
+
const pluginVersion = typeof manifest?.version === 'string' ? manifest.version : null;
|
|
139
|
+
const packageVersion = typeof pkg?.version === 'string' ? pkg.version : null;
|
|
140
|
+
const problems = [];
|
|
141
|
+
if (!manifest)
|
|
142
|
+
problems.push('plugin.json missing or not JSON');
|
|
143
|
+
else {
|
|
144
|
+
if (manifest.name !== PLUGIN_NAME)
|
|
145
|
+
problems.push(`name is ${JSON.stringify(manifest.name)}, expected ${PLUGIN_NAME}`);
|
|
146
|
+
if (!packageVersion)
|
|
147
|
+
problems.push('package.json version missing');
|
|
148
|
+
else if (pluginVersion !== packageVersion)
|
|
149
|
+
problems.push(`version ${pluginVersion} != package.json ${packageVersion}`);
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
check: {
|
|
153
|
+
id: 'plugin.manifest',
|
|
154
|
+
ok: problems.length === 0,
|
|
155
|
+
detail: problems.length === 0 ? `${PLUGIN_NAME}@${pluginVersion}` : problems.join('; '),
|
|
156
|
+
},
|
|
157
|
+
pluginVersion,
|
|
158
|
+
packageVersion,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function checkGeneratedHooks(ctx) {
|
|
162
|
+
const scripts = ['scripts/copilot/build-hooks.mjs', 'scripts/copilot/build-agents.mjs'];
|
|
163
|
+
const failures = [];
|
|
164
|
+
const evidence = [];
|
|
165
|
+
for (const script of scripts) {
|
|
166
|
+
const res = ctx.spawnSyncFn(process.execPath, [script, '--verify'], {
|
|
167
|
+
cwd: ctx.root,
|
|
168
|
+
env: ctx.env,
|
|
169
|
+
encoding: 'utf8',
|
|
170
|
+
timeout: ctx.timeoutMs,
|
|
171
|
+
windowsHide: true,
|
|
172
|
+
});
|
|
173
|
+
const out = `${res.stdout ?? ''}${res.stderr ?? ''}`.trim();
|
|
174
|
+
if (res.status !== 0) {
|
|
175
|
+
failures.push(`${script} --verify exit ${res.status ?? res.error?.message ?? 'null'}`);
|
|
176
|
+
evidence.push(out);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
id: 'plugin.hooks',
|
|
181
|
+
ok: failures.length === 0,
|
|
182
|
+
detail: failures.length === 0 ? 'build-hooks and build-agents --verify pass' : failures.join('; '),
|
|
183
|
+
...(failures.length ? { evidence: excerpt(evidence.join('\n')) } : {}),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function checkAgents(root) {
|
|
187
|
+
const source = countFiles(join(root, 'agents'), '.md');
|
|
188
|
+
const generated = countFiles(join(root, 'copilot', 'agents'), '.md');
|
|
189
|
+
const ok = source > 0 && source === generated;
|
|
190
|
+
return {
|
|
191
|
+
id: 'copilot.agents',
|
|
192
|
+
ok,
|
|
193
|
+
detail: `copilot/agents ${generated} vs agents ${source} *.md`,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function runCopilotSync(ctx, bin, args, env, cwd) {
|
|
197
|
+
const plan = buildHostBinarySpawn(bin, args);
|
|
198
|
+
return ctx.spawnSyncFn(plan.command, plan.args, {
|
|
199
|
+
cwd,
|
|
200
|
+
env,
|
|
201
|
+
encoding: 'utf8',
|
|
202
|
+
timeout: ctx.timeoutMs,
|
|
203
|
+
windowsHide: true,
|
|
204
|
+
windowsVerbatimArguments: plan.windowsVerbatimArguments,
|
|
205
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
function checkPluginList(ctx, bin, env, cwd, packageVersion) {
|
|
209
|
+
const res = runCopilotSync(ctx, bin, ['--plugin-dir', ctx.root, '--no-auto-update', 'plugin', 'list', '--json'], env, cwd);
|
|
210
|
+
const stdout = String(res.stdout ?? '');
|
|
211
|
+
const list = extractJsonArray(stdout);
|
|
212
|
+
if (res.status !== 0 || !list) {
|
|
213
|
+
return { id: 'copilot.plugin_list', ok: false, detail: `plugin list exit ${res.status}${list ? '' : ', no JSON array'}`, evidence: excerpt(`${stdout}${res.stderr ?? ''}`) };
|
|
214
|
+
}
|
|
215
|
+
// A bad --plugin-dir only warns and returns [] with exit 0: assert the exact entry.
|
|
216
|
+
const entry = list.find((p) => p.name === PLUGIN_NAME);
|
|
217
|
+
const ok = !!entry && entry.version === packageVersion && entry.source === 'external';
|
|
218
|
+
return {
|
|
219
|
+
id: 'copilot.plugin_list',
|
|
220
|
+
ok,
|
|
221
|
+
detail: entry
|
|
222
|
+
? `${PLUGIN_NAME}@${String(entry.version)} source=${String(entry.source)}${ok ? '' : ` (expected ${packageVersion}, external)`}`
|
|
223
|
+
: `${PLUGIN_NAME} not listed (${list.length} plugin(s))`,
|
|
224
|
+
...(ok ? {} : { evidence: excerpt(`${stdout}${res.stderr ?? ''}`) }),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
/** Canonical path for comparisons: realpath (symlinked roots), case-folded on win32. */
|
|
228
|
+
function normalizePath(p) {
|
|
229
|
+
let out = resolve(p);
|
|
230
|
+
try {
|
|
231
|
+
out = realpathSync.native(out);
|
|
232
|
+
}
|
|
233
|
+
catch { /* missing path: keep the resolved form */ }
|
|
234
|
+
return process.platform === 'win32' ? out.toLowerCase() : out;
|
|
235
|
+
}
|
|
236
|
+
function checkSkillList(ctx, bin, env, cwd) {
|
|
237
|
+
const expected = countDirsWith(join(ctx.root, 'skills'), 'SKILL.md');
|
|
238
|
+
const res = runCopilotSync(ctx, bin, ['--plugin-dir', ctx.root, '--no-auto-update', 'skill', 'list', '--json'], env, cwd);
|
|
239
|
+
const stdout = String(res.stdout ?? '');
|
|
240
|
+
const list = extractJsonArray(stdout);
|
|
241
|
+
if (res.status !== 0 || !list) {
|
|
242
|
+
return { id: 'copilot.skill_list', ok: false, detail: `skill list exit ${res.status}${list ? '' : ', no JSON array'}`, evidence: excerpt(`${stdout}${res.stderr ?? ''}`) };
|
|
243
|
+
}
|
|
244
|
+
// Copilot also surfaces plugin `commands/*.md` as plugin skills, so count only
|
|
245
|
+
// plugin-sourced entries whose path is a skills/<dir>.
|
|
246
|
+
const skillsDir = normalizePath(join(ctx.root, 'skills')) + sep;
|
|
247
|
+
const pluginSkills = list.filter((s) => s.source === 'plugin');
|
|
248
|
+
const fromSkillsDir = new Set(pluginSkills
|
|
249
|
+
.filter((s) => typeof s.path === 'string' && normalizePath(s.path).startsWith(skillsDir))
|
|
250
|
+
.map((s) => normalizePath(s.path).slice(skillsDir.length).split(/[\\/]/)[0]));
|
|
251
|
+
const missing = expected.filter((d) => !fromSkillsDir.has(process.platform === 'win32' ? d.toLowerCase() : d));
|
|
252
|
+
const ok = expected.length > 0 && missing.length === 0 && fromSkillsDir.size === expected.length;
|
|
253
|
+
return {
|
|
254
|
+
id: 'copilot.skill_list',
|
|
255
|
+
ok,
|
|
256
|
+
detail: `${fromSkillsDir.size}/${expected.length} skills/*/SKILL.md loaded as source=plugin (${pluginSkills.length} plugin entries incl. commands)`,
|
|
257
|
+
...(ok ? {} : { evidence: excerpt(missing.length ? `missing: ${missing.join(', ')}` : stdout) }),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
async function defaultExpectedToolCount() {
|
|
261
|
+
const registry = await import('../mcp/tool-registry.js');
|
|
262
|
+
return registry.allTools.length;
|
|
263
|
+
}
|
|
264
|
+
async function checkMcpListTools(ctx) {
|
|
265
|
+
const id = 'mcp.list_tools';
|
|
266
|
+
const server = join(ctx.root, 'dist', 'mcp', 'standalone-server.js');
|
|
267
|
+
if (!existsSync(server))
|
|
268
|
+
return { id, ok: false, detail: `${server} missing; run npm run build` };
|
|
269
|
+
let expected;
|
|
270
|
+
try {
|
|
271
|
+
expected = await (ctx.deps.loadExpectedToolCount ?? defaultExpectedToolCount)();
|
|
272
|
+
}
|
|
273
|
+
catch (err) {
|
|
274
|
+
return { id, ok: false, detail: `could not load tool registry: ${err.message}` };
|
|
275
|
+
}
|
|
276
|
+
const env = { ...ctx.env };
|
|
277
|
+
delete env.OMC_DISABLE_TOOLS; // compare against the full registry
|
|
278
|
+
return new Promise((resolveCheck) => {
|
|
279
|
+
let settled = false;
|
|
280
|
+
let buffer = '';
|
|
281
|
+
let stderr = '';
|
|
282
|
+
const child = ctx.spawnFn(process.execPath, [server], {
|
|
283
|
+
cwd: ctx.root,
|
|
284
|
+
env,
|
|
285
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
286
|
+
windowsHide: true,
|
|
287
|
+
detached: useProcessGroup(),
|
|
288
|
+
});
|
|
289
|
+
const done = (check) => {
|
|
290
|
+
if (settled)
|
|
291
|
+
return;
|
|
292
|
+
settled = true;
|
|
293
|
+
clearTimeout(timer);
|
|
294
|
+
try {
|
|
295
|
+
child.stdin?.end();
|
|
296
|
+
}
|
|
297
|
+
catch { /* closed */ }
|
|
298
|
+
if (!hasExited(child))
|
|
299
|
+
killProcessTree(child.pid, ctx.spawnSyncFn);
|
|
300
|
+
resolveCheck(check);
|
|
301
|
+
};
|
|
302
|
+
const timer = setTimeout(() => done({ id, ok: false, detail: `no tools/list response within ${MCP_LIST_TIMEOUT_MS} ms`, evidence: excerpt(stderr) }), MCP_LIST_TIMEOUT_MS);
|
|
303
|
+
const send = (msg) => { try {
|
|
304
|
+
child.stdin?.write(`${JSON.stringify(msg)}\n`);
|
|
305
|
+
}
|
|
306
|
+
catch { /* closed */ } };
|
|
307
|
+
child.on('error', (err) => done({ id, ok: false, detail: `spawn failed: ${err.message}` }));
|
|
308
|
+
child.on('close', (code) => done({ id, ok: false, detail: `server exited (${code}) before tools/list`, evidence: excerpt(stderr) }));
|
|
309
|
+
child.stdin?.on('error', () => { });
|
|
310
|
+
child.stderr?.setEncoding('utf8');
|
|
311
|
+
child.stderr?.on('data', (c) => { if (stderr.length < 64_000)
|
|
312
|
+
stderr += c; });
|
|
313
|
+
child.stdout?.setEncoding('utf8');
|
|
314
|
+
child.stdout?.on('data', (chunk) => {
|
|
315
|
+
buffer += chunk;
|
|
316
|
+
let nl;
|
|
317
|
+
while ((nl = buffer.indexOf('\n')) !== -1) {
|
|
318
|
+
const line = buffer.slice(0, nl).trim();
|
|
319
|
+
buffer = buffer.slice(nl + 1);
|
|
320
|
+
if (!line)
|
|
321
|
+
continue;
|
|
322
|
+
let msg;
|
|
323
|
+
try {
|
|
324
|
+
msg = JSON.parse(line);
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (msg.id === 1) {
|
|
330
|
+
if (msg.error) {
|
|
331
|
+
done({ id, ok: false, detail: 'initialize failed', evidence: excerpt(JSON.stringify(msg.error)) });
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
send({ jsonrpc: '2.0', method: 'notifications/initialized' });
|
|
335
|
+
send({ jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} });
|
|
336
|
+
}
|
|
337
|
+
else if (msg.id === 2) {
|
|
338
|
+
const tools = msg.result?.tools;
|
|
339
|
+
if (!Array.isArray(tools)) {
|
|
340
|
+
done({ id, ok: false, detail: 'tools/list returned no tools array', evidence: excerpt(line) });
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const ok = tools.length === expected;
|
|
344
|
+
done({ id, ok, detail: `tools/list returned ${tools.length}, registry allTools ${expected}${ok ? '' : ' (stale dist? run npm run build)'}` });
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
send({
|
|
350
|
+
jsonrpc: '2.0',
|
|
351
|
+
id: 1,
|
|
352
|
+
method: 'initialize',
|
|
353
|
+
params: { protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 'omg-smoke', version: '1' } },
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
// ---------------------------------------------------------------------------
|
|
358
|
+
// Tier 1
|
|
359
|
+
// ---------------------------------------------------------------------------
|
|
360
|
+
function initProject(ctx, projectDir, env) {
|
|
361
|
+
const res = ctx.spawnSyncFn('git', ['init', '-q'], { cwd: projectDir, env, encoding: 'utf8', timeout: 30_000, windowsHide: true });
|
|
362
|
+
if (res.status !== 0 || !existsSync(join(projectDir, '.git')))
|
|
363
|
+
mkdirSync(join(projectDir, '.git'), { recursive: true });
|
|
364
|
+
}
|
|
365
|
+
function readText(path) {
|
|
366
|
+
try {
|
|
367
|
+
return readFileSync(path, 'utf-8');
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
/** All process logs under the log dir, newest last, joined; plus the newest path. */
|
|
374
|
+
function readDebugLogs(logDir) {
|
|
375
|
+
let files;
|
|
376
|
+
try {
|
|
377
|
+
files = readdirSync(logDir).filter((f) => f.endsWith('.log')).map((f) => join(logDir, f));
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
return { text: null };
|
|
381
|
+
}
|
|
382
|
+
if (files.length === 0)
|
|
383
|
+
return { text: null };
|
|
384
|
+
files.sort((a, b) => statSync(a).mtimeMs - statSync(b).mtimeMs);
|
|
385
|
+
return { text: files.map((f) => readText(f) ?? '').join('\n'), path: files[files.length - 1] };
|
|
386
|
+
}
|
|
387
|
+
function mcpServerNames(root) {
|
|
388
|
+
const mcp = readJson(join(root, '.mcp.json'));
|
|
389
|
+
const servers = mcp?.mcpServers;
|
|
390
|
+
return servers && typeof servers === 'object' ? Object.keys(servers) : [];
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Run the live session. Registers its temp dir in `cleanup` and its paths in
|
|
394
|
+
* `artifacts` as soon as they exist, so the caller's `finally` removes them
|
|
395
|
+
* even when this throws midway.
|
|
396
|
+
*/
|
|
397
|
+
async function runTier1(ctx, bin, opts, cleanup, artifacts) {
|
|
398
|
+
// One parent so --keep-home leaves home/ and project/ side by side.
|
|
399
|
+
const parent = makeTempDir('omg-smoke-');
|
|
400
|
+
cleanup.push(parent);
|
|
401
|
+
const home = join(parent, 'home');
|
|
402
|
+
artifacts.copilotHome = home;
|
|
403
|
+
const projectDir = join(parent, 'project');
|
|
404
|
+
mkdirSync(home);
|
|
405
|
+
mkdirSync(projectDir);
|
|
406
|
+
const logDir = join(home, 'logs');
|
|
407
|
+
const sessionId = (ctx.deps.randomUUID ?? randomUUID)();
|
|
408
|
+
const identity = loginIdentity(ctx.deps.userConfigDir ?? getCopilotConfigDir());
|
|
409
|
+
const env = buildSessionEnv(ctx.env, home, { session: true, hasLogin: identity.loggedInUsers !== undefined });
|
|
410
|
+
const config = { ...identity, trustedFolders: [projectDir] };
|
|
411
|
+
writeFileSync(join(home, 'config.json'), `${JSON.stringify(config, null, 2)}\n`);
|
|
412
|
+
initProject(ctx, projectDir, env);
|
|
413
|
+
const prompt = opts.prompt ?? (opts.delegate ? DELEGATE_SMOKE_PROMPT : DEFAULT_SMOKE_PROMPT);
|
|
414
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIER1_TIMEOUT_MS;
|
|
415
|
+
const args = [
|
|
416
|
+
'--plugin-dir', ctx.root,
|
|
417
|
+
'--session-id', sessionId,
|
|
418
|
+
'--output-format', 'json',
|
|
419
|
+
...SMOKE_PERMISSION_FLAGS,
|
|
420
|
+
...sessionSandboxFlags(!!opts.delegate),
|
|
421
|
+
'--no-auto-update',
|
|
422
|
+
'--log-level', 'debug',
|
|
423
|
+
'--log-dir', logDir,
|
|
424
|
+
...((opts.model ?? DEFAULT_SMOKE_MODEL) ? ['--model', (opts.model ?? DEFAULT_SMOKE_MODEL)] : []),
|
|
425
|
+
'--max-ai-credits', String(opts.maxCredits ?? DEFAULT_MAX_CREDITS),
|
|
426
|
+
];
|
|
427
|
+
// No -p: `-p <text>` would put the prompt on argv. Piped stdin is the
|
|
428
|
+
// CLI's other non-interactive entry (verified on 1.0.91).
|
|
429
|
+
const plan = buildHostBinarySpawn(bin, args);
|
|
430
|
+
const run = await runAsync(ctx.spawnFn, ctx.spawnSyncFn, plan.command, plan.args, {
|
|
431
|
+
cwd: projectDir,
|
|
432
|
+
env,
|
|
433
|
+
input: `${prompt}\n`,
|
|
434
|
+
timeoutMs,
|
|
435
|
+
windowsVerbatimArguments: plan.windowsVerbatimArguments,
|
|
436
|
+
});
|
|
437
|
+
const stdoutPath = join(home, 'smoke-stdout.jsonl');
|
|
438
|
+
try {
|
|
439
|
+
writeFileSync(stdoutPath, run.stdout);
|
|
440
|
+
}
|
|
441
|
+
catch { /* best effort */ }
|
|
442
|
+
const eventsPath = join(home, 'session-state', sessionId, 'events.jsonl');
|
|
443
|
+
const logs = readDebugLogs(logDir);
|
|
444
|
+
Object.assign(artifacts, { eventsLog: eventsPath, debugLog: logs.path, stdout: stdoutPath });
|
|
445
|
+
return evaluateTier1({
|
|
446
|
+
exitCode: run.code,
|
|
447
|
+
timedOut: run.timedOut,
|
|
448
|
+
error: run.error,
|
|
449
|
+
stdout: run.stdout,
|
|
450
|
+
stderr: run.stderr,
|
|
451
|
+
timeoutMs,
|
|
452
|
+
eventsPath,
|
|
453
|
+
eventsText: readText(eventsPath),
|
|
454
|
+
logText: logs.text,
|
|
455
|
+
projectDir,
|
|
456
|
+
mcpServerNames: mcpServerNames(ctx.root),
|
|
457
|
+
delegate: !!opts.delegate,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
/** Evaluate captured Tier 1 artifacts (exported for fixture-driven tests). */
|
|
461
|
+
export function evaluateTier1(input) {
|
|
462
|
+
const checks = [];
|
|
463
|
+
checks.push(evaluateSessionExit(input));
|
|
464
|
+
const { check: eventsCheck, events } = evaluateEventsLog(input.eventsPath, input.eventsText);
|
|
465
|
+
checks.push(eventsCheck);
|
|
466
|
+
// Ephemeral events (e.g. MCP status changes) only reach stdout, so evaluate both streams.
|
|
467
|
+
const stdoutEvents = parseJsonl(input.stdout).events;
|
|
468
|
+
const allEvents = [...events, ...stdoutEvents.filter((e) => !e.id || !events.some((p) => p.id === e.id))];
|
|
469
|
+
checks.push(...evaluateHooks(events.length ? events : stdoutEvents));
|
|
470
|
+
checks.push(evaluateAdapterErrors([
|
|
471
|
+
{ name: 'debug log', text: input.logText },
|
|
472
|
+
{ name: 'session stderr', text: input.stderr },
|
|
473
|
+
{ name: 'events.jsonl', text: input.eventsText },
|
|
474
|
+
{ name: 'stdout', text: input.stdout },
|
|
475
|
+
]));
|
|
476
|
+
checks.push(evaluatePluginsLoaded(input.logText));
|
|
477
|
+
checks.push(evaluateMcpLoaded(allEvents, input.logText, input.mcpServerNames.length ? input.mcpServerNames : ['t']));
|
|
478
|
+
checks.push(evaluateStateWritten(input.projectDir));
|
|
479
|
+
if (input.delegate)
|
|
480
|
+
checks.push(evaluateSubagent(allEvents, DELEGATE_AGENT));
|
|
481
|
+
return checks.map((c) => (c.evidence && c.evidence.length > EVIDENCE_MAX ? { ...c, evidence: excerpt(c.evidence) } : c));
|
|
482
|
+
}
|
|
483
|
+
// ---------------------------------------------------------------------------
|
|
484
|
+
// Entry point
|
|
485
|
+
// ---------------------------------------------------------------------------
|
|
486
|
+
export const LIVE_RUN_REFUSED_DETAIL = 'live run refused under vitest — set OMC_LIVE_SMOKE=1 (tier 1) / =2 (tier 2 scenarios)';
|
|
487
|
+
/**
|
|
488
|
+
* Hard no-billing guard: under a test runner (`VITEST` or `NODE_ENV=test` in
|
|
489
|
+
* the real process env) a run that would call a model (tier 1; tier 2 with
|
|
490
|
+
* any scenario, the default included) is refused before any subprocess or SDK
|
|
491
|
+
* client, unless `OMC_LIVE_SMOKE` names that tier or the test injected the
|
|
492
|
+
* seam that would otherwise bill (`deps.spawn` at tier 1, `deps.loadSdk` at
|
|
493
|
+
* tier 2). Returns the refusal detail, or null to proceed.
|
|
494
|
+
*/
|
|
495
|
+
export function liveRunRefusal(tier, scenarios, deps, env = process.env) {
|
|
496
|
+
const underTest = !!env.VITEST || env.NODE_ENV === 'test';
|
|
497
|
+
if (!underTest)
|
|
498
|
+
return null;
|
|
499
|
+
const billable = tier === 1 || (tier === 2 && (scenarios ?? DEFAULT_SCENARIOS).length > 0);
|
|
500
|
+
if (!billable)
|
|
501
|
+
return null;
|
|
502
|
+
if (env.OMC_LIVE_SMOKE === String(tier))
|
|
503
|
+
return null;
|
|
504
|
+
if ((tier === 1 && deps.spawn) || (tier === 2 && deps.loadSdk))
|
|
505
|
+
return null;
|
|
506
|
+
return LIVE_RUN_REFUSED_DETAIL;
|
|
507
|
+
}
|
|
508
|
+
export async function runCopilotSmoke(input = {}) {
|
|
509
|
+
const started = Date.now();
|
|
510
|
+
const env = input.env ?? process.env;
|
|
511
|
+
const deps = input.deps ?? {};
|
|
512
|
+
const tier = input.tier === 1 || input.tier === 2 ? input.tier : 0;
|
|
513
|
+
const root = resolve(input.pluginRoot ?? resolveDefaultPluginRoot(env));
|
|
514
|
+
const opts = { ...input, pluginRoot: root, tier };
|
|
515
|
+
if (opts.prompt !== undefined && /\0/.test(opts.prompt))
|
|
516
|
+
throw new Error('smoke prompt must not contain NUL');
|
|
517
|
+
const unknown = (opts.scenarios ?? []).filter((s) => !KNOWN_SCENARIOS.includes(s));
|
|
518
|
+
if (unknown.length)
|
|
519
|
+
throw new Error(`unknown smoke scenario(s): ${unknown.join(', ')} (known: ${KNOWN_SCENARIOS.join(', ')})`);
|
|
520
|
+
const refusal = liveRunRefusal(tier, opts.scenarios, deps);
|
|
521
|
+
if (refusal) {
|
|
522
|
+
return {
|
|
523
|
+
ok: false,
|
|
524
|
+
tier,
|
|
525
|
+
pluginRoot: root,
|
|
526
|
+
pluginVersion: null,
|
|
527
|
+
copilot: { bin: null, version: null },
|
|
528
|
+
checks: [{ id: 'cli.guard', ok: false, detail: refusal }],
|
|
529
|
+
artifacts: {},
|
|
530
|
+
durationMs: Date.now() - started,
|
|
531
|
+
skipped: refusal,
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
const ctx = {
|
|
535
|
+
root,
|
|
536
|
+
env: { ...env },
|
|
537
|
+
spawnFn: deps.spawn ?? spawn,
|
|
538
|
+
spawnSyncFn: deps.spawnSync ?? spawnSync,
|
|
539
|
+
deps,
|
|
540
|
+
timeoutMs: tier === 0 ? (opts.timeoutMs ?? DEFAULT_TIER0_TIMEOUT_MS) : DEFAULT_TIER0_TIMEOUT_MS,
|
|
541
|
+
};
|
|
542
|
+
const checks = [];
|
|
543
|
+
const artifacts = {};
|
|
544
|
+
const cleanup = [];
|
|
545
|
+
try {
|
|
546
|
+
return await runChecks(ctx, opts, started, checks, artifacts, cleanup);
|
|
547
|
+
}
|
|
548
|
+
finally {
|
|
549
|
+
if (!opts.keepHome) {
|
|
550
|
+
for (const dir of cleanup)
|
|
551
|
+
removeDir(dir);
|
|
552
|
+
for (const key of Object.keys(artifacts))
|
|
553
|
+
delete artifacts[key];
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
async function runChecks(ctx, opts, started, checks, artifacts, cleanup) {
|
|
558
|
+
const { root, deps, env } = ctx;
|
|
559
|
+
const tier = opts.tier;
|
|
560
|
+
let skipped;
|
|
561
|
+
const manifest = checkManifest(root);
|
|
562
|
+
checks.push(manifest.check);
|
|
563
|
+
checks.push(checkGeneratedHooks(ctx));
|
|
564
|
+
const resolution = resolveCopilotBinary(opts.copilotBin, env, deps);
|
|
565
|
+
let version = null;
|
|
566
|
+
if (!resolution.bin) {
|
|
567
|
+
skipped = opts.copilotBin
|
|
568
|
+
? `copilot binary not found: ${opts.copilotBin}`
|
|
569
|
+
: (deps.platform ?? process.platform) === 'win32'
|
|
570
|
+
? 'copilot binary not found on PATH, COPILOT_CLI_PATH, or the WinGet package dir'
|
|
571
|
+
: 'copilot binary not found on PATH or COPILOT_CLI_PATH (npm i -g @github/copilot)';
|
|
572
|
+
checks.push({ id: 'copilot.binary', ok: false, detail: skipped });
|
|
573
|
+
for (const id of TIER0_COPILOT_IDS.slice(1))
|
|
574
|
+
checks.push({ id, ok: false, detail: 'skipped' });
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
const bin = resolution.bin;
|
|
578
|
+
// cwd is tmpdir, not the plugin root: a missing --plugin-root must not
|
|
579
|
+
// surface as an ENOENT on the binary probe.
|
|
580
|
+
const res = runCopilotSync(ctx, bin, ['--version'], ctx.env, tmpdir());
|
|
581
|
+
version = parseCopilotVersion(String(res.stdout ?? ''));
|
|
582
|
+
const binaryOk = res.status === 0 && version !== null;
|
|
583
|
+
checks.push({
|
|
584
|
+
id: 'copilot.binary',
|
|
585
|
+
ok: binaryOk,
|
|
586
|
+
detail: binaryOk ? `${bin} (${resolution.source}) v${version}` : `${bin} --version exit ${res.status ?? res.error?.message}`,
|
|
587
|
+
...(binaryOk ? {} : { evidence: excerpt(`${res.stdout ?? ''}${res.stderr ?? ''}`) }),
|
|
588
|
+
});
|
|
589
|
+
const listHome = makeTempDir('omg-smoke-list-');
|
|
590
|
+
cleanup.push(listHome);
|
|
591
|
+
artifacts.listHome = listHome;
|
|
592
|
+
const listEnv = buildSessionEnv(ctx.env, listHome);
|
|
593
|
+
checks.push(checkPluginList(ctx, bin, listEnv, listHome, manifest.packageVersion));
|
|
594
|
+
checks.push(checkSkillList(ctx, bin, listEnv, listHome));
|
|
595
|
+
}
|
|
596
|
+
checks.push(checkAgents(root));
|
|
597
|
+
checks.push(await checkMcpListTools(ctx));
|
|
598
|
+
if (tier === 1) {
|
|
599
|
+
if (!resolution.bin) {
|
|
600
|
+
for (const id of tier1Ids(!!opts.delegate))
|
|
601
|
+
checks.push({ id, ok: false, detail: 'skipped' });
|
|
602
|
+
}
|
|
603
|
+
else {
|
|
604
|
+
checks.push(...await runTier1(ctx, resolution.bin, opts, cleanup, artifacts));
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
let tier2;
|
|
608
|
+
if (tier === 2) {
|
|
609
|
+
const scenarios = [...new Set(opts.scenarios ?? DEFAULT_SCENARIOS)];
|
|
610
|
+
if (!resolution.bin) {
|
|
611
|
+
checks.push(...failedTier2Checks(scenarios, 'skipped'));
|
|
612
|
+
}
|
|
613
|
+
else {
|
|
614
|
+
tier2 = await runTier2(ctx, resolution.bin, version, manifest.packageVersion, opts, scenarios, cleanup, artifacts);
|
|
615
|
+
checks.push(...tier2.checks);
|
|
616
|
+
skipped ??= tier2.skipped;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
return {
|
|
620
|
+
ok: checks.every((c) => c.ok),
|
|
621
|
+
tier,
|
|
622
|
+
pluginRoot: root,
|
|
623
|
+
pluginVersion: manifest.pluginVersion,
|
|
624
|
+
copilot: { bin: resolution.bin, version },
|
|
625
|
+
checks,
|
|
626
|
+
artifacts,
|
|
627
|
+
durationMs: Date.now() - started,
|
|
628
|
+
...(skipped ? { skipped } : {}),
|
|
629
|
+
...(tier2?.sdk ? { sdk: tier2.sdk } : {}),
|
|
630
|
+
...(tier2?.cost ? { cost: tier2.cost } : {}),
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
// ---------------------------------------------------------------------------
|
|
634
|
+
// Tier 2
|
|
635
|
+
// ---------------------------------------------------------------------------
|
|
636
|
+
/**
|
|
637
|
+
* Same isolation as tier 1 (throwaway COPILOT_HOME with the copied login
|
|
638
|
+
* identity and trustedFolders, {@link buildSessionEnv} with
|
|
639
|
+
* OMC_HOOK_FAIL_CLOSED=1), driven through one SDK client. Env is per client,
|
|
640
|
+
* so OMC_GIT_GUARDRAILS=1 holds for every scenario; only the guardrail
|
|
641
|
+
* scenario is permitted a shell, so the others never reach the guardrail.
|
|
642
|
+
*/
|
|
643
|
+
async function runTier2(ctx, bin, binVersion, packageVersion, opts, scenarios, cleanup, artifacts) {
|
|
644
|
+
const parent = makeTempDir('omg-smoke-sdk-');
|
|
645
|
+
cleanup.push(parent);
|
|
646
|
+
const home = join(parent, 'home');
|
|
647
|
+
const projectDir = join(parent, 'project');
|
|
648
|
+
mkdirSync(home);
|
|
649
|
+
artifacts.copilotHome = home;
|
|
650
|
+
const identity = loginIdentity(ctx.deps.userConfigDir ?? getCopilotConfigDir());
|
|
651
|
+
const env = buildSessionEnv(ctx.env, home, { session: true, hasLogin: identity.loggedInUsers !== undefined });
|
|
652
|
+
env.OMC_GIT_GUARDRAILS = '1';
|
|
653
|
+
writeFileSync(join(home, 'config.json'), `${JSON.stringify({ ...identity, trustedFolders: [projectDir] }, null, 2)}\n`);
|
|
654
|
+
let agentFiles = [];
|
|
655
|
+
try {
|
|
656
|
+
agentFiles = readdirSync(join(ctx.root, 'agents')).filter((f) => f.endsWith('.md')).sort();
|
|
657
|
+
}
|
|
658
|
+
catch { /* evaluated as 0 */ }
|
|
659
|
+
const result = await runSdkTier({
|
|
660
|
+
root: ctx.root,
|
|
661
|
+
bin,
|
|
662
|
+
binVersion,
|
|
663
|
+
packageVersion,
|
|
664
|
+
home,
|
|
665
|
+
projectDir,
|
|
666
|
+
remoteDir: join(parent, 'remote.git'),
|
|
667
|
+
env,
|
|
668
|
+
scenarios,
|
|
669
|
+
model: opts.model,
|
|
670
|
+
timeoutMs: opts.timeoutMs ?? DEFAULT_SCENARIO_TIMEOUT_MS,
|
|
671
|
+
maxCredits: opts.maxCredits ?? DEFAULT_MAX_CREDITS,
|
|
672
|
+
keepHome: !!opts.keepHome,
|
|
673
|
+
loadSdk: ctx.deps.loadSdk ?? (() => loadCopilotSdk(ctx.env, ctx.spawnSyncFn)),
|
|
674
|
+
loadExpectedToolCount: ctx.deps.loadExpectedToolCount ?? defaultExpectedToolCount,
|
|
675
|
+
spawnSync: ctx.spawnSyncFn,
|
|
676
|
+
skillDirs: countDirsWith(join(ctx.root, 'skills'), 'SKILL.md'),
|
|
677
|
+
agentFiles,
|
|
678
|
+
mcpServer: mcpServerNames(ctx.root)[0] ?? 't',
|
|
679
|
+
runChain: runChainScenario,
|
|
680
|
+
});
|
|
681
|
+
if (Object.keys(result.events).length)
|
|
682
|
+
artifacts.events = result.events;
|
|
683
|
+
const logs = readDebugLogs(join(home, 'logs'));
|
|
684
|
+
if (logs.path)
|
|
685
|
+
artifacts.debugLog = logs.path;
|
|
686
|
+
return result;
|
|
687
|
+
}
|
|
688
|
+
//# sourceMappingURL=copilot-smoke.js.map
|