orchestrator-workflow 0.40.1 → 0.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/init.d.ts CHANGED
@@ -70,6 +70,32 @@ export interface InitOptions {
70
70
  * too, the same as `null`.
71
71
  */
72
72
  pin?: string | null;
73
+ /**
74
+ * Configured knowledge-bundle locations, or `undefined` to carry the
75
+ * previous manifest's `knowledge` forward unchanged (the same
76
+ * omitted-means-sticky convention `routing`/`pin` already use). A present
77
+ * array (including `[]`) replaces the previous value outright and is
78
+ * validated with {@link validateKnowledgeBundles}: an invalid entry throws
79
+ * rather than writing a malformed record. Absent (never set on any
80
+ * install) or `[]` means today's behaviour: `docs/okf/` is the sole,
81
+ * implicit bundle location every kit site assumes. The CLI has no flag for
82
+ * this; operators hand-edit the manifest field instead.
83
+ */
84
+ knowledge?: KnowledgeBundle[];
85
+ }
86
+ /**
87
+ * One configured knowledge-bundle location. `path` is the bundle directory
88
+ * (for example `docs/okf`) and `repoRoot` the root of the repository the
89
+ * bundle's sources live in (`"."` when that is the worktree itself, a
90
+ * sub-repo's path for a workspace-level bundle whose sources live
91
+ * elsewhere). Each is resolved against the worktree top level on its own:
92
+ * `path` is not nested under `repoRoot`. Both are stored normalised
93
+ * (see {@link checkKnowledgeEntry}) and must not escape the worktree top
94
+ * level; no check argv lives here (see verification-set docs).
95
+ */
96
+ export interface KnowledgeBundle {
97
+ path: string;
98
+ repoRoot: string;
73
99
  }
74
100
  export declare const MANIFEST_PATH: string;
75
101
  export interface Manifest extends OpencodeModelMaps {
@@ -114,6 +140,17 @@ export interface Manifest extends OpencodeModelMaps {
114
140
  * recorded" and therefore never sticky.
115
141
  */
116
142
  harnessesRecordedEmpty?: boolean;
143
+ /**
144
+ * Configured knowledge-bundle locations. Absent when never configured on
145
+ * any install of this repo: every kit site that reads this field then
146
+ * falls back to the implicit `docs/okf/` default, today's behaviour; an
147
+ * empty list means the same. When present, an entry that fails
148
+ * validation (a non-relative or top-level-escaping `path`/`repoRoot`) is
149
+ * dropped rather than carried forward, the same per-entry degradation
150
+ * style `files`/`models` above already use for a hand-written or damaged
151
+ * manifest; `doctor` reports each dropped entry.
152
+ */
153
+ knowledge?: KnowledgeBundle[];
117
154
  }
118
155
  /**
119
156
  * A manifest can be hand-written or tampered with, and uninstall deletes by
@@ -122,6 +159,38 @@ export interface Manifest extends OpencodeModelMaps {
122
159
  * can never reach an unlink.
123
160
  */
124
161
  export declare function isContainedRelativePath(relativePath: string): boolean;
162
+ /**
163
+ * Checks one raw `knowledge` entry. `path` (the bundle directory) and
164
+ * `repoRoot` (the root of the repository its sources live in) are each
165
+ * resolved against the worktree top level, independently of one another, so
166
+ * a workspace-level bundle whose sources live in a sub-repo is
167
+ * `{ path: "docs/okf", repoRoot: "sub" }`. `repoRoot` defaults to `"."`
168
+ * when omitted. Returns the normalised entry, or the reason it is invalid
169
+ * (naming the offending field) for the caller to throw or report.
170
+ */
171
+ export declare function checkKnowledgeEntry(entry: unknown): {
172
+ bundle: KnowledgeBundle;
173
+ } | {
174
+ reason: string;
175
+ };
176
+ /**
177
+ * Lists every problem in a raw on-disk `knowledge` value: `[]` when the
178
+ * field is absent or every entry is valid, one item for a non-array value,
179
+ * otherwise one item per invalid entry with its index and reason. `doctor`
180
+ * reports these, since {@link parseKnowledgeBundles} drops such entries on
181
+ * read and a re-install that rewrites the manifest would remove them from
182
+ * disk without notice.
183
+ */
184
+ export declare function knowledgeEntryProblems(raw: unknown): string[];
185
+ /**
186
+ * Validates an explicitly-supplied `options.knowledge` array for `runInit`
187
+ * with {@link checkKnowledgeEntry} and returns the normalised entries.
188
+ * Throws on the first invalid entry -- explicit input refuses to write a
189
+ * malformed record rather than dropping it (contrast
190
+ * {@link parseKnowledgeBundles}, which degrades a hand-edited/damaged
191
+ * on-disk manifest by dropping bad entries instead of throwing).
192
+ */
193
+ export declare function validateKnowledgeBundles(knowledge: unknown): KnowledgeBundle[];
125
194
  /**
126
195
  * Reads the manifest of a previous install, if any. Manifests can be written
127
196
  * by hand (manual agent installs) or damaged, so every field is sanitized;
package/dist/init.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { existsSync, readFileSync, statSync } from "node:fs";
3
- import { isAbsolute, join, normalize, sep } from "node:path";
3
+ import { isAbsolute, join, normalize, posix, sep } from "node:path";
4
4
  import { PACKAGE_VERSION, listSkillReferenceNames, listTemplateNames, readAgentAsset, readAsset, } from "./assets.js";
5
5
  import { HARNESSES } from "./detect.js";
6
6
  import { CLASS_MODELS, DEFAULT_PROFILE, DEFAULT_TIER, READ_ONLY_ROLES, ROLES, ROLE_TIERS, TIER_DEFS, assertValidModelId, claudeModelValue, isProfile, opencodeModelValue, rolesForProfile, } from "./models.js";
@@ -25,6 +25,124 @@ export function isContainedRelativePath(relativePath) {
25
25
  const normalized = normalize(relativePath);
26
26
  return normalized !== ".." && !normalized.startsWith(`..${sep}`);
27
27
  }
28
+ /**
29
+ * Normalises one knowledge-bundle path field (`path` or `repoRoot`) and
30
+ * returns it, or a reason string when it is not acceptable. Both fields are
31
+ * worktree-relative, so spellings of the same location compare equal after
32
+ * this step: POSIX `normalize`, then any trailing `/` stripped (`docs/okf/`,
33
+ * `./docs/okf` and `docs/okf` all become `docs/okf`). An empty string, an
34
+ * absolute path, and a path that normalises to a `..` escape are rejected;
35
+ * `.` (the worktree top level itself) is accepted only when `allowTop` is
36
+ * set, which is the case for `repoRoot` and not for `path`.
37
+ */
38
+ function normalizeKnowledgePathField(value, allowTop) {
39
+ if (typeof value !== "string")
40
+ return { reason: "is not a string" };
41
+ if (value === "")
42
+ return { reason: "is empty" };
43
+ if (isAbsolute(value) || posix.isAbsolute(value)) {
44
+ return { reason: "is an absolute path" };
45
+ }
46
+ let normalized = posix.normalize(value);
47
+ while (normalized.length > 1 && normalized.endsWith("/")) {
48
+ normalized = normalized.slice(0, -1);
49
+ }
50
+ if (normalized === ".." || normalized.startsWith("../")) {
51
+ return { reason: "escapes the worktree top level" };
52
+ }
53
+ if (normalized === "." && !allowTop) {
54
+ return { reason: "names the worktree top level itself" };
55
+ }
56
+ return { value: normalized };
57
+ }
58
+ /**
59
+ * Checks one raw `knowledge` entry. `path` (the bundle directory) and
60
+ * `repoRoot` (the root of the repository its sources live in) are each
61
+ * resolved against the worktree top level, independently of one another, so
62
+ * a workspace-level bundle whose sources live in a sub-repo is
63
+ * `{ path: "docs/okf", repoRoot: "sub" }`. `repoRoot` defaults to `"."`
64
+ * when omitted. Returns the normalised entry, or the reason it is invalid
65
+ * (naming the offending field) for the caller to throw or report.
66
+ */
67
+ export function checkKnowledgeEntry(entry) {
68
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
69
+ return { reason: "is not an object" };
70
+ }
71
+ const candidate = entry;
72
+ const path = normalizeKnowledgePathField(candidate.path, false);
73
+ if ("reason" in path)
74
+ return { reason: `path ${path.reason}` };
75
+ const repoRoot = normalizeKnowledgePathField(candidate.repoRoot === undefined ? "." : candidate.repoRoot, true);
76
+ if ("reason" in repoRoot)
77
+ return { reason: `repoRoot ${repoRoot.reason}` };
78
+ return { bundle: { path: path.value, repoRoot: repoRoot.value } };
79
+ }
80
+ /**
81
+ * Lists every problem in a raw on-disk `knowledge` value: `[]` when the
82
+ * field is absent or every entry is valid, one item for a non-array value,
83
+ * otherwise one item per invalid entry with its index and reason. `doctor`
84
+ * reports these, since {@link parseKnowledgeBundles} drops such entries on
85
+ * read and a re-install that rewrites the manifest would remove them from
86
+ * disk without notice.
87
+ */
88
+ export function knowledgeEntryProblems(raw) {
89
+ if (raw === undefined)
90
+ return [];
91
+ if (!Array.isArray(raw)) {
92
+ return ["knowledge is not an array and is ignored"];
93
+ }
94
+ const problems = [];
95
+ raw.forEach((entry, index) => {
96
+ const checked = checkKnowledgeEntry(entry);
97
+ if ("reason" in checked) {
98
+ problems.push(`knowledge[${index}] ${checked.reason} and is ignored`);
99
+ }
100
+ });
101
+ return problems;
102
+ }
103
+ /**
104
+ * Validates an explicitly-supplied `options.knowledge` array for `runInit`
105
+ * with {@link checkKnowledgeEntry} and returns the normalised entries.
106
+ * Throws on the first invalid entry -- explicit input refuses to write a
107
+ * malformed record rather than dropping it (contrast
108
+ * {@link parseKnowledgeBundles}, which degrades a hand-edited/damaged
109
+ * on-disk manifest by dropping bad entries instead of throwing).
110
+ */
111
+ export function validateKnowledgeBundles(knowledge) {
112
+ if (!Array.isArray(knowledge)) {
113
+ throw new Error("options.knowledge must be an array of { path, repoRoot } entries");
114
+ }
115
+ return knowledge.map((entry, index) => {
116
+ const checked = checkKnowledgeEntry(entry);
117
+ if ("reason" in checked) {
118
+ throw new Error(`options.knowledge[${index}] ${checked.reason}; path and repoRoot must be relative paths inside the worktree top level`);
119
+ }
120
+ return checked.bundle;
121
+ });
122
+ }
123
+ /**
124
+ * Sanitizes a possibly hand-edited or damaged on-disk manifest's raw
125
+ * `knowledge` field for {@link readInstalledManifest}: an entry that fails
126
+ * {@link checkKnowledgeEntry} is dropped rather than throwing (the read path
127
+ * can never crash a re-install on tampered input, the same reasoning
128
+ * `files`/`models` already document above), and a valid entry is kept in
129
+ * its normalised spelling. Returns `undefined` when the raw field is absent
130
+ * or not an array, so the "never configured" and "configured but empty"
131
+ * states stay distinct the same way `routing`'s own `"routing" in
132
+ * candidate` check does.
133
+ */
134
+ function parseKnowledgeBundles(candidate) {
135
+ if (!("knowledge" in candidate) || !Array.isArray(candidate.knowledge)) {
136
+ return undefined;
137
+ }
138
+ const result = [];
139
+ for (const entry of candidate.knowledge) {
140
+ const checked = checkKnowledgeEntry(entry);
141
+ if ("bundle" in checked)
142
+ result.push(checked.bundle);
143
+ }
144
+ return result;
145
+ }
28
146
  /**
29
147
  * Reads the manifest of a previous install, if any. Manifests can be written
30
148
  * by hand (manual agent installs) or damaged, so every field is sanitized;
@@ -102,6 +220,7 @@ export function readInstalledManifest(targetDir) {
102
220
  if ("routing" in candidate) {
103
221
  routing = parseRouting(candidate.routing);
104
222
  }
223
+ const knowledge = parseKnowledgeBundles(candidate);
105
224
  // A hand-written or damaged manifest may carry a non-string `pin`; that
106
225
  // degrades to "no recorded pin" here (the same per-field-degradation
107
226
  // style as `profile`/`tiers` above) rather than throwing. An empty or
@@ -117,6 +236,7 @@ export function readInstalledManifest(targetDir) {
117
236
  profile,
118
237
  tiers,
119
238
  ...(routing !== undefined ? { routing } : {}),
239
+ ...(knowledge !== undefined ? { knowledge } : {}),
120
240
  ...opencodeMaps,
121
241
  files,
122
242
  installedAt: typeof candidate.installedAt === "string" ? candidate.installedAt : "",
@@ -348,6 +468,14 @@ export function runInit(options) {
348
468
  updateOpencodeModels: options.opencodeModels !== undefined,
349
469
  updateOpencodeClassModels: options.opencodeClassModels !== undefined,
350
470
  });
471
+ // `undefined` carries the previous manifest's `knowledge` forward
472
+ // unchanged, the same omitted-means-sticky convention `routing`/`pin` use
473
+ // above; a present array (including `[]`) is validated and replaces the
474
+ // previous value outright, throwing before any file is written on an
475
+ // invalid entry rather than persisting a malformed record.
476
+ const knowledge = options.knowledge !== undefined
477
+ ? validateKnowledgeBundles(options.knowledge)
478
+ : previous?.knowledge;
351
479
  // Check only rendered selections before the first mutation.
352
480
  for (const warning of codexCatalogWarnings(routing, { harnesses: options.harnesses, profile, tiers }, options.codexCatalog)) {
353
481
  report.notes.push(`Codex catalog: ${warning}`);
@@ -677,6 +805,7 @@ export function runInit(options) {
677
805
  profile,
678
806
  tiers,
679
807
  routing,
808
+ ...(knowledge !== undefined ? { knowledge } : {}),
680
809
  ...compatibility,
681
810
  files: installedFiles,
682
811
  ...(pin !== undefined ? { pin } : {}),
@@ -691,6 +820,9 @@ export function runInit(options) {
691
820
  profile: previous.profile,
692
821
  tiers: previous.tiers,
693
822
  ...(previous.routing !== undefined ? { routing: previous.routing } : {}),
823
+ ...(previous.knowledge !== undefined
824
+ ? { knowledge: previous.knowledge }
825
+ : {}),
694
826
  ...legacyOpencodeFallbacks(previous),
695
827
  files: previous.files,
696
828
  ...(previous.pin !== undefined ? { pin: previous.pin } : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orchestrator-workflow",
3
- "version": "0.40.1",
3
+ "version": "0.41.0",
4
4
  "description": "Installer for an orchestrator-led agent workflow: .ai/ run state, an AGENTS.md policy section, and per-harness subagent definitions for Claude Code, OpenAI Codex, and opencode",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",