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,620 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier 2 of `omg smoke copilot`: scenario definitions and the pure evaluators
|
|
3
|
+
* for the SDK static checks (`sdk.*`) and the scenario checks (`scn.<name>.*`).
|
|
4
|
+
* No SDK, process or network access here, so the unit tests replay captured
|
|
5
|
+
* event streams through the same functions the live driver uses.
|
|
6
|
+
*/
|
|
7
|
+
import { realpathSync } from 'fs';
|
|
8
|
+
import { basename, join, resolve, sep } from 'path';
|
|
9
|
+
import { assistantText, BENIGN_ADAPTER_NOTES, collectHookRuns, excerpt, SMOKE_TOKEN, } from './copilot-session-eval.js';
|
|
10
|
+
/** What `--scenario all` runs: the SDK-session scenarios (one premium request each). */
|
|
11
|
+
export const ALL_SCENARIOS = ['smoke', 'guardrail', 'skill', 'delegate'];
|
|
12
|
+
/**
|
|
13
|
+
* Named explicitly only, never in `all` or the default: `chain` spawns two
|
|
14
|
+
* real headless `copilot -p` factory links (about 2 premium requests) outside
|
|
15
|
+
* the SDK runtime's credit cap.
|
|
16
|
+
*/
|
|
17
|
+
export const OPT_IN_SCENARIOS = ['chain'];
|
|
18
|
+
export const KNOWN_SCENARIOS = [...ALL_SCENARIOS, ...OPT_IN_SCENARIOS];
|
|
19
|
+
/** About 2 premium requests (one per scenario). */
|
|
20
|
+
export const DEFAULT_SCENARIOS = ['smoke', 'guardrail'];
|
|
21
|
+
export const PLUGIN_NAME = 'oh-my-copilot';
|
|
22
|
+
export const DELEGATE_AGENT = 'oh-my-copilot:architect';
|
|
23
|
+
export const MIN_PROTOCOL_VERSION = 3;
|
|
24
|
+
export const SDK_PACKAGE = '@github/copilot-sdk';
|
|
25
|
+
/** Lane B's CLI keys "pure skip" exit code 2 on this exact prefix. */
|
|
26
|
+
export const SDK_MISSING_DETAIL = `${SDK_PACKAGE} not installed — npm i -g ${SDK_PACKAGE} --omit=optional --ignore-scripts`;
|
|
27
|
+
/**
|
|
28
|
+
* Never offered: our own `host_smoke` MCP tool (a smoke that can start a smoke).
|
|
29
|
+
* MCP tools are named `<server>-<tool>` (server = the first `.mcp.json` key);
|
|
30
|
+
* `t:host_smoke` / `t(host_smoke)` match nothing.
|
|
31
|
+
*/
|
|
32
|
+
export function hostSmokeToolName(mcpServer) {
|
|
33
|
+
return `${mcpServer}-host_smoke`;
|
|
34
|
+
}
|
|
35
|
+
/** Built-in shell and file-writing tools, removed for scenarios that must not use them. */
|
|
36
|
+
export const NO_SHELL_OR_WRITE_TOOLS = ['powershell', 'shell', 'create', 'edit'];
|
|
37
|
+
export const NO_WRITE_TOOLS = ['create', 'edit'];
|
|
38
|
+
const denyAll = () => false;
|
|
39
|
+
/** The one command the guardrail scenario may run (should our preToolUse hook not deny it first). */
|
|
40
|
+
export const GUARDRAIL_COMMAND = /^\s*git\s+push\s+(--force|-f)\s+origin\s+main\s*$/;
|
|
41
|
+
/** Path identity: realpath when it exists, resolved, case-folded on win32. */
|
|
42
|
+
function samePathKey(p, platform) {
|
|
43
|
+
let out = resolve(p);
|
|
44
|
+
try {
|
|
45
|
+
out = realpathSync.native(out);
|
|
46
|
+
}
|
|
47
|
+
catch { /* missing: keep the resolved form */ }
|
|
48
|
+
return canon(out, platform);
|
|
49
|
+
}
|
|
50
|
+
function isUnder(p, root, platform) {
|
|
51
|
+
const child = samePathKey(p, platform);
|
|
52
|
+
const parent = samePathKey(root, platform);
|
|
53
|
+
return child === parent || child.startsWith(`${parent.replace(/\/$/, '')}/`);
|
|
54
|
+
}
|
|
55
|
+
/** The request's working directory, under any of the spellings the runtime uses. */
|
|
56
|
+
function requestCwd(req) {
|
|
57
|
+
for (const key of ['cwd', 'workingDirectory', 'resolvedWorkingDirectory']) {
|
|
58
|
+
if (typeof req[key] === 'string' && req[key])
|
|
59
|
+
return req[key];
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
/** Only the exact guardrail push, and only from the sandbox project when the request names a cwd. */
|
|
64
|
+
export function permitGuardrailPush(req, ctx) {
|
|
65
|
+
if (req.kind !== 'shell' || !GUARDRAIL_COMMAND.test(String(req.fullCommandText ?? '')))
|
|
66
|
+
return false;
|
|
67
|
+
const cwd = requestCwd(req);
|
|
68
|
+
const platform = ctx.platform ?? process.platform;
|
|
69
|
+
return cwd === undefined || samePathKey(cwd, platform) === samePathKey(ctx.projectDir, platform);
|
|
70
|
+
}
|
|
71
|
+
/** Reads only; when the request carries a path it must be under the plugin root or the sandbox project. */
|
|
72
|
+
export function permitScopedRead(req, ctx) {
|
|
73
|
+
if (req.kind !== 'read')
|
|
74
|
+
return false;
|
|
75
|
+
if (typeof req.path !== 'string' || !req.path)
|
|
76
|
+
return true;
|
|
77
|
+
const platform = ctx.platform ?? process.platform;
|
|
78
|
+
return isUnder(req.path, ctx.pluginRoot, platform) || isUnder(req.path, ctx.projectDir, platform);
|
|
79
|
+
}
|
|
80
|
+
export const SCENARIOS = {
|
|
81
|
+
smoke: {
|
|
82
|
+
name: 'smoke',
|
|
83
|
+
prompt: `Reply with exactly: ${SMOKE_TOKEN}. Do not use tools.`,
|
|
84
|
+
excludedTools: [...NO_SHELL_OR_WRITE_TOOLS],
|
|
85
|
+
permit: denyAll,
|
|
86
|
+
},
|
|
87
|
+
guardrail: {
|
|
88
|
+
name: 'guardrail',
|
|
89
|
+
prompt: 'Run the shell command: git push --force origin main — then report the exact result.',
|
|
90
|
+
excludedTools: [...NO_WRITE_TOOLS],
|
|
91
|
+
// Our preToolUse hook denies before any permission prompt. Should it not,
|
|
92
|
+
// only this exact push is approved, and the remote is a local bare repo
|
|
93
|
+
// (scn.guardrail.no_push then fails).
|
|
94
|
+
permit: permitGuardrailPush,
|
|
95
|
+
},
|
|
96
|
+
skill: {
|
|
97
|
+
name: 'skill',
|
|
98
|
+
prompt: 'Use the /oh-my-copilot:plan skill to outline a two-step plan for adding a README. Keep it under 5 lines.',
|
|
99
|
+
excludedTools: [...NO_SHELL_OR_WRITE_TOOLS],
|
|
100
|
+
permit: permitScopedRead, // loading SKILL.md is a read request
|
|
101
|
+
},
|
|
102
|
+
delegate: {
|
|
103
|
+
name: 'delegate',
|
|
104
|
+
prompt: `Delegate a one-line summary of this directory to the ${DELEGATE_AGENT} agent and return its answer.`,
|
|
105
|
+
excludedTools: [...NO_SHELL_OR_WRITE_TOOLS],
|
|
106
|
+
permit: permitScopedRead,
|
|
107
|
+
},
|
|
108
|
+
// Not an SDK session: the prompt is link 1's, spawned as a real `copilot -p`
|
|
109
|
+
// factory link (copilot-chain-scenario.ts) under the AFK profile.
|
|
110
|
+
chain: {
|
|
111
|
+
name: 'chain',
|
|
112
|
+
prompt: 'Reply with exactly: CHAIN_LINK_1. Do not use tools.',
|
|
113
|
+
excludedTools: [],
|
|
114
|
+
permit: denyAll,
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Chain scenario (two real factory links)
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
/** Intent id of the smoke chain; its stop marker is `chain-<id>.stopped.json`. */
|
|
121
|
+
export const CHAIN_INTENT_ID = 'omg-smoke-chain';
|
|
122
|
+
/** Project skill link 2 is routed to (`/chain-ack ...`); it replies with one token. */
|
|
123
|
+
export const CHAIN_SKILL = 'chain-ack';
|
|
124
|
+
export const CHAIN_LINK1_STAGE = 'link-1';
|
|
125
|
+
export const CHAIN_LINK2_STAGE = 'link-2';
|
|
126
|
+
/** Two links, one user prompt each. */
|
|
127
|
+
export const CHAIN_MAX_PREMIUM_REQUESTS = 2;
|
|
128
|
+
const ledgerStr = (ledger, key) => str(ledger?.[key]);
|
|
129
|
+
export function chainCost(chain) {
|
|
130
|
+
let premiumRequests = 0;
|
|
131
|
+
let credits = 0;
|
|
132
|
+
for (const s of chain.sessions) {
|
|
133
|
+
const c = scenarioCost(s.events ?? []);
|
|
134
|
+
premiumRequests += c.premiumRequests;
|
|
135
|
+
credits += c.credits;
|
|
136
|
+
}
|
|
137
|
+
const found = chain.sessions.filter((s) => s.events !== null).length;
|
|
138
|
+
return { premiumRequests, credits, source: `${found}/${chain.sessions.length} link session(s) events.jsonl` };
|
|
139
|
+
}
|
|
140
|
+
function evaluateChain(run) {
|
|
141
|
+
const chain = run.chain;
|
|
142
|
+
if (!chain)
|
|
143
|
+
return scenarioCheckIds('chain').slice(0, 5).map((id) => ({ id, ok: false, detail: 'no chain evidence collected' }));
|
|
144
|
+
const link1 = chain.ledgers.find((l) => l.file === chain.firstLink);
|
|
145
|
+
const children = chain.ledgers.filter((l) => ledgerStr(l, 'parentLink') === chain.firstLink);
|
|
146
|
+
const link2 = children[0];
|
|
147
|
+
const link1Host = ledgerStr(link1, 'hostSessionId');
|
|
148
|
+
const link2Id = link2?.file ?? '';
|
|
149
|
+
const link2Host = ledgerStr(link2, 'hostSessionId');
|
|
150
|
+
const decisionFor = (linkId) => chain.decisions.filter((d) => d.sessionId === linkId && d.decision !== 'chain-link-rejected').at(-1);
|
|
151
|
+
const rejected = chain.decisions.filter((d) => d.decision === 'chain-link-rejected');
|
|
152
|
+
const open = chain.ledgers.filter((l) => !ledgerStr(l, 'closedAt'));
|
|
153
|
+
const cost = chainCost(chain);
|
|
154
|
+
const stopReason = str(chain.stopped?.reason);
|
|
155
|
+
const ledgerEvidence = excerpt(JSON.stringify({ ledgers: chain.ledgers, decisions: chain.decisions, stopped: chain.stopped }));
|
|
156
|
+
// Link 1 was resolved by OMC_CHAIN_LINK: its ledger was closed by a host session with another id.
|
|
157
|
+
const link1Ok = !!link1 && !!link1Host && link1Host !== chain.firstLink && ledgerStr(link1, 'decision') === 'enqueued';
|
|
158
|
+
const spawnedOk = children.length === 1 && ledgerStr(link2, 'host') === 'copilot' && ledgerStr(link2, 'stage') === CHAIN_LINK2_STAGE;
|
|
159
|
+
// Link 2 ended under its own inherited identity: its ledger (named after the
|
|
160
|
+
// id the worker handed over) was closed by a third session id, and no
|
|
161
|
+
// SessionEnd was rejected along the way.
|
|
162
|
+
const inheritedOk = spawnedOk && !!link2Host && link2Host !== link2Id && link2Host !== link1Host
|
|
163
|
+
&& str(decisionFor(link2Id)?.hostSessionId) === link2Host && rejected.length === 0;
|
|
164
|
+
const closedOk = chain.ledgers.length === 2 && open.length === 0 && stopReason === `loop-capped:${CHAIN_LINK2_STAGE}`;
|
|
165
|
+
const sessionsFound = chain.sessions.length === 2 && chain.sessions.every((s) => s.events !== null);
|
|
166
|
+
const premiumOk = sessionsFound && cost.premiumRequests <= CHAIN_MAX_PREMIUM_REQUESTS;
|
|
167
|
+
return [
|
|
168
|
+
{
|
|
169
|
+
id: 'scn.chain.link1',
|
|
170
|
+
ok: link1Ok,
|
|
171
|
+
detail: link1Ok
|
|
172
|
+
? `link 1 ${chain.firstLink} closed by host session ${link1Host} via OMC_CHAIN_LINK, decision enqueued`
|
|
173
|
+
: !link1 ? 'link 1 ledger missing' : !link1Host ? 'link 1 ledger never closed (its SessionEnd did not resolve OMC_CHAIN_LINK)'
|
|
174
|
+
: `link 1 closed with decision ${ledgerStr(link1, 'decision') || '?'} by ${link1Host}`,
|
|
175
|
+
...(link1Ok ? {} : { evidence: ledgerEvidence }),
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
id: 'scn.chain.spawned',
|
|
179
|
+
ok: spawnedOk,
|
|
180
|
+
detail: spawnedOk
|
|
181
|
+
? `link 2 ${link2Id} spawned by link 1's SessionEnd worker (host copilot, stage ${CHAIN_LINK2_STAGE})`
|
|
182
|
+
: `${children.length} ledger(s) with parentLink ${chain.firstLink}`,
|
|
183
|
+
...(spawnedOk ? {} : { evidence: ledgerEvidence }),
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
id: 'scn.chain.inherited',
|
|
187
|
+
ok: inheritedOk,
|
|
188
|
+
detail: inheritedOk
|
|
189
|
+
? `link 2 ended as chain link ${link2Id} (host session ${link2Host}); no rejected OMC_CHAIN_LINK`
|
|
190
|
+
: `link 2 host session ${link2Host || 'none'}${rejected.length ? `; ${rejected.length} chain-link-rejected decision(s)` : ''}`,
|
|
191
|
+
...(inheritedOk ? {} : { evidence: ledgerEvidence }),
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
id: 'scn.chain.closed',
|
|
195
|
+
ok: closedOk,
|
|
196
|
+
detail: closedOk
|
|
197
|
+
? `chain closed out: 2 links, 0 open ledgers, stop marker ${stopReason}`
|
|
198
|
+
: `${chain.ledgers.length} link(s), ${open.length} open, stop marker ${stopReason || 'none'}`,
|
|
199
|
+
...(closedOk ? {} : { evidence: ledgerEvidence }),
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
id: 'scn.chain.premium',
|
|
203
|
+
ok: premiumOk,
|
|
204
|
+
detail: `${cost.premiumRequests} premium request(s) over ${cost.source} (max ${CHAIN_MAX_PREMIUM_REQUESTS})`,
|
|
205
|
+
},
|
|
206
|
+
];
|
|
207
|
+
}
|
|
208
|
+
/** The session's excludedTools for a scenario: its built-ins plus our host_smoke tool. */
|
|
209
|
+
export function scenarioExcludedTools(name, mcpServer) {
|
|
210
|
+
return [hostSmokeToolName(mcpServer), ...SCENARIOS[name].excludedTools];
|
|
211
|
+
}
|
|
212
|
+
/** Check ids a scenario yields, in report order (stable; Lane B's docs list them). */
|
|
213
|
+
export function scenarioCheckIds(name) {
|
|
214
|
+
const own = {
|
|
215
|
+
smoke: ['reply', 'hooks', 'no_tools'],
|
|
216
|
+
guardrail: ['denied', 'no_push', 'hooks'],
|
|
217
|
+
skill: ['invoked'],
|
|
218
|
+
delegate: ['selected', 'hooks', 'completed'],
|
|
219
|
+
chain: ['link1', 'spawned', 'inherited', 'closed', 'premium'],
|
|
220
|
+
};
|
|
221
|
+
return [...own[name], 'exit', 'adapter_errors', 'cost'].map((s) => `scn.${name}.${s}`);
|
|
222
|
+
}
|
|
223
|
+
export const SDK_STATIC_IDS = ['sdk.available', 'sdk.runtime', 'sdk.plugins', 'sdk.skills', 'sdk.agents', 'sdk.mcp', 'sdk.tools_excluded'];
|
|
224
|
+
/** Every sdk.* and scn.* id failed with one detail (SDK missing, binary missing, runtime start failure). */
|
|
225
|
+
export function failedTier2Checks(scenarios, detail, from = 'all') {
|
|
226
|
+
const sdkIds = from === 'all' ? [...SDK_STATIC_IDS] : SDK_STATIC_IDS.slice(1);
|
|
227
|
+
return [...sdkIds, ...scenarios.flatMap(scenarioCheckIds)].map((id) => ({ id, ok: false, detail }));
|
|
228
|
+
}
|
|
229
|
+
/** Copilot CLI rejects `--max-ai-credits` below this (also in `--headless` runtime mode, CLI 1.0.91). */
|
|
230
|
+
export const RUNTIME_MIN_MAX_CREDITS = 30;
|
|
231
|
+
/** Reported for scenarios not started once the run's credit cap was reached. */
|
|
232
|
+
export const CREDIT_CAP_SKIP_DETAIL = 'skipped: credit cap reached';
|
|
233
|
+
/** A scenario's checks all failed with one detail (not started). */
|
|
234
|
+
export function skippedScenarioChecks(name, detail) {
|
|
235
|
+
return scenarioCheckIds(name).map((id) => ({ id, ok: false, detail }));
|
|
236
|
+
}
|
|
237
|
+
function rpcFail(id, what, r) {
|
|
238
|
+
return { id, ok: false, detail: `${what} failed`, evidence: excerpt(r.error) };
|
|
239
|
+
}
|
|
240
|
+
function arr(value, key) {
|
|
241
|
+
const inner = value && typeof value === 'object' && !Array.isArray(value) ? value[key] : value;
|
|
242
|
+
return Array.isArray(inner) ? inner.filter((x) => !!x && typeof x === 'object') : [];
|
|
243
|
+
}
|
|
244
|
+
function str(v) {
|
|
245
|
+
return typeof v === 'string' ? v : '';
|
|
246
|
+
}
|
|
247
|
+
/** Canonical path for prefix tests: resolved, `/` separators, case-folded on win32. */
|
|
248
|
+
function canon(p, platform = process.platform) {
|
|
249
|
+
const out = resolve(p).split(sep).join('/');
|
|
250
|
+
return platform === 'win32' ? out.toLowerCase() : out;
|
|
251
|
+
}
|
|
252
|
+
export function evaluateSdkRuntime(status, binVersion, model) {
|
|
253
|
+
const id = 'sdk.runtime';
|
|
254
|
+
if (!status.ok)
|
|
255
|
+
return rpcFail(id, 'getStatus()', status);
|
|
256
|
+
const version = str(status.value.version);
|
|
257
|
+
const protocol = typeof status.value.protocolVersion === 'number' ? status.value.protocolVersion : NaN;
|
|
258
|
+
const problems = [];
|
|
259
|
+
if (binVersion && version !== binVersion)
|
|
260
|
+
problems.push(`runtime ${version || '?'} != copilot --version ${binVersion} (not the installed exe?)`);
|
|
261
|
+
if (!(protocol >= MIN_PROTOCOL_VERSION))
|
|
262
|
+
problems.push(`protocolVersion ${String(status.value.protocolVersion)} < ${MIN_PROTOCOL_VERSION}`);
|
|
263
|
+
return {
|
|
264
|
+
id,
|
|
265
|
+
ok: problems.length === 0,
|
|
266
|
+
detail: problems.length === 0 ? `runtime ${version}, protocol ${protocol}; model ${model}` : problems.join('; '),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
export function evaluateSdkPlugins(plugins, packageVersion) {
|
|
270
|
+
const id = 'sdk.plugins';
|
|
271
|
+
if (!plugins.ok)
|
|
272
|
+
return rpcFail(id, 'plugins.list', plugins);
|
|
273
|
+
const list = arr(plugins.value, 'plugins');
|
|
274
|
+
const entry = list.find((p) => p.name === PLUGIN_NAME);
|
|
275
|
+
const ok = !!entry && entry.version === packageVersion && entry.enabled !== false;
|
|
276
|
+
return {
|
|
277
|
+
id,
|
|
278
|
+
ok,
|
|
279
|
+
detail: entry
|
|
280
|
+
? `${PLUGIN_NAME}@${String(entry.version)} enabled=${String(entry.enabled)}${ok ? '' : ` (expected ${packageVersion}, enabled)`}`
|
|
281
|
+
: `${PLUGIN_NAME} not listed (${list.length} plugin(s))`,
|
|
282
|
+
...(ok ? {} : { evidence: excerpt(JSON.stringify(plugins.value)) }),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
export function evaluateSdkSkills(skills, root, skillDirs, platform = process.platform) {
|
|
286
|
+
const id = 'sdk.skills';
|
|
287
|
+
if (!skills.ok)
|
|
288
|
+
return rpcFail(id, 'skills.list', skills);
|
|
289
|
+
const prefix = `${canon(join(root, 'skills'), platform)}/`;
|
|
290
|
+
const pluginSkills = arr(skills.value, 'skills').filter((s) => s.source === 'plugin' && s.pluginName === PLUGIN_NAME);
|
|
291
|
+
const fromSkillsDir = new Set();
|
|
292
|
+
const other = [];
|
|
293
|
+
for (const s of pluginSkills) {
|
|
294
|
+
const p = canon(str(s.path), platform);
|
|
295
|
+
if (str(s.path) && p.startsWith(prefix))
|
|
296
|
+
fromSkillsDir.add(p.slice(prefix.length).split('/')[0]);
|
|
297
|
+
else
|
|
298
|
+
other.push(str(s.commandName) || str(s.name));
|
|
299
|
+
}
|
|
300
|
+
const fold = (d) => (platform === 'win32' ? d.toLowerCase() : d);
|
|
301
|
+
const missing = skillDirs.filter((d) => !fromSkillsDir.has(fold(d)));
|
|
302
|
+
const ok = skillDirs.length > 0 && missing.length === 0 && fromSkillsDir.size === skillDirs.length;
|
|
303
|
+
return {
|
|
304
|
+
id,
|
|
305
|
+
ok,
|
|
306
|
+
detail: `${fromSkillsDir.size}/${skillDirs.length} skills/*/SKILL.md listed as plugin skills`
|
|
307
|
+
+ (other.length ? `; plus ${other.length} non-skills/ plugin entr${other.length === 1 ? 'y' : 'ies'} (commands/*.md: ${other.join(', ')})` : ''),
|
|
308
|
+
...(ok ? {} : { evidence: excerpt(missing.length ? `missing: ${missing.join(', ')}` : JSON.stringify(pluginSkills.map((s) => s.path))) }),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
export function evaluateSdkAgents(agents, agentFiles) {
|
|
312
|
+
const id = 'sdk.agents';
|
|
313
|
+
if (!agents.ok)
|
|
314
|
+
return rpcFail(id, 'agent.list', agents);
|
|
315
|
+
const names = new Set(arr(agents.value, 'agents').map((a) => str(a.name)));
|
|
316
|
+
const expected = agentFiles.map((f) => `${PLUGIN_NAME}:${basename(f, '.md')}`);
|
|
317
|
+
const missing = expected.filter((n) => !names.has(n));
|
|
318
|
+
const ok = expected.length > 0 && missing.length === 0;
|
|
319
|
+
return {
|
|
320
|
+
id,
|
|
321
|
+
ok,
|
|
322
|
+
detail: `${expected.length - missing.length}/${expected.length} ${PLUGIN_NAME}:<agent> listed (${names.size} agents total)`,
|
|
323
|
+
...(ok ? {} : { evidence: excerpt(`missing: ${missing.join(', ')}`) }),
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
export function evaluateSdkMcp(mcp, tools, server, expected) {
|
|
327
|
+
const id = 'sdk.mcp';
|
|
328
|
+
if (!mcp.ok)
|
|
329
|
+
return rpcFail(id, 'mcp.list', mcp);
|
|
330
|
+
const entry = arr(mcp.value, 'servers').find((s) => s.name === server);
|
|
331
|
+
if (!entry || entry.status !== 'connected') {
|
|
332
|
+
return { id, ok: false, detail: entry ? `server ${server} status ${String(entry.status)}` : `server ${server} not listed`, evidence: excerpt(JSON.stringify(mcp.value)) };
|
|
333
|
+
}
|
|
334
|
+
if (!tools.ok)
|
|
335
|
+
return rpcFail(id, `mcp.listTools(${server})`, tools);
|
|
336
|
+
const count = arr(tools.value, 'tools').length;
|
|
337
|
+
if (typeof expected === 'string')
|
|
338
|
+
return { id, ok: false, detail: `server ${server} connected with ${count} tools; registry unavailable: ${expected}` };
|
|
339
|
+
const ok = count === expected;
|
|
340
|
+
return { id, ok, detail: `server ${server} connected, ${count} tools, registry allTools ${expected}${ok ? '' : ' (stale dist? run npm run build)'}` };
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* `excludedTools` really removed our host_smoke tool: it is absent from the
|
|
344
|
+
* session's initialized tool list (`session.rpc.tools.getCurrentMetadata`),
|
|
345
|
+
* while other tools of the same server are present (else the absence proves
|
|
346
|
+
* nothing, e.g. the MCP tools were never offered).
|
|
347
|
+
*/
|
|
348
|
+
export function evaluateSdkToolsExcluded(meta, server) {
|
|
349
|
+
const id = 'sdk.tools_excluded';
|
|
350
|
+
const excluded = hostSmokeToolName(server);
|
|
351
|
+
if (!meta.ok)
|
|
352
|
+
return rpcFail(id, 'tools.getCurrentMetadata', meta);
|
|
353
|
+
const tools = arr(meta.value, 'tools');
|
|
354
|
+
if (tools.length === 0)
|
|
355
|
+
return { id, ok: false, detail: 'session tool list empty or not initialized', evidence: excerpt(JSON.stringify(meta.value)) };
|
|
356
|
+
const fromServer = tools.filter((t) => t.mcpServerName === server || str(t.name).startsWith(`${server}-`));
|
|
357
|
+
const leaked = fromServer.filter((t) => t.name === excluded || t.namespacedName === excluded || t.mcpToolName === 'host_smoke');
|
|
358
|
+
const ok = leaked.length === 0 && fromServer.length > 0;
|
|
359
|
+
return {
|
|
360
|
+
id,
|
|
361
|
+
ok,
|
|
362
|
+
detail: leaked.length ? `${excluded} still offered to the model`
|
|
363
|
+
: fromServer.length === 0 ? `no ${server}-* tools in the session's ${tools.length} tools; exclusion of ${excluded} unproven`
|
|
364
|
+
: `${excluded} absent; ${fromServer.length} other ${server}-* tools offered (${tools.length} total)`,
|
|
365
|
+
...(ok ? {} : { evidence: excerpt(JSON.stringify((leaked.length ? leaked : tools).map((t) => t.name))) }),
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Split runtime log text into windows by each line's leading ISO timestamp:
|
|
370
|
+
* window i holds lines stamped in [starts[i], starts[i+1]); the last window is
|
|
371
|
+
* open-ended. Lines before starts[0] are dropped; unstamped (continuation)
|
|
372
|
+
* lines follow the previous stamped line. Attributing by time, not byte
|
|
373
|
+
* offset, keeps a late SessionEnd line of scenario N out of scenario N+1.
|
|
374
|
+
*/
|
|
375
|
+
export function sliceLogByTime(text, starts) {
|
|
376
|
+
const out = starts.map(() => []);
|
|
377
|
+
let current = -1;
|
|
378
|
+
for (const line of text.split(/\r?\n/)) {
|
|
379
|
+
const stamp = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z)/.exec(line);
|
|
380
|
+
if (stamp) {
|
|
381
|
+
const t = Date.parse(stamp[1]);
|
|
382
|
+
current = -1;
|
|
383
|
+
for (let i = starts.length - 1; i >= 0; i--)
|
|
384
|
+
if (t >= starts[i]) {
|
|
385
|
+
current = i;
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (current >= 0 && line)
|
|
390
|
+
out[current].push(line);
|
|
391
|
+
}
|
|
392
|
+
return out.map((lines) => lines.join('\n'));
|
|
393
|
+
}
|
|
394
|
+
/** One nano-AIU is 1e-9 AI credits (the unit of `--max-ai-credits`). */
|
|
395
|
+
const NANO = 1e9;
|
|
396
|
+
function num(v) {
|
|
397
|
+
return typeof v === 'number' && Number.isFinite(v) ? v : 0;
|
|
398
|
+
}
|
|
399
|
+
/** AI credits billed by one `assistant.usage` event (0 for any other event). */
|
|
400
|
+
export function usageCredits(event) {
|
|
401
|
+
if (event.type !== 'assistant.usage')
|
|
402
|
+
return 0;
|
|
403
|
+
return num(event.data?.copilotUsage?.totalNanoAiu) / NANO;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Prefer `session.shutdown` totals (emitted at disconnect); else sum
|
|
407
|
+
* `assistant.usage`: credits from `copilotUsage.totalNanoAiu`, premium
|
|
408
|
+
* requests counted per user-initiated call (agent-initiated follow-ups of the
|
|
409
|
+
* same turn are not billed again).
|
|
410
|
+
*/
|
|
411
|
+
export function scenarioCost(events) {
|
|
412
|
+
const shutdown = [...events].reverse().find((e) => e.type === 'session.shutdown');
|
|
413
|
+
if (shutdown?.data && (typeof shutdown.data.totalPremiumRequests === 'number' || typeof shutdown.data.totalNanoAiu === 'number')) {
|
|
414
|
+
return { premiumRequests: num(shutdown.data.totalPremiumRequests), credits: num(shutdown.data.totalNanoAiu) / NANO, source: 'session.shutdown' };
|
|
415
|
+
}
|
|
416
|
+
const usage = events.filter((e) => e.type === 'assistant.usage');
|
|
417
|
+
const credits = usage.reduce((sum, e) => sum + usageCredits(e), 0);
|
|
418
|
+
const premiumRequests = usage.filter((e) => e.data?.initiator === 'user').length;
|
|
419
|
+
return { premiumRequests, credits, source: `${usage.length} assistant.usage event(s)` };
|
|
420
|
+
}
|
|
421
|
+
function hookCheck(id, events, required, optional = {}) {
|
|
422
|
+
const runs = collectHookRuns(events);
|
|
423
|
+
const parts = [];
|
|
424
|
+
const bad = [];
|
|
425
|
+
for (const type of [...required, ...Object.keys(optional)]) {
|
|
426
|
+
const mine = runs.filter((r) => r.hookType === type);
|
|
427
|
+
const ended = mine.filter((r) => r.ended);
|
|
428
|
+
const failed = ended.filter((r) => r.success !== true);
|
|
429
|
+
if (mine.length === 0 && type in optional) {
|
|
430
|
+
parts.push(`${type} ${optional[type]}`);
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
if (ended.length === 0 || failed.length > 0) {
|
|
434
|
+
bad.push(`${type}: ${mine.length ? failed.map((r) => r.error ?? `success=${String(r.success)}`).join(', ') || 'no hook.end' : 'never fired'}`);
|
|
435
|
+
}
|
|
436
|
+
parts.push(`${type} ${ended.length - failed.length}/${mine.length}`);
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
id,
|
|
440
|
+
ok: bad.length === 0,
|
|
441
|
+
detail: `hook.end success:true — ${parts.join(', ')}`,
|
|
442
|
+
...(bad.length ? { evidence: excerpt(bad.join(' | ')) } : {}),
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
function evaluateSmoke(run) {
|
|
446
|
+
const text = assistantText(run.events);
|
|
447
|
+
const tools = run.events.filter((e) => e.type === 'tool.execution_start');
|
|
448
|
+
return [
|
|
449
|
+
{
|
|
450
|
+
id: 'scn.smoke.reply',
|
|
451
|
+
ok: text.includes(SMOKE_TOKEN),
|
|
452
|
+
detail: text.includes(SMOKE_TOKEN) ? `assistant replied ${SMOKE_TOKEN}` : `${SMOKE_TOKEN} missing from assistant output`,
|
|
453
|
+
...(text.includes(SMOKE_TOKEN) ? {} : { evidence: excerpt(text) }),
|
|
454
|
+
},
|
|
455
|
+
// sessionStart is lazy (fires after the first userPromptSubmitted); sessionEnd fires per turn.
|
|
456
|
+
hookCheck('scn.smoke.hooks', run.events, ['sessionStart', 'userPromptSubmitted', 'agentStop', 'sessionEnd']),
|
|
457
|
+
{
|
|
458
|
+
id: 'scn.smoke.no_tools',
|
|
459
|
+
ok: tools.length === 0,
|
|
460
|
+
detail: tools.length === 0 ? 'no tool.execution_start' : `${tools.length} tool call(s): ${tools.map((e) => str(e.data?.toolName)).join(', ')}`,
|
|
461
|
+
},
|
|
462
|
+
];
|
|
463
|
+
}
|
|
464
|
+
const GUARDRAIL_MARKER = 'Git guardrail';
|
|
465
|
+
function evaluateGuardrail(run) {
|
|
466
|
+
const denyHook = run.events.find((e) => e.type === 'hook.end' && e.data?.hookType === 'preToolUse'
|
|
467
|
+
&& JSON.stringify(e.data?.output ?? '').includes(GUARDRAIL_MARKER));
|
|
468
|
+
const deniedTool = run.events.find((e) => e.type === 'tool.execution_complete' && e.data?.success === false
|
|
469
|
+
&& e.data?.error?.code === 'denied');
|
|
470
|
+
const denied = !!denyHook && !!deniedTool;
|
|
471
|
+
const toolSummary = run.events.filter((e) => e.type === 'tool.execution_start' || e.type === 'tool.execution_complete')
|
|
472
|
+
.map((e) => `${e.type} ${JSON.stringify(e.data?.arguments ?? e.data?.error ?? e.data?.success)}`).join('\n');
|
|
473
|
+
const refs = run.remoteRefs;
|
|
474
|
+
// A denied tool never runs, so postToolUse only fires when some tool completed.
|
|
475
|
+
const ranTool = run.events.some((e) => e.type === 'tool.execution_complete' && e.data?.success === true);
|
|
476
|
+
return [
|
|
477
|
+
{
|
|
478
|
+
id: 'scn.guardrail.denied',
|
|
479
|
+
ok: denied,
|
|
480
|
+
detail: denied
|
|
481
|
+
? `preToolUse denied: ${excerpt(str((deniedTool.data?.error).message).split('\n')[0], 120)}`
|
|
482
|
+
: `${denyHook ? '' : `no preToolUse hook.end containing "${GUARDRAIL_MARKER}"; `}${deniedTool ? '' : 'no tool.execution_complete with error.code=denied'}`.replace(/; $/, ''),
|
|
483
|
+
...(denied ? {} : { evidence: excerpt(toolSummary || assistantText(run.events)) }),
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
id: 'scn.guardrail.no_push',
|
|
487
|
+
ok: refs !== null && refs !== undefined && refs.trim() === '',
|
|
488
|
+
detail: refs === null || refs === undefined ? 'could not inspect the bare remote' : refs.trim() === '' ? 'bare remote has no refs' : 'bare remote received refs: the push went through',
|
|
489
|
+
...(refs && refs.trim() ? { evidence: excerpt(refs) } : {}),
|
|
490
|
+
},
|
|
491
|
+
ranTool
|
|
492
|
+
? hookCheck('scn.guardrail.hooks', run.events, ['preToolUse', 'postToolUse'])
|
|
493
|
+
: hookCheck('scn.guardrail.hooks', run.events, ['preToolUse'], { postToolUse: 'n/a (the denied tool never ran)' }),
|
|
494
|
+
];
|
|
495
|
+
}
|
|
496
|
+
function evaluateSkill(run) {
|
|
497
|
+
const invoked = run.events.filter((e) => e.type === 'skill.invoked');
|
|
498
|
+
// Assert on PATH: `name` is the SKILL.md frontmatter name (omc-plan), not oh-my-copilot:plan.
|
|
499
|
+
const hit = invoked.find((e) => /(^|[\\/])skills[\\/]plan[\\/]SKILL\.md$/.test(str(e.data?.path)) && e.data?.pluginName === PLUGIN_NAME);
|
|
500
|
+
return [{
|
|
501
|
+
id: 'scn.skill.invoked',
|
|
502
|
+
ok: !!hit,
|
|
503
|
+
detail: hit
|
|
504
|
+
? `skill.invoked ${str(hit.data?.name)} (${PLUGIN_NAME}, path …skills/plan/SKILL.md)`
|
|
505
|
+
: invoked.length ? `skill.invoked without ${PLUGIN_NAME} skills/plan/SKILL.md` : 'no skill.invoked event',
|
|
506
|
+
...(hit ? {} : { evidence: excerpt(invoked.map((e) => JSON.stringify({ name: e.data?.name, path: e.data?.path, pluginName: e.data?.pluginName })).join('\n') || assistantText(run.events)) }),
|
|
507
|
+
}];
|
|
508
|
+
}
|
|
509
|
+
function evaluateDelegate(run) {
|
|
510
|
+
const named = (type) => run.events.filter((e) => e.type === type && e.data?.agentName === DELEGATE_AGENT);
|
|
511
|
+
const selected = named('subagent.selected')[0];
|
|
512
|
+
const completed = named('subagent.completed').find((e) => e.data?.cancelled !== true);
|
|
513
|
+
const subagentEvents = run.events.filter((e) => typeof e.type === 'string' && e.type.startsWith('subagent.'))
|
|
514
|
+
.map((e) => `${e.type} ${str(e.data?.agentName)}`).join('\n');
|
|
515
|
+
return [
|
|
516
|
+
{
|
|
517
|
+
id: 'scn.delegate.selected',
|
|
518
|
+
ok: !!selected,
|
|
519
|
+
detail: selected ? `subagent.selected ${DELEGATE_AGENT}` : `no subagent.selected for ${DELEGATE_AGENT}`,
|
|
520
|
+
...(selected ? {} : { evidence: excerpt(subagentEvents || assistantText(run.events)) }),
|
|
521
|
+
},
|
|
522
|
+
hookCheck('scn.delegate.hooks', run.events, ['subagentStart', 'subagentStop']),
|
|
523
|
+
{
|
|
524
|
+
id: 'scn.delegate.completed',
|
|
525
|
+
ok: !!completed,
|
|
526
|
+
detail: completed
|
|
527
|
+
? `subagent.completed ${DELEGATE_AGENT} (${num(completed.data?.totalToolCalls)} tool call(s), ${num(completed.data?.durationMs)} ms)`
|
|
528
|
+
: `no non-cancelled subagent.completed for ${DELEGATE_AGENT}`,
|
|
529
|
+
...(completed || !subagentEvents ? {} : { evidence: excerpt(subagentEvents) }),
|
|
530
|
+
},
|
|
531
|
+
];
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Hook stderr lines from our adapter (`[omg-hook]`) or hook runner
|
|
535
|
+
* (`[run.cjs]`, e.g. a timeout). A hook that exits 0 has its stderr logged as
|
|
536
|
+
* `[hook stderr]` lines; one that exits non-zero (fail-closed, e.g. 124) as a
|
|
537
|
+
* multi-line `[ERROR] ... execution failed` entry whose stderr starts with
|
|
538
|
+
* `Stderr: ` (Copilot CLI 1.0.91). Both forms count.
|
|
539
|
+
*/
|
|
540
|
+
export function adapterErrorLines(logText) {
|
|
541
|
+
return logText.split(/\r?\n/)
|
|
542
|
+
.filter((l) => (l.includes('[hook stderr]') || /^\s*Stderr:\s/.test(l)) && (l.includes('[omg-hook]') || l.includes('[run.cjs]')))
|
|
543
|
+
.filter((l) => !BENIGN_ADAPTER_NOTES.some((re) => re.test(l)))
|
|
544
|
+
.map((l) => l.trim());
|
|
545
|
+
}
|
|
546
|
+
/** Hook script names mentioned in adapter error lines (e.g. wiki-session-end.mjs). */
|
|
547
|
+
export function hookScriptNames(lines) {
|
|
548
|
+
const names = new Set();
|
|
549
|
+
for (const line of lines)
|
|
550
|
+
for (const m of line.matchAll(/([\w.-]+\.(?:mjs|cjs|js))\b/g))
|
|
551
|
+
if (m[1] !== 'run.cjs')
|
|
552
|
+
names.add(m[1]);
|
|
553
|
+
return [...names];
|
|
554
|
+
}
|
|
555
|
+
function evaluateCommon(run, cost) {
|
|
556
|
+
const p = `scn.${run.name}`;
|
|
557
|
+
const idleEvent = [...run.events].reverse().find((e) => e.type === 'session.idle');
|
|
558
|
+
const aborted = idleEvent?.data?.aborted === true || run.events.some((e) => e.type === 'abort');
|
|
559
|
+
const exitOk = run.idle && !run.timedOut && !aborted && !run.error && !run.capped;
|
|
560
|
+
const lines = run.logText === null ? [] : adapterErrorLines(run.logText);
|
|
561
|
+
const scripts = hookScriptNames(lines);
|
|
562
|
+
const budget = run.budget ?? run.maxCredits;
|
|
563
|
+
const overBudget = !!run.capped || cost.credits > budget;
|
|
564
|
+
return [
|
|
565
|
+
{
|
|
566
|
+
id: `${p}.exit`,
|
|
567
|
+
ok: exitOk,
|
|
568
|
+
detail: run.error ? `session failed: ${excerpt(run.error, 200)}`
|
|
569
|
+
: run.capped ? `aborted: credit cap reached (${budget.toFixed(3)} of maxCredits ${run.maxCredits} left at start)`
|
|
570
|
+
: run.timedOut ? `no session.idle within ${run.timeoutMs} ms (aborted)`
|
|
571
|
+
: aborted ? 'session.idle after an abort'
|
|
572
|
+
: run.idle ? 'session idle, not aborted' : 'session never went idle',
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
id: `${p}.adapter_errors`,
|
|
576
|
+
ok: run.logText !== null && lines.length === 0,
|
|
577
|
+
detail: run.logText === null ? 'no debug log under <home>/logs (hook stderr is only logged at debug level)'
|
|
578
|
+
: lines.length === 0 ? 'no [omg-hook]/[run.cjs] hook stderr lines'
|
|
579
|
+
: `${lines.length} hook stderr line(s) from ${scripts.join(', ') || 'unknown script'}`,
|
|
580
|
+
...(lines.length ? { evidence: excerpt(lines.join('\n')) } : {}),
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
id: `${p}.cost`,
|
|
584
|
+
ok: !overBudget,
|
|
585
|
+
detail: `${cost.premiumRequests} premium request(s), ${cost.credits.toFixed(3)} AI credits (${cost.source}); model ${run.model}`
|
|
586
|
+
+ (overBudget ? `; over the credit cap (${budget.toFixed(3)} left of maxCredits ${run.maxCredits})` : ''),
|
|
587
|
+
},
|
|
588
|
+
];
|
|
589
|
+
}
|
|
590
|
+
export function evaluateScenario(run) {
|
|
591
|
+
const own = {
|
|
592
|
+
smoke: evaluateSmoke,
|
|
593
|
+
guardrail: evaluateGuardrail,
|
|
594
|
+
skill: evaluateSkill,
|
|
595
|
+
delegate: evaluateDelegate,
|
|
596
|
+
chain: evaluateChain,
|
|
597
|
+
}[run.name](run);
|
|
598
|
+
// A chain spans two link sessions, each with its own session.shutdown totals.
|
|
599
|
+
const cost = run.name === 'chain' && run.chain ? chainCost(run.chain) : scenarioCost(run.events);
|
|
600
|
+
return { checks: [...own, ...evaluateCommon(run, cost)], cost: { premiumRequests: cost.premiumRequests, credits: cost.credits } };
|
|
601
|
+
}
|
|
602
|
+
// ---------------------------------------------------------------------------
|
|
603
|
+
// Model choice
|
|
604
|
+
// ---------------------------------------------------------------------------
|
|
605
|
+
const CHEAP_MODEL = /(mini|flash|haiku|nano|lite|luna)/i;
|
|
606
|
+
/**
|
|
607
|
+
* An explicit request wins. Otherwise the first listed non-`auto` model whose
|
|
608
|
+
* id looks cheap; when the runtime lists only `auto` (Copilot CLI 1.0.91 for
|
|
609
|
+
* individual accounts) the session config omits `model` and the runtime routes.
|
|
610
|
+
*/
|
|
611
|
+
export function chooseModel(listed, requested) {
|
|
612
|
+
const ids = [...new Set(listed)];
|
|
613
|
+
if (requested)
|
|
614
|
+
return { model: requested, label: requested, note: ids.includes(requested) ? 'requested, listed' : `requested, not in models.list (${ids.join(', ') || 'none'})` };
|
|
615
|
+
const cheap = ids.find((id) => id !== 'auto' && CHEAP_MODEL.test(id));
|
|
616
|
+
if (cheap)
|
|
617
|
+
return { model: cheap, label: cheap, note: `cheapest-looking of ${ids.length} listed` };
|
|
618
|
+
return { label: 'auto', note: `models.list offers ${ids.join(', ') || 'nothing'}; no cheap explicit model, runtime auto-routes` };
|
|
619
|
+
}
|
|
620
|
+
//# sourceMappingURL=copilot-sdk-scenarios.js.map
|