orchestrator-workflow 0.40.1 → 0.42.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/CHANGELOG.md +127 -0
- package/README.md +37 -5
- package/assets/agents/advisor.md +5 -0
- package/assets/agents/explorer.md +9 -3
- package/assets/agents/implementer.md +30 -2
- package/assets/agents/reviewer.md +32 -1
- package/assets/agents/task-slicer.md +32 -2
- package/assets/agents-md-section.md +55 -0
- package/assets/skill/SKILL.md +56 -7
- package/assets/skill/references/bundle-gate-in-ci.md +242 -0
- package/assets/skill/references/contracts.md +52 -3
- package/assets/skill/references/evidence-and-probes.md +88 -9
- package/assets/skill/references/review-and-recovery.md +9 -1
- package/assets/skill/references/run-state-and-harness.md +31 -0
- package/assets/templates/00-goal.md +8 -0
- package/assets/templates/02-tasks.md +6 -2
- package/assets/templates/06-handoff.md +22 -2
- package/dist/cli.js +6 -0
- package/dist/doctor.d.ts +13 -0
- package/dist/doctor.js +61 -1
- package/dist/init.d.ts +77 -0
- package/dist/init.js +176 -1
- package/package.json +1 -1
package/dist/doctor.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { PACKAGE_VERSION } from "./assets.js";
|
|
5
|
-
import { MANIFEST_PATH, readInstalledManifest } from "./init.js";
|
|
5
|
+
import { MANIFEST_PATH, knowledgeEntryProblems, readInstalledManifest, } from "./init.js";
|
|
6
6
|
import { DEFAULT_MODELS, rolesForProfile } from "./models.js";
|
|
7
7
|
import { compareRoutingState } from "./routing-state.js";
|
|
8
8
|
import { OPERATOR_MANIFEST_FILENAME, operatorManifestState, updateOperatorManifest, } from "./operator-manifest.js";
|
|
@@ -17,6 +17,9 @@ export function targetReportToJson(report) {
|
|
|
17
17
|
...(report.routingComparisonGaps?.length
|
|
18
18
|
? { routingComparisonGaps: report.routingComparisonGaps }
|
|
19
19
|
: {}),
|
|
20
|
+
...(report.knowledgeWarnings?.length
|
|
21
|
+
? { knowledgeWarnings: report.knowledgeWarnings }
|
|
22
|
+
: {}),
|
|
20
23
|
versionLag: report.versionLag,
|
|
21
24
|
reason: report.reason,
|
|
22
25
|
};
|
|
@@ -111,6 +114,61 @@ function computeDriftFiles(targetPath, manifest) {
|
|
|
111
114
|
}
|
|
112
115
|
return drifted;
|
|
113
116
|
}
|
|
117
|
+
/** The kit-wide implicit default knowledge-bundle location every site falls
|
|
118
|
+
* back to when a target configures no `knowledge` list at all. */
|
|
119
|
+
const DEFAULT_KNOWLEDGE_PATH = "docs/okf";
|
|
120
|
+
/** The raw `knowledge` value of a target's manifest file, before
|
|
121
|
+
* `readInstalledManifest` drops invalid entries; `undefined` when the field
|
|
122
|
+
* is absent or the file cannot be re-read. */
|
|
123
|
+
function readRawKnowledge(targetPath) {
|
|
124
|
+
try {
|
|
125
|
+
const parsed = JSON.parse(readFileSync(join(targetPath, MANIFEST_PATH), "utf8"));
|
|
126
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
127
|
+
return undefined;
|
|
128
|
+
return parsed.knowledge;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function isDirectoryAt(path) {
|
|
135
|
+
const stat = statOrClassify(path);
|
|
136
|
+
return stat.kind === "ok" && stat.stat.isDirectory();
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Checks a target's configured `knowledge` list against its own filesystem.
|
|
140
|
+
* Each raw entry `readInstalledManifest` dropped as malformed is reported
|
|
141
|
+
* with its index and reason. For each kept entry, `path` and `repoRoot` are
|
|
142
|
+
* resolved against the target's top level independently (the manifest's
|
|
143
|
+
* model; `path` is not nested under `repoRoot`) and each one that is not a
|
|
144
|
+
* directory is reported by name. A non-empty list whose normalised `path`s
|
|
145
|
+
* omit an existing `docs/okf/` at the target's root (the implicit default
|
|
146
|
+
* every kit site assumes absent a `knowledge` field) is reported once.
|
|
147
|
+
* Absent or empty `knowledge` is today's default behaviour and never warns
|
|
148
|
+
* about `docs/okf/`: a repo that never configured `knowledge` is not warned
|
|
149
|
+
* about lacking an entry for its own default.
|
|
150
|
+
*/
|
|
151
|
+
function computeKnowledgeWarnings(targetPath, manifest) {
|
|
152
|
+
const knowledge = manifest.knowledge ?? [];
|
|
153
|
+
const warnings = knowledgeEntryProblems(readRawKnowledge(targetPath));
|
|
154
|
+
for (const bundle of knowledge) {
|
|
155
|
+
if (!isDirectoryAt(join(targetPath, bundle.path))) {
|
|
156
|
+
warnings.push(`missing configured knowledge path: ${bundle.path}`);
|
|
157
|
+
}
|
|
158
|
+
if (bundle.repoRoot !== "." &&
|
|
159
|
+
!isDirectoryAt(join(targetPath, bundle.repoRoot))) {
|
|
160
|
+
warnings.push(`missing configured knowledge repoRoot: ${bundle.repoRoot}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (knowledge.length > 0) {
|
|
164
|
+
const docsOkfExists = isDirectoryAt(join(targetPath, DEFAULT_KNOWLEDGE_PATH));
|
|
165
|
+
const docsOkfConfigured = knowledge.some((bundle) => bundle.path === DEFAULT_KNOWLEDGE_PATH);
|
|
166
|
+
if (docsOkfExists && !docsOkfConfigured) {
|
|
167
|
+
warnings.push(`${DEFAULT_KNOWLEDGE_PATH}/ exists but is not in the configured knowledge list`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return warnings;
|
|
171
|
+
}
|
|
114
172
|
function baseReport(target, operator, status, reason) {
|
|
115
173
|
return {
|
|
116
174
|
path: target.path,
|
|
@@ -210,6 +268,7 @@ export function inspectTarget(target, operator, kitVersion) {
|
|
|
210
268
|
const versionLag = hasPin
|
|
211
269
|
? manifest.pin !== manifest.version
|
|
212
270
|
: manifest.version !== kitVersion;
|
|
271
|
+
const knowledgeWarnings = computeKnowledgeWarnings(target.path, manifest);
|
|
213
272
|
let status;
|
|
214
273
|
if (driftFiles.length > 0) {
|
|
215
274
|
status = "drift";
|
|
@@ -236,6 +295,7 @@ export function inspectTarget(target, operator, kitVersion) {
|
|
|
236
295
|
...(routingComparison.gaps.length > 0
|
|
237
296
|
? { routingComparisonGaps: routingComparison.gaps }
|
|
238
297
|
: {}),
|
|
298
|
+
...(knowledgeWarnings.length > 0 ? { knowledgeWarnings } : {}),
|
|
239
299
|
repoProfile: manifest.profile,
|
|
240
300
|
operatorProfile: operator.defaults.profile,
|
|
241
301
|
repoTiers: manifest.tiers,
|
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,25 @@ 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, and a re-install that
|
|
152
|
+
* rewrites the manifest notes each one it removes from disk.
|
|
153
|
+
*/
|
|
154
|
+
knowledge?: KnowledgeBundle[];
|
|
155
|
+
/**
|
|
156
|
+
* The {@link knowledgeEntryProblems} of the raw on-disk `knowledge` value,
|
|
157
|
+
* set by `readInstalledManifest` only when there is at least one. Never
|
|
158
|
+
* written back: `runInit` turns each into a report note when it rewrites
|
|
159
|
+
* the manifest and so removes the offending value from disk.
|
|
160
|
+
*/
|
|
161
|
+
knowledgeProblems?: string[];
|
|
117
162
|
}
|
|
118
163
|
/**
|
|
119
164
|
* A manifest can be hand-written or tampered with, and uninstall deletes by
|
|
@@ -122,6 +167,38 @@ export interface Manifest extends OpencodeModelMaps {
|
|
|
122
167
|
* can never reach an unlink.
|
|
123
168
|
*/
|
|
124
169
|
export declare function isContainedRelativePath(relativePath: string): boolean;
|
|
170
|
+
/**
|
|
171
|
+
* Checks one raw `knowledge` entry. `path` (the bundle directory) and
|
|
172
|
+
* `repoRoot` (the root of the repository its sources live in) are each
|
|
173
|
+
* resolved against the worktree top level, independently of one another, so
|
|
174
|
+
* a workspace-level bundle whose sources live in a sub-repo is
|
|
175
|
+
* `{ path: "docs/okf", repoRoot: "sub" }`. `repoRoot` defaults to `"."`
|
|
176
|
+
* when omitted. Returns the normalised entry, or the reason it is invalid
|
|
177
|
+
* (naming the offending field) for the caller to throw or report.
|
|
178
|
+
*/
|
|
179
|
+
export declare function checkKnowledgeEntry(entry: unknown): {
|
|
180
|
+
bundle: KnowledgeBundle;
|
|
181
|
+
} | {
|
|
182
|
+
reason: string;
|
|
183
|
+
};
|
|
184
|
+
/**
|
|
185
|
+
* Lists every problem in a raw on-disk `knowledge` value: `[]` when the
|
|
186
|
+
* field is absent or every entry is valid, one item for a non-array value,
|
|
187
|
+
* otherwise one item per invalid entry with its index and reason. `doctor`
|
|
188
|
+
* reports these, since {@link parseKnowledgeBundles} drops such entries on
|
|
189
|
+
* read; `runInit` reports them as notes when a re-install that carries
|
|
190
|
+
* `knowledge` forward rewrites the manifest and so removes them from disk.
|
|
191
|
+
*/
|
|
192
|
+
export declare function knowledgeEntryProblems(raw: unknown): string[];
|
|
193
|
+
/**
|
|
194
|
+
* Validates an explicitly-supplied `options.knowledge` array for `runInit`
|
|
195
|
+
* with {@link checkKnowledgeEntry} and returns the normalised entries.
|
|
196
|
+
* Throws on the first invalid entry -- explicit input refuses to write a
|
|
197
|
+
* malformed record rather than dropping it (contrast
|
|
198
|
+
* {@link parseKnowledgeBundles}, which degrades a hand-edited/damaged
|
|
199
|
+
* on-disk manifest by dropping bad entries instead of throwing).
|
|
200
|
+
*/
|
|
201
|
+
export declare function validateKnowledgeBundles(knowledge: unknown): KnowledgeBundle[];
|
|
125
202
|
/**
|
|
126
203
|
* Reads the manifest of a previous install, if any. Manifests can be written
|
|
127
204
|
* 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, win32 } 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,155 @@ export function isContainedRelativePath(relativePath) {
|
|
|
25
25
|
const normalized = normalize(relativePath);
|
|
26
26
|
return normalized !== ".." && !normalized.startsWith(`..${sep}`);
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* The containment problem of one knowledge-bundle path spelling, or
|
|
30
|
+
* `undefined` when it stays inside the worktree top level: an absolute path
|
|
31
|
+
* (native, POSIX, or Windows such as `C:/x`), any other value starting with
|
|
32
|
+
* a Windows drive letter (the drive-relative `C:x`, `C:..` or `C:`, which
|
|
33
|
+
* Windows resolves against that drive's current directory), or a `..`
|
|
34
|
+
* escape. The `..` test is only meaningful on a normalised value.
|
|
35
|
+
*/
|
|
36
|
+
function knowledgePathContainmentProblem(value) {
|
|
37
|
+
if (isAbsolute(value) || posix.isAbsolute(value) || win32.isAbsolute(value)) {
|
|
38
|
+
return "is an absolute path";
|
|
39
|
+
}
|
|
40
|
+
if (/^[A-Za-z]:/.test(value))
|
|
41
|
+
return "is a Windows drive path";
|
|
42
|
+
if (value === ".." || value.startsWith("../")) {
|
|
43
|
+
return "escapes the worktree top level";
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Normalises one knowledge-bundle path field (`path` or `repoRoot`) and
|
|
49
|
+
* returns it, or a reason string when it is not acceptable. Both fields are
|
|
50
|
+
* worktree-relative, so spellings of the same location compare equal after
|
|
51
|
+
* this step: POSIX `normalize`, then any trailing `/` stripped (`docs/okf/`,
|
|
52
|
+
* `./docs/okf` and `docs/okf` all become `docs/okf`). An empty string, any
|
|
53
|
+
* backslash, and every value with a
|
|
54
|
+
* {@link knowledgePathContainmentProblem} are rejected. The containment
|
|
55
|
+
* check runs on the normalised value, which is the one that is stored and
|
|
56
|
+
* later resolved, so a prefix that normalisation removes cannot smuggle a
|
|
57
|
+
* drive or absolute form past it (`./C:x` and `a/../C:x` store `C:x`,
|
|
58
|
+
* `docs/../C:/x` stores `C:/x`; all rejected). It also runs on the value as
|
|
59
|
+
* written, so an absolute or drive path is never silently reinterpreted as
|
|
60
|
+
* a relative one (`C:/../docs` would normalise to `docs`). An accepted
|
|
61
|
+
* value's stored form is therefore accepted again unchanged. `.` (the
|
|
62
|
+
* worktree top level itself) is accepted only when `allowTop` is set, which
|
|
63
|
+
* is the case for `repoRoot` and not for `path`. The backslash rule is
|
|
64
|
+
* platform-independent: POSIX normalisation treats `\` as an ordinary
|
|
65
|
+
* character, so `..\outside` would pass the `..` test here and still resolve
|
|
66
|
+
* outside the worktree under Windows path semantics; the stored separator
|
|
67
|
+
* is always `/`.
|
|
68
|
+
*/
|
|
69
|
+
function normalizeKnowledgePathField(value, allowTop) {
|
|
70
|
+
if (typeof value !== "string")
|
|
71
|
+
return { reason: "is not a string" };
|
|
72
|
+
if (value === "")
|
|
73
|
+
return { reason: "is empty" };
|
|
74
|
+
if (value.includes("\\"))
|
|
75
|
+
return { reason: "contains a backslash" };
|
|
76
|
+
let normalized = posix.normalize(value);
|
|
77
|
+
while (normalized.length > 1 && normalized.endsWith("/")) {
|
|
78
|
+
normalized = normalized.slice(0, -1);
|
|
79
|
+
}
|
|
80
|
+
const problem = knowledgePathContainmentProblem(value) ??
|
|
81
|
+
knowledgePathContainmentProblem(normalized);
|
|
82
|
+
if (problem !== undefined)
|
|
83
|
+
return { reason: problem };
|
|
84
|
+
if (normalized === "." && !allowTop) {
|
|
85
|
+
return { reason: "names the worktree top level itself" };
|
|
86
|
+
}
|
|
87
|
+
return { value: normalized };
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Checks one raw `knowledge` entry. `path` (the bundle directory) and
|
|
91
|
+
* `repoRoot` (the root of the repository its sources live in) are each
|
|
92
|
+
* resolved against the worktree top level, independently of one another, so
|
|
93
|
+
* a workspace-level bundle whose sources live in a sub-repo is
|
|
94
|
+
* `{ path: "docs/okf", repoRoot: "sub" }`. `repoRoot` defaults to `"."`
|
|
95
|
+
* when omitted. Returns the normalised entry, or the reason it is invalid
|
|
96
|
+
* (naming the offending field) for the caller to throw or report.
|
|
97
|
+
*/
|
|
98
|
+
export function checkKnowledgeEntry(entry) {
|
|
99
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
100
|
+
return { reason: "is not an object" };
|
|
101
|
+
}
|
|
102
|
+
const candidate = entry;
|
|
103
|
+
const path = normalizeKnowledgePathField(candidate.path, false);
|
|
104
|
+
if ("reason" in path)
|
|
105
|
+
return { reason: `path ${path.reason}` };
|
|
106
|
+
const repoRoot = normalizeKnowledgePathField(candidate.repoRoot === undefined ? "." : candidate.repoRoot, true);
|
|
107
|
+
if ("reason" in repoRoot)
|
|
108
|
+
return { reason: `repoRoot ${repoRoot.reason}` };
|
|
109
|
+
return { bundle: { path: path.value, repoRoot: repoRoot.value } };
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Lists every problem in a raw on-disk `knowledge` value: `[]` when the
|
|
113
|
+
* field is absent or every entry is valid, one item for a non-array value,
|
|
114
|
+
* otherwise one item per invalid entry with its index and reason. `doctor`
|
|
115
|
+
* reports these, since {@link parseKnowledgeBundles} drops such entries on
|
|
116
|
+
* read; `runInit` reports them as notes when a re-install that carries
|
|
117
|
+
* `knowledge` forward rewrites the manifest and so removes them from disk.
|
|
118
|
+
*/
|
|
119
|
+
export function knowledgeEntryProblems(raw) {
|
|
120
|
+
if (raw === undefined)
|
|
121
|
+
return [];
|
|
122
|
+
if (!Array.isArray(raw)) {
|
|
123
|
+
return ["knowledge is not an array and is ignored"];
|
|
124
|
+
}
|
|
125
|
+
const problems = [];
|
|
126
|
+
raw.forEach((entry, index) => {
|
|
127
|
+
const checked = checkKnowledgeEntry(entry);
|
|
128
|
+
if ("reason" in checked) {
|
|
129
|
+
problems.push(`knowledge[${index}] ${checked.reason} and is ignored`);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
return problems;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Validates an explicitly-supplied `options.knowledge` array for `runInit`
|
|
136
|
+
* with {@link checkKnowledgeEntry} and returns the normalised entries.
|
|
137
|
+
* Throws on the first invalid entry -- explicit input refuses to write a
|
|
138
|
+
* malformed record rather than dropping it (contrast
|
|
139
|
+
* {@link parseKnowledgeBundles}, which degrades a hand-edited/damaged
|
|
140
|
+
* on-disk manifest by dropping bad entries instead of throwing).
|
|
141
|
+
*/
|
|
142
|
+
export function validateKnowledgeBundles(knowledge) {
|
|
143
|
+
if (!Array.isArray(knowledge)) {
|
|
144
|
+
throw new Error("options.knowledge must be an array of { path, repoRoot } entries");
|
|
145
|
+
}
|
|
146
|
+
return knowledge.map((entry, index) => {
|
|
147
|
+
const checked = checkKnowledgeEntry(entry);
|
|
148
|
+
if ("reason" in checked) {
|
|
149
|
+
throw new Error(`options.knowledge[${index}] ${checked.reason}; path and repoRoot must be relative paths inside the worktree top level`);
|
|
150
|
+
}
|
|
151
|
+
return checked.bundle;
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Sanitizes a possibly hand-edited or damaged on-disk manifest's raw
|
|
156
|
+
* `knowledge` field for {@link readInstalledManifest}: an entry that fails
|
|
157
|
+
* {@link checkKnowledgeEntry} is dropped rather than throwing (the read path
|
|
158
|
+
* can never crash a re-install on tampered input, the same reasoning
|
|
159
|
+
* `files`/`models` already document above), and a valid entry is kept in
|
|
160
|
+
* its normalised spelling. Returns `undefined` when the raw field is absent
|
|
161
|
+
* or not an array, so the "never configured" and "configured but empty"
|
|
162
|
+
* states stay distinct the same way `routing`'s own `"routing" in
|
|
163
|
+
* candidate` check does.
|
|
164
|
+
*/
|
|
165
|
+
function parseKnowledgeBundles(candidate) {
|
|
166
|
+
if (!("knowledge" in candidate) || !Array.isArray(candidate.knowledge)) {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
const result = [];
|
|
170
|
+
for (const entry of candidate.knowledge) {
|
|
171
|
+
const checked = checkKnowledgeEntry(entry);
|
|
172
|
+
if ("bundle" in checked)
|
|
173
|
+
result.push(checked.bundle);
|
|
174
|
+
}
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
28
177
|
/**
|
|
29
178
|
* Reads the manifest of a previous install, if any. Manifests can be written
|
|
30
179
|
* by hand (manual agent installs) or damaged, so every field is sanitized;
|
|
@@ -102,6 +251,8 @@ export function readInstalledManifest(targetDir) {
|
|
|
102
251
|
if ("routing" in candidate) {
|
|
103
252
|
routing = parseRouting(candidate.routing);
|
|
104
253
|
}
|
|
254
|
+
const knowledge = parseKnowledgeBundles(candidate);
|
|
255
|
+
const knowledgeProblems = knowledgeEntryProblems(candidate.knowledge);
|
|
105
256
|
// A hand-written or damaged manifest may carry a non-string `pin`; that
|
|
106
257
|
// degrades to "no recorded pin" here (the same per-field-degradation
|
|
107
258
|
// style as `profile`/`tiers` above) rather than throwing. An empty or
|
|
@@ -117,6 +268,8 @@ export function readInstalledManifest(targetDir) {
|
|
|
117
268
|
profile,
|
|
118
269
|
tiers,
|
|
119
270
|
...(routing !== undefined ? { routing } : {}),
|
|
271
|
+
...(knowledge !== undefined ? { knowledge } : {}),
|
|
272
|
+
...(knowledgeProblems.length > 0 ? { knowledgeProblems } : {}),
|
|
120
273
|
...opencodeMaps,
|
|
121
274
|
files,
|
|
122
275
|
installedAt: typeof candidate.installedAt === "string" ? candidate.installedAt : "",
|
|
@@ -348,6 +501,14 @@ export function runInit(options) {
|
|
|
348
501
|
updateOpencodeModels: options.opencodeModels !== undefined,
|
|
349
502
|
updateOpencodeClassModels: options.opencodeClassModels !== undefined,
|
|
350
503
|
});
|
|
504
|
+
// `undefined` carries the previous manifest's `knowledge` forward
|
|
505
|
+
// unchanged, the same omitted-means-sticky convention `routing`/`pin` use
|
|
506
|
+
// above; a present array (including `[]`) is validated and replaces the
|
|
507
|
+
// previous value outright, throwing before any file is written on an
|
|
508
|
+
// invalid entry rather than persisting a malformed record.
|
|
509
|
+
const knowledge = options.knowledge !== undefined
|
|
510
|
+
? validateKnowledgeBundles(options.knowledge)
|
|
511
|
+
: previous?.knowledge;
|
|
351
512
|
// Check only rendered selections before the first mutation.
|
|
352
513
|
for (const warning of codexCatalogWarnings(routing, { harnesses: options.harnesses, profile, tiers }, options.codexCatalog)) {
|
|
353
514
|
report.notes.push(`Codex catalog: ${warning}`);
|
|
@@ -677,6 +838,7 @@ export function runInit(options) {
|
|
|
677
838
|
profile,
|
|
678
839
|
tiers,
|
|
679
840
|
routing,
|
|
841
|
+
...(knowledge !== undefined ? { knowledge } : {}),
|
|
680
842
|
...compatibility,
|
|
681
843
|
files: installedFiles,
|
|
682
844
|
...(pin !== undefined ? { pin } : {}),
|
|
@@ -691,6 +853,9 @@ export function runInit(options) {
|
|
|
691
853
|
profile: previous.profile,
|
|
692
854
|
tiers: previous.tiers,
|
|
693
855
|
...(previous.routing !== undefined ? { routing: previous.routing } : {}),
|
|
856
|
+
...(previous.knowledge !== undefined
|
|
857
|
+
? { knowledge: previous.knowledge }
|
|
858
|
+
: {}),
|
|
694
859
|
...legacyOpencodeFallbacks(previous),
|
|
695
860
|
files: previous.files,
|
|
696
861
|
...(previous.pin !== undefined ? { pin: previous.pin } : {}),
|
|
@@ -698,6 +863,16 @@ export function runInit(options) {
|
|
|
698
863
|
report.skipped.push(manifestPath);
|
|
699
864
|
}
|
|
700
865
|
else {
|
|
866
|
+
// `previous.knowledge` was sanitized on read, so rewriting the manifest
|
|
867
|
+
// from it removes each invalid hand-edited entry (or a non-array value)
|
|
868
|
+
// from disk, after which `doctor` has nothing left to report. Name each
|
|
869
|
+
// one here instead. An explicit `options.knowledge` replaces the whole
|
|
870
|
+
// field on purpose and gets no note.
|
|
871
|
+
if (options.knowledge === undefined) {
|
|
872
|
+
for (const problem of previous?.knowledgeProblems ?? []) {
|
|
873
|
+
report.notes.push(`manifest: ${problem}; dropped from the rewritten manifest`);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
701
876
|
const manifest = {
|
|
702
877
|
...desired,
|
|
703
878
|
installedAt: previous?.installedAt || new Date().toISOString(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orchestrator-workflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.42.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",
|