@moreih29/nexus-core 0.12.0 → 0.14.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/README.md +76 -57
- package/assets/agents/architect/body.ko.md +177 -0
- package/{agents → assets/agents}/architect/body.md +16 -0
- package/assets/agents/designer/body.ko.md +125 -0
- package/{agents → assets/agents}/designer/body.md +16 -0
- package/assets/agents/engineer/body.ko.md +106 -0
- package/{agents → assets/agents}/engineer/body.md +14 -0
- package/assets/agents/lead/body.ko.md +70 -0
- package/assets/agents/lead/body.md +70 -0
- package/assets/agents/postdoc/body.ko.md +122 -0
- package/{agents → assets/agents}/postdoc/body.md +16 -0
- package/assets/agents/researcher/body.ko.md +137 -0
- package/{agents → assets/agents}/researcher/body.md +15 -0
- package/assets/agents/reviewer/body.ko.md +138 -0
- package/{agents → assets/agents}/reviewer/body.md +15 -0
- package/assets/agents/strategist/body.ko.md +116 -0
- package/{agents → assets/agents}/strategist/body.md +16 -0
- package/assets/agents/tester/body.ko.md +195 -0
- package/{agents → assets/agents}/tester/body.md +15 -0
- package/assets/agents/writer/body.ko.md +122 -0
- package/{agents → assets/agents}/writer/body.md +14 -0
- package/assets/capability-matrix.yml +198 -0
- package/assets/hooks/agent-bootstrap/handler.test.ts +368 -0
- package/assets/hooks/agent-bootstrap/handler.ts +119 -0
- package/assets/hooks/agent-bootstrap/meta.yml +10 -0
- package/assets/hooks/agent-finalize/handler.test.ts +368 -0
- package/assets/hooks/agent-finalize/handler.ts +76 -0
- package/assets/hooks/agent-finalize/meta.yml +10 -0
- package/assets/hooks/capability-matrix.yml +313 -0
- package/assets/hooks/post-tool-telemetry/handler.test.ts +302 -0
- package/assets/hooks/post-tool-telemetry/handler.ts +49 -0
- package/assets/hooks/post-tool-telemetry/meta.yml +11 -0
- package/assets/hooks/prompt-router/handler.test.ts +801 -0
- package/assets/hooks/prompt-router/handler.ts +261 -0
- package/assets/hooks/prompt-router/meta.yml +11 -0
- package/assets/hooks/session-init/handler.test.ts +274 -0
- package/assets/hooks/session-init/handler.ts +30 -0
- package/assets/hooks/session-init/meta.yml +9 -0
- package/assets/lsp-servers.json +55 -0
- package/assets/schema/lsp-servers.schema.json +67 -0
- package/assets/skills/nx-init/body.ko.md +197 -0
- package/{skills → assets/skills}/nx-init/body.md +11 -0
- package/assets/skills/nx-plan/body.ko.md +361 -0
- package/{skills → assets/skills}/nx-plan/body.md +13 -0
- package/assets/skills/nx-run/body.ko.md +161 -0
- package/{skills → assets/skills}/nx-run/body.md +11 -0
- package/assets/skills/nx-sync/body.ko.md +92 -0
- package/{skills → assets/skills}/nx-sync/body.md +10 -0
- package/assets/tools/tool-name-map.yml +353 -0
- package/dist/assets/hooks/agent-bootstrap/handler.d.ts +4 -0
- package/dist/assets/hooks/agent-bootstrap/handler.d.ts.map +1 -0
- package/dist/assets/hooks/agent-bootstrap/handler.js +100 -0
- package/dist/assets/hooks/agent-bootstrap/handler.js.map +1 -0
- package/dist/assets/hooks/agent-finalize/handler.d.ts +4 -0
- package/dist/assets/hooks/agent-finalize/handler.d.ts.map +1 -0
- package/dist/assets/hooks/agent-finalize/handler.js +63 -0
- package/dist/assets/hooks/agent-finalize/handler.js.map +1 -0
- package/dist/assets/hooks/post-tool-telemetry/handler.d.ts +4 -0
- package/dist/assets/hooks/post-tool-telemetry/handler.d.ts.map +1 -0
- package/dist/assets/hooks/post-tool-telemetry/handler.js +40 -0
- package/dist/assets/hooks/post-tool-telemetry/handler.js.map +1 -0
- package/dist/assets/hooks/prompt-router/handler.d.ts +4 -0
- package/dist/assets/hooks/prompt-router/handler.d.ts.map +1 -0
- package/dist/assets/hooks/prompt-router/handler.js +204 -0
- package/dist/assets/hooks/prompt-router/handler.js.map +1 -0
- package/dist/assets/hooks/session-init/handler.d.ts +4 -0
- package/dist/assets/hooks/session-init/handler.d.ts.map +1 -0
- package/dist/assets/hooks/session-init/handler.js +23 -0
- package/dist/assets/hooks/session-init/handler.js.map +1 -0
- package/dist/hooks/agent-bootstrap.js +105 -0
- package/dist/hooks/agent-finalize.js +164 -0
- package/dist/hooks/post-tool-telemetry.js +55 -0
- package/dist/hooks/prompt-router.js +7300 -0
- package/dist/hooks/session-init.js +21 -0
- package/dist/manifests/claude-hooks.json +52 -0
- package/dist/manifests/codex-hooks.json +28 -0
- package/dist/manifests/opencode-manifest.json +44 -0
- package/dist/manifests/portability-report.json +87 -0
- package/dist/scripts/build-agents.d.ts +157 -0
- package/dist/scripts/build-agents.d.ts.map +1 -0
- package/dist/scripts/build-agents.js +737 -0
- package/dist/scripts/build-agents.js.map +1 -0
- package/dist/scripts/build-hooks.d.ts +16 -0
- package/dist/scripts/build-hooks.d.ts.map +1 -0
- package/dist/scripts/build-hooks.js +388 -0
- package/dist/scripts/build-hooks.js.map +1 -0
- package/dist/scripts/cli.d.ts +54 -0
- package/dist/scripts/cli.d.ts.map +1 -0
- package/dist/scripts/cli.js +467 -0
- package/dist/scripts/cli.js.map +1 -0
- package/dist/src/hooks/opencode-mount.d.ts +35 -0
- package/dist/src/hooks/opencode-mount.d.ts.map +1 -0
- package/dist/src/hooks/opencode-mount.js +352 -0
- package/dist/src/hooks/opencode-mount.js.map +1 -0
- package/dist/src/hooks/runtime.d.ts +37 -0
- package/dist/src/hooks/runtime.d.ts.map +1 -0
- package/dist/src/hooks/runtime.js +274 -0
- package/dist/src/hooks/runtime.js.map +1 -0
- package/dist/src/hooks/types.d.ts +196 -0
- package/dist/src/hooks/types.d.ts.map +1 -0
- package/dist/src/hooks/types.js +85 -0
- package/dist/src/hooks/types.js.map +1 -0
- package/dist/src/lsp/cache.d.ts +9 -0
- package/dist/src/lsp/cache.d.ts.map +1 -0
- package/dist/src/lsp/cache.js +216 -0
- package/dist/src/lsp/cache.js.map +1 -0
- package/dist/src/lsp/client.d.ts +24 -0
- package/dist/src/lsp/client.d.ts.map +1 -0
- package/dist/src/lsp/client.js +166 -0
- package/dist/src/lsp/client.js.map +1 -0
- package/dist/src/lsp/detect.d.ts +77 -0
- package/dist/src/lsp/detect.d.ts.map +1 -0
- package/dist/src/lsp/detect.js +116 -0
- package/dist/src/lsp/detect.js.map +1 -0
- package/dist/src/mcp/server.d.ts +5 -0
- package/dist/src/mcp/server.d.ts.map +1 -0
- package/dist/src/mcp/server.js +34 -0
- package/dist/src/mcp/server.js.map +1 -0
- package/dist/src/mcp/tools/artifact.d.ts +4 -0
- package/dist/src/mcp/tools/artifact.d.ts.map +1 -0
- package/dist/src/mcp/tools/artifact.js +36 -0
- package/dist/src/mcp/tools/artifact.js.map +1 -0
- package/dist/src/mcp/tools/history.d.ts +3 -0
- package/dist/src/mcp/tools/history.d.ts.map +1 -0
- package/dist/src/mcp/tools/history.js +29 -0
- package/dist/src/mcp/tools/history.js.map +1 -0
- package/dist/src/mcp/tools/lsp.d.ts +13 -0
- package/dist/src/mcp/tools/lsp.d.ts.map +1 -0
- package/dist/src/mcp/tools/lsp.js +225 -0
- package/dist/src/mcp/tools/lsp.js.map +1 -0
- package/dist/src/mcp/tools/plan.d.ts +3 -0
- package/dist/src/mcp/tools/plan.d.ts.map +1 -0
- package/dist/src/mcp/tools/plan.js +317 -0
- package/dist/src/mcp/tools/plan.js.map +1 -0
- package/dist/src/mcp/tools/task.d.ts +3 -0
- package/dist/src/mcp/tools/task.d.ts.map +1 -0
- package/dist/src/mcp/tools/task.js +252 -0
- package/dist/src/mcp/tools/task.js.map +1 -0
- package/dist/src/shared/invocations.d.ts +74 -0
- package/dist/src/shared/invocations.d.ts.map +1 -0
- package/dist/src/shared/invocations.js +247 -0
- package/dist/src/shared/invocations.js.map +1 -0
- package/dist/src/shared/json-store.d.ts +37 -0
- package/dist/src/shared/json-store.d.ts.map +1 -0
- package/dist/src/shared/json-store.js +163 -0
- package/dist/src/shared/json-store.js.map +1 -0
- package/dist/src/shared/mcp-utils.d.ts +3 -0
- package/dist/src/shared/mcp-utils.d.ts.map +1 -0
- package/dist/src/shared/mcp-utils.js +6 -0
- package/dist/src/shared/mcp-utils.js.map +1 -0
- package/dist/src/shared/paths.d.ts +21 -0
- package/dist/src/shared/paths.d.ts.map +1 -0
- package/dist/src/shared/paths.js +81 -0
- package/dist/src/shared/paths.js.map +1 -0
- package/dist/src/shared/tool-log.d.ts +8 -0
- package/dist/src/shared/tool-log.d.ts.map +1 -0
- package/dist/src/shared/tool-log.js +22 -0
- package/dist/src/shared/tool-log.js.map +1 -0
- package/dist/src/types/state.d.ts +862 -0
- package/dist/src/types/state.d.ts.map +1 -0
- package/dist/src/types/state.js +66 -0
- package/dist/src/types/state.js.map +1 -0
- package/docs/consuming/codex-lead-merge.md +106 -0
- package/docs/plugin-guide.md +396 -0
- package/docs/plugin-template/claude/.github/workflows/build.yml +60 -0
- package/docs/plugin-template/claude/README.md +110 -0
- package/docs/plugin-template/claude/package.json +16 -0
- package/docs/plugin-template/codex/.github/workflows/build.yml +51 -0
- package/docs/plugin-template/codex/README.md +147 -0
- package/docs/plugin-template/codex/package.json +17 -0
- package/docs/plugin-template/opencode/.github/workflows/build.yml +61 -0
- package/docs/plugin-template/opencode/README.md +121 -0
- package/docs/plugin-template/opencode/package.json +25 -0
- package/package.json +36 -28
- package/agents/architect/meta.yml +0 -13
- package/agents/designer/meta.yml +0 -13
- package/agents/engineer/meta.yml +0 -11
- package/agents/postdoc/meta.yml +0 -13
- package/agents/researcher/meta.yml +0 -12
- package/agents/reviewer/meta.yml +0 -12
- package/agents/strategist/meta.yml +0 -13
- package/agents/tester/meta.yml +0 -12
- package/agents/writer/meta.yml +0 -11
- package/conformance/README.md +0 -311
- package/conformance/examples/plan.extension.schema.example.json +0 -25
- package/conformance/lifecycle/README.md +0 -48
- package/conformance/lifecycle/agent-complete.json +0 -44
- package/conformance/lifecycle/agent-resume.json +0 -43
- package/conformance/lifecycle/agent-spawn.json +0 -36
- package/conformance/lifecycle/memory-access-record.json +0 -27
- package/conformance/lifecycle/session-end.json +0 -48
- package/conformance/scenarios/full-plan-cycle.json +0 -147
- package/conformance/scenarios/task-deps-ordering.json +0 -95
- package/conformance/schema/fixture.schema.json +0 -354
- package/conformance/state-schemas/agent-tracker.schema.json +0 -63
- package/conformance/state-schemas/history.schema.json +0 -134
- package/conformance/state-schemas/memory-access.schema.json +0 -36
- package/conformance/state-schemas/plan.schema.json +0 -77
- package/conformance/state-schemas/tasks.schema.json +0 -98
- package/conformance/tools/artifact-write.json +0 -97
- package/conformance/tools/context.json +0 -172
- package/conformance/tools/history-search.json +0 -219
- package/conformance/tools/plan-decide.json +0 -139
- package/conformance/tools/plan-start.json +0 -81
- package/conformance/tools/plan-status.json +0 -127
- package/conformance/tools/plan-update.json +0 -341
- package/conformance/tools/task-add.json +0 -156
- package/conformance/tools/task-close.json +0 -161
- package/conformance/tools/task-list.json +0 -177
- package/conformance/tools/task-update.json +0 -167
- package/docs/behavioral-contracts.md +0 -145
- package/docs/consumer-implementation-guide.md +0 -840
- package/docs/memory-lifecycle-contract.md +0 -119
- package/docs/nexus-layout.md +0 -224
- package/docs/nexus-outputs-contract.md +0 -344
- package/docs/nexus-state-overview.md +0 -170
- package/docs/nexus-tools-contract.md +0 -438
- package/manifest.json +0 -448
- package/schema/README.md +0 -69
- package/schema/agent.schema.json +0 -23
- package/schema/common.schema.json +0 -17
- package/schema/manifest.schema.json +0 -78
- package/schema/memory-policy.schema.json +0 -98
- package/schema/skill.schema.json +0 -54
- package/schema/task-exceptions.schema.json +0 -40
- package/schema/vocabulary.schema.json +0 -167
- package/scripts/.gitkeep +0 -0
- package/scripts/conformance-coverage.ts +0 -466
- package/scripts/import-from-claude-nexus.ts +0 -403
- package/scripts/lib/frontmatter.ts +0 -71
- package/scripts/lib/lint.ts +0 -348
- package/scripts/lib/structure.ts +0 -159
- package/scripts/lib/validate.ts +0 -796
- package/scripts/validate.ts +0 -90
- package/skills/nx-init/meta.yml +0 -8
- package/skills/nx-plan/meta.yml +0 -10
- package/skills/nx-run/meta.yml +0 -8
- package/skills/nx-sync/meta.yml +0 -7
- package/vocabulary/capabilities.yml +0 -65
- package/vocabulary/categories.yml +0 -11
- package/vocabulary/invocations.yml +0 -147
- package/vocabulary/memory_policy.yml +0 -88
- package/vocabulary/resume-tiers.yml +0 -11
- package/vocabulary/tags.yml +0 -60
- package/vocabulary/task-exceptions.yml +0 -29
|
@@ -0,0 +1,801 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prompt-router/handler.test.ts
|
|
3
|
+
*
|
|
4
|
+
* Verifies all 11 tag handlers × state combinations described in Task #18.
|
|
5
|
+
* Uses bun:test. Fixtures placed in a tmp directory per describe block.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { test, expect, describe, beforeAll, afterAll, beforeEach, afterEach } from "bun:test";
|
|
9
|
+
import * as fs from "node:fs";
|
|
10
|
+
import * as os from "node:os";
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
import type { NexusHookInput, NexusHookOutput } from "../../../src/hooks/types.js";
|
|
13
|
+
import handler from "./handler.js";
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Fixture helpers
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
/** Root tmp dir shared across all tests in this module. */
|
|
20
|
+
let ROOT: string;
|
|
21
|
+
const SESSION_ID = "test-session-001";
|
|
22
|
+
|
|
23
|
+
function sessionDir(): string {
|
|
24
|
+
return path.join(ROOT, ".nexus", "state", SESSION_ID);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function planPath(): string {
|
|
28
|
+
return path.join(sessionDir(), "plan.json");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function tasksPath(): string {
|
|
32
|
+
return path.join(sessionDir(), "tasks.json");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function ensureSessionDir(): void {
|
|
36
|
+
fs.mkdirSync(sessionDir(), { recursive: true });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function writePlan(topic = "test-topic", pendingCount = 2): void {
|
|
40
|
+
ensureSessionDir();
|
|
41
|
+
const issues = [
|
|
42
|
+
...Array.from({ length: pendingCount }, (_, i) => ({
|
|
43
|
+
id: `I${i + 1}`,
|
|
44
|
+
status: "pending",
|
|
45
|
+
})),
|
|
46
|
+
{ id: "I99", status: "completed" },
|
|
47
|
+
];
|
|
48
|
+
fs.writeFileSync(planPath(), JSON.stringify({ topic, issues }));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function writeTasks(pendingCount = 3): void {
|
|
52
|
+
ensureSessionDir();
|
|
53
|
+
const tasks = [
|
|
54
|
+
...Array.from({ length: pendingCount }, (_, i) => ({
|
|
55
|
+
id: `T${i + 1}`,
|
|
56
|
+
status: "open",
|
|
57
|
+
})),
|
|
58
|
+
{ id: "T99", status: "completed" },
|
|
59
|
+
];
|
|
60
|
+
fs.writeFileSync(tasksPath(), JSON.stringify({ tasks }));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function removePlan(): void {
|
|
64
|
+
try {
|
|
65
|
+
fs.unlinkSync(planPath());
|
|
66
|
+
} catch {
|
|
67
|
+
// already absent
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function removeTasks(): void {
|
|
72
|
+
try {
|
|
73
|
+
fs.unlinkSync(tasksPath());
|
|
74
|
+
} catch {
|
|
75
|
+
// already absent
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function makeInput(prompt: string): NexusHookInput {
|
|
80
|
+
return {
|
|
81
|
+
hook_event_name: "UserPromptSubmit",
|
|
82
|
+
session_id: SESSION_ID,
|
|
83
|
+
cwd: ROOT,
|
|
84
|
+
prompt,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Assert output has additional_context and does NOT block. */
|
|
89
|
+
function assertNotice(
|
|
90
|
+
result: NexusHookOutput | void,
|
|
91
|
+
expected: RegExp | string
|
|
92
|
+
): void {
|
|
93
|
+
expect(result).not.toBeUndefined();
|
|
94
|
+
const r = result as NexusHookOutput;
|
|
95
|
+
expect(r.decision).toBeUndefined();
|
|
96
|
+
expect(r.additional_context).toMatch(expected);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Assert output is a block decision. */
|
|
100
|
+
function assertBlock(
|
|
101
|
+
result: NexusHookOutput | void,
|
|
102
|
+
reasonPattern?: RegExp | string
|
|
103
|
+
): void {
|
|
104
|
+
expect(result).not.toBeUndefined();
|
|
105
|
+
const r = result as NexusHookOutput;
|
|
106
|
+
expect(r.decision).toBe("block");
|
|
107
|
+
if (reasonPattern) {
|
|
108
|
+
expect(r.block_reason).toMatch(reasonPattern);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// Setup / Teardown
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
beforeAll(() => {
|
|
117
|
+
ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "nexus-prompt-router-"));
|
|
118
|
+
// Create mock agent and skill directories so loadValidRuleTargets works.
|
|
119
|
+
for (const name of ["architect", "engineer", "reviewer"]) {
|
|
120
|
+
fs.mkdirSync(path.join(ROOT, "assets", "agents", name), { recursive: true });
|
|
121
|
+
}
|
|
122
|
+
for (const name of ["nx-plan", "nx-run", "nx-sync", "nx-init"]) {
|
|
123
|
+
fs.mkdirSync(path.join(ROOT, "assets", "skills", name), { recursive: true });
|
|
124
|
+
}
|
|
125
|
+
// Ensure session dir exists (plan/tasks written per-test).
|
|
126
|
+
ensureSessionDir();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
afterAll(() => {
|
|
130
|
+
fs.rmSync(ROOT, { recursive: true, force: true });
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// Scenario 1 — [plan] alone → nx-plan skill, no "auto" args
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
describe("scenario 1 — [plan] alone", () => {
|
|
138
|
+
test("additional_context mentions nx-plan", async () => {
|
|
139
|
+
removePlan();
|
|
140
|
+
removeTasks();
|
|
141
|
+
const result = await handler(makeInput("[plan] structured planning"));
|
|
142
|
+
assertNotice(result, /nx-plan/);
|
|
143
|
+
assertNotice(result, /<system-notice>/);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("does NOT mention 'auto'", async () => {
|
|
147
|
+
const result = await handler(makeInput("[plan] structured planning")) as NexusHookOutput;
|
|
148
|
+
expect(result?.additional_context).not.toMatch(/auto/);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
// Scenario 2 — [plan:auto] → nx-plan + "auto", prioritised over [plan]
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
describe("scenario 2 — [plan:auto] priority over [plan]", () => {
|
|
157
|
+
test('[plan:auto] context includes "auto"', async () => {
|
|
158
|
+
removePlan();
|
|
159
|
+
removeTasks();
|
|
160
|
+
const result = await handler(makeInput("[plan:auto] auto planning"));
|
|
161
|
+
assertNotice(result, /auto/);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("[plan:auto] is dispatched even when [plan] also appears in prompt", async () => {
|
|
165
|
+
// [plan:auto] appears first → base "plan" is locked → [plan] variant skipped
|
|
166
|
+
const result = await handler(
|
|
167
|
+
makeInput("[plan:auto] [plan] do not double-dispatch")
|
|
168
|
+
) as NexusHookOutput;
|
|
169
|
+
// Should mention auto (from plan:auto) exactly once
|
|
170
|
+
const ctx = result?.additional_context ?? "";
|
|
171
|
+
expect((ctx.match(/plan:auto/g) ?? []).length).toBe(1);
|
|
172
|
+
// Must NOT also contain a plain [plan] notice (no second system-notice for plain plan)
|
|
173
|
+
expect((ctx.match(/<system-notice>/g) ?? []).length).toBe(1);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// Scenario 3 — [run] + tasks.json present → nx-run
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
describe("scenario 3 — [run] with tasks.json present", () => {
|
|
182
|
+
test("invokes nx-run skill", async () => {
|
|
183
|
+
removePlan();
|
|
184
|
+
writeTasks();
|
|
185
|
+
const result = await handler(makeInput("[run] execute tasks"));
|
|
186
|
+
assertNotice(result, /nx-run/);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// Scenario 4 — [run] + tasks.json absent → nx-plan auto guidance
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
describe("scenario 4 — [run] without tasks.json", () => {
|
|
195
|
+
test("recommends nx-plan auto first", async () => {
|
|
196
|
+
removePlan();
|
|
197
|
+
removeTasks();
|
|
198
|
+
const result = await handler(makeInput("[run] go"));
|
|
199
|
+
assertNotice(result, /nx-plan/);
|
|
200
|
+
assertNotice(result, /auto/);
|
|
201
|
+
// Must NOT directly invoke nx-run
|
|
202
|
+
const r = result as NexusHookOutput;
|
|
203
|
+
expect(r.additional_context).not.toMatch(/Invoke `nx-run`/);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
// Scenario 5 — [d] + plan.json present → nx_plan_decide guidance
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
describe("scenario 5 — [d] with active plan", () => {
|
|
212
|
+
test("guides to nx_plan_decide", async () => {
|
|
213
|
+
writePlan();
|
|
214
|
+
removeTasks();
|
|
215
|
+
const result = await handler(makeInput("[d] decide now"));
|
|
216
|
+
assertNotice(result, /nx_plan_decide/);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// Scenario 6 — [d] + plan.json absent → decision:block
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
describe("scenario 6 — [d] without plan — decision:block", () => {
|
|
225
|
+
test("blocks with reason about nx-plan", async () => {
|
|
226
|
+
removePlan();
|
|
227
|
+
removeTasks();
|
|
228
|
+
const result = await handler(makeInput("[d] decide"));
|
|
229
|
+
assertBlock(result, /nx-plan/);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("block_reason is English text", async () => {
|
|
233
|
+
removePlan();
|
|
234
|
+
removeTasks();
|
|
235
|
+
const result = await handler(makeInput("[d] decide")) as NexusHookOutput;
|
|
236
|
+
// Must be a non-empty English string (ASCII)
|
|
237
|
+
expect(result.block_reason).toBeTruthy();
|
|
238
|
+
expect(result.block_reason).toMatch(/[a-zA-Z]/);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
// Scenario 7 — [m] → memory notice, distinct from [m:gc]
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
|
|
246
|
+
describe("scenario 7 — [m] memory save", () => {
|
|
247
|
+
test("mentions .nexus/memory/ save guidance", async () => {
|
|
248
|
+
removePlan();
|
|
249
|
+
removeTasks();
|
|
250
|
+
const result = await handler(makeInput("[m] remember this"));
|
|
251
|
+
assertNotice(result, /\.nexus\/memory\//);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("[m] notice does NOT mention 'consolidate' (that's [m:gc])", async () => {
|
|
255
|
+
const result = await handler(makeInput("[m] remember this")) as NexusHookOutput;
|
|
256
|
+
expect(result.additional_context).not.toMatch(/consolidate/);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
// Scenario 8 — [m:gc] → gc guidance, distinct from [m]
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
describe("scenario 8 — [m:gc] garbage-collect", () => {
|
|
265
|
+
test("mentions consolidate or stale entry cleanup", async () => {
|
|
266
|
+
removePlan();
|
|
267
|
+
removeTasks();
|
|
268
|
+
const result = await handler(makeInput("[m:gc] clean memory"));
|
|
269
|
+
assertNotice(result, /consolidate|stale/i);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("[m:gc] notice does NOT reference prefix/pattern (that's [m])", async () => {
|
|
273
|
+
const result = await handler(makeInput("[m:gc] clean memory")) as NexusHookOutput;
|
|
274
|
+
// [m] notice mentions "Prefix:" — [m:gc] should not
|
|
275
|
+
expect(result.additional_context).not.toMatch(/Prefix:/);
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
// Scenario 9 — [rule] alone → valid targets listed
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
describe("scenario 9 — [rule] without name", () => {
|
|
284
|
+
test("lists valid targets from agents + skills dirs", async () => {
|
|
285
|
+
removePlan();
|
|
286
|
+
removeTasks();
|
|
287
|
+
const result = await handler(makeInput("[rule] add a new rule"));
|
|
288
|
+
// Our fixtures: architect, engineer, reviewer, nx-plan, nx-run, nx-sync, nx-init
|
|
289
|
+
assertNotice(result, /architect/);
|
|
290
|
+
assertNotice(result, /nx-plan/);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
// Scenario 10 — [rule:valid_name] → update guidance
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
|
|
298
|
+
describe("scenario 10 — [rule:architect] valid target", () => {
|
|
299
|
+
test("guides to update .nexus/rules/architect.md", async () => {
|
|
300
|
+
removePlan();
|
|
301
|
+
removeTasks();
|
|
302
|
+
const result = await handler(makeInput("[rule:architect] new rule content"));
|
|
303
|
+
assertNotice(result, /\.nexus\/rules\/architect\.md/);
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
// Scenario 11 — [rule:invalid_name] → decision:block
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
|
|
311
|
+
describe("scenario 11 — [rule:nonexistent] invalid target — decision:block", () => {
|
|
312
|
+
test("blocks with reason listing valid targets", async () => {
|
|
313
|
+
removePlan();
|
|
314
|
+
removeTasks();
|
|
315
|
+
const result = await handler(makeInput("[rule:nonexistent] bad rule"));
|
|
316
|
+
assertBlock(result, /nonexistent/);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("block_reason mentions valid alternatives", async () => {
|
|
320
|
+
const result = await handler(
|
|
321
|
+
makeInput("[rule:nonexistent] bad rule")
|
|
322
|
+
) as NexusHookOutput;
|
|
323
|
+
// Should mention at least one valid target
|
|
324
|
+
expect(result.block_reason).toMatch(/architect|engineer|nx-plan/);
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// ---------------------------------------------------------------------------
|
|
329
|
+
// Scenario 12 — [sync] → nx-sync guidance
|
|
330
|
+
// ---------------------------------------------------------------------------
|
|
331
|
+
|
|
332
|
+
describe("scenario 12 — [sync]", () => {
|
|
333
|
+
test("invokes nx-sync skill guidance", async () => {
|
|
334
|
+
removePlan();
|
|
335
|
+
removeTasks();
|
|
336
|
+
const result = await handler(makeInput("[sync] synchronize context"));
|
|
337
|
+
assertNotice(result, /nx-sync/);
|
|
338
|
+
assertNotice(result, /\.nexus\/context\//);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// ---------------------------------------------------------------------------
|
|
343
|
+
// Scenario 13 — [init] vs [init:reset] distinction
|
|
344
|
+
// ---------------------------------------------------------------------------
|
|
345
|
+
|
|
346
|
+
describe("scenario 13 — [init] and [init:reset] distinction", () => {
|
|
347
|
+
test("[init] mentions nx-init for onboarding", async () => {
|
|
348
|
+
removePlan();
|
|
349
|
+
removeTasks();
|
|
350
|
+
const result = await handler(makeInput("[init] project setup"));
|
|
351
|
+
assertNotice(result, /nx-init/);
|
|
352
|
+
// Must NOT say "reset"
|
|
353
|
+
const r = result as NexusHookOutput;
|
|
354
|
+
expect(r.additional_context).not.toMatch(/reset/);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test("[init:reset] includes 'reset' args", async () => {
|
|
358
|
+
removePlan();
|
|
359
|
+
removeTasks();
|
|
360
|
+
const result = await handler(makeInput("[init:reset] full reset"));
|
|
361
|
+
assertNotice(result, /reset/);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("[init:reset] is dispatched instead of [init] when both appear", async () => {
|
|
365
|
+
// [init:reset] appears first in TAG_PATTERNS → base "init" seen → [init] skipped
|
|
366
|
+
const result = await handler(
|
|
367
|
+
makeInput("[init:reset] [init] should not double")
|
|
368
|
+
) as NexusHookOutput;
|
|
369
|
+
const ctx = result?.additional_context ?? "";
|
|
370
|
+
expect((ctx.match(/<system-notice>/g) ?? []).length).toBe(1);
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// ---------------------------------------------------------------------------
|
|
375
|
+
// Scenario 14 — multiple tags in single prompt ([plan] [m])
|
|
376
|
+
// ---------------------------------------------------------------------------
|
|
377
|
+
|
|
378
|
+
describe("scenario 14 — multiple tags processed together", () => {
|
|
379
|
+
test("[plan] and [m] both produce notices", async () => {
|
|
380
|
+
removePlan();
|
|
381
|
+
removeTasks();
|
|
382
|
+
const result = await handler(
|
|
383
|
+
makeInput("[plan] create plan [m] remember this lesson")
|
|
384
|
+
) as NexusHookOutput;
|
|
385
|
+
expect(result).not.toBeUndefined();
|
|
386
|
+
expect(result.decision).toBeUndefined();
|
|
387
|
+
const ctx = result.additional_context ?? "";
|
|
388
|
+
expect(ctx).toMatch(/nx-plan/);
|
|
389
|
+
expect(ctx).toMatch(/\.nexus\/memory\//);
|
|
390
|
+
// Two system-notice blocks
|
|
391
|
+
expect((ctx.match(/<system-notice>/g) ?? []).length).toBe(2);
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// ---------------------------------------------------------------------------
|
|
396
|
+
// Scenario 15 — no tags + active plan.json → state notice
|
|
397
|
+
// ---------------------------------------------------------------------------
|
|
398
|
+
|
|
399
|
+
describe("scenario 15 — no tags with active plan.json", () => {
|
|
400
|
+
test("emits active plan session notice", async () => {
|
|
401
|
+
writePlan("my-feature", 3);
|
|
402
|
+
removeTasks();
|
|
403
|
+
const result = await handler(makeInput("just a regular message"));
|
|
404
|
+
assertNotice(result, /my-feature/);
|
|
405
|
+
assertNotice(result, /pending/);
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// ---------------------------------------------------------------------------
|
|
410
|
+
// Scenario 16 — no tags + no plan.json + no tasks.json → no output
|
|
411
|
+
// ---------------------------------------------------------------------------
|
|
412
|
+
|
|
413
|
+
describe("scenario 16 — no tags, no state files", () => {
|
|
414
|
+
test("returns undefined (no output)", async () => {
|
|
415
|
+
removePlan();
|
|
416
|
+
removeTasks();
|
|
417
|
+
const result = await handler(makeInput("just chatting, no tags"));
|
|
418
|
+
expect(result).toBeUndefined();
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// ---------------------------------------------------------------------------
|
|
423
|
+
// Scenario 17 — all notices are English + wrapped in <system-notice>
|
|
424
|
+
// ---------------------------------------------------------------------------
|
|
425
|
+
|
|
426
|
+
describe("scenario 17 — all notices are English and use <system-notice>", () => {
|
|
427
|
+
const tagCases: Array<[string, string]> = [
|
|
428
|
+
["[plan]", "[plan] do something"],
|
|
429
|
+
["[plan:auto]", "[plan:auto] do auto"],
|
|
430
|
+
["[run] with tasks", "[run] run tasks"], // will have tasks.json
|
|
431
|
+
["[m]", "[m] save this"],
|
|
432
|
+
["[m:gc]", "[m:gc] gc"],
|
|
433
|
+
["[rule]", "[rule] add rule"],
|
|
434
|
+
["[rule:architect]", "[rule:architect] update"],
|
|
435
|
+
["[sync]", "[sync] sync now"],
|
|
436
|
+
["[init]", "[init] project"],
|
|
437
|
+
["[init:reset]", "[init:reset] reset"],
|
|
438
|
+
];
|
|
439
|
+
|
|
440
|
+
for (const [label, prompt] of tagCases) {
|
|
441
|
+
test(`${label} — notice is English + <system-notice> wrapped`, async () => {
|
|
442
|
+
if (label === "[run] with tasks") {
|
|
443
|
+
writeTasks();
|
|
444
|
+
} else {
|
|
445
|
+
removeTasks();
|
|
446
|
+
}
|
|
447
|
+
removePlan();
|
|
448
|
+
|
|
449
|
+
const result = await handler(makeInput(prompt));
|
|
450
|
+
// Must return something (not undefined, not block for these cases)
|
|
451
|
+
expect(result).not.toBeUndefined();
|
|
452
|
+
const r = result as NexusHookOutput;
|
|
453
|
+
|
|
454
|
+
// For non-blocking cases: check additional_context
|
|
455
|
+
if (r.decision !== "block") {
|
|
456
|
+
const ctx = r.additional_context ?? "";
|
|
457
|
+
expect(ctx).toMatch(/<system-notice>/);
|
|
458
|
+
expect(ctx).toMatch(/<\/system-notice>/);
|
|
459
|
+
// Must be primarily ASCII English (no Korean/CJK characters)
|
|
460
|
+
expect(ctx).not.toMatch(/[\u4e00-\u9fff\uac00-\ud7a3\u3040-\u30ff]/);
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
// ---------------------------------------------------------------------------
|
|
467
|
+
// Regex priority verification
|
|
468
|
+
// ---------------------------------------------------------------------------
|
|
469
|
+
|
|
470
|
+
describe("regex priority — specific variants win over generic", () => {
|
|
471
|
+
test("[plan:auto] wins over [plan] — base 'plan' not double-dispatched", async () => {
|
|
472
|
+
removePlan();
|
|
473
|
+
removeTasks();
|
|
474
|
+
const result = await handler(makeInput("[plan:auto] [plan]")) as NexusHookOutput;
|
|
475
|
+
// Only one system-notice emitted (plan:auto wins; plain plan skipped)
|
|
476
|
+
const ctx = result?.additional_context ?? "";
|
|
477
|
+
expect((ctx.match(/<system-notice>/g) ?? []).length).toBe(1);
|
|
478
|
+
expect(ctx).toMatch(/plan:auto/);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
test("[m:gc] wins over [m] when both appear", async () => {
|
|
482
|
+
removePlan();
|
|
483
|
+
removeTasks();
|
|
484
|
+
const result = await handler(makeInput("[m:gc] [m]")) as NexusHookOutput;
|
|
485
|
+
const ctx = result?.additional_context ?? "";
|
|
486
|
+
expect((ctx.match(/<system-notice>/g) ?? []).length).toBe(1);
|
|
487
|
+
expect(ctx).toMatch(/m:gc/);
|
|
488
|
+
expect(ctx).not.toMatch(/Prefix:/);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
test("[init:reset] wins over [init] when both appear", async () => {
|
|
492
|
+
removePlan();
|
|
493
|
+
removeTasks();
|
|
494
|
+
const result = await handler(makeInput("[init:reset] [init]")) as NexusHookOutput;
|
|
495
|
+
const ctx = result?.additional_context ?? "";
|
|
496
|
+
expect((ctx.match(/<system-notice>/g) ?? []).length).toBe(1);
|
|
497
|
+
expect(ctx).toMatch(/reset/);
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
// ---------------------------------------------------------------------------
|
|
502
|
+
// Non-UserPromptSubmit events are ignored
|
|
503
|
+
// ---------------------------------------------------------------------------
|
|
504
|
+
|
|
505
|
+
describe("non-UserPromptSubmit events are ignored", () => {
|
|
506
|
+
test("SessionStart returns undefined", async () => {
|
|
507
|
+
const input: NexusHookInput = {
|
|
508
|
+
hook_event_name: "SessionStart",
|
|
509
|
+
session_id: SESSION_ID,
|
|
510
|
+
cwd: ROOT,
|
|
511
|
+
};
|
|
512
|
+
const result = await handler(input);
|
|
513
|
+
expect(result).toBeUndefined();
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
// ---------------------------------------------------------------------------
|
|
518
|
+
// Harness matrix — 3 harnesses × primary tag cases (invocations SSOT)
|
|
519
|
+
// ---------------------------------------------------------------------------
|
|
520
|
+
// Expected expanded strings per harness (skill_activation template):
|
|
521
|
+
// claude: Skill({ command: "<skill>" })
|
|
522
|
+
// opencode: skill({ name: "<skill>" })
|
|
523
|
+
// codex: $<skill>
|
|
524
|
+
|
|
525
|
+
describe("harness matrix — claude", () => {
|
|
526
|
+
let _savedHarness: string | undefined;
|
|
527
|
+
|
|
528
|
+
beforeEach(() => {
|
|
529
|
+
_savedHarness = process.env["NEXUS_HARNESS"];
|
|
530
|
+
process.env["NEXUS_HARNESS"] = "claude";
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
afterEach(() => {
|
|
534
|
+
if (_savedHarness === undefined) {
|
|
535
|
+
delete process.env["NEXUS_HARNESS"];
|
|
536
|
+
} else {
|
|
537
|
+
process.env["NEXUS_HARNESS"] = _savedHarness;
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
test("[plan] → Skill({ command: \"nx-plan\" })", async () => {
|
|
542
|
+
removePlan();
|
|
543
|
+
removeTasks();
|
|
544
|
+
const result = await handler(makeInput("[plan] go")) as NexusHookOutput;
|
|
545
|
+
expect(result.additional_context).toMatch(/Skill\(\{ command: "nx-plan" \}\)/);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
test("[plan:auto] → Skill({ command: \"nx-plan\" }) + auto in tag text", async () => {
|
|
549
|
+
removePlan();
|
|
550
|
+
removeTasks();
|
|
551
|
+
const result = await handler(makeInput("[plan:auto] go")) as NexusHookOutput;
|
|
552
|
+
expect(result.additional_context).toMatch(/Skill\(\{ command: "nx-plan" \}\)/);
|
|
553
|
+
expect(result.additional_context).toMatch(/auto/);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
test("[run] with tasks → Skill({ command: \"nx-run\" })", async () => {
|
|
557
|
+
removePlan();
|
|
558
|
+
writeTasks();
|
|
559
|
+
const result = await handler(makeInput("[run] go")) as NexusHookOutput;
|
|
560
|
+
expect(result.additional_context).toMatch(/Skill\(\{ command: "nx-run" \}\)/);
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
test("[run] no tasks → Skill({ command: \"nx-plan\" }) + auto", async () => {
|
|
564
|
+
removePlan();
|
|
565
|
+
removeTasks();
|
|
566
|
+
const result = await handler(makeInput("[run] go")) as NexusHookOutput;
|
|
567
|
+
expect(result.additional_context).toMatch(/Skill\(\{ command: "nx-plan" \}\)/);
|
|
568
|
+
expect(result.additional_context).toMatch(/auto/);
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
test("[sync] → Skill({ command: \"nx-sync\" })", async () => {
|
|
572
|
+
removePlan();
|
|
573
|
+
removeTasks();
|
|
574
|
+
const result = await handler(makeInput("[sync] go")) as NexusHookOutput;
|
|
575
|
+
expect(result.additional_context).toMatch(/Skill\(\{ command: "nx-sync" \}\)/);
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
test("[init] → Skill({ command: \"nx-init\" })", async () => {
|
|
579
|
+
removePlan();
|
|
580
|
+
removeTasks();
|
|
581
|
+
const result = await handler(makeInput("[init] go")) as NexusHookOutput;
|
|
582
|
+
expect(result.additional_context).toMatch(/Skill\(\{ command: "nx-init" \}\)/);
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
test("[init:reset] → Skill({ command: \"nx-init\" }) + reset in tag text", async () => {
|
|
586
|
+
removePlan();
|
|
587
|
+
removeTasks();
|
|
588
|
+
const result = await handler(makeInput("[init:reset] go")) as NexusHookOutput;
|
|
589
|
+
expect(result.additional_context).toMatch(/Skill\(\{ command: "nx-init" \}\)/);
|
|
590
|
+
expect(result.additional_context).toMatch(/reset/);
|
|
591
|
+
});
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
describe("harness matrix — opencode", () => {
|
|
595
|
+
let _savedHarness: string | undefined;
|
|
596
|
+
|
|
597
|
+
beforeEach(() => {
|
|
598
|
+
_savedHarness = process.env["NEXUS_HARNESS"];
|
|
599
|
+
process.env["NEXUS_HARNESS"] = "opencode";
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
afterEach(() => {
|
|
603
|
+
if (_savedHarness === undefined) {
|
|
604
|
+
delete process.env["NEXUS_HARNESS"];
|
|
605
|
+
} else {
|
|
606
|
+
process.env["NEXUS_HARNESS"] = _savedHarness;
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
test("[plan] → skill({ name: \"nx-plan\" })", async () => {
|
|
611
|
+
removePlan();
|
|
612
|
+
removeTasks();
|
|
613
|
+
const result = await handler(makeInput("[plan] go")) as NexusHookOutput;
|
|
614
|
+
expect(result.additional_context).toMatch(/skill\(\{ name: "nx-plan" \}\)/);
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
test("[plan:auto] → skill({ name: \"nx-plan\" }) + auto", async () => {
|
|
618
|
+
removePlan();
|
|
619
|
+
removeTasks();
|
|
620
|
+
const result = await handler(makeInput("[plan:auto] go")) as NexusHookOutput;
|
|
621
|
+
expect(result.additional_context).toMatch(/skill\(\{ name: "nx-plan" \}\)/);
|
|
622
|
+
expect(result.additional_context).toMatch(/auto/);
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
test("[run] with tasks → skill({ name: \"nx-run\" })", async () => {
|
|
626
|
+
removePlan();
|
|
627
|
+
writeTasks();
|
|
628
|
+
const result = await handler(makeInput("[run] go")) as NexusHookOutput;
|
|
629
|
+
expect(result.additional_context).toMatch(/skill\(\{ name: "nx-run" \}\)/);
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
test("[run] no tasks → skill({ name: \"nx-plan\" }) + auto", async () => {
|
|
633
|
+
removePlan();
|
|
634
|
+
removeTasks();
|
|
635
|
+
const result = await handler(makeInput("[run] go")) as NexusHookOutput;
|
|
636
|
+
expect(result.additional_context).toMatch(/skill\(\{ name: "nx-plan" \}\)/);
|
|
637
|
+
expect(result.additional_context).toMatch(/auto/);
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
test("[sync] → skill({ name: \"nx-sync\" })", async () => {
|
|
641
|
+
removePlan();
|
|
642
|
+
removeTasks();
|
|
643
|
+
const result = await handler(makeInput("[sync] go")) as NexusHookOutput;
|
|
644
|
+
expect(result.additional_context).toMatch(/skill\(\{ name: "nx-sync" \}\)/);
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
test("[init] → skill({ name: \"nx-init\" })", async () => {
|
|
648
|
+
removePlan();
|
|
649
|
+
removeTasks();
|
|
650
|
+
const result = await handler(makeInput("[init] go")) as NexusHookOutput;
|
|
651
|
+
expect(result.additional_context).toMatch(/skill\(\{ name: "nx-init" \}\)/);
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
test("[init:reset] → skill({ name: \"nx-init\" }) + reset", async () => {
|
|
655
|
+
removePlan();
|
|
656
|
+
removeTasks();
|
|
657
|
+
const result = await handler(makeInput("[init:reset] go")) as NexusHookOutput;
|
|
658
|
+
expect(result.additional_context).toMatch(/skill\(\{ name: "nx-init" \}\)/);
|
|
659
|
+
expect(result.additional_context).toMatch(/reset/);
|
|
660
|
+
});
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
describe("harness matrix — codex", () => {
|
|
664
|
+
let _savedHarness: string | undefined;
|
|
665
|
+
|
|
666
|
+
beforeEach(() => {
|
|
667
|
+
_savedHarness = process.env["NEXUS_HARNESS"];
|
|
668
|
+
process.env["NEXUS_HARNESS"] = "codex";
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
afterEach(() => {
|
|
672
|
+
if (_savedHarness === undefined) {
|
|
673
|
+
delete process.env["NEXUS_HARNESS"];
|
|
674
|
+
} else {
|
|
675
|
+
process.env["NEXUS_HARNESS"] = _savedHarness;
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
test("[plan] → $nx-plan", async () => {
|
|
680
|
+
removePlan();
|
|
681
|
+
removeTasks();
|
|
682
|
+
const result = await handler(makeInput("[plan] go")) as NexusHookOutput;
|
|
683
|
+
expect(result.additional_context).toMatch(/\$nx-plan/);
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
test("[plan:auto] → $nx-plan + auto in tag text", async () => {
|
|
687
|
+
removePlan();
|
|
688
|
+
removeTasks();
|
|
689
|
+
const result = await handler(makeInput("[plan:auto] go")) as NexusHookOutput;
|
|
690
|
+
expect(result.additional_context).toMatch(/\$nx-plan/);
|
|
691
|
+
expect(result.additional_context).toMatch(/auto/);
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
test("[run] with tasks → $nx-run", async () => {
|
|
695
|
+
removePlan();
|
|
696
|
+
writeTasks();
|
|
697
|
+
const result = await handler(makeInput("[run] go")) as NexusHookOutput;
|
|
698
|
+
expect(result.additional_context).toMatch(/\$nx-run/);
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
test("[run] no tasks → $nx-plan + auto", async () => {
|
|
702
|
+
removePlan();
|
|
703
|
+
removeTasks();
|
|
704
|
+
const result = await handler(makeInput("[run] go")) as NexusHookOutput;
|
|
705
|
+
expect(result.additional_context).toMatch(/\$nx-plan/);
|
|
706
|
+
expect(result.additional_context).toMatch(/auto/);
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
test("[sync] → $nx-sync", async () => {
|
|
710
|
+
removePlan();
|
|
711
|
+
removeTasks();
|
|
712
|
+
const result = await handler(makeInput("[sync] go")) as NexusHookOutput;
|
|
713
|
+
expect(result.additional_context).toMatch(/\$nx-sync/);
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
test("[init] → $nx-init", async () => {
|
|
717
|
+
removePlan();
|
|
718
|
+
removeTasks();
|
|
719
|
+
const result = await handler(makeInput("[init] go")) as NexusHookOutput;
|
|
720
|
+
expect(result.additional_context).toMatch(/\$nx-init/);
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
test("[init:reset] → $nx-init + reset in tag text", async () => {
|
|
724
|
+
removePlan();
|
|
725
|
+
removeTasks();
|
|
726
|
+
const result = await handler(makeInput("[init:reset] go")) as NexusHookOutput;
|
|
727
|
+
expect(result.additional_context).toMatch(/\$nx-init/);
|
|
728
|
+
expect(result.additional_context).toMatch(/reset/);
|
|
729
|
+
});
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
// ---------------------------------------------------------------------------
|
|
733
|
+
// 모듈 전역 상태 격리
|
|
734
|
+
// ---------------------------------------------------------------------------
|
|
735
|
+
|
|
736
|
+
describe("모듈 전역 상태 격리", () => {
|
|
737
|
+
function makePromptInput(cwd: string, prompt: string): NexusHookInput {
|
|
738
|
+
return {
|
|
739
|
+
hook_event_name: "UserPromptSubmit",
|
|
740
|
+
session_id: "sess-isolation",
|
|
741
|
+
cwd,
|
|
742
|
+
prompt,
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Test A: two cwds with different agents/skills → each sees only its own targets
|
|
747
|
+
test("Test A: different cwds return only their own valid rule targets", async () => {
|
|
748
|
+
const tmpDir1 = fs.mkdtempSync(path.join(os.tmpdir(), "nexus-pr-isolation-a1-"));
|
|
749
|
+
const tmpDir2 = fs.mkdtempSync(path.join(os.tmpdir(), "nexus-pr-isolation-a2-"));
|
|
750
|
+
|
|
751
|
+
try {
|
|
752
|
+
// tmpDir1: agent "architect" only
|
|
753
|
+
fs.mkdirSync(path.join(tmpDir1, "assets/agents/architect"), { recursive: true });
|
|
754
|
+
|
|
755
|
+
// tmpDir2: agent "engineer" only
|
|
756
|
+
fs.mkdirSync(path.join(tmpDir2, "assets/agents/engineer"), { recursive: true });
|
|
757
|
+
|
|
758
|
+
// [rule:architect] is valid in tmpDir1, invalid in tmpDir2
|
|
759
|
+
const r1 = await handler(makePromptInput(tmpDir1, "[rule:architect] new rule")) as NexusHookOutput;
|
|
760
|
+
expect(r1.decision).toBeUndefined();
|
|
761
|
+
expect(r1.additional_context).toMatch(/architect/);
|
|
762
|
+
|
|
763
|
+
const r2 = await handler(makePromptInput(tmpDir2, "[rule:architect] new rule")) as NexusHookOutput;
|
|
764
|
+
expect(r2.decision).toBe("block");
|
|
765
|
+
|
|
766
|
+
// [rule:engineer] is invalid in tmpDir1, valid in tmpDir2
|
|
767
|
+
const r3 = await handler(makePromptInput(tmpDir1, "[rule:engineer] new rule")) as NexusHookOutput;
|
|
768
|
+
expect(r3.decision).toBe("block");
|
|
769
|
+
|
|
770
|
+
const r4 = await handler(makePromptInput(tmpDir2, "[rule:engineer] new rule")) as NexusHookOutput;
|
|
771
|
+
expect(r4.decision).toBeUndefined();
|
|
772
|
+
expect(r4.additional_context).toMatch(/engineer/);
|
|
773
|
+
} finally {
|
|
774
|
+
fs.rmSync(tmpDir1, { recursive: true, force: true });
|
|
775
|
+
fs.rmSync(tmpDir2, { recursive: true, force: true });
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
// Test B: newly added directory is reflected on the next call
|
|
780
|
+
test("Test B: newly added agent directory is recognized in subsequent call", async () => {
|
|
781
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nexus-pr-isolation-b-"));
|
|
782
|
+
|
|
783
|
+
try {
|
|
784
|
+
// Start with "architect" only — "foo" is not a valid target yet
|
|
785
|
+
fs.mkdirSync(path.join(tmpDir, "assets/agents/architect"), { recursive: true });
|
|
786
|
+
|
|
787
|
+
const r1 = await handler(makePromptInput(tmpDir, "[rule:foo] test")) as NexusHookOutput;
|
|
788
|
+
expect(r1.decision).toBe("block");
|
|
789
|
+
|
|
790
|
+
// Add "foo" agent directory
|
|
791
|
+
fs.mkdirSync(path.join(tmpDir, "assets/agents/foo"), { recursive: true });
|
|
792
|
+
|
|
793
|
+
// Next call should see "foo" as a valid target
|
|
794
|
+
const r2 = await handler(makePromptInput(tmpDir, "[rule:foo] test")) as NexusHookOutput;
|
|
795
|
+
expect(r2.decision).toBeUndefined();
|
|
796
|
+
expect(r2.additional_context).toMatch(/foo/);
|
|
797
|
+
} finally {
|
|
798
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
});
|