@chllming/wave-orchestration 0.5.1

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 (68) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +549 -0
  3. package/docs/agents/wave-deploy-verifier-role.md +34 -0
  4. package/docs/agents/wave-documentation-role.md +30 -0
  5. package/docs/agents/wave-evaluator-role.md +43 -0
  6. package/docs/agents/wave-infra-role.md +34 -0
  7. package/docs/agents/wave-integration-role.md +32 -0
  8. package/docs/agents/wave-launcher-role.md +37 -0
  9. package/docs/context7/bundles.json +91 -0
  10. package/docs/plans/component-cutover-matrix.json +112 -0
  11. package/docs/plans/component-cutover-matrix.md +49 -0
  12. package/docs/plans/context7-wave-orchestrator.md +130 -0
  13. package/docs/plans/current-state.md +44 -0
  14. package/docs/plans/master-plan.md +16 -0
  15. package/docs/plans/migration.md +23 -0
  16. package/docs/plans/wave-orchestrator.md +254 -0
  17. package/docs/plans/waves/wave-0.md +165 -0
  18. package/docs/reference/github-packages-setup.md +52 -0
  19. package/docs/reference/migration-0.2-to-0.5.md +622 -0
  20. package/docs/reference/npmjs-trusted-publishing.md +55 -0
  21. package/docs/reference/repository-guidance.md +18 -0
  22. package/docs/reference/runtime-config/README.md +85 -0
  23. package/docs/reference/runtime-config/claude.md +105 -0
  24. package/docs/reference/runtime-config/codex.md +81 -0
  25. package/docs/reference/runtime-config/opencode.md +93 -0
  26. package/docs/research/agent-context-sources.md +57 -0
  27. package/docs/roadmap.md +626 -0
  28. package/package.json +53 -0
  29. package/releases/manifest.json +101 -0
  30. package/scripts/context7-api-check.sh +21 -0
  31. package/scripts/context7-export-env.sh +52 -0
  32. package/scripts/research/agent-context-archive.mjs +472 -0
  33. package/scripts/research/generate-agent-context-indexes.mjs +85 -0
  34. package/scripts/research/import-agent-context-archive.mjs +793 -0
  35. package/scripts/research/manifests/harness-and-blackboard-2026-03-21.mjs +201 -0
  36. package/scripts/wave-autonomous.mjs +13 -0
  37. package/scripts/wave-cli-bootstrap.mjs +27 -0
  38. package/scripts/wave-dashboard.mjs +11 -0
  39. package/scripts/wave-human-feedback.mjs +11 -0
  40. package/scripts/wave-launcher.mjs +11 -0
  41. package/scripts/wave-local-executor.mjs +13 -0
  42. package/scripts/wave-orchestrator/agent-state.mjs +416 -0
  43. package/scripts/wave-orchestrator/autonomous.mjs +367 -0
  44. package/scripts/wave-orchestrator/clarification-triage.mjs +605 -0
  45. package/scripts/wave-orchestrator/config.mjs +848 -0
  46. package/scripts/wave-orchestrator/context7.mjs +464 -0
  47. package/scripts/wave-orchestrator/coord-cli.mjs +286 -0
  48. package/scripts/wave-orchestrator/coordination-store.mjs +987 -0
  49. package/scripts/wave-orchestrator/coordination.mjs +768 -0
  50. package/scripts/wave-orchestrator/dashboard-renderer.mjs +254 -0
  51. package/scripts/wave-orchestrator/dashboard-state.mjs +473 -0
  52. package/scripts/wave-orchestrator/dep-cli.mjs +219 -0
  53. package/scripts/wave-orchestrator/docs-queue.mjs +75 -0
  54. package/scripts/wave-orchestrator/executors.mjs +385 -0
  55. package/scripts/wave-orchestrator/feedback.mjs +372 -0
  56. package/scripts/wave-orchestrator/install.mjs +540 -0
  57. package/scripts/wave-orchestrator/launcher.mjs +3879 -0
  58. package/scripts/wave-orchestrator/ledger.mjs +332 -0
  59. package/scripts/wave-orchestrator/local-executor.mjs +263 -0
  60. package/scripts/wave-orchestrator/replay.mjs +246 -0
  61. package/scripts/wave-orchestrator/roots.mjs +10 -0
  62. package/scripts/wave-orchestrator/routing-state.mjs +542 -0
  63. package/scripts/wave-orchestrator/shared.mjs +405 -0
  64. package/scripts/wave-orchestrator/terminals.mjs +209 -0
  65. package/scripts/wave-orchestrator/traces.mjs +1094 -0
  66. package/scripts/wave-orchestrator/wave-files.mjs +1923 -0
  67. package/scripts/wave.mjs +103 -0
  68. package/wave.config.json +115 -0
@@ -0,0 +1,101 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "packageName": "@chllming/wave-orchestration",
4
+ "releases": [
5
+ {
6
+ "version": "0.5.1",
7
+ "date": "2026-03-22",
8
+ "summary": "Phase 4 fix release plus npmjs trusted-publishing bootstrap support.",
9
+ "features": [
10
+ "Final lane completion now stays blocked on unresolved human feedback and escalation tickets from completed waves, matching the intended Phase 4 barrier behavior.",
11
+ "Hermetic trace fixtures now rewrite seeded per-agent executor blocks so local replay coverage cannot accidentally launch live Codex, Claude Code, or OpenCode sessions.",
12
+ "A dedicated npmjs publish workflow now exists alongside the GitHub Packages workflow, with `package.json` publish metadata updated so each workflow can target its own registry cleanly.",
13
+ "Install and maintainer docs now distinguish the current GitHub Packages path from the future zero-token npmjs path and document trusted-publishing bootstrap."
14
+ ],
15
+ "manualSteps": [
16
+ "Push `main` and tag `v0.5.1` before publishing, so the npm tarball matches the tagged source exactly.",
17
+ "For the first npmjs release, authenticate locally and run `npm publish --access public --registry=https://registry.npmjs.org` from the `0.5.1` checkout.",
18
+ "After the first npmjs release exists, configure npm trusted publishing for `chllming / wave-orchestration / publish-npm.yml` so later tagged releases no longer need manual npm auth."
19
+ ],
20
+ "breaking": false
21
+ },
22
+ {
23
+ "version": "0.5.0",
24
+ "date": "2026-03-22",
25
+ "summary": "Phase 4 helper assignment routing, cross-lane dependency workflows, and publish-ready package metadata.",
26
+ "features": [
27
+ "Capability-targeted requests now become explicit helper assignments with deterministic assignee selection, assignment snapshots, ledger/task coverage, inbox visibility, and closure barriers.",
28
+ "Typed cross-lane dependency workflows now have `wave dep` operator commands, per-wave inbound/outbound dependency projections, dependency-aware gating, and replay-visible dependency state.",
29
+ "Phase 3 replay acceptance was tightened around the runtime-orchestration layer with stored outcome snapshots, launcher-generated local trace fixtures, and stronger replay comparison coverage.",
30
+ "The package now carries explicit `repository`, `homepage`, and `bugs` metadata for cleaner GitHub Packages linking."
31
+ ],
32
+ "manualSteps": [
33
+ "After upgrading, dry-run the target lane with `pnpm exec wave launch --lane main --dry-run --no-dashboard` to inspect helper-assignment and dependency projections before running real executors.",
34
+ "Use `pnpm exec wave dep show --lane <lane> --wave <n> --json` to inspect inbound and outbound dependency state when a lane appears blocked.",
35
+ "If you want helper-task routing beyond ownership defaults, add or refine `### Capabilities` blocks in your wave files and `capabilityRouting.preferredAgents` in `wave.config.json`."
36
+ ],
37
+ "breaking": false
38
+ },
39
+ {
40
+ "version": "0.4.0",
41
+ "date": "2026-03-21",
42
+ "summary": "Runtime surface upgrade plus hermetic traces for Codex, Claude Code, and OpenCode.",
43
+ "features": [
44
+ "Expanded Codex `exec` support for model, profile, inline config, search, images, extra directories, JSON mode, and ephemeral sessions, alongside Claude settings overlay merging and OpenCode merged config overlays plus multi-file attachments.",
45
+ "Dry-run materialization of prompts, merged runtime overlays, and executor launch previews for all supported real runtimes.",
46
+ "Dedicated runtime configuration reference docs under `docs/reference/runtime-config/`, installed by `wave init` for config/default/profile/lane/agent authoring.",
47
+ "Hermetic `traceVersion: 2` trace bundles with copied launched-agent artifacts, cumulative quality metrics, hash validation, and an internal read-only replay validator.",
48
+ "Retry and coordination hardening for policy-safe fallback blocking, clarification-follow-up matching, and artifact-linked inbox visibility."
49
+ ],
50
+ "manualSteps": [
51
+ "Review any per-agent `### Executor` blocks that want the new runtime fields before cutting real waves on the upgraded package.",
52
+ "Run `pnpm exec wave launch --lane main --dry-run --no-dashboard` after upgrading to confirm prompts and executor overlays materialize as expected.",
53
+ "Use the docs under `docs/reference/runtime-config/` as the canonical reference when migrating runtime defaults, profiles, or per-agent executor blocks.",
54
+ "Treat internal trace replay as a publish gate for `0.4.0`, but keep it internal-only until a later operator-facing replay surface is designed."
55
+ ],
56
+ "breaking": false
57
+ },
58
+ {
59
+ "version": "0.1.0",
60
+ "date": "2026-03-21",
61
+ "summary": "Generic wave orchestrator runtime with Context7 and multi-executor support.",
62
+ "features": [
63
+ "Generic wave parsing, validation, dashboards, closure sweep, and human feedback queue.",
64
+ "Context7 bundle resolution, prefetch, and prompt injection.",
65
+ "Headless executors for Codex, Claude Code, OpenCode, and the local smoke executor."
66
+ ],
67
+ "manualSteps": [],
68
+ "breaking": false
69
+ },
70
+ {
71
+ "version": "0.2.0",
72
+ "date": "2026-03-21",
73
+ "summary": "Package-first install and non-destructive upgrade workflow.",
74
+ "features": [
75
+ "Workspace-root aware runtime so the orchestrator can run from an installed package against any target repo.",
76
+ "New `wave init`, `wave upgrade`, `wave changelog`, and `wave doctor` commands.",
77
+ "Install-state tracking and upgrade-history reports under `.wave/` without overwriting repo-owned plans, waves, or config."
78
+ ],
79
+ "manualSteps": [
80
+ "For fresh repos, install the package and run `pnpm exec wave init`.",
81
+ "For repos that already have Wave config or plans, run `pnpm exec wave init --adopt-existing` once so upgrades stay non-destructive."
82
+ ],
83
+ "breaking": false
84
+ },
85
+ {
86
+ "version": "0.3.0",
87
+ "date": "2026-03-21",
88
+ "summary": "Phase 1 and 2 roadmap runtime: typed coordination, integration gating, and runtime planning.",
89
+ "features": [
90
+ "Canonical coordination log materialization, generated board projection, compiled shared summary, per-agent inboxes, and a durable wave ledger.",
91
+ "Planning-time executor profiles, lane runtime policy, hard runtime-mix enforcement, and retry fallback reassignment recorded into ledger, integration, and traces.",
92
+ "Orchestrator-first clarification triage, explicit integration summaries, and staged closure that runs integration before documentation and evaluator closure."
93
+ ],
94
+ "manualSteps": [
95
+ "Run `pnpm exec wave init --adopt-existing` in older repos if they do not yet have the newer role prompts and docs surfaces.",
96
+ "Review `wave.config.json` lane runtime policy before relying on automatic fallback behavior or strict runtime-mix limits."
97
+ ],
98
+ "breaking": false
99
+ }
100
+ ]
101
+ }
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env bash
2
+ # Minimal Context7 API smoke test (expects CONTEXT7_API_KEY in the environment).
3
+ set -euo pipefail
4
+
5
+ if [[ -z "${CONTEXT7_API_KEY:-}" ]]; then
6
+ echo "context7-api-check: CONTEXT7_API_KEY is not set" >&2
7
+ exit 1
8
+ fi
9
+
10
+ URL='https://context7.com/api/v2/libs/search?libraryName=temporal&query=go%20workflow'
11
+ echo "GET $URL" >&2
12
+ RESP="$(curl -fsS "$URL" -H "Authorization: Bearer ${CONTEXT7_API_KEY}" -H "Accept: application/json")"
13
+ RESP_JSON="$RESP" node -e "
14
+ const j = JSON.parse(process.env.RESP_JSON || '{}');
15
+ const list = Array.isArray(j) ? j : (j.results ?? j.items ?? []);
16
+ const first = list[0];
17
+ if (!first) { console.error('Unexpected response shape:', Object.keys(j)); process.exit(1); }
18
+ const id = first.id ?? first.libraryId;
19
+ const name = first.title ?? first.name;
20
+ console.log('ok — first library:', id || name || JSON.stringify(first).slice(0, 120));
21
+ "
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env bash
2
+ # Load CONTEXT7_API_KEY from repo-root .env.local, then export it for child processes.
3
+ #
4
+ # Usage:
5
+ # source scripts/context7-export-env.sh
6
+ # — merges .env.local into the current shell (only CONTEXT7_* vars need to be present;
7
+ # the whole file is sourced with set -a).
8
+ #
9
+ # bash scripts/context7-export-env.sh run <command> [args...]
10
+ # — loads .env.local, requires CONTEXT7_API_KEY, exports it, then execs the command.
11
+ #
12
+ # Examples:
13
+ # pnpm context7:api-check
14
+ # bash scripts/context7-export-env.sh run env | grep CONTEXT7
15
+
16
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
17
+ REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
18
+ ENV_FILE="$REPO_ROOT/.env.local"
19
+
20
+ if [[ -f "$ENV_FILE" ]]; then
21
+ set -a
22
+ # shellcheck disable=SC1090
23
+ source "$ENV_FILE"
24
+ set +a
25
+ fi
26
+
27
+ if [[ "${1:-}" == "run" ]]; then
28
+ set -euo pipefail
29
+ shift
30
+ if [[ $# -lt 1 ]]; then
31
+ echo "context7-export-env: run requires a command (e.g. bash scripts/context7-export-env.sh run curl ...)" >&2
32
+ exit 1
33
+ fi
34
+ if [[ -z "${CONTEXT7_API_KEY:-}" ]]; then
35
+ echo "context7-export-env: CONTEXT7_API_KEY is not set. Add it to ${ENV_FILE} at repo root." >&2
36
+ exit 1
37
+ fi
38
+ export CONTEXT7_API_KEY
39
+ exec "$@"
40
+ fi
41
+
42
+ # Invoked (not "run"): print hint; sourcing skips this when $0 is bash.
43
+ if [[ "${BASH_SOURCE[0]:-}" == "${0:-}" ]]; then
44
+ echo "Usage: source scripts/context7-export-env.sh" >&2
45
+ echo " or: bash scripts/context7-export-env.sh run <command> [args...]" >&2
46
+ exit 2
47
+ fi
48
+
49
+ # Sourced: export if set so subprocesses (e.g. codex) inherit it.
50
+ if [[ -n "${CONTEXT7_API_KEY:-}" ]]; then
51
+ export CONTEXT7_API_KEY
52
+ fi
@@ -0,0 +1,472 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ export const TOPIC_DEFINITIONS = [
5
+ {
6
+ id: "harnesses-and-practice",
7
+ title: "Harnesses and Practice",
8
+ description:
9
+ "Current guidance and recent papers on agent harness design, reviewer loops, terminal-native execution, and practical coding-agent workflows.",
10
+ },
11
+ {
12
+ id: "long-running-agents-and-compaction",
13
+ title: "Long-Running Agents and Compaction",
14
+ description:
15
+ "Long-horizon execution, resumability, memory systems, compaction, and evolving-task evaluation for agents that span many sessions.",
16
+ },
17
+ {
18
+ id: "blackboard-and-shared-workspaces",
19
+ title: "Blackboard and Shared Workspaces",
20
+ description:
21
+ "Shared-workspace coordination, blackboard-style agent systems, explicit consensus mechanics, and distributed reasoning under coordination constraints.",
22
+ },
23
+ {
24
+ id: "repo-context-and-evaluation",
25
+ title: "Repo Context and Evaluation",
26
+ description:
27
+ "Repository-level context files, harness evaluation methods, and evidence on what improves or harms coding-agent performance.",
28
+ },
29
+ ];
30
+
31
+ export const PAPER_SECTION_ORDER = [
32
+ "P0 direct hits",
33
+ "P1 strong adjacent work",
34
+ "P2 lineage and older references",
35
+ ];
36
+
37
+ const PAPER_SECTION_OVERRIDES = {
38
+ "an-open-agent-architecture": "P2 lineage and older references",
39
+ "evaluating-agents-md-are-repository-level-context-files-helpful-for-coding-agents":
40
+ "P1 strong adjacent work",
41
+ "memory-for-autonomous-llm-agents-mechanisms-evaluation-and-emerging-frontiers":
42
+ "P1 strong adjacent work",
43
+ };
44
+
45
+ const TOPIC_OVERRIDE_MAP = {
46
+ "evaluating-agents-md-are-repository-level-context-files-helpful-for-coding-agents":
47
+ ["repo-context-and-evaluation"],
48
+ };
49
+
50
+ function escapeInlinePipes(value) {
51
+ return String(value ?? "").replaceAll("|", "\\|");
52
+ }
53
+
54
+ function normalizeWhitespace(value) {
55
+ return String(value ?? "")
56
+ .replaceAll("\u00a0", " ")
57
+ .replaceAll(/\s+/g, " ")
58
+ .trim();
59
+ }
60
+
61
+ function safeSlugFromPath(relPath) {
62
+ return path.basename(relPath, ".md");
63
+ }
64
+
65
+ function stripQuotes(value) {
66
+ const text = String(value ?? "").trim();
67
+ if (
68
+ (text.startsWith("'") && text.endsWith("'")) ||
69
+ (text.startsWith("\"") && text.endsWith("\""))
70
+ ) {
71
+ return text.slice(1, -1);
72
+ }
73
+ return text;
74
+ }
75
+
76
+ function parseFrontmatter(markdown) {
77
+ const match = markdown.match(/^---\n([\s\S]*?)\n---\n/);
78
+ return match ? match[1] : "";
79
+ }
80
+
81
+ function parseFrontmatterScalar(frontmatter, key) {
82
+ const match = frontmatter.match(new RegExp(`^${key}:\\s*(.+)$`, "m"));
83
+ return match ? stripQuotes(match[1]) : null;
84
+ }
85
+
86
+ function parseFrontmatterList(frontmatter, key) {
87
+ const match = frontmatter.match(new RegExp(`^${key}:\\n((?: - .*\\n?)*)`, "m"));
88
+ if (!match) {
89
+ return [];
90
+ }
91
+ return match[1]
92
+ .split("\n")
93
+ .map((line) => line.match(/^ - (.+)$/)?.[1] ?? null)
94
+ .map((entry) => stripQuotes(entry))
95
+ .map((entry) => normalizeWhitespace(entry))
96
+ .filter(Boolean);
97
+ }
98
+
99
+ function extractFirstUrl(value) {
100
+ const match = String(value ?? "").match(/\((https?:\/\/[^)]+)\)/);
101
+ return match ? match[1] : null;
102
+ }
103
+
104
+ function parseMetadataRows(markdown) {
105
+ const sectionMatch = markdown.match(/## Metadata\s+([\s\S]*?)(?:\n## |\n# |$)/);
106
+ if (!sectionMatch) {
107
+ return new Map();
108
+ }
109
+
110
+ const rows = new Map();
111
+ for (const line of sectionMatch[1].split("\n")) {
112
+ if (!line.startsWith("|")) {
113
+ continue;
114
+ }
115
+ const cells = line
116
+ .split("|")
117
+ .slice(1, -1)
118
+ .map((cell) => cell.trim());
119
+ if (cells.length < 2 || cells[0] === "Field" || cells[0] === "---") {
120
+ continue;
121
+ }
122
+ rows.set(cells[0].toLowerCase(), cells[1]);
123
+ }
124
+ return rows;
125
+ }
126
+
127
+ function parseYear(value) {
128
+ if (!value) {
129
+ return null;
130
+ }
131
+ const match = String(value).match(/\b(19|20)\d{2}\b/);
132
+ return match ? Number(match[0]) : null;
133
+ }
134
+
135
+ export function parseArchiveEntry(markdown, relPath) {
136
+ const frontmatter = parseFrontmatter(markdown);
137
+ const rows = parseMetadataRows(markdown);
138
+ const kind =
139
+ parseFrontmatterScalar(frontmatter, "kind") ??
140
+ (relPath.startsWith("articles/") ? "article" : "paper");
141
+
142
+ return {
143
+ slug: safeSlugFromPath(relPath),
144
+ path: relPath,
145
+ kind,
146
+ title: parseFrontmatterScalar(frontmatter, "title") ?? safeSlugFromPath(relPath),
147
+ summary: parseFrontmatterScalar(frontmatter, "summary"),
148
+ topics: parseFrontmatterList(frontmatter, "topics"),
149
+ year: parseYear(rows.get("year")),
150
+ venue: normalizeWhitespace(rows.get("venue")),
151
+ bucket: normalizeWhitespace(rows.get("research bucket")),
152
+ mapsTo: normalizeWhitespace(rows.get("maps to")),
153
+ fit: normalizeWhitespace(rows.get("harness fit")),
154
+ sourcePage: extractFirstUrl(rows.get("source page")),
155
+ sourcePdf: extractFirstUrl(rows.get("source pdf")),
156
+ additionalSource: extractFirstUrl(rows.get("additional source")),
157
+ additionalPdf: extractFirstUrl(rows.get("additional pdf")),
158
+ };
159
+ }
160
+
161
+ export function parsePaperSectionMap(indexMarkdown) {
162
+ const sectionMap = new Map();
163
+ let currentSection = null;
164
+
165
+ for (const line of String(indexMarkdown).split("\n")) {
166
+ if (line.startsWith("## ")) {
167
+ currentSection = line.slice(3).trim();
168
+ continue;
169
+ }
170
+ const match = line.match(
171
+ /\]\((?:(?:\.\/|\.\.\/)(?:papers\/)?|\/(?:[^)]+\/)?agent-context-cache\/(?:papers\/)?)?([a-z0-9-]+)(?:\.md)?\)/i,
172
+ );
173
+ if (!match || !currentSection) {
174
+ continue;
175
+ }
176
+ sectionMap.set(match[1], currentSection);
177
+ }
178
+
179
+ return sectionMap;
180
+ }
181
+
182
+ function unique(values) {
183
+ return [...new Set(values.filter(Boolean))];
184
+ }
185
+
186
+ export function inferTopics(entry, section = null) {
187
+ const topics = [...(entry.topics ?? [])];
188
+ const override = TOPIC_OVERRIDE_MAP[entry.slug];
189
+ if (override) {
190
+ topics.push(...override);
191
+ }
192
+
193
+ if (topics.length > 0) {
194
+ return unique(topics);
195
+ }
196
+
197
+ const haystack = `${entry.slug} ${entry.title} ${entry.mapsTo} ${entry.fit}`.toLowerCase();
198
+
199
+ if (
200
+ /agents-md|repository-level|repo context|repository context|evaluation harness|benchmark/.test(
201
+ haystack,
202
+ )
203
+ ) {
204
+ topics.push("repo-context-and-evaluation");
205
+ }
206
+
207
+ if (
208
+ /blackboard|shared workspace|shared workspaces|distributed coordination|coordination|consensus|communication-reasoning gap|silo-bench|symphony|dova|open agent architecture/.test(
209
+ haystack,
210
+ )
211
+ ) {
212
+ topics.push("blackboard-and-shared-workspaces");
213
+ }
214
+
215
+ if (
216
+ /long-running|long horizon|long-horizon|compaction|context engineering|memory|continuous software evolution|resumability|initializer|trace|versioned snapshots|reviewer loop/.test(
217
+ haystack,
218
+ )
219
+ ) {
220
+ topics.push("long-running-agents-and-compaction");
221
+ }
222
+
223
+ if (
224
+ entry.kind === "article" || /harness|codex|terminal|engineering|reviewer|agent-first/.test(haystack)
225
+ ) {
226
+ topics.push("harnesses-and-practice");
227
+ }
228
+
229
+ if (topics.length === 0) {
230
+ topics.push(entry.kind === "article" ? "harnesses-and-practice" : "long-running-agents-and-compaction");
231
+ }
232
+
233
+ return unique(topics);
234
+ }
235
+
236
+ export async function loadArchiveEntries(archiveRoot) {
237
+ const entries = [];
238
+
239
+ for (const folder of ["papers", "articles"]) {
240
+ const dirPath = path.join(archiveRoot, folder);
241
+ let fileNames = [];
242
+ try {
243
+ fileNames = await fs.readdir(dirPath);
244
+ } catch {
245
+ continue;
246
+ }
247
+
248
+ for (const fileName of fileNames.sort()) {
249
+ if (!fileName.endsWith(".md") || fileName === "index.md") {
250
+ continue;
251
+ }
252
+ const relPath = path.posix.join(folder, fileName);
253
+ const markdown = await fs.readFile(path.join(archiveRoot, relPath), "utf8");
254
+ entries.push(parseArchiveEntry(markdown, relPath));
255
+ }
256
+ }
257
+
258
+ return entries.sort((left, right) => left.title.localeCompare(right.title));
259
+ }
260
+
261
+ function relativeLink(fromDir, toPath) {
262
+ return path.relative(fromDir, toPath).split(path.sep).join("/");
263
+ }
264
+
265
+ function formatLocalLink(fromDir, targetPath, label) {
266
+ const relPath = relativeLink(fromDir, targetPath);
267
+ return `[${escapeInlinePipes(label)}](${relPath})`;
268
+ }
269
+
270
+ function formatSourceLink(entry) {
271
+ const url = entry.sourcePage ?? entry.additionalSource ?? entry.sourcePdf ?? entry.additionalPdf;
272
+ return url ? `[Source](${url})` : "—";
273
+ }
274
+
275
+ function formatCell(value, fallback = "—") {
276
+ const normalized = normalizeWhitespace(value);
277
+ return normalized ? escapeInlinePipes(normalized) : fallback;
278
+ }
279
+
280
+ function renderTable(headers, rows) {
281
+ const headerLine = `| ${headers.join(" | ")} |`;
282
+ const dividerLine = `| ${headers.map(() => "---").join(" | ")} |`;
283
+ return [headerLine, dividerLine, ...rows].join("\n");
284
+ }
285
+
286
+ export function buildPaperSectionAssignments(entries, existingSectionMap) {
287
+ const assignments = new Map();
288
+ for (const entry of entries.filter((item) => item.kind === "paper")) {
289
+ const requestedBucket = PAPER_SECTION_ORDER.includes(entry.bucket) ? entry.bucket : null;
290
+ assignments.set(
291
+ entry.slug,
292
+ existingSectionMap.get(entry.slug) ??
293
+ requestedBucket ??
294
+ PAPER_SECTION_OVERRIDES[entry.slug] ??
295
+ "P2 lineage and older references",
296
+ );
297
+ }
298
+ return assignments;
299
+ }
300
+
301
+ export function renderPaperIndex(entries, sectionAssignments) {
302
+ const paperEntries = entries.filter((entry) => entry.kind === "paper");
303
+ const grouped = new Map(PAPER_SECTION_ORDER.map((section) => [section, []]));
304
+ for (const entry of paperEntries) {
305
+ const section = sectionAssignments.get(entry.slug) ?? "P2 lineage and older references";
306
+ if (!grouped.has(section)) {
307
+ grouped.set(section, []);
308
+ }
309
+ grouped.get(section).push(entry);
310
+ }
311
+
312
+ const sections = PAPER_SECTION_ORDER.filter((section) => (grouped.get(section) ?? []).length > 0).map(
313
+ (section) => {
314
+ const rows = grouped
315
+ .get(section)
316
+ .slice()
317
+ .sort((left, right) => left.title.localeCompare(right.title))
318
+ .map((entry) => {
319
+ const fileLabel = formatLocalLink("papers", path.posix.join("papers", `${entry.slug}.md`), entry.title);
320
+ return `| ${fileLabel} | ${entry.year ?? "Unknown"} | ${formatCell(entry.venue)} | ${formatCell(entry.mapsTo)} | ${formatCell(entry.fit)} | ${formatSourceLink(entry)} |`;
321
+ });
322
+ return `## ${section}\n\n${renderTable(
323
+ ["Paper", "Year", "Venue", "Maps to", "Fit", "Source"],
324
+ rows,
325
+ )}`;
326
+ },
327
+ );
328
+
329
+ return `---
330
+ summary: "Index of local-only agent-context papers and reports converted to Markdown with source links"
331
+ read_when:
332
+ - Browsing the local harness and blackboard paper archive
333
+ - Looking for the paper copy of a source in the local cache
334
+ title: "Paper Archive"
335
+ ---
336
+
337
+ # Paper Archive
338
+
339
+ <Note>
340
+ This local-only archive contains ${paperEntries.length} papers and reports. Source
341
+ documents were fetched transiently, converted to Markdown, and removed from
342
+ disk after extraction. This directory is gitignored and is not shipped as part
343
+ of the repository docs.
344
+ </Note>
345
+
346
+ ## Coverage
347
+
348
+ - Harnesses and practice
349
+ - Long-running agents and compaction
350
+ - Blackboard and shared workspaces
351
+ - Repo context and evaluation
352
+
353
+ ${sections.join("\n\n")}
354
+ `;
355
+ }
356
+
357
+ export function renderArticleIndex(entries) {
358
+ const articleEntries = entries.filter((entry) => entry.kind === "article");
359
+ const rows = articleEntries
360
+ .slice()
361
+ .sort((left, right) => left.title.localeCompare(right.title))
362
+ .map((entry) => {
363
+ const localLink = formatLocalLink(
364
+ "articles",
365
+ path.posix.join("articles", `${entry.slug}.md`),
366
+ entry.title,
367
+ );
368
+ return `| ${localLink} | ${entry.year ?? "Unknown"} | ${formatCell(entry.venue)} | ${formatCell(entry.mapsTo)} | ${formatSourceLink(entry)} |`;
369
+ });
370
+
371
+ return `---
372
+ summary: "Index of local-only practice articles cached alongside the agent-context paper archive"
373
+ read_when:
374
+ - Browsing current practice articles in the local harness archive
375
+ - Jumping from topic indexes into cached OpenAI and Anthropic guidance
376
+ title: "Practice Articles"
377
+ ---
378
+
379
+ # Practice Articles
380
+
381
+ <Note>
382
+ These cached articles are local-only working copies of vendor guidance that was
383
+ useful for the harness archive. The source URLs remain the canonical
384
+ references.
385
+ </Note>
386
+
387
+ ${renderTable(["Article", "Year", "Venue", "Maps to", "Source"], rows)}
388
+ `;
389
+ }
390
+
391
+ export function buildTopicGroups(entries, sectionAssignments) {
392
+ const groups = new Map(TOPIC_DEFINITIONS.map((topic) => [topic.id, []]));
393
+ for (const entry of entries) {
394
+ const section = entry.kind === "paper" ? sectionAssignments.get(entry.slug) : null;
395
+ for (const topic of inferTopics(entry, section)) {
396
+ if (!groups.has(topic)) {
397
+ groups.set(topic, []);
398
+ }
399
+ groups.get(topic).push(entry);
400
+ }
401
+ }
402
+ return groups;
403
+ }
404
+
405
+ function renderTopicList(fromDir, entries) {
406
+ return entries
407
+ .slice()
408
+ .sort((left, right) => left.title.localeCompare(right.title))
409
+ .map((entry) => {
410
+ const targetPath = path.posix.join(entry.kind === "article" ? "articles" : "papers", `${entry.slug}.md`);
411
+ const localLink = formatLocalLink(fromDir, targetPath, entry.title);
412
+ const label = entry.kind === "article" ? "Article" : "Paper";
413
+ return `- ${localLink} (${label}; ${entry.year ?? "Unknown"}; ${formatCell(entry.venue)})`;
414
+ })
415
+ .join("\n");
416
+ }
417
+
418
+ export function renderTopicsIndex(topicGroups) {
419
+ const lines = TOPIC_DEFINITIONS.map((topic) => {
420
+ const count = (topicGroups.get(topic.id) ?? []).length;
421
+ return `- [${topic.title}](./${topic.id}.md) (${count})\n ${topic.description}`;
422
+ }).join("\n");
423
+
424
+ return `---
425
+ summary: "Topic-based guides into the local-only agent-context archive"
426
+ read_when:
427
+ - You want to browse the local archive by theme instead of by source type
428
+ - You are building a future reading list around a specific agent-system topic
429
+ title: "Topic Indexes"
430
+ ---
431
+
432
+ # Topic Indexes
433
+
434
+ <Note>
435
+ These indexes group the whole local archive by theme while leaving the cached
436
+ Markdown files flat in their original \`papers/\` and \`articles/\` directories.
437
+ </Note>
438
+
439
+ ${lines}
440
+ `;
441
+ }
442
+
443
+ export function renderTopicPage(topic, entries) {
444
+ const articleEntries = entries.filter((entry) => entry.kind === "article");
445
+ const paperEntries = entries.filter((entry) => entry.kind === "paper");
446
+ const sections = [];
447
+
448
+ if (articleEntries.length > 0) {
449
+ sections.push(`## Articles\n\n${renderTopicList("topics", articleEntries)}`);
450
+ }
451
+ if (paperEntries.length > 0) {
452
+ sections.push(`## Papers and reports\n\n${renderTopicList("topics", paperEntries)}`);
453
+ }
454
+
455
+ return `---
456
+ summary: '${topic.description.replaceAll("'", "''")}'
457
+ read_when:
458
+ - You want a curated reading slice of the local agent-context archive
459
+ - You need related practice articles and papers in one place
460
+ title: '${topic.title.replaceAll("'", "''")}'
461
+ ---
462
+
463
+ # ${topic.title}
464
+
465
+ <Note>
466
+ ${topic.description} This page is a local-only grouping aid for the cache under
467
+ \`docs/research/agent-context-cache/\`.
468
+ </Note>
469
+
470
+ ${sections.join("\n\n")}
471
+ `;
472
+ }