@warpgogol/forge 5.2.0 → 5.2.4
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/AGENTS.md +4 -1
- package/dist/os/adr/adr.module.js +1 -1
- package/dist/os/adr/adr.module.js.map +1 -1
- package/dist/os/compass/compass.module.js +2 -2
- package/dist/os/compass/compass.module.js.map +1 -1
- package/dist/os/compass/handlers/compass-audit-handler.d.ts +3 -0
- package/dist/os/compass/handlers/compass-audit-handler.d.ts.map +1 -1
- package/dist/os/compass/handlers/compass-audit-handler.js +87 -8
- package/dist/os/compass/handlers/compass-audit-handler.js.map +1 -1
- package/dist/os/core/core.module.d.ts.map +1 -1
- package/dist/os/core/core.module.js +15 -1
- package/dist/os/core/core.module.js.map +1 -1
- package/dist/os/mission/handlers/archive.d.ts.map +1 -1
- package/dist/os/mission/handlers/archive.js +22 -6
- package/dist/os/mission/handlers/archive.js.map +1 -1
- package/dist/os/program/lease.js +1 -1
- package/dist/os/program/lease.js.map +1 -1
- package/dist/os/queue/queue.module.js +1 -1
- package/dist/os/queue/queue.module.js.map +1 -1
- package/dist/os/rfc/handlers/list-create.d.ts.map +1 -1
- package/dist/os/rfc/handlers/list-create.js +79 -32
- package/dist/os/rfc/handlers/list-create.js.map +1 -1
- package/dist/os/rfc/rfc.module.js +1 -1
- package/dist/os/rfc/rfc.module.js.map +1 -1
- package/dist/os/spec/spec.module.d.ts.map +1 -1
- package/dist/os/spec/spec.module.js +2 -0
- package/dist/os/spec/spec.module.js.map +1 -1
- package/dist/src/config/forge-config.d.ts +9 -0
- package/dist/src/config/forge-config.d.ts.map +1 -1
- package/dist/src/config/forge-config.js +7 -0
- package/dist/src/config/forge-config.js.map +1 -1
- package/dist/src/knowledge/index.d.ts +1 -0
- package/dist/src/knowledge/index.d.ts.map +1 -1
- package/dist/src/knowledge/index.js +1 -0
- package/dist/src/knowledge/index.js.map +1 -1
- package/dist/src/knowledge/sync.d.ts +24 -0
- package/dist/src/knowledge/sync.d.ts.map +1 -0
- package/dist/src/knowledge/sync.js +127 -0
- package/dist/src/knowledge/sync.js.map +1 -0
- package/dist/src/onboarding/agents-generate.d.ts +2 -0
- package/dist/src/onboarding/agents-generate.d.ts.map +1 -1
- package/dist/src/onboarding/agents-generate.js +14 -18
- package/dist/src/onboarding/agents-generate.js.map +1 -1
- package/dist/src/onboarding/doctor.d.ts +6 -0
- package/dist/src/onboarding/doctor.d.ts.map +1 -1
- package/dist/src/onboarding/doctor.js +124 -14
- package/dist/src/onboarding/doctor.js.map +1 -1
- package/dist/src/onboarding/init.d.ts.map +1 -1
- package/dist/src/onboarding/init.js +15 -8
- package/dist/src/onboarding/init.js.map +1 -1
- package/dist/src/onboarding/memory-compact.d.ts +22 -0
- package/dist/src/onboarding/memory-compact.d.ts.map +1 -0
- package/dist/src/onboarding/memory-compact.js +147 -0
- package/dist/src/onboarding/memory-compact.js.map +1 -0
- package/dist/src/onboarding/memory-scaffold.d.ts +1 -0
- package/dist/src/onboarding/memory-scaffold.d.ts.map +1 -1
- package/dist/src/onboarding/memory-scaffold.js +1 -1
- package/dist/src/onboarding/memory-scaffold.js.map +1 -1
- package/dist/src/onboarding/nested-agents-generate.d.ts.map +1 -1
- package/dist/src/onboarding/nested-agents-generate.js +1 -1
- package/dist/src/onboarding/nested-agents-generate.js.map +1 -1
- package/dist/src/onboarding/upgrade.d.ts.map +1 -1
- package/dist/src/onboarding/upgrade.js +42 -13
- package/dist/src/onboarding/upgrade.js.map +1 -1
- package/dist/src/onboarding/workspace-discovery.d.ts +1 -1
- package/dist/src/onboarding/workspace-discovery.d.ts.map +1 -1
- package/dist/src/onboarding/workspace-discovery.js +14 -2
- package/dist/src/onboarding/workspace-discovery.js.map +1 -1
- package/dist/src/types.d.ts +1 -2
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +1 -0
- package/dist/src/types.js.map +1 -1
- package/dist/src/utils/fs-atomic.js +1 -1
- package/dist/src/utils/fs-atomic.js.map +1 -1
- package/docs/reference/forge-yaml.md +12 -0
- package/os/adr/adr.module.ts +1 -1
- package/os/compass/compass.module.ts +2 -2
- package/os/compass/handlers/compass-audit-handler.ts +116 -8
- package/os/compass/handlers/tests/compass-audit-plan.test.ts +98 -0
- package/os/compass/handlers/tests/compass-audit-record.test.ts +105 -0
- package/os/compass/handlers/tests/compass-audit-validate.test.ts +133 -0
- package/os/compass/handlers/tests/compass-ledger-scope.test.ts +124 -0
- package/os/core/core.module.ts +16 -1
- package/os/mission/handlers/archive.test.ts +49 -0
- package/os/mission/handlers/archive.ts +21 -6
- package/os/program/lease.ts +1 -1
- package/os/queue/queue.module.ts +1 -1
- package/os/rfc/handlers/list-create.ts +83 -35
- package/os/rfc/handlers/validate-rules.test.ts +50 -0
- package/os/rfc/rfc-create-concurrent.test.ts +118 -0
- package/os/rfc/rfc-create-hint.test.ts +90 -0
- package/os/rfc/rfc-read-only-no-side-effects.test.ts +83 -0
- package/os/rfc/rfc.module.ts +1 -1
- package/os/spec/spec.module.ts +2 -0
- package/package.json +1 -1
- package/profiles/forge-shell.yaml +3 -0
- package/profiles/godot-game.yaml +3 -0
- package/profiles/phaser-game.yaml +3 -0
- package/profiles/site-workshop.yaml +567 -564
- package/src/config/forge-config.ts +11 -0
- package/src/knowledge/__tests__/sync.test.ts +227 -0
- package/src/knowledge/index.ts +7 -0
- package/src/knowledge/sync.ts +161 -0
- package/src/onboarding/agents-generate.ts +18 -18
- package/src/onboarding/doctor.ts +141 -15
- package/src/onboarding/init.ts +15 -8
- package/src/onboarding/memory-compact.ts +188 -0
- package/src/onboarding/memory-scaffold.ts +1 -1
- package/src/onboarding/nested-agents-generate.ts +5 -1
- package/src/onboarding/upgrade.ts +47 -16
- package/src/onboarding/workspace-discovery.ts +15 -1
- package/src/tests/agents-generate.test.ts +26 -3
- package/src/tests/doctor-dist-freshness.test.ts +110 -0
- package/src/tests/doctor-fix.test.ts +145 -0
- package/src/tests/doctor-nested-agents.test.ts +108 -0
- package/src/tests/memory-compact.test.ts +143 -0
- package/src/tests/upgrade.test.ts +53 -0
- package/src/tests/workspace-discovery.test.ts +28 -0
- package/src/types.ts +2 -2
- package/src/utils/fs-atomic.ts +1 -1
|
@@ -36,6 +36,13 @@ const SKIP_DIRS = new Set([
|
|
|
36
36
|
".turbo",
|
|
37
37
|
".cache",
|
|
38
38
|
".agents",
|
|
39
|
+
// Pipeline-managed mission workpieces and archives — their AGENTS.md is
|
|
40
|
+
// generated by the site pipeline, not by forge.agents.generate.
|
|
41
|
+
"missions",
|
|
42
|
+
// Test fixture workshops — not real project workspaces.
|
|
43
|
+
"test-fixtures",
|
|
44
|
+
// Stryker mutation-testing sandboxes — transient temp dirs.
|
|
45
|
+
".stryker-tmp",
|
|
39
46
|
]);
|
|
40
47
|
|
|
41
48
|
export function detectWorkspaceType(
|
|
@@ -110,8 +117,15 @@ function matchesWorkspaceType(dirPath: string, wt: ProfileWorkspaceType): boolea
|
|
|
110
117
|
export function discoverWorkspaces(
|
|
111
118
|
workspaceRoot: string,
|
|
112
119
|
workspaceTypes?: ProfileWorkspaceType[],
|
|
120
|
+
extraSkipDirs?: readonly string[],
|
|
113
121
|
): WorkspaceDir[] {
|
|
114
122
|
const results: WorkspaceDir[] = [];
|
|
123
|
+
// RFC-1150: consumer-declared exclusions (bindings.workspaces.skipDirs)
|
|
124
|
+
// merge over the SKIP_DIRS defaults.
|
|
125
|
+
const skipDirs =
|
|
126
|
+
extraSkipDirs && extraSkipDirs.length > 0
|
|
127
|
+
? new Set([...SKIP_DIRS, ...extraSkipDirs])
|
|
128
|
+
: SKIP_DIRS;
|
|
115
129
|
|
|
116
130
|
function scanDir(dirPath: string, depth: number): void {
|
|
117
131
|
if (depth > 5) return;
|
|
@@ -125,7 +139,7 @@ export function discoverWorkspaces(
|
|
|
125
139
|
|
|
126
140
|
for (const entry of entries) {
|
|
127
141
|
if (!entry.isDirectory()) continue;
|
|
128
|
-
if (
|
|
142
|
+
if (skipDirs.has(entry.name)) continue;
|
|
129
143
|
|
|
130
144
|
const childPath = path.join(dirPath, entry.name);
|
|
131
145
|
const type = detectWorkspaceType(childPath, workspaceTypes);
|
|
@@ -224,13 +224,36 @@ test("agents-generate is idempotent — running twice produces the same content"
|
|
|
224
224
|
expect(second).toBe(first);
|
|
225
225
|
});
|
|
226
226
|
|
|
227
|
-
|
|
227
|
+
// RFC-1150: hand-written root guard is non-fatal — skip root, continue nested
|
|
228
|
+
|
|
229
|
+
test("agents-generate skips hand-written root and still generates nested", async () => {
|
|
228
230
|
await makeForgeYaml(tempDir);
|
|
229
231
|
await writeFile(join(tempDir, "AGENTS.md"), "# Hand-written\nNo marker.\n", "utf8");
|
|
232
|
+
await mkdir(join(tempDir, "packages", "my-pkg"), { recursive: true });
|
|
233
|
+
await writeFile(
|
|
234
|
+
join(tempDir, "packages", "my-pkg", "package.json"),
|
|
235
|
+
JSON.stringify({ name: "@test/my-pkg" }),
|
|
236
|
+
);
|
|
230
237
|
|
|
231
238
|
const result = await runAgentsGenerate({ argv: [], flags: {} }, makeContext(tempDir));
|
|
232
|
-
expect(result.exitCode).toBe(
|
|
233
|
-
expect(result.data?.status).toBe("
|
|
239
|
+
expect(result.exitCode).toBe(0);
|
|
240
|
+
expect(result.data?.status).toBe("pass");
|
|
241
|
+
expect(result.data?.rootSkipped).toBe(true);
|
|
242
|
+
expect(result.data?.rootSkipReason).toBe("hand-written");
|
|
243
|
+
expect(result.data?.skipped).toContain("AGENTS.md (hand-written)");
|
|
244
|
+
expect(result.data?.generated).toContain("packages/my-pkg/AGENTS.md");
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("agents-generate does not modify hand-written root AGENTS.md", async () => {
|
|
248
|
+
await makeForgeYaml(tempDir);
|
|
249
|
+
const original = "# Hand-written\nNo marker.\n";
|
|
250
|
+
await writeFile(join(tempDir, "AGENTS.md"), original, "utf8");
|
|
251
|
+
|
|
252
|
+
const result = await runAgentsGenerate({ argv: [], flags: {} }, makeContext(tempDir));
|
|
253
|
+
expect(result.exitCode).toBe(0);
|
|
254
|
+
|
|
255
|
+
const after = await readFile(join(tempDir, "AGENTS.md"), "utf8");
|
|
256
|
+
expect(after).toBe(original);
|
|
234
257
|
});
|
|
235
258
|
|
|
236
259
|
// RFC-0611: nested AGENTS.md generation tests
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Unit tests for the dist-freshness doctor check — warns when a forge
|
|
4
|
+
source checkout's dist/ is older than the newest src/ file (RFC-1151).</purpose>
|
|
5
|
+
</MODULE_CONTRACT>
|
|
6
|
+
<CHANGE_SUMMARY>
|
|
7
|
+
<item>RFC-1151: initial dist-freshness tests.</item>
|
|
8
|
+
</CHANGE_SUMMARY>
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { test, expect, beforeEach, afterEach } from "vitest";
|
|
12
|
+
import { mkdtemp, rm, mkdir, writeFile, utimes } from "node:fs/promises";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { runDoctor } from "../onboarding/doctor.ts";
|
|
16
|
+
import type { ForgeCommandInput, ForgeRuntimeContext } from "../types.ts";
|
|
17
|
+
|
|
18
|
+
let tempDir: string;
|
|
19
|
+
|
|
20
|
+
beforeEach(async () => {
|
|
21
|
+
tempDir = await mkdtemp(join(tmpdir(), "doctor-dist-test-"));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(async () => {
|
|
25
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function makeContext(): ForgeRuntimeContext {
|
|
29
|
+
return {
|
|
30
|
+
workspaceRoot: tempDir,
|
|
31
|
+
// forgeRoot falls back to <workspaceRoot>/packages/forge when unresolvable
|
|
32
|
+
forgeRoot: join(tempDir, "packages", "forge"),
|
|
33
|
+
logger: {
|
|
34
|
+
section: () => {},
|
|
35
|
+
success: () => {},
|
|
36
|
+
warn: () => {},
|
|
37
|
+
error: () => {},
|
|
38
|
+
info: () => {},
|
|
39
|
+
},
|
|
40
|
+
dryRun: false,
|
|
41
|
+
outputFormat: "json",
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const FORGE_YAML = `schema: "forge/config@1"
|
|
46
|
+
project:
|
|
47
|
+
name: test-project
|
|
48
|
+
stack: []
|
|
49
|
+
packageManager: pnpm
|
|
50
|
+
paths:
|
|
51
|
+
rfcsDir: docs/rfcs
|
|
52
|
+
adrsDir: docs/adrs
|
|
53
|
+
plansDir: docs/plans
|
|
54
|
+
auditsDir: docs/audits
|
|
55
|
+
specsDir: docs/specs
|
|
56
|
+
skillsDir: .agents/skills
|
|
57
|
+
sessionsDir: docs/sessions
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
async function makeForgeCheckout(srcMtime: Date, distMtime: Date): Promise<void> {
|
|
61
|
+
const forgeRoot = join(tempDir, "packages", "forge");
|
|
62
|
+
await mkdir(join(forgeRoot, "src"), { recursive: true });
|
|
63
|
+
await mkdir(join(forgeRoot, "dist"), { recursive: true });
|
|
64
|
+
await writeFile(join(forgeRoot, "package.json"), JSON.stringify({ name: "@warpgogol/forge" }));
|
|
65
|
+
await writeFile(join(forgeRoot, "src", "index.ts"), "export {};\n");
|
|
66
|
+
await writeFile(join(forgeRoot, "dist", "index.js"), "export {};\n");
|
|
67
|
+
await utimes(join(forgeRoot, "src", "index.ts"), srcMtime, srcMtime);
|
|
68
|
+
await utimes(join(forgeRoot, "dist"), distMtime, distMtime);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
test("dist-freshness warns when dist is older than newest src file", async () => {
|
|
72
|
+
await writeFile(join(tempDir, "forge.yaml"), FORGE_YAML, "utf8");
|
|
73
|
+
const old = new Date("2026-01-01T00:00:00Z");
|
|
74
|
+
const recent = new Date("2026-09-01T00:00:00Z");
|
|
75
|
+
await makeForgeCheckout(recent, old);
|
|
76
|
+
|
|
77
|
+
const input: ForgeCommandInput = { argv: [], flags: {} };
|
|
78
|
+
const result = await runDoctor(input, makeContext());
|
|
79
|
+
|
|
80
|
+
const check = result.data!.checks.find((c) => c.name === "dist-freshness");
|
|
81
|
+
expect(check).toBeDefined();
|
|
82
|
+
expect(check!.status).toBe("warn");
|
|
83
|
+
expect(check!.message).toContain("pnpm run build");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("dist-freshness passes when dist is absent (npm consumer layout)", async () => {
|
|
87
|
+
await writeFile(join(tempDir, "forge.yaml"), FORGE_YAML, "utf8");
|
|
88
|
+
// No packages/forge at all — forgeRoot fallback dir doesn't exist
|
|
89
|
+
const input: ForgeCommandInput = { argv: [], flags: {} };
|
|
90
|
+
const result = await runDoctor(input, makeContext());
|
|
91
|
+
|
|
92
|
+
const check = result.data!.checks.find((c) => c.name === "dist-freshness");
|
|
93
|
+
expect(check).toBeDefined();
|
|
94
|
+
expect(check!.status).toBe("pass");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("dist-freshness passes when dist is newer than src", async () => {
|
|
98
|
+
await writeFile(join(tempDir, "forge.yaml"), FORGE_YAML, "utf8");
|
|
99
|
+
const old = new Date("2026-01-01T00:00:00Z");
|
|
100
|
+
const recent = new Date("2026-09-01T00:00:00Z");
|
|
101
|
+
await makeForgeCheckout(old, recent);
|
|
102
|
+
|
|
103
|
+
const input: ForgeCommandInput = { argv: [], flags: {} };
|
|
104
|
+
const result = await runDoctor(input, makeContext());
|
|
105
|
+
|
|
106
|
+
const check = result.data!.checks.find((c) => c.name === "dist-freshness");
|
|
107
|
+
expect(check).toBeDefined();
|
|
108
|
+
expect(check!.status).toBe("pass");
|
|
109
|
+
expect(check!.message).toContain("fresh");
|
|
110
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Unit tests for forge doctor --fix — safe remediations for warn/fail
|
|
4
|
+
checks: nested-AGENTS.md regeneration and memory-layer compaction (RFC-1151).</purpose>
|
|
5
|
+
</MODULE_CONTRACT>
|
|
6
|
+
<CHANGE_SUMMARY>
|
|
7
|
+
<item>RFC-1151: initial doctor --fix tests.</item>
|
|
8
|
+
</CHANGE_SUMMARY>
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { test, expect, beforeEach, afterEach } from "vitest";
|
|
12
|
+
import { mkdtemp, rm, mkdir, writeFile, readFile } from "node:fs/promises";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { runDoctor } from "../onboarding/doctor.ts";
|
|
16
|
+
import type { ForgeCommandInput, ForgeRuntimeContext } from "../types.ts";
|
|
17
|
+
|
|
18
|
+
let tempDir: string;
|
|
19
|
+
|
|
20
|
+
beforeEach(async () => {
|
|
21
|
+
tempDir = await mkdtemp(join(tmpdir(), "doctor-fix-test-"));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(async () => {
|
|
25
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function makeContext(dryRun = false): ForgeRuntimeContext {
|
|
29
|
+
return {
|
|
30
|
+
workspaceRoot: tempDir,
|
|
31
|
+
logger: {
|
|
32
|
+
section: () => {},
|
|
33
|
+
success: () => {},
|
|
34
|
+
warn: () => {},
|
|
35
|
+
error: () => {},
|
|
36
|
+
info: () => {},
|
|
37
|
+
},
|
|
38
|
+
dryRun,
|
|
39
|
+
outputFormat: "json",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const FORGE_YAML = `schema: "forge/config@1"
|
|
44
|
+
project:
|
|
45
|
+
name: test-project
|
|
46
|
+
stack: []
|
|
47
|
+
packageManager: pnpm
|
|
48
|
+
paths:
|
|
49
|
+
rfcsDir: docs/rfcs
|
|
50
|
+
adrsDir: docs/adrs
|
|
51
|
+
plansDir: docs/plans
|
|
52
|
+
auditsDir: docs/audits
|
|
53
|
+
specsDir: docs/specs
|
|
54
|
+
skillsDir: .agents/skills
|
|
55
|
+
sessionsDir: docs/sessions
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
const GENERATED_MARKER =
|
|
59
|
+
"<!--\n GENERATED. Do not change this line unless the file contains project specific changes.\n-->\n";
|
|
60
|
+
|
|
61
|
+
async function makeStaleWorkspace(): Promise<void> {
|
|
62
|
+
await mkdir(join(tempDir, "packages", "my-pkg"), { recursive: true });
|
|
63
|
+
await writeFile(
|
|
64
|
+
join(tempDir, "packages", "my-pkg", "package.json"),
|
|
65
|
+
JSON.stringify({ name: "@test/my-pkg" }),
|
|
66
|
+
);
|
|
67
|
+
// Generated marker + outdated content → stale
|
|
68
|
+
await writeFile(
|
|
69
|
+
join(tempDir, "packages", "my-pkg", "AGENTS.md"),
|
|
70
|
+
`${GENERATED_MARKER}# Old content\n`,
|
|
71
|
+
"utf8",
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
test("doctor --fix regenerates stale nested AGENTS.md and subsequent run passes", async () => {
|
|
76
|
+
await writeFile(join(tempDir, "forge.yaml"), FORGE_YAML, "utf8");
|
|
77
|
+
await makeStaleWorkspace();
|
|
78
|
+
|
|
79
|
+
const fixInput: ForgeCommandInput = { argv: [], flags: { fix: true } };
|
|
80
|
+
const fixResult = await runDoctor(fixInput, makeContext());
|
|
81
|
+
|
|
82
|
+
const fixEntry = fixResult.data!.fixes?.find((f) => f.check === "nested-AGENTS.md");
|
|
83
|
+
expect(fixEntry).toBeDefined();
|
|
84
|
+
expect(fixEntry!.action).toBe("fixed");
|
|
85
|
+
|
|
86
|
+
const regenerated = await readFile(
|
|
87
|
+
join(tempDir, "packages", "my-pkg", "AGENTS.md"),
|
|
88
|
+
"utf8",
|
|
89
|
+
);
|
|
90
|
+
expect(regenerated).toContain("`@test/my-pkg` — Agent Guide");
|
|
91
|
+
expect(regenerated).not.toContain("Old content");
|
|
92
|
+
|
|
93
|
+
// Subsequent doctor run: nested-AGENTS.md no longer reports this file stale
|
|
94
|
+
const verifyResult = await runDoctor({ argv: [], flags: {} }, makeContext());
|
|
95
|
+
const check = verifyResult.data!.checks.find((c) => c.name === "nested-AGENTS.md");
|
|
96
|
+
expect(check!.message).not.toContain("my-pkg/AGENTS.md stale");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("doctor --fix does not modify hand-written nested AGENTS.md", async () => {
|
|
100
|
+
await writeFile(join(tempDir, "forge.yaml"), FORGE_YAML, "utf8");
|
|
101
|
+
await mkdir(join(tempDir, "packages", "my-pkg"), { recursive: true });
|
|
102
|
+
await writeFile(
|
|
103
|
+
join(tempDir, "packages", "my-pkg", "package.json"),
|
|
104
|
+
JSON.stringify({ name: "@test/my-pkg" }),
|
|
105
|
+
);
|
|
106
|
+
const handwritten = "# Custom\nNo marker.\n";
|
|
107
|
+
await writeFile(join(tempDir, "packages", "my-pkg", "AGENTS.md"), handwritten, "utf8");
|
|
108
|
+
|
|
109
|
+
const result = await runDoctor({ argv: [], flags: { fix: true } }, makeContext());
|
|
110
|
+
expect(result.data!.fixes).toBeDefined();
|
|
111
|
+
|
|
112
|
+
const after = await readFile(join(tempDir, "packages", "my-pkg", "AGENTS.md"), "utf8");
|
|
113
|
+
expect(after).toBe(handwritten);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("doctor --fix compacts over-budget MEMORY.md", async () => {
|
|
117
|
+
await writeFile(join(tempDir, "forge.yaml"), FORGE_YAML, "utf8");
|
|
118
|
+
await mkdir(join(tempDir, ".agents", "memory"), { recursive: true });
|
|
119
|
+
const env = Array.from({ length: 30 }, (_, i) => `- env ${i} ${"x".repeat(200)}`).join("\n");
|
|
120
|
+
await writeFile(
|
|
121
|
+
join(tempDir, ".agents", "memory", "MEMORY.md"),
|
|
122
|
+
`# Project Memory\n\n## Current focus\n\n- keep\n\n## Environment notes\n\n${env}\n`,
|
|
123
|
+
"utf8",
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const result = await runDoctor({ argv: [], flags: { fix: true } }, makeContext());
|
|
127
|
+
const fixEntry = result.data!.fixes?.find((f) => f.check === "memory-layer");
|
|
128
|
+
expect(fixEntry).toBeDefined();
|
|
129
|
+
expect(fixEntry!.action).toBe("fixed");
|
|
130
|
+
|
|
131
|
+
const after = await readFile(join(tempDir, ".agents", "memory", "MEMORY.md"), "utf8");
|
|
132
|
+
expect(after.length).toBeLessThanOrEqual(4096);
|
|
133
|
+
expect(after).toContain("- keep");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("doctor without --fix produces no fixes array and does not mutate", async () => {
|
|
137
|
+
await writeFile(join(tempDir, "forge.yaml"), FORGE_YAML, "utf8");
|
|
138
|
+
await makeStaleWorkspace();
|
|
139
|
+
|
|
140
|
+
const result = await runDoctor({ argv: [], flags: {} }, makeContext());
|
|
141
|
+
expect(result.data!.fixes).toBeUndefined();
|
|
142
|
+
|
|
143
|
+
const after = await readFile(join(tempDir, "packages", "my-pkg", "AGENTS.md"), "utf8");
|
|
144
|
+
expect(after).toContain("Old content");
|
|
145
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Unit tests for the nested-AGENTS.md doctor check — consumer-declared
|
|
4
|
+
workspace discovery exclusions via bindings.workspaces.skipDirs (RFC-1150).</purpose>
|
|
5
|
+
</MODULE_CONTRACT>
|
|
6
|
+
<CHANGE_SUMMARY>
|
|
7
|
+
<item>RFC-1150: initial nested-AGENTS.md skipDirs tests.</item>
|
|
8
|
+
</CHANGE_SUMMARY>
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { test, expect, beforeEach, afterEach } from "vitest";
|
|
12
|
+
import { mkdtemp, rm, mkdir, writeFile } from "node:fs/promises";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { runDoctor } from "../onboarding/doctor.ts";
|
|
16
|
+
import type { ForgeCommandInput, ForgeRuntimeContext } from "../types.ts";
|
|
17
|
+
|
|
18
|
+
let tempDir: string;
|
|
19
|
+
|
|
20
|
+
beforeEach(async () => {
|
|
21
|
+
tempDir = await mkdtemp(join(tmpdir(), "doctor-nested-test-"));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(async () => {
|
|
25
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function makeContext(): ForgeRuntimeContext {
|
|
29
|
+
return {
|
|
30
|
+
workspaceRoot: tempDir,
|
|
31
|
+
logger: {
|
|
32
|
+
section: () => {},
|
|
33
|
+
success: () => {},
|
|
34
|
+
warn: () => {},
|
|
35
|
+
error: () => {},
|
|
36
|
+
info: () => {},
|
|
37
|
+
},
|
|
38
|
+
dryRun: false,
|
|
39
|
+
outputFormat: "json",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function forgeYaml(skipDirs: string[]): string {
|
|
44
|
+
const skip = skipDirs.length > 0 ? `\n workspaces:\n skipDirs: [${skipDirs.join(", ")}]` : "";
|
|
45
|
+
return `schema: "forge/config@1"
|
|
46
|
+
project:
|
|
47
|
+
name: test-project
|
|
48
|
+
stack: []
|
|
49
|
+
packageManager: pnpm
|
|
50
|
+
paths:
|
|
51
|
+
rfcsDir: docs/rfcs
|
|
52
|
+
adrsDir: docs/adrs
|
|
53
|
+
plansDir: docs/plans
|
|
54
|
+
auditsDir: docs/audits
|
|
55
|
+
specsDir: docs/specs
|
|
56
|
+
skillsDir: .agents/skills
|
|
57
|
+
sessionsDir: docs/sessions
|
|
58
|
+
bindings:
|
|
59
|
+
schema: forge/bindings@1
|
|
60
|
+
commands:
|
|
61
|
+
validateRfc: null
|
|
62
|
+
validateAdr: null
|
|
63
|
+
implementStamp: null
|
|
64
|
+
typecheck: null
|
|
65
|
+
test: null
|
|
66
|
+
scopedBuild: null
|
|
67
|
+
specValidate: null
|
|
68
|
+
sessionSave: null
|
|
69
|
+
paths:
|
|
70
|
+
invariantsFile: null
|
|
71
|
+
compassDocs: []
|
|
72
|
+
reviewsDir: null
|
|
73
|
+
handoffsDir: null
|
|
74
|
+
sessionsDir: null${skip}
|
|
75
|
+
`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
test("nested-AGENTS.md check excludes dirs declared in workspaces.skipDirs", async () => {
|
|
79
|
+
await writeFile(join(tempDir, "forge.yaml"), forgeYaml(["builds"]), "utf8");
|
|
80
|
+
// Artifact dir that would otherwise be discovered as a workspace
|
|
81
|
+
await mkdir(join(tempDir, "builds", "out"), { recursive: true });
|
|
82
|
+
await writeFile(join(tempDir, "builds", "out", "package.json"), "{}");
|
|
83
|
+
// Real workspace without AGENTS.md — must still be reported missing
|
|
84
|
+
await mkdir(join(tempDir, "packages", "real-pkg"), { recursive: true });
|
|
85
|
+
await writeFile(join(tempDir, "packages", "real-pkg", "package.json"), "{}");
|
|
86
|
+
|
|
87
|
+
const input: ForgeCommandInput = { argv: [], flags: {} };
|
|
88
|
+
const result = await runDoctor(input, makeContext());
|
|
89
|
+
|
|
90
|
+
const check = result.data!.checks.find((c) => c.name === "nested-AGENTS.md");
|
|
91
|
+
expect(check).toBeDefined();
|
|
92
|
+
// missing-only reports pass (warn requires stale>0) — assert on message
|
|
93
|
+
expect(check!.message).toContain("packages/real-pkg");
|
|
94
|
+
expect(check!.message).not.toContain("builds");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("nested-AGENTS.md check reports artifact dir as missing without skipDirs", async () => {
|
|
98
|
+
await writeFile(join(tempDir, "forge.yaml"), forgeYaml([]), "utf8");
|
|
99
|
+
await mkdir(join(tempDir, "builds", "out"), { recursive: true });
|
|
100
|
+
await writeFile(join(tempDir, "builds", "out", "package.json"), "{}");
|
|
101
|
+
|
|
102
|
+
const input: ForgeCommandInput = { argv: [], flags: {} };
|
|
103
|
+
const result = await runDoctor(input, makeContext());
|
|
104
|
+
|
|
105
|
+
const check = result.data!.checks.find((c) => c.name === "nested-AGENTS.md");
|
|
106
|
+
expect(check).toBeDefined();
|
|
107
|
+
expect(check!.message).toContain("builds");
|
|
108
|
+
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Unit tests for forge memory.compact — mechanical MEMORY.md budget
|
|
4
|
+
enforcement via oldest-first Environment notes truncation (RFC-1151).</purpose>
|
|
5
|
+
</MODULE_CONTRACT>
|
|
6
|
+
<CHANGE_SUMMARY>
|
|
7
|
+
<item>RFC-1151: initial memory.compact tests.</item>
|
|
8
|
+
</CHANGE_SUMMARY>
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { test, expect, beforeEach, afterEach } from "vitest";
|
|
12
|
+
import { mkdtemp, rm, mkdir, writeFile, readFile } from "node:fs/promises";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { runMemoryCompact, compactMemoryMd } from "../onboarding/memory-compact.ts";
|
|
16
|
+
import type { ForgeRuntimeContext } from "../types.ts";
|
|
17
|
+
|
|
18
|
+
let tempDir: string;
|
|
19
|
+
|
|
20
|
+
beforeEach(async () => {
|
|
21
|
+
tempDir = await mkdtemp(join(tmpdir(), "memory-compact-test-"));
|
|
22
|
+
await mkdir(join(tempDir, ".agents", "memory"), { recursive: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(async () => {
|
|
26
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
function makeContext(dryRun = false): ForgeRuntimeContext {
|
|
30
|
+
return {
|
|
31
|
+
workspaceRoot: tempDir,
|
|
32
|
+
logger: {
|
|
33
|
+
section: () => {},
|
|
34
|
+
success: () => {},
|
|
35
|
+
warn: () => {},
|
|
36
|
+
error: () => {},
|
|
37
|
+
info: () => {},
|
|
38
|
+
},
|
|
39
|
+
dryRun,
|
|
40
|
+
outputFormat: "json",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function memoryMd(envBullets: number, bulletSize = 200): string {
|
|
45
|
+
const env = Array.from(
|
|
46
|
+
{ length: envBullets },
|
|
47
|
+
(_, i) => `- env note ${i} ${"x".repeat(bulletSize)}`,
|
|
48
|
+
).join("\n");
|
|
49
|
+
return [
|
|
50
|
+
"# Project Memory",
|
|
51
|
+
"",
|
|
52
|
+
"## Current focus",
|
|
53
|
+
"",
|
|
54
|
+
"- keep me",
|
|
55
|
+
"",
|
|
56
|
+
"## Decisions in flight",
|
|
57
|
+
"",
|
|
58
|
+
"- keep me too",
|
|
59
|
+
"",
|
|
60
|
+
"## Environment notes",
|
|
61
|
+
"",
|
|
62
|
+
env,
|
|
63
|
+
"",
|
|
64
|
+
].join("\n");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
test("compactMemoryMd removes oldest Environment notes bullets until within budget", () => {
|
|
68
|
+
const content = memoryMd(20);
|
|
69
|
+
const budget = 2000;
|
|
70
|
+
const { lines, removedLines, fitsBudget } = compactMemoryMd(content, budget);
|
|
71
|
+
|
|
72
|
+
expect(fitsBudget).toBe(true);
|
|
73
|
+
expect(removedLines.length).toBeGreaterThan(0);
|
|
74
|
+
// Oldest-first: env note 0 removed before env note 19
|
|
75
|
+
expect(removedLines[0]).toContain("env note 0");
|
|
76
|
+
expect(lines.join("\n").length).toBeLessThanOrEqual(budget);
|
|
77
|
+
// Protected sections untouched
|
|
78
|
+
expect(lines.join("\n")).toContain("- keep me");
|
|
79
|
+
expect(lines.join("\n")).toContain("- keep me too");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("compactMemoryMd returns empty removedLines when already within budget", () => {
|
|
83
|
+
const content = memoryMd(2, 10);
|
|
84
|
+
const { removedLines, fitsBudget } = compactMemoryMd(content, 4096);
|
|
85
|
+
expect(removedLines).toHaveLength(0);
|
|
86
|
+
expect(fitsBudget).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("runMemoryCompact writes compacted file and reports removed count", async () => {
|
|
90
|
+
const content = memoryMd(30);
|
|
91
|
+
await writeFile(join(tempDir, ".agents", "memory", "MEMORY.md"), content, "utf8");
|
|
92
|
+
|
|
93
|
+
const result = await runMemoryCompact({ argv: [], flags: {} }, makeContext());
|
|
94
|
+
expect(result.exitCode).toBe(0);
|
|
95
|
+
expect(result.data?.status).toBe("pass");
|
|
96
|
+
expect(result.data?.removed).toBeGreaterThan(0);
|
|
97
|
+
expect(result.data?.remainingChars).toBeLessThanOrEqual(result.data!.budget);
|
|
98
|
+
|
|
99
|
+
const after = await readFile(join(tempDir, ".agents", "memory", "MEMORY.md"), "utf8");
|
|
100
|
+
expect(after.length).toBeLessThanOrEqual(result.data!.budget);
|
|
101
|
+
expect(after).toContain("- keep me");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("runMemoryCompact dryRun reports without writing", async () => {
|
|
105
|
+
const content = memoryMd(30);
|
|
106
|
+
await writeFile(join(tempDir, ".agents", "memory", "MEMORY.md"), content, "utf8");
|
|
107
|
+
|
|
108
|
+
const result = await runMemoryCompact({ argv: [], flags: {} }, makeContext(true));
|
|
109
|
+
expect(result.exitCode).toBe(0);
|
|
110
|
+
expect(result.data?.removed).toBeGreaterThan(0);
|
|
111
|
+
expect(result.data?.removedLines?.length).toBe(result.data?.removed);
|
|
112
|
+
|
|
113
|
+
const after = await readFile(join(tempDir, ".agents", "memory", "MEMORY.md"), "utf8");
|
|
114
|
+
expect(after).toBe(content);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("runMemoryCompact passes when MEMORY.md absent", async () => {
|
|
118
|
+
const result = await runMemoryCompact({ argv: [], flags: {} }, makeContext());
|
|
119
|
+
expect(result.exitCode).toBe(0);
|
|
120
|
+
expect(result.data?.removed).toBe(0);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("runMemoryCompact fails when protected sections alone exceed budget", async () => {
|
|
124
|
+
// Huge Current focus — Environment notes empty, nothing removable
|
|
125
|
+
const content = [
|
|
126
|
+
"# Project Memory",
|
|
127
|
+
"",
|
|
128
|
+
"## Current focus",
|
|
129
|
+
"",
|
|
130
|
+
`- ${"y".repeat(6000)}`,
|
|
131
|
+
"",
|
|
132
|
+
"## Environment notes",
|
|
133
|
+
"",
|
|
134
|
+
"- small note",
|
|
135
|
+
"",
|
|
136
|
+
].join("\n");
|
|
137
|
+
await writeFile(join(tempDir, ".agents", "memory", "MEMORY.md"), content, "utf8");
|
|
138
|
+
|
|
139
|
+
const result = await runMemoryCompact({ argv: [], flags: {} }, makeContext());
|
|
140
|
+
expect(result.exitCode).toBe(1);
|
|
141
|
+
expect(result.data?.status).toBe("fail");
|
|
142
|
+
expect(result.data?.errors[0]).toContain("manual editorial compaction");
|
|
143
|
+
});
|
|
@@ -431,3 +431,56 @@ test("forge.upgrade without --update-npm does not attempt npm install", async ()
|
|
|
431
431
|
expect(result.data?.npmUpdated).toBe(false);
|
|
432
432
|
expect(result.data?.npmUpdateSkipped).toBe(null);
|
|
433
433
|
});
|
|
434
|
+
|
|
435
|
+
function qaLog(entries: { id: string; title: string; body: string }[]): string {
|
|
436
|
+
const blocks = entries.map(
|
|
437
|
+
(e) =>
|
|
438
|
+
`### ${e.id}: ${e.title}\n\n\`\`\`knowledge-entry\nid: ${e.id}\nlayer: L0\ncreated: 2026-08-03\nstatus: active\n\`\`\`\n\n${e.body}\n`,
|
|
439
|
+
);
|
|
440
|
+
return `<!-- knowledge-layer: L0 -->\n\n# Q&A Log (L0)\n\n${blocks.join("\n")}`;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
test("forge.upgrade preserves project-accumulated knowledge entries (append-only merge)", async () => {
|
|
444
|
+
await setupForgeSource(tempDir, "0.2.0");
|
|
445
|
+
// Declare a knowledge file on the fixture skill
|
|
446
|
+
await writeFile(
|
|
447
|
+
join(tempDir, "packages", "forge", "skills", "fo", "fo-idea", "SKILL.md"),
|
|
448
|
+
"---\nname: fo-idea\nknowledge: [qa-log.md]\n---\n# fo-idea\n",
|
|
449
|
+
"utf8",
|
|
450
|
+
);
|
|
451
|
+
// Package ships K-0001 + new K-0003
|
|
452
|
+
await writeFile(
|
|
453
|
+
join(tempDir, "packages", "forge", "skills", "fo", "fo-idea", "qa-log.md"),
|
|
454
|
+
qaLog([
|
|
455
|
+
{ id: "K-0001", title: "First", body: "Package body." },
|
|
456
|
+
{ id: "K-0003", title: "Third", body: "New package entry." },
|
|
457
|
+
]),
|
|
458
|
+
"utf8",
|
|
459
|
+
);
|
|
460
|
+
await setupConsumerForgeYaml(tempDir, null);
|
|
461
|
+
|
|
462
|
+
// Local copy already accumulated: edited K-0001 + project-only K-0002
|
|
463
|
+
await mkdir(join(tempDir, ".agents", "skills", "fo-idea"), { recursive: true });
|
|
464
|
+
await writeFile(
|
|
465
|
+
join(tempDir, ".agents", "skills", "fo-idea", "qa-log.md"),
|
|
466
|
+
qaLog([
|
|
467
|
+
{ id: "K-0001", title: "First", body: "Locally edited body." },
|
|
468
|
+
{ id: "K-0002", title: "Local record", body: "Project-accumulated entry." },
|
|
469
|
+
]),
|
|
470
|
+
"utf8",
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
const result = await runUpgrade({ argv: [], flags: {} }, makeContext(tempDir, false));
|
|
474
|
+
|
|
475
|
+
expect(result.data?.status).toBe("pass");
|
|
476
|
+
const merged = await readFile(join(tempDir, ".agents", "skills", "fo-idea", "qa-log.md"), "utf8");
|
|
477
|
+
// Local entries preserved — the data-loss bug would have wiped K-0002
|
|
478
|
+
expect(merged).toContain("K-0002");
|
|
479
|
+
expect(merged).toContain("Project-accumulated entry.");
|
|
480
|
+
// Local version of the conflicting entry wins
|
|
481
|
+
expect(merged).toContain("Locally edited body.");
|
|
482
|
+
expect(merged).not.toContain("Package body.");
|
|
483
|
+
// New package entry appended
|
|
484
|
+
expect(merged).toContain("K-0003");
|
|
485
|
+
expect(merged).toContain("New package entry.");
|
|
486
|
+
});
|
|
@@ -104,6 +104,34 @@ test("discoverWorkspaces skips node_modules, .git, dist, .turbo, .cache, .agents
|
|
|
104
104
|
expect(workspaces).toHaveLength(0);
|
|
105
105
|
});
|
|
106
106
|
|
|
107
|
+
test("discoverWorkspaces skips missions, test-fixtures, .stryker-tmp", async () => {
|
|
108
|
+
// Pipeline-managed workpieces/archives, fixture workshops, and mutation-testing
|
|
109
|
+
// sandboxes are not project workspaces — their AGENTS.md is owned elsewhere.
|
|
110
|
+
for (const skip of ["missions", "test-fixtures", ".stryker-tmp"]) {
|
|
111
|
+
await mkdir(join(tempDir, skip, "sub"), { recursive: true });
|
|
112
|
+
await writeFile(join(tempDir, skip, "sub", "package.json"), "{}");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const workspaces = discoverWorkspaces(tempDir);
|
|
116
|
+
expect(workspaces).toHaveLength(0);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// RFC-1150: consumer-declared exclusions (bindings.workspaces.skipDirs)
|
|
120
|
+
|
|
121
|
+
test("discoverWorkspaces honors extraSkipDirs for consumer artifact dirs", async () => {
|
|
122
|
+
await mkdir(join(tempDir, "builds", "out"), { recursive: true });
|
|
123
|
+
await writeFile(join(tempDir, "builds", "out", "package.json"), "{}");
|
|
124
|
+
await mkdir(join(tempDir, "packages", "real-pkg"), { recursive: true });
|
|
125
|
+
await writeFile(join(tempDir, "packages", "real-pkg", "package.json"), "{}");
|
|
126
|
+
|
|
127
|
+
const without = discoverWorkspaces(tempDir);
|
|
128
|
+
expect(without.map((w) => w.path)).toContain(join("builds", "out"));
|
|
129
|
+
|
|
130
|
+
const withSkip = discoverWorkspaces(tempDir, undefined, ["builds"]);
|
|
131
|
+
expect(withSkip.map((w) => w.path)).not.toContain(join("builds", "out"));
|
|
132
|
+
expect(withSkip.map((w) => w.path)).toContain(join("packages", "real-pkg"));
|
|
133
|
+
});
|
|
134
|
+
|
|
107
135
|
test("discoverWorkspaces detects generated AGENTS.md", async () => {
|
|
108
136
|
await mkdir(join(tempDir, "packages", "my-pkg"), { recursive: true });
|
|
109
137
|
await writeFile(join(tempDir, "packages", "my-pkg", "package.json"), "{}");
|