waypoint-codex 0.1.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.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +156 -0
  3. package/bin/waypoint.js +3 -0
  4. package/dist/src/cli.js +140 -0
  5. package/dist/src/core.js +605 -0
  6. package/dist/src/docs-index.js +95 -0
  7. package/dist/src/templates.js +34 -0
  8. package/dist/src/types.js +1 -0
  9. package/package.json +47 -0
  10. package/templates/.agents/skills/error-audit/SKILL.md +69 -0
  11. package/templates/.agents/skills/error-audit/references/error-patterns.md +37 -0
  12. package/templates/.agents/skills/observability-audit/SKILL.md +67 -0
  13. package/templates/.agents/skills/observability-audit/references/observability-patterns.md +35 -0
  14. package/templates/.agents/skills/planning/SKILL.md +133 -0
  15. package/templates/.agents/skills/ux-states-audit/SKILL.md +63 -0
  16. package/templates/.agents/skills/ux-states-audit/references/ux-patterns.md +34 -0
  17. package/templates/.codex/agents/code-health-reviewer.toml +14 -0
  18. package/templates/.codex/agents/code-reviewer.toml +14 -0
  19. package/templates/.codex/agents/docs-researcher.toml +14 -0
  20. package/templates/.codex/agents/plan-reviewer.toml +14 -0
  21. package/templates/.codex/config.toml +19 -0
  22. package/templates/.gitignore.snippet +3 -0
  23. package/templates/.waypoint/README.md +13 -0
  24. package/templates/.waypoint/SOUL.md +54 -0
  25. package/templates/.waypoint/agent-operating-manual.md +79 -0
  26. package/templates/.waypoint/agents/code-health-reviewer.md +87 -0
  27. package/templates/.waypoint/agents/code-reviewer.md +102 -0
  28. package/templates/.waypoint/agents/docs-researcher.md +73 -0
  29. package/templates/.waypoint/agents/plan-reviewer.md +86 -0
  30. package/templates/.waypoint/automations/README.md +18 -0
  31. package/templates/.waypoint/automations/docs-garden.toml +7 -0
  32. package/templates/.waypoint/automations/repo-health.toml +8 -0
  33. package/templates/.waypoint/config.toml +13 -0
  34. package/templates/.waypoint/rules/README.md +6 -0
  35. package/templates/.waypoint/scripts/build-docs-index.mjs +153 -0
  36. package/templates/.waypoint/scripts/prepare-context.mjs +174 -0
  37. package/templates/WORKSPACE.md +26 -0
  38. package/templates/docs/README.md +16 -0
  39. package/templates/docs/code-guide.md +31 -0
  40. package/templates/managed-agents-block.md +21 -0
@@ -0,0 +1,605 @@
1
+ import { createHash } from "node:crypto";
2
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync, } from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import * as TOML from "@iarna/toml";
6
+ import { renderDocsIndex } from "./docs-index.js";
7
+ import { readTemplate, renderWaypointConfig, MANAGED_BLOCK_END, MANAGED_BLOCK_START, templatePath } from "./templates.js";
8
+ const DEFAULT_CONFIG_PATH = ".waypoint/config.toml";
9
+ const DEFAULT_DOCS_DIR = "docs";
10
+ const DEFAULT_DOCS_INDEX = "DOCS_INDEX.md";
11
+ const DEFAULT_WORKSPACE = "WORKSPACE.md";
12
+ const STATE_DIR = ".waypoint/state";
13
+ const SYNC_RECORDS_FILE = ".waypoint/state/sync-records.json";
14
+ function ensureDir(dirPath) {
15
+ mkdirSync(dirPath, { recursive: true });
16
+ }
17
+ function writeIfMissing(filePath, content) {
18
+ if (!existsSync(filePath)) {
19
+ ensureDir(path.dirname(filePath));
20
+ writeFileSync(filePath, content, "utf8");
21
+ }
22
+ }
23
+ function writeText(filePath, content) {
24
+ ensureDir(path.dirname(filePath));
25
+ writeFileSync(filePath, content, "utf8");
26
+ }
27
+ function removePathIfExists(targetPath) {
28
+ if (existsSync(targetPath)) {
29
+ rmSync(targetPath, { recursive: true, force: true });
30
+ }
31
+ }
32
+ function appendGitignoreSnippet(projectRoot) {
33
+ const gitignorePath = path.join(projectRoot, ".gitignore");
34
+ const snippet = readTemplate(".gitignore.snippet").trim();
35
+ if (!existsSync(gitignorePath)) {
36
+ writeText(gitignorePath, `${snippet}\n`);
37
+ return;
38
+ }
39
+ const content = readFileSync(gitignorePath, "utf8");
40
+ if (content.includes(snippet)) {
41
+ return;
42
+ }
43
+ writeText(gitignorePath, `${content.trimEnd()}\n\n${snippet}\n`);
44
+ }
45
+ function upsertManagedBlock(filePath, block) {
46
+ if (!existsSync(filePath)) {
47
+ writeText(filePath, `${block.trim()}\n`);
48
+ return;
49
+ }
50
+ const existing = readFileSync(filePath, "utf8");
51
+ const startIndex = existing.indexOf(MANAGED_BLOCK_START);
52
+ const endIndex = existing.indexOf(MANAGED_BLOCK_END);
53
+ if (startIndex !== -1 && endIndex !== -1 && endIndex >= startIndex) {
54
+ const afterEnd = endIndex + MANAGED_BLOCK_END.length;
55
+ const before = existing.slice(0, startIndex).trimEnd();
56
+ const after = existing.slice(afterEnd).trimStart();
57
+ const pieces = [before, block.trim(), after].filter((piece) => piece.length > 0);
58
+ writeText(filePath, `${pieces.join("\n\n")}\n`);
59
+ return;
60
+ }
61
+ const merged = existing.trimEnd();
62
+ writeText(filePath, `${merged.length > 0 ? `${merged}\n\n` : ""}${block.trim()}\n`);
63
+ }
64
+ function copyTemplateTree(sourceDir, targetDir) {
65
+ for (const entry of readdirSync(sourceDir)) {
66
+ const sourcePath = path.join(sourceDir, entry);
67
+ const targetPath = path.join(targetDir, entry);
68
+ const stat = statSync(sourcePath);
69
+ if (stat.isDirectory()) {
70
+ ensureDir(targetPath);
71
+ copyTemplateTree(sourcePath, targetPath);
72
+ }
73
+ else {
74
+ writeText(targetPath, readFileSync(sourcePath, "utf8"));
75
+ }
76
+ }
77
+ }
78
+ function scaffoldSkills(projectRoot) {
79
+ const targetRoot = path.join(projectRoot, ".agents/skills");
80
+ copyTemplateTree(templatePath(".agents/skills"), targetRoot);
81
+ }
82
+ function scaffoldWaypointOptionalTemplates(projectRoot) {
83
+ copyTemplateTree(templatePath(".waypoint/agents"), path.join(projectRoot, ".waypoint/agents"));
84
+ copyTemplateTree(templatePath(".waypoint/automations"), path.join(projectRoot, ".waypoint/automations"));
85
+ copyTemplateTree(templatePath(".waypoint/rules"), path.join(projectRoot, ".waypoint/rules"));
86
+ copyTemplateTree(templatePath(".waypoint/scripts"), path.join(projectRoot, ".waypoint/scripts"));
87
+ }
88
+ function scaffoldOptionalCodex(projectRoot) {
89
+ copyTemplateTree(templatePath(".codex"), path.join(projectRoot, ".codex"));
90
+ }
91
+ export function initRepository(projectRoot, options) {
92
+ ensureDir(projectRoot);
93
+ for (const deprecatedPath of [
94
+ ".agents/skills/waypoint-planning",
95
+ ".agents/skills/waypoint-docs",
96
+ ".agents/skills/waypoint-review",
97
+ ".agents/skills/waypoint-maintain",
98
+ ".agents/skills/waypoint-error-audit",
99
+ ".agents/skills/waypoint-observability-audit",
100
+ ".agents/skills/waypoint-ux-states-audit",
101
+ ".agents/skills/waypoint-frontmatter",
102
+ ".agents/skills/waypoint-explore",
103
+ ".agents/skills/waypoint-research",
104
+ ".codex/agents/explorer.toml",
105
+ ".codex/agents/reviewer.toml",
106
+ ".codex/agents/architect.toml",
107
+ ".codex/agents/implementer.toml",
108
+ ]) {
109
+ removePathIfExists(path.join(projectRoot, deprecatedPath));
110
+ }
111
+ ensureDir(path.join(projectRoot, ".waypoint/automations"));
112
+ ensureDir(path.join(projectRoot, ".waypoint/rules"));
113
+ ensureDir(path.join(projectRoot, STATE_DIR));
114
+ writeText(path.join(projectRoot, ".waypoint/README.md"), readTemplate(".waypoint/README.md"));
115
+ writeText(path.join(projectRoot, ".waypoint/SOUL.md"), readTemplate(".waypoint/SOUL.md"));
116
+ writeText(path.join(projectRoot, ".waypoint/agent-operating-manual.md"), readTemplate(".waypoint/agent-operating-manual.md"));
117
+ scaffoldWaypointOptionalTemplates(projectRoot);
118
+ writeText(path.join(projectRoot, DEFAULT_CONFIG_PATH), renderWaypointConfig({
119
+ profile: options.profile,
120
+ roles: options.withRoles,
121
+ rules: options.withRules,
122
+ automations: options.withAutomations,
123
+ }));
124
+ writeIfMissing(path.join(projectRoot, DEFAULT_WORKSPACE), readTemplate("WORKSPACE.md"));
125
+ ensureDir(path.join(projectRoot, DEFAULT_DOCS_DIR));
126
+ writeIfMissing(path.join(projectRoot, "docs/README.md"), readTemplate("docs/README.md"));
127
+ writeIfMissing(path.join(projectRoot, "docs/code-guide.md"), readTemplate("docs/code-guide.md"));
128
+ upsertManagedBlock(path.join(projectRoot, "AGENTS.md"), readTemplate("managed-agents-block.md"));
129
+ scaffoldSkills(projectRoot);
130
+ if (options.withRoles) {
131
+ scaffoldOptionalCodex(projectRoot);
132
+ }
133
+ appendGitignoreSnippet(projectRoot);
134
+ const docsIndex = renderDocsIndex(projectRoot, path.join(projectRoot, DEFAULT_DOCS_DIR));
135
+ writeText(path.join(projectRoot, DEFAULT_DOCS_INDEX), `${docsIndex.content}\n`);
136
+ return [
137
+ "Initialized Waypoint scaffold",
138
+ "Installed managed AGENTS block",
139
+ "Created WORKSPACE.md and docs/ scaffold",
140
+ "Installed repo-local Waypoint skills",
141
+ "Generated DOCS_INDEX.md",
142
+ ];
143
+ }
144
+ export function loadWaypointConfig(projectRoot) {
145
+ const configPath = path.join(projectRoot, DEFAULT_CONFIG_PATH);
146
+ if (!existsSync(configPath)) {
147
+ return {};
148
+ }
149
+ return TOML.parse(readFileSync(configPath, "utf8"));
150
+ }
151
+ function loadSyncRecords(projectRoot) {
152
+ const syncPath = path.join(projectRoot, SYNC_RECORDS_FILE);
153
+ if (!existsSync(syncPath)) {
154
+ return {};
155
+ }
156
+ try {
157
+ return JSON.parse(readFileSync(syncPath, "utf8"));
158
+ }
159
+ catch {
160
+ return {};
161
+ }
162
+ }
163
+ function saveSyncRecords(projectRoot, records) {
164
+ writeText(path.join(projectRoot, SYNC_RECORDS_FILE), `${JSON.stringify(records, null, 2)}\n`);
165
+ }
166
+ function hashFile(filePath) {
167
+ return createHash("sha256").update(readFileSync(filePath)).digest("hex");
168
+ }
169
+ function codexHome() {
170
+ return process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex");
171
+ }
172
+ function renderCodexAutomation(spec, cwd) {
173
+ const now = Date.now();
174
+ const rrule = spec.rrule?.startsWith("RRULE:") ? spec.rrule : `RRULE:${spec.rrule}`;
175
+ return [
176
+ "version = 1",
177
+ `id = ${JSON.stringify(spec.id)}`,
178
+ `name = ${JSON.stringify(spec.name)}`,
179
+ `prompt = ${JSON.stringify(spec.prompt)}`,
180
+ `status = ${JSON.stringify(spec.status ?? "ACTIVE")}`,
181
+ `rrule = ${JSON.stringify(rrule)}`,
182
+ `execution_environment = ${JSON.stringify(spec.execution_environment ?? "worktree")}`,
183
+ `cwds = ${JSON.stringify(spec.cwds ?? [cwd])}`,
184
+ `created_at = ${now}`,
185
+ `updated_at = ${now}`,
186
+ ].join("\n") + "\n";
187
+ }
188
+ function isKebabCase(value) {
189
+ return /^[a-z0-9-]+$/.test(value);
190
+ }
191
+ function validateAutomationSpecFile(filePath) {
192
+ const errors = [];
193
+ let parsed;
194
+ try {
195
+ parsed = TOML.parse(readFileSync(filePath, "utf8"));
196
+ }
197
+ catch (error) {
198
+ return [`invalid TOML: ${error instanceof Error ? error.message : String(error)}`];
199
+ }
200
+ for (const key of ["id", "name", "prompt", "rrule"]) {
201
+ if (!parsed[key]) {
202
+ errors.push(`missing required key \`${key}\``);
203
+ }
204
+ }
205
+ if (parsed.id && !isKebabCase(parsed.id)) {
206
+ errors.push("automation id must use lowercase kebab-case");
207
+ }
208
+ return errors;
209
+ }
210
+ function syncAutomations(projectRoot) {
211
+ const sourceDir = path.join(projectRoot, ".waypoint/automations");
212
+ if (!existsSync(sourceDir)) {
213
+ return [];
214
+ }
215
+ const targetRoot = path.join(codexHome(), "automations");
216
+ ensureDir(targetRoot);
217
+ const records = loadSyncRecords(projectRoot);
218
+ const results = [];
219
+ for (const entry of readdirSync(sourceDir)) {
220
+ if (!entry.endsWith(".toml")) {
221
+ continue;
222
+ }
223
+ const sourcePath = path.join(sourceDir, entry);
224
+ const errors = validateAutomationSpecFile(sourcePath);
225
+ if (errors.length > 0) {
226
+ results.push(`Skipped ${entry}: ${errors.join("; ")}`);
227
+ continue;
228
+ }
229
+ const spec = TOML.parse(readFileSync(sourcePath, "utf8"));
230
+ if (spec.enabled === false) {
231
+ continue;
232
+ }
233
+ const targetDir = path.join(targetRoot, spec.id);
234
+ const targetPath = path.join(targetDir, "automation.toml");
235
+ ensureDir(targetDir);
236
+ writeText(targetPath, renderCodexAutomation(spec, projectRoot));
237
+ records[sourcePath] = {
238
+ artifact_type: "automation",
239
+ source_path: sourcePath,
240
+ target_path: targetPath,
241
+ source_hash: hashFile(sourcePath),
242
+ target_hash: hashFile(targetPath),
243
+ };
244
+ results.push(`Synced automation \`${spec.id}\``);
245
+ }
246
+ saveSyncRecords(projectRoot, records);
247
+ return results;
248
+ }
249
+ function syncRules(projectRoot) {
250
+ const sourceDir = path.join(projectRoot, ".waypoint/rules");
251
+ if (!existsSync(sourceDir)) {
252
+ return [];
253
+ }
254
+ const targetRoot = path.join(codexHome(), "rules");
255
+ ensureDir(targetRoot);
256
+ const records = loadSyncRecords(projectRoot);
257
+ const results = [];
258
+ for (const entry of readdirSync(sourceDir)) {
259
+ if (!entry.endsWith(".rules")) {
260
+ continue;
261
+ }
262
+ const sourcePath = path.join(sourceDir, entry);
263
+ const targetPath = path.join(targetRoot, `waypoint-${entry}`);
264
+ copyFileSync(sourcePath, targetPath);
265
+ records[sourcePath] = {
266
+ artifact_type: "rules",
267
+ source_path: sourcePath,
268
+ target_path: targetPath,
269
+ source_hash: hashFile(sourcePath),
270
+ target_hash: hashFile(targetPath),
271
+ };
272
+ results.push(`Synced rules \`${entry}\``);
273
+ }
274
+ saveSyncRecords(projectRoot, records);
275
+ return results;
276
+ }
277
+ export function doctorRepository(projectRoot) {
278
+ const findings = [];
279
+ const config = loadWaypointConfig(projectRoot);
280
+ if (Object.keys(config).length === 0) {
281
+ findings.push({
282
+ severity: "error",
283
+ category: "install",
284
+ message: "Waypoint config is missing.",
285
+ remediation: "Run `waypoint init` in the repository.",
286
+ paths: [path.join(projectRoot, DEFAULT_CONFIG_PATH)],
287
+ });
288
+ return findings;
289
+ }
290
+ const agentsPath = path.join(projectRoot, "AGENTS.md");
291
+ if (!existsSync(agentsPath)) {
292
+ findings.push({
293
+ severity: "error",
294
+ category: "install",
295
+ message: "AGENTS.md is missing.",
296
+ remediation: "Run `waypoint init` or restore AGENTS.md.",
297
+ paths: [agentsPath],
298
+ });
299
+ }
300
+ else {
301
+ const agentsContent = readFileSync(agentsPath, "utf8");
302
+ if (!agentsContent.includes(MANAGED_BLOCK_START) || !agentsContent.includes(MANAGED_BLOCK_END)) {
303
+ findings.push({
304
+ severity: "error",
305
+ category: "install",
306
+ message: "Waypoint managed block is missing from AGENTS.md.",
307
+ remediation: "Run `waypoint init` to repair the managed block.",
308
+ paths: [agentsPath],
309
+ });
310
+ }
311
+ }
312
+ const workspacePath = path.join(projectRoot, config.workspace_file ?? DEFAULT_WORKSPACE);
313
+ if (!existsSync(workspacePath)) {
314
+ findings.push({
315
+ severity: "error",
316
+ category: "workspace",
317
+ message: "WORKSPACE.md is missing.",
318
+ remediation: "Run `waypoint init` to scaffold the workspace file.",
319
+ paths: [workspacePath],
320
+ });
321
+ }
322
+ else {
323
+ const workspaceText = readFileSync(workspacePath, "utf8");
324
+ for (const section of [
325
+ "## Active Goal",
326
+ "## Current State",
327
+ "## In Progress",
328
+ "## Next",
329
+ "## Parked",
330
+ "## Done Recently",
331
+ ]) {
332
+ if (!workspaceText.includes(section)) {
333
+ findings.push({
334
+ severity: "warn",
335
+ category: "workspace",
336
+ message: `Workspace file is missing section \`${section}\`.`,
337
+ remediation: "Restore the standard Waypoint workspace sections.",
338
+ paths: [workspacePath],
339
+ });
340
+ }
341
+ }
342
+ }
343
+ for (const requiredFile of [
344
+ path.join(projectRoot, ".waypoint", "SOUL.md"),
345
+ path.join(projectRoot, ".waypoint", "agent-operating-manual.md"),
346
+ path.join(projectRoot, ".waypoint", "scripts", "prepare-context.mjs"),
347
+ path.join(projectRoot, ".waypoint", "scripts", "build-docs-index.mjs"),
348
+ ]) {
349
+ if (!existsSync(requiredFile)) {
350
+ findings.push({
351
+ severity: "error",
352
+ category: "install",
353
+ message: `Required Waypoint file is missing: ${path.relative(projectRoot, requiredFile)}`,
354
+ remediation: "Run `waypoint init` to restore the missing file.",
355
+ paths: [requiredFile],
356
+ });
357
+ }
358
+ }
359
+ const docsDir = path.join(projectRoot, config.docs_dir ?? DEFAULT_DOCS_DIR);
360
+ const docsIndexPath = path.join(projectRoot, config.docs_index_file ?? DEFAULT_DOCS_INDEX);
361
+ const docsIndex = renderDocsIndex(projectRoot, docsDir);
362
+ if (!existsSync(docsDir)) {
363
+ findings.push({
364
+ severity: "error",
365
+ category: "docs",
366
+ message: "docs/ directory is missing.",
367
+ remediation: "Run `waypoint init` to scaffold docs.",
368
+ paths: [docsDir],
369
+ });
370
+ }
371
+ for (const relPath of docsIndex.invalidDocs) {
372
+ findings.push({
373
+ severity: "warn",
374
+ category: "docs",
375
+ message: `Doc is missing valid frontmatter: ${relPath}`,
376
+ remediation: "Add `summary` and `read_when` frontmatter.",
377
+ paths: [path.join(projectRoot, relPath)],
378
+ });
379
+ }
380
+ if (!existsSync(docsIndexPath)) {
381
+ findings.push({
382
+ severity: "warn",
383
+ category: "docs",
384
+ message: "DOCS_INDEX.md is missing.",
385
+ remediation: "Run `waypoint sync` to generate the docs index.",
386
+ paths: [docsIndexPath],
387
+ });
388
+ }
389
+ else if (readFileSync(docsIndexPath, "utf8").trimEnd() !== docsIndex.content.trimEnd()) {
390
+ findings.push({
391
+ severity: "warn",
392
+ category: "docs",
393
+ message: "DOCS_INDEX.md is stale.",
394
+ remediation: "Run `waypoint sync` to rebuild the docs index.",
395
+ paths: [docsIndexPath],
396
+ });
397
+ }
398
+ for (const skillName of [
399
+ "planning",
400
+ "error-audit",
401
+ "observability-audit",
402
+ "ux-states-audit",
403
+ ]) {
404
+ const skillPath = path.join(projectRoot, ".agents/skills", skillName, "SKILL.md");
405
+ if (!existsSync(skillPath)) {
406
+ findings.push({
407
+ severity: "error",
408
+ category: "skills",
409
+ message: `Repo skill \`${skillName}\` is missing.`,
410
+ remediation: "Run `waypoint init` to restore repo-local skills.",
411
+ paths: [skillPath],
412
+ });
413
+ }
414
+ }
415
+ const featureMap = config.features ?? {};
416
+ if (featureMap.automations) {
417
+ const records = loadSyncRecords(projectRoot);
418
+ const automationDir = path.join(projectRoot, ".waypoint/automations");
419
+ if (existsSync(automationDir)) {
420
+ for (const entry of readdirSync(automationDir)) {
421
+ if (!entry.endsWith(".toml")) {
422
+ continue;
423
+ }
424
+ const filePath = path.join(automationDir, entry);
425
+ const errors = validateAutomationSpecFile(filePath);
426
+ for (const error of errors) {
427
+ findings.push({
428
+ severity: "error",
429
+ category: "automations",
430
+ message: `${path.relative(projectRoot, filePath)}: ${error}`,
431
+ remediation: "Fix the automation spec and rerun `waypoint sync`.",
432
+ paths: [filePath],
433
+ });
434
+ }
435
+ if (errors.length === 0) {
436
+ const spec = TOML.parse(readFileSync(filePath, "utf8"));
437
+ if (spec.enabled === false) {
438
+ continue;
439
+ }
440
+ }
441
+ if (errors.length === 0 && !records[filePath]) {
442
+ findings.push({
443
+ severity: "info",
444
+ category: "automations",
445
+ message: `Automation \`${path.basename(entry, ".toml")}\` has not been synced.`,
446
+ remediation: "Run `waypoint sync` to install it into Codex home.",
447
+ paths: [filePath],
448
+ });
449
+ }
450
+ }
451
+ }
452
+ }
453
+ if (featureMap.roles) {
454
+ const codexConfigPath = path.join(projectRoot, ".codex/config.toml");
455
+ if (!existsSync(codexConfigPath)) {
456
+ findings.push({
457
+ severity: "warn",
458
+ category: "roles",
459
+ message: "Role support is enabled but .codex/config.toml is missing.",
460
+ remediation: "Run `waypoint init --with-roles` or create the project Codex config files.",
461
+ paths: [codexConfigPath],
462
+ });
463
+ }
464
+ }
465
+ return findings;
466
+ }
467
+ export function syncRepository(projectRoot) {
468
+ const config = loadWaypointConfig(projectRoot);
469
+ const docsDir = path.join(projectRoot, config.docs_dir ?? DEFAULT_DOCS_DIR);
470
+ const docsIndexPath = path.join(projectRoot, config.docs_index_file ?? DEFAULT_DOCS_INDEX);
471
+ const docsIndex = renderDocsIndex(projectRoot, docsDir);
472
+ writeText(docsIndexPath, `${docsIndex.content}\n`);
473
+ const results = ["Rebuilt DOCS_INDEX.md"];
474
+ const featureMap = config.features ?? {};
475
+ if (featureMap.rules) {
476
+ results.push(...syncRules(projectRoot));
477
+ }
478
+ if (featureMap.automations) {
479
+ results.push(...syncAutomations(projectRoot));
480
+ }
481
+ return results;
482
+ }
483
+ export function importLegacyRepo(sourceRepo, targetRepo, options = {}) {
484
+ const sourceDocsDir = path.join(sourceRepo, ".meridian/docs");
485
+ const sourceSkillsDir = path.join(sourceRepo, "skills");
486
+ const sourceCommandsDir = path.join(sourceRepo, "commands");
487
+ const sourceAgentsDir = path.join(sourceRepo, "agents");
488
+ const sourceHooksPath = path.join(sourceRepo, "hooks/hooks.json");
489
+ const sourceScriptsDir = path.join(sourceRepo, "scripts");
490
+ const portableDocs = existsSync(sourceDocsDir)
491
+ ? readdirSync(sourceDocsDir).filter((entry) => entry.endsWith(".md")).sort()
492
+ : [];
493
+ const portableSkills = existsSync(sourceSkillsDir)
494
+ ? readdirSync(sourceSkillsDir)
495
+ .map((entry) => path.join("skills", entry, "SKILL.md"))
496
+ .filter((relPath) => existsSync(path.join(sourceRepo, relPath)))
497
+ .sort()
498
+ : [];
499
+ const portableCommands = existsSync(sourceCommandsDir)
500
+ ? readdirSync(sourceCommandsDir)
501
+ .filter((entry) => entry.endsWith(".md"))
502
+ .map((entry) => path.join("commands", entry))
503
+ .sort()
504
+ : [];
505
+ const agentFiles = existsSync(sourceAgentsDir)
506
+ ? readdirSync(sourceAgentsDir)
507
+ .filter((entry) => entry.endsWith(".md"))
508
+ .map((entry) => path.join("agents", entry))
509
+ .sort()
510
+ : [];
511
+ const hookFiles = existsSync(sourceHooksPath) ? [path.join("hooks", "hooks.json")] : [];
512
+ const scriptFiles = existsSync(sourceScriptsDir)
513
+ ? collectFiles(sourceScriptsDir)
514
+ .filter((filePath) => filePath.endsWith(".py"))
515
+ .map((filePath) => path.relative(sourceRepo, filePath))
516
+ .sort()
517
+ : [];
518
+ const actions = [];
519
+ if (targetRepo && options.initTarget) {
520
+ actions.push(...initRepository(targetRepo, {
521
+ profile: "universal",
522
+ withRoles: false,
523
+ withRules: false,
524
+ withAutomations: false,
525
+ }));
526
+ }
527
+ if (targetRepo) {
528
+ const importDir = path.join(targetRepo, "docs/legacy-import");
529
+ ensureDir(importDir);
530
+ for (const docName of portableDocs) {
531
+ copyFileSync(path.join(sourceDocsDir, docName), path.join(importDir, docName));
532
+ }
533
+ if (portableDocs.length > 0) {
534
+ actions.push(`Copied ${portableDocs.length} legacy docs into ${importDir}`);
535
+ }
536
+ const docsIndex = renderDocsIndex(targetRepo, path.join(targetRepo, DEFAULT_DOCS_DIR));
537
+ writeText(path.join(targetRepo, DEFAULT_DOCS_INDEX), `${docsIndex.content}\n`);
538
+ }
539
+ const report = [
540
+ "# Legacy Repository Adoption Report",
541
+ "",
542
+ `Source: \`${sourceRepo}\``,
543
+ "",
544
+ "## Portable as-is or with light rewriting",
545
+ "",
546
+ `- Docs: ${portableDocs.length}`,
547
+ `- Skills: ${portableSkills.length}`,
548
+ `- Commands/prompts worth reviewing for skill conversion: ${portableCommands.length}`,
549
+ "",
550
+ "### Docs",
551
+ ...(portableDocs.length > 0
552
+ ? portableDocs.map((entry) => `- \`.meridian/docs/${entry}\``)
553
+ : ["- None"]),
554
+ "",
555
+ "### Skills",
556
+ ...(portableSkills.length > 0 ? portableSkills.map((entry) => `- \`${entry}\``) : ["- None"]),
557
+ "",
558
+ "## Replace with explicit Waypoint patterns",
559
+ "",
560
+ "- hook-based session injection -> AGENTS routing, context generation, repo-local skills, doctor/sync",
561
+ "- hidden stop-hook policing -> advisory workflow skills and visible repo state",
562
+ "- transcript learners -> maintenance skills and optional app automations",
563
+ "- opaque reviewer plumbing -> optional Codex roles where actually useful",
564
+ "",
565
+ "## Legacy machinery to drop",
566
+ "",
567
+ `- Hook registration files: ${hookFiles.length}`,
568
+ `- Hook/runtime scripts: ${scriptFiles.length}`,
569
+ "",
570
+ "### Hook/runtime files",
571
+ ...((hookFiles.length + scriptFiles.length) > 0
572
+ ? [...hookFiles, ...scriptFiles].map((entry) => `- \`${entry}\``)
573
+ : ["- None"]),
574
+ "",
575
+ "## Agent files to reinterpret, not port literally",
576
+ "",
577
+ ...(agentFiles.length > 0 ? agentFiles.map((entry) => `- \`${entry}\``) : ["- None"]),
578
+ "",
579
+ "## Notes",
580
+ "",
581
+ "- The strongest reusable assets are methodology, docs patterns, and review/planning discipline.",
582
+ "- The weakest portability surface is hook-dependent session machinery and transcript-coupled automation.",
583
+ "",
584
+ ].join("\n");
585
+ if (targetRepo) {
586
+ const reportPath = path.join(targetRepo, "WAYPOINT_MIGRATION.md");
587
+ writeText(reportPath, report);
588
+ actions.push(`Wrote migration report to ${reportPath}`);
589
+ }
590
+ return { report, actions };
591
+ }
592
+ function collectFiles(rootDir) {
593
+ const output = [];
594
+ for (const entry of readdirSync(rootDir)) {
595
+ const fullPath = path.join(rootDir, entry);
596
+ const stat = statSync(fullPath);
597
+ if (stat.isDirectory()) {
598
+ output.push(...collectFiles(fullPath));
599
+ }
600
+ else {
601
+ output.push(fullPath);
602
+ }
603
+ }
604
+ return output;
605
+ }
@@ -0,0 +1,95 @@
1
+ import { readFileSync, existsSync, readdirSync, statSync } from "node:fs";
2
+ import path from "node:path";
3
+ const SKIP_DIRS = new Set([
4
+ ".git",
5
+ ".next",
6
+ "node_modules",
7
+ "dist",
8
+ "build",
9
+ "__pycache__",
10
+ ".waypoint",
11
+ ]);
12
+ const SKIP_NAMES = new Set(["README.md", "CHANGELOG.md", "LICENSE.md"]);
13
+ function parseFrontmatter(filePath) {
14
+ const text = readFileSync(filePath, "utf8");
15
+ if (!text.startsWith("---\n")) {
16
+ return { summary: "", readWhen: [] };
17
+ }
18
+ const endIndex = text.indexOf("\n---\n", 4);
19
+ if (endIndex === -1) {
20
+ return { summary: "", readWhen: [] };
21
+ }
22
+ const frontmatter = text.slice(4, endIndex);
23
+ let summary = "";
24
+ const readWhen = [];
25
+ let collectingReadWhen = false;
26
+ for (const rawLine of frontmatter.split("\n")) {
27
+ const line = rawLine.trim();
28
+ if (line.startsWith("summary:")) {
29
+ summary = line.slice("summary:".length).trim().replace(/^['"]|['"]$/g, "");
30
+ collectingReadWhen = false;
31
+ continue;
32
+ }
33
+ if (line.startsWith("read_when:")) {
34
+ collectingReadWhen = true;
35
+ continue;
36
+ }
37
+ if (collectingReadWhen && line.startsWith("- ")) {
38
+ readWhen.push(line.slice(2).trim());
39
+ continue;
40
+ }
41
+ if (collectingReadWhen && line.length > 0) {
42
+ collectingReadWhen = false;
43
+ }
44
+ }
45
+ return { summary, readWhen };
46
+ }
47
+ function walkDocs(projectRoot, currentDir, output, invalid) {
48
+ for (const entry of readdirSync(currentDir)) {
49
+ const fullPath = path.join(currentDir, entry);
50
+ const stat = statSync(fullPath);
51
+ if (stat.isDirectory()) {
52
+ if (SKIP_DIRS.has(entry)) {
53
+ continue;
54
+ }
55
+ walkDocs(projectRoot, fullPath, output, invalid);
56
+ continue;
57
+ }
58
+ if (!entry.endsWith(".md") || SKIP_NAMES.has(entry)) {
59
+ continue;
60
+ }
61
+ const { summary, readWhen } = parseFrontmatter(fullPath);
62
+ const relPath = path.relative(projectRoot, fullPath);
63
+ if (!summary || readWhen.length === 0) {
64
+ invalid.push(relPath);
65
+ continue;
66
+ }
67
+ output.push({ path: relPath, summary, readWhen });
68
+ }
69
+ }
70
+ export function renderDocsIndex(projectRoot, docsDir) {
71
+ const entries = [];
72
+ const invalidDocs = [];
73
+ if (existsSync(docsDir)) {
74
+ walkDocs(projectRoot, docsDir, entries, invalidDocs);
75
+ }
76
+ const lines = [
77
+ "# Docs Index",
78
+ "",
79
+ "Auto-generated by `waypoint sync` / `waypoint doctor`. Read matching docs before working on a task.",
80
+ "",
81
+ "## docs/",
82
+ "",
83
+ ];
84
+ if (entries.length === 0) {
85
+ lines.push("No routable docs found.");
86
+ }
87
+ else {
88
+ for (const entry of entries.sort((a, b) => a.path.localeCompare(b.path))) {
89
+ lines.push(`- **${entry.path}** — ${entry.summary}`);
90
+ lines.push(` Read when: ${entry.readWhen.join("; ")}`);
91
+ }
92
+ }
93
+ lines.push("");
94
+ return { content: `${lines.join("\n")}`, invalidDocs };
95
+ }