pactwright 0.0.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.
- package/LICENSE +201 -0
- package/README.md +68 -0
- package/dist/adapter/claude-code.d.ts +53 -0
- package/dist/adapter/claude-code.js +241 -0
- package/dist/adapter/commands.d.ts +19 -0
- package/dist/adapter/commands.js +162 -0
- package/dist/atomic.d.ts +6 -0
- package/dist/atomic.js +11 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +561 -0
- package/dist/config/config.d.ts +55 -0
- package/dist/config/config.js +199 -0
- package/dist/config/lifecycle.d.ts +34 -0
- package/dist/config/lifecycle.js +81 -0
- package/dist/config/lock.d.ts +43 -0
- package/dist/config/lock.js +141 -0
- package/dist/context.d.ts +59 -0
- package/dist/context.js +111 -0
- package/dist/errors.d.ts +21 -0
- package/dist/errors.js +25 -0
- package/dist/eval/case.d.ts +123 -0
- package/dist/eval/case.js +17 -0
- package/dist/eval/core-suite.d.ts +3 -0
- package/dist/eval/core-suite.js +431 -0
- package/dist/eval/runner.d.ts +75 -0
- package/dist/eval/runner.js +159 -0
- package/dist/eval/sandbox.d.ts +39 -0
- package/dist/eval/sandbox.js +143 -0
- package/dist/extension/manage.d.ts +65 -0
- package/dist/extension/manage.js +372 -0
- package/dist/extension/manifest.d.ts +36 -0
- package/dist/extension/manifest.js +164 -0
- package/dist/extension/resolve.d.ts +77 -0
- package/dist/extension/resolve.js +271 -0
- package/dist/graph/edge-schema.d.ts +55 -0
- package/dist/graph/edge-schema.js +0 -0
- package/dist/graph/edges.d.ts +22 -0
- package/dist/graph/edges.js +63 -0
- package/dist/graph/ids.d.ts +14 -0
- package/dist/graph/ids.js +38 -0
- package/dist/graph/lineage.d.ts +48 -0
- package/dist/graph/lineage.js +226 -0
- package/dist/graph/mutations.d.ts +108 -0
- package/dist/graph/mutations.js +356 -0
- package/dist/graph/nodes.d.ts +46 -0
- package/dist/graph/nodes.js +137 -0
- package/dist/graph/revision.d.ts +50 -0
- package/dist/graph/revision.js +75 -0
- package/dist/graph/schema.d.ts +54 -0
- package/dist/graph/schema.js +90 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +33 -0
- package/dist/init.d.ts +47 -0
- package/dist/init.js +132 -0
- package/dist/lifecycle/engine.d.ts +75 -0
- package/dist/lifecycle/engine.js +146 -0
- package/dist/lifecycle/record.d.ts +18 -0
- package/dist/lifecycle/record.js +157 -0
- package/dist/lifecycle/run.d.ts +62 -0
- package/dist/lifecycle/run.js +167 -0
- package/dist/loader.d.ts +38 -0
- package/dist/loader.js +64 -0
- package/dist/pack/capabilities.d.ts +22 -0
- package/dist/pack/capabilities.js +31 -0
- package/dist/pack/locate.d.ts +22 -0
- package/dist/pack/locate.js +80 -0
- package/dist/pack/manifest.d.ts +34 -0
- package/dist/pack/manifest.js +168 -0
- package/dist/pack/resolve.d.ts +92 -0
- package/dist/pack/resolve.js +238 -0
- package/dist/project.d.ts +22 -0
- package/dist/project.js +37 -0
- package/dist/sync.d.ts +54 -0
- package/dist/sync.js +98 -0
- package/dist/validate.d.ts +23 -0
- package/dist/validate.js +32 -0
- package/dist/validation.d.ts +24 -0
- package/dist/validation.js +83 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +8 -0
- package/dist/yaml.d.ts +12 -0
- package/dist/yaml.js +32 -0
- package/package.json +65 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { PactwrightError } from "../errors.js";
|
|
2
|
+
import { deriveLineage } from "../graph/lineage.js";
|
|
3
|
+
import { loadProject } from "../loader.js";
|
|
4
|
+
import { isActive, isTransientStage, nextActionFor, selectLineages } from "./engine.js";
|
|
5
|
+
/**
|
|
6
|
+
* The runtime has no way to perform automatic stages until an agent pack is
|
|
7
|
+
* installed (Checkpoint 1, Step 10): every automatic stage fails, so `run`
|
|
8
|
+
* stops there instead of pretending.
|
|
9
|
+
*/
|
|
10
|
+
export const noExecutor = ({ stage }) => ({
|
|
11
|
+
status: "failed",
|
|
12
|
+
message: `no executor for automatic stage "${stage}"; agent-pack execution arrives with the default agent pack`,
|
|
13
|
+
});
|
|
14
|
+
function load(root) {
|
|
15
|
+
try {
|
|
16
|
+
return loadProject({ root });
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (error instanceof PactwrightError)
|
|
20
|
+
return error;
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function validationStop(intent, error, executed) {
|
|
25
|
+
return {
|
|
26
|
+
...(intent === undefined ? {} : { intent }),
|
|
27
|
+
stop: "validation-error",
|
|
28
|
+
executed,
|
|
29
|
+
message: error.message,
|
|
30
|
+
problems: error.problems,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** Runs one lineage (or the capture-intent entry point) until it stops. */
|
|
34
|
+
async function runLineage(options, intent, first) {
|
|
35
|
+
const executed = [];
|
|
36
|
+
const done = new Set();
|
|
37
|
+
let project = first;
|
|
38
|
+
let previousState;
|
|
39
|
+
const tag = intent === undefined ? {} : { intent };
|
|
40
|
+
for (;;) {
|
|
41
|
+
let lineage;
|
|
42
|
+
if (intent !== undefined) {
|
|
43
|
+
lineage = deriveLineage(intent, project.graph.nodes, project.graph.edges);
|
|
44
|
+
if (lineage === undefined) {
|
|
45
|
+
// The loader validated the graph, so only a vanished intent gets here.
|
|
46
|
+
return {
|
|
47
|
+
...tag,
|
|
48
|
+
stop: "validation-error",
|
|
49
|
+
executed,
|
|
50
|
+
message: `intent "${intent}" has no unambiguous lineage`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (lineage.state !== previousState)
|
|
54
|
+
done.clear();
|
|
55
|
+
previousState = lineage.state;
|
|
56
|
+
}
|
|
57
|
+
const next = nextActionFor(project, lineage, done);
|
|
58
|
+
if (next.stage === undefined)
|
|
59
|
+
return { ...tag, stop: "completed", executed };
|
|
60
|
+
if (next.gate) {
|
|
61
|
+
return { ...tag, stop: "human-gate", stage: next.stage, requiredActor: "human", executed };
|
|
62
|
+
}
|
|
63
|
+
let outcome;
|
|
64
|
+
try {
|
|
65
|
+
outcome = await options.execute({
|
|
66
|
+
stage: next.stage,
|
|
67
|
+
project,
|
|
68
|
+
...(lineage ? { lineage } : {}),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
outcome = {
|
|
73
|
+
status: "failed",
|
|
74
|
+
message: error instanceof Error ? error.message : String(error),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (outcome.status === "failed") {
|
|
78
|
+
return {
|
|
79
|
+
...tag,
|
|
80
|
+
stop: "stage-failed",
|
|
81
|
+
stage: next.stage,
|
|
82
|
+
executed,
|
|
83
|
+
message: outcome.message,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
executed.push(next.stage);
|
|
87
|
+
// Repository state is re-read after every stage: a validation error stops the run.
|
|
88
|
+
const reloaded = load(options.root);
|
|
89
|
+
if (reloaded instanceof PactwrightError)
|
|
90
|
+
return validationStop(intent, reloaded, executed);
|
|
91
|
+
project = reloaded;
|
|
92
|
+
if (isTransientStage(next.stage)) {
|
|
93
|
+
done.add(next.stage);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
// A graph-marking stage must have advanced the lineage, else the run would loop forever.
|
|
97
|
+
const advanced = intent === undefined
|
|
98
|
+
? selectLineages(project).some((candidate) => candidate !== undefined && isActive(candidate))
|
|
99
|
+
: deriveLineage(intent, project.graph.nodes, project.graph.edges)?.state !== previousState;
|
|
100
|
+
if (!advanced) {
|
|
101
|
+
return {
|
|
102
|
+
...tag,
|
|
103
|
+
stop: "stage-failed",
|
|
104
|
+
stage: next.stage,
|
|
105
|
+
executed,
|
|
106
|
+
message: `${next.stage} completed without advancing the graph`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (intent === undefined) {
|
|
110
|
+
// capture-intent created the first active lineage(s); the caller picks them up.
|
|
111
|
+
return { ...tag, stop: "completed", executed };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* `lifecycle run` (Delivery Graph §20): runs automatic stages of every
|
|
117
|
+
* active lineage (or the one `intentId`) until a human gate, completion, a
|
|
118
|
+
* stage failure or a validation error. Gates are checked by the runtime on
|
|
119
|
+
* every step, so a configured gate is never skipped whatever the executor
|
|
120
|
+
* could do. Never throws for expected failures.
|
|
121
|
+
*/
|
|
122
|
+
export async function runLifecycle(options) {
|
|
123
|
+
const project = load(options.root);
|
|
124
|
+
if (project instanceof PactwrightError)
|
|
125
|
+
return [validationStop(options.intentId, project, [])];
|
|
126
|
+
let targets;
|
|
127
|
+
try {
|
|
128
|
+
targets = selectLineages(project, options.intentId);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (!(error instanceof PactwrightError))
|
|
132
|
+
throw error;
|
|
133
|
+
return [validationStop(options.intentId, error, [])];
|
|
134
|
+
}
|
|
135
|
+
const results = [];
|
|
136
|
+
let current = project;
|
|
137
|
+
for (const target of targets) {
|
|
138
|
+
const result = await runLineage(options, target?.intent.id, current);
|
|
139
|
+
results.push(result);
|
|
140
|
+
if (target === undefined && result.stop === "completed" && result.executed.length > 0) {
|
|
141
|
+
// capture-intent ran: continue with the lineages it created, reloading
|
|
142
|
+
// before each one so lineage N+1 starts from the graph lineage N wrote.
|
|
143
|
+
const reloaded = load(options.root);
|
|
144
|
+
if (reloaded instanceof PactwrightError) {
|
|
145
|
+
results.push(validationStop(undefined, reloaded, []));
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
const createdIds = selectLineages(reloaded)
|
|
149
|
+
.filter((created) => created !== undefined && isActive(created))
|
|
150
|
+
.map((created) => created.intent.id);
|
|
151
|
+
for (const created of createdIds) {
|
|
152
|
+
const fresh = load(options.root);
|
|
153
|
+
if (fresh instanceof PactwrightError) {
|
|
154
|
+
results.push(validationStop(created, fresh, []));
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
results.push(await runLineage(options, created, fresh));
|
|
158
|
+
}
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
const reloaded = load(options.root);
|
|
162
|
+
if (reloaded instanceof PactwrightError)
|
|
163
|
+
break;
|
|
164
|
+
current = reloaded;
|
|
165
|
+
}
|
|
166
|
+
return results;
|
|
167
|
+
}
|
package/dist/loader.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type PactwrightConfig } from "./config/config.js";
|
|
2
|
+
import { type LifecycleConfig } from "./config/lifecycle.js";
|
|
3
|
+
import { type LockFile } from "./config/lock.js";
|
|
4
|
+
import { type ResolvedExtension } from "./extension/resolve.js";
|
|
5
|
+
import { type Edge } from "./graph/edges.js";
|
|
6
|
+
import { type GraphNode } from "./graph/nodes.js";
|
|
7
|
+
import { type ProjectPaths } from "./project.js";
|
|
8
|
+
/** Fully loaded canonical project state. */
|
|
9
|
+
export interface Project {
|
|
10
|
+
readonly paths: ProjectPaths;
|
|
11
|
+
readonly config: PactwrightConfig;
|
|
12
|
+
readonly lifecycle: LifecycleConfig;
|
|
13
|
+
readonly lock: LockFile;
|
|
14
|
+
/** Every configured extension, resolved; empty when none are configured. */
|
|
15
|
+
readonly extensions: readonly ResolvedExtension[];
|
|
16
|
+
readonly graph: {
|
|
17
|
+
readonly nodes: readonly GraphNode[];
|
|
18
|
+
readonly edges: readonly Edge[];
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export interface LoadProjectOptions {
|
|
22
|
+
/** Directory to start searching for the project root from. Defaults to `process.cwd()`. */
|
|
23
|
+
readonly cwd?: string;
|
|
24
|
+
/** Use this root directly instead of searching upward from `cwd`. */
|
|
25
|
+
readonly root?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The single canonical loading path for a Pactwright project.
|
|
29
|
+
*
|
|
30
|
+
* Reads, in order: config → lifecycle → lock → configured extensions (whose
|
|
31
|
+
* registered graph types extend the schema registries) → nodes (then node
|
|
32
|
+
* schemas) → edges (then the typed-edge registry) → current-lineage
|
|
33
|
+
* derivation. Every file is parsed even after an earlier one fails so the
|
|
34
|
+
* caller sees all problems at once; if any problem was found a
|
|
35
|
+
* `PactwrightError` with code `project-load-failed` is thrown carrying the
|
|
36
|
+
* full list.
|
|
37
|
+
*/
|
|
38
|
+
export declare function loadProject(options?: LoadProjectOptions): Project;
|
package/dist/loader.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { PactwrightError } from "./errors.js";
|
|
2
|
+
import { loadConfig } from "./config/config.js";
|
|
3
|
+
import { loadLifecycle } from "./config/lifecycle.js";
|
|
4
|
+
import { loadLock } from "./config/lock.js";
|
|
5
|
+
import { composedRegistries, resolveExtensions, } from "./extension/resolve.js";
|
|
6
|
+
import { CORE_EDGE_SCHEMAS, validateEdges } from "./graph/edge-schema.js";
|
|
7
|
+
import { loadEdges } from "./graph/edges.js";
|
|
8
|
+
import { validateLineages } from "./graph/lineage.js";
|
|
9
|
+
import { loadNodes } from "./graph/nodes.js";
|
|
10
|
+
import { CORE_NODE_SCHEMAS, validateNodes } from "./graph/schema.js";
|
|
11
|
+
import { findProjectRoot, projectPaths } from "./project.js";
|
|
12
|
+
/**
|
|
13
|
+
* The single canonical loading path for a Pactwright project.
|
|
14
|
+
*
|
|
15
|
+
* Reads, in order: config → lifecycle → lock → configured extensions (whose
|
|
16
|
+
* registered graph types extend the schema registries) → nodes (then node
|
|
17
|
+
* schemas) → edges (then the typed-edge registry) → current-lineage
|
|
18
|
+
* derivation. Every file is parsed even after an earlier one fails so the
|
|
19
|
+
* caller sees all problems at once; if any problem was found a
|
|
20
|
+
* `PactwrightError` with code `project-load-failed` is thrown carrying the
|
|
21
|
+
* full list.
|
|
22
|
+
*/
|
|
23
|
+
export function loadProject(options = {}) {
|
|
24
|
+
const root = options.root ?? findProjectRoot(options.cwd);
|
|
25
|
+
const paths = projectPaths(root);
|
|
26
|
+
const problems = [];
|
|
27
|
+
const config = loadConfig(paths.config);
|
|
28
|
+
problems.push(...config.problems);
|
|
29
|
+
const lifecycle = loadLifecycle(paths.lifecycle);
|
|
30
|
+
problems.push(...lifecycle.problems);
|
|
31
|
+
const lock = loadLock(paths.lock);
|
|
32
|
+
problems.push(...lock.problems);
|
|
33
|
+
let extensions = [];
|
|
34
|
+
let nodeRegistry = CORE_NODE_SCHEMAS;
|
|
35
|
+
let edgeRegistry = CORE_EDGE_SCHEMAS;
|
|
36
|
+
if (config.value !== undefined && Object.keys(config.value.extensions).length > 0) {
|
|
37
|
+
const resolved = resolveExtensions({ root: paths.root, config: config.value });
|
|
38
|
+
if (resolved.value === undefined) {
|
|
39
|
+
problems.push(...resolved.problems);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
extensions = resolved.value;
|
|
43
|
+
({ nodes: nodeRegistry, edges: edgeRegistry } = composedRegistries(extensions));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const nodes = loadNodes(paths.nodesDir);
|
|
47
|
+
problems.push(...nodes.problems);
|
|
48
|
+
problems.push(...validateNodes(nodes.nodes, nodeRegistry));
|
|
49
|
+
const edges = loadEdges(paths.edges);
|
|
50
|
+
problems.push(...edges.problems);
|
|
51
|
+
problems.push(...validateEdges(edges.edges, nodes.nodes, edgeRegistry, paths.edges));
|
|
52
|
+
problems.push(...validateLineages(nodes.nodes, edges.edges));
|
|
53
|
+
if (problems.length > 0 || !config.value || !lifecycle.value || !lock.value) {
|
|
54
|
+
throw PactwrightError.fromProblems("project-load-failed", problems);
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
paths,
|
|
58
|
+
config: config.value,
|
|
59
|
+
lifecycle: lifecycle.value,
|
|
60
|
+
lock: lock.value,
|
|
61
|
+
extensions,
|
|
62
|
+
graph: { nodes: nodes.nodes, edges: edges.edges },
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { PackManifest } from "./manifest.js";
|
|
2
|
+
/**
|
|
3
|
+
* The capabilities every Delivery project requires of its agent pack
|
|
4
|
+
* (Distribution §7): the Specification, Delivery and Review
|
|
5
|
+
* responsibilities of Delivery Graph §16. Graph mutation is a runtime
|
|
6
|
+
* responsibility, never a pack capability.
|
|
7
|
+
*/
|
|
8
|
+
export declare const CORE_CAPABILITIES: readonly ["delivery-specification", "delivery-execution", "delivery-review"];
|
|
9
|
+
export type CoreCapability = (typeof CORE_CAPABILITIES)[number];
|
|
10
|
+
/** Capability names are kebab-case identifiers, like `operations-analysis`. */
|
|
11
|
+
export declare const CAPABILITY_PATTERN: RegExp;
|
|
12
|
+
/**
|
|
13
|
+
* The capability set the selected pack must satisfy: the core set plus the
|
|
14
|
+
* `agent_capabilities` of every enabled extension (Distribution §5, §7).
|
|
15
|
+
* Callers pass the manifests of the enabled extensions, so only capabilities
|
|
16
|
+
* an enabled extension asks for are mandatory.
|
|
17
|
+
*/
|
|
18
|
+
export declare function requiredCapabilities(extensions?: ReadonlyArray<{
|
|
19
|
+
readonly agentCapabilities: readonly string[];
|
|
20
|
+
}>): readonly string[];
|
|
21
|
+
/** Required capabilities the manifest does not map to an agent, sorted. */
|
|
22
|
+
export declare function missingCapabilities(manifest: PackManifest, required: readonly string[]): readonly string[];
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The capabilities every Delivery project requires of its agent pack
|
|
3
|
+
* (Distribution §7): the Specification, Delivery and Review
|
|
4
|
+
* responsibilities of Delivery Graph §16. Graph mutation is a runtime
|
|
5
|
+
* responsibility, never a pack capability.
|
|
6
|
+
*/
|
|
7
|
+
export const CORE_CAPABILITIES = [
|
|
8
|
+
"delivery-specification",
|
|
9
|
+
"delivery-execution",
|
|
10
|
+
"delivery-review",
|
|
11
|
+
];
|
|
12
|
+
/** Capability names are kebab-case identifiers, like `operations-analysis`. */
|
|
13
|
+
export const CAPABILITY_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
14
|
+
/**
|
|
15
|
+
* The capability set the selected pack must satisfy: the core set plus the
|
|
16
|
+
* `agent_capabilities` of every enabled extension (Distribution §5, §7).
|
|
17
|
+
* Callers pass the manifests of the enabled extensions, so only capabilities
|
|
18
|
+
* an enabled extension asks for are mandatory.
|
|
19
|
+
*/
|
|
20
|
+
export function requiredCapabilities(extensions = []) {
|
|
21
|
+
const union = new Set(CORE_CAPABILITIES);
|
|
22
|
+
for (const manifest of extensions) {
|
|
23
|
+
for (const capability of manifest.agentCapabilities)
|
|
24
|
+
union.add(capability);
|
|
25
|
+
}
|
|
26
|
+
return [...union].sort();
|
|
27
|
+
}
|
|
28
|
+
/** Required capabilities the manifest does not map to an agent, sorted. */
|
|
29
|
+
export function missingCapabilities(manifest, required) {
|
|
30
|
+
return required.filter((capability) => manifest.capabilities[capability] === undefined).sort();
|
|
31
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Problem } from "../errors.js";
|
|
2
|
+
export declare function sha256(text: string): string;
|
|
3
|
+
/** Whether `range` is a shape `satisfiesRange` understands: `x.y.z` or `^x.y.z`. */
|
|
4
|
+
export declare function isValidRange(range: string): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Whether `version` satisfies `range`: an exact version, or a `^x.y.z`
|
|
7
|
+
* caret range with npm semantics (`^0.0.z` is exact, `^0.y.z` fixes the
|
|
8
|
+
* minor, `^x.y.z` fixes the major). During the `0.0.x` series packs declare
|
|
9
|
+
* the exact runtime version, so caret only matters from `0.1.0` onward.
|
|
10
|
+
*/
|
|
11
|
+
export declare function satisfiesRange(version: string, range: string): boolean;
|
|
12
|
+
/** Whether `source` is a filesystem path rather than a package name. */
|
|
13
|
+
export declare function isPathSource(source: string): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Where a package-backed component (agent pack or extension) lives. A path
|
|
16
|
+
* source resolves from the project root. A package source resolves like a
|
|
17
|
+
* dependency of the project first (`<root>/node_modules`), then like a
|
|
18
|
+
* dependency of the runtime — which is how `@pactwright/standard`, a
|
|
19
|
+
* dependency of `pactwright`, is always found after one
|
|
20
|
+
* `pnpm add -D pactwright`. `kind` names the component in problem messages.
|
|
21
|
+
*/
|
|
22
|
+
export declare function locatePackage(root: string, source: string, kind: string): string | Problem;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
|
+
export function sha256(text) {
|
|
5
|
+
return `sha256:${createHash("sha256").update(text, "utf8").digest("hex")}`;
|
|
6
|
+
}
|
|
7
|
+
function parseVersion(text) {
|
|
8
|
+
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(text);
|
|
9
|
+
if (match === null)
|
|
10
|
+
return undefined;
|
|
11
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
12
|
+
}
|
|
13
|
+
/** Whether `range` is a shape `satisfiesRange` understands: `x.y.z` or `^x.y.z`. */
|
|
14
|
+
export function isValidRange(range) {
|
|
15
|
+
return parseVersion(range.startsWith("^") ? range.slice(1) : range) !== undefined;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Whether `version` satisfies `range`: an exact version, or a `^x.y.z`
|
|
19
|
+
* caret range with npm semantics (`^0.0.z` is exact, `^0.y.z` fixes the
|
|
20
|
+
* minor, `^x.y.z` fixes the major). During the `0.0.x` series packs declare
|
|
21
|
+
* the exact runtime version, so caret only matters from `0.1.0` onward.
|
|
22
|
+
*/
|
|
23
|
+
export function satisfiesRange(version, range) {
|
|
24
|
+
const caret = range.startsWith("^");
|
|
25
|
+
const want = parseVersion(caret ? range.slice(1) : range);
|
|
26
|
+
const have = parseVersion(version);
|
|
27
|
+
if (want === undefined || have === undefined)
|
|
28
|
+
return false;
|
|
29
|
+
if (!caret)
|
|
30
|
+
return have.every((part, i) => part === want[i]);
|
|
31
|
+
const cmp = (i) => have[i] - want[i];
|
|
32
|
+
const notBelow = cmp(0) !== 0 ? cmp(0) > 0 : cmp(1) !== 0 ? cmp(1) > 0 : cmp(2) >= 0;
|
|
33
|
+
if (!notBelow)
|
|
34
|
+
return false;
|
|
35
|
+
if (want[0] !== 0)
|
|
36
|
+
return have[0] === want[0];
|
|
37
|
+
if (want[1] !== 0)
|
|
38
|
+
return have[0] === 0 && have[1] === want[1];
|
|
39
|
+
return have[0] === 0 && have[1] === 0 && have[2] === want[2];
|
|
40
|
+
}
|
|
41
|
+
/** Whether `source` is a filesystem path rather than a package name. */
|
|
42
|
+
export function isPathSource(source) {
|
|
43
|
+
return source.startsWith("./") || source.startsWith("../") || isAbsolute(source);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Where a package-backed component (agent pack or extension) lives. A path
|
|
47
|
+
* source resolves from the project root. A package source resolves like a
|
|
48
|
+
* dependency of the project first (`<root>/node_modules`), then like a
|
|
49
|
+
* dependency of the runtime — which is how `@pactwright/standard`, a
|
|
50
|
+
* dependency of `pactwright`, is always found after one
|
|
51
|
+
* `pnpm add -D pactwright`. `kind` names the component in problem messages.
|
|
52
|
+
*/
|
|
53
|
+
export function locatePackage(root, source, kind) {
|
|
54
|
+
if (isPathSource(source))
|
|
55
|
+
return resolve(root, source);
|
|
56
|
+
const candidates = [join(root, "package.json"), import.meta.url];
|
|
57
|
+
let unexported = false;
|
|
58
|
+
for (const from of candidates) {
|
|
59
|
+
try {
|
|
60
|
+
return dirname(createRequire(from).resolve(`${source}/package.json`));
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
// An installed package whose `exports` map hides package.json is a
|
|
64
|
+
// different failure from an absent one; try the next location.
|
|
65
|
+
const code = error.code;
|
|
66
|
+
if (code === "ERR_PACKAGE_PATH_NOT_EXPORTED")
|
|
67
|
+
unexported = true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (unexported) {
|
|
71
|
+
return {
|
|
72
|
+
code: "pack-not-exported",
|
|
73
|
+
message: `${kind} "${source}" is installed but its package "exports" does not expose ./package.json, so it cannot be used as a ${kind}`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
code: "pack-not-found",
|
|
78
|
+
message: `${kind} "${source}" is not installed: it resolves neither from ${root} nor from the runtime`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ParseResult } from "../config/config.js";
|
|
2
|
+
/** File name of an agent-pack manifest at the pack root. */
|
|
3
|
+
export declare const PACK_MANIFEST_FILE = "pack.yml";
|
|
4
|
+
/** Skills are looked up by name at `skills/<name>.md` under the pack root. */
|
|
5
|
+
export declare const SKILLS_DIR = "skills";
|
|
6
|
+
export interface PackAgent {
|
|
7
|
+
/** Prompt path relative to the pack root, e.g. `agents/spec.md`. */
|
|
8
|
+
readonly prompt: string;
|
|
9
|
+
/** Skill names, in manifest order, each resolving to `skills/<name>.md`. */
|
|
10
|
+
readonly skills: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
/** An agent-pack manifest (Distribution §7). */
|
|
13
|
+
export interface PackManifest {
|
|
14
|
+
readonly name: string;
|
|
15
|
+
readonly version: string;
|
|
16
|
+
/** Compatible runtime: an exact version or a `^x.y.z` caret range. */
|
|
17
|
+
readonly pactwright: string;
|
|
18
|
+
/** Capability → agent key; every value names an entry of `agents`. */
|
|
19
|
+
readonly capabilities: Readonly<Record<string, string>>;
|
|
20
|
+
readonly agents: Readonly<Record<string, PackAgent>>;
|
|
21
|
+
}
|
|
22
|
+
export declare const VERSION_PATTERN: RegExp;
|
|
23
|
+
export declare const COMPAT_PATTERN: RegExp;
|
|
24
|
+
export declare const PACKAGE_NAME_PATTERN: RegExp;
|
|
25
|
+
/** Parses manifest data; structural checks only, no filesystem access. */
|
|
26
|
+
export declare function parsePackManifest(raw: unknown, path: string): ParseResult<PackManifest>;
|
|
27
|
+
export declare function skillPath(dir: string, skill: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* Loads and validates the manifest at `<dir>/pack.yml`, then checks that
|
|
30
|
+
* every agent prompt and every referenced skill exists as a non-empty file.
|
|
31
|
+
*/
|
|
32
|
+
export declare function loadPackManifest(dir: string): ParseResult<PackManifest>;
|
|
33
|
+
/** Reads a pack file with LF line endings, the bytes that are hashed. */
|
|
34
|
+
export declare function readPackFile(dir: string, relative: string): string;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, join, normalize, sep } from "node:path";
|
|
3
|
+
import { Checker, expectRecord, expectString, rejectUnknownKeys, requireKeys, } from "../validation.js";
|
|
4
|
+
import { readYamlFile } from "../yaml.js";
|
|
5
|
+
import { CAPABILITY_PATTERN } from "./capabilities.js";
|
|
6
|
+
/** File name of an agent-pack manifest at the pack root. */
|
|
7
|
+
export const PACK_MANIFEST_FILE = "pack.yml";
|
|
8
|
+
/** Skills are looked up by name at `skills/<name>.md` under the pack root. */
|
|
9
|
+
export const SKILLS_DIR = "skills";
|
|
10
|
+
export const VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
|
|
11
|
+
export const COMPAT_PATTERN = /^\^?\d+\.\d+\.\d+$/;
|
|
12
|
+
const NAME_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
|
|
13
|
+
// npm package name, optionally scoped; capped at npm's 214-character limit.
|
|
14
|
+
export const PACKAGE_NAME_PATTERN = /^(@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
|
|
15
|
+
const PACK_NAME_PATTERN = PACKAGE_NAME_PATTERN;
|
|
16
|
+
function expectRelativeFile(c, value, label) {
|
|
17
|
+
const text = expectString(c, value, label);
|
|
18
|
+
if (text === undefined)
|
|
19
|
+
return undefined;
|
|
20
|
+
const clean = normalize(text);
|
|
21
|
+
// Reject a leading `..` path *segment*, not any name starting with two
|
|
22
|
+
// dots: `agents/..foo.md` is a legitimate (if odd) file name.
|
|
23
|
+
if (isAbsolute(clean) || clean === ".." || clean.startsWith(`..${sep}`)) {
|
|
24
|
+
return c.fail("invalid-path", `${label} must be a relative path inside the pack`);
|
|
25
|
+
}
|
|
26
|
+
return text;
|
|
27
|
+
}
|
|
28
|
+
function parseAgent(c, raw, label) {
|
|
29
|
+
const record = expectRecord(c, raw, label);
|
|
30
|
+
if (record === undefined)
|
|
31
|
+
return c.fail("invalid-type", `${label} must be a mapping`);
|
|
32
|
+
requireKeys(c, record, label, ["prompt"]);
|
|
33
|
+
rejectUnknownKeys(c, record, label, ["prompt", "skills"]);
|
|
34
|
+
const prompt = expectRelativeFile(c, record["prompt"], `${label}.prompt`);
|
|
35
|
+
const skills = [];
|
|
36
|
+
if (record["skills"] !== undefined) {
|
|
37
|
+
if (!Array.isArray(record["skills"])) {
|
|
38
|
+
c.fail("invalid-type", `${label}.skills must be a list of skill names`);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
record["skills"].forEach((item, index) => {
|
|
42
|
+
const name = expectString(c, item, `${label}.skills[${index}]`);
|
|
43
|
+
if (name === undefined)
|
|
44
|
+
return;
|
|
45
|
+
if (!NAME_PATTERN.test(name)) {
|
|
46
|
+
c.fail("invalid-value", `${label}.skills[${index}] "${name}" is not a valid skill name`);
|
|
47
|
+
}
|
|
48
|
+
else if (skills.includes(name)) {
|
|
49
|
+
c.fail("duplicate-skill", `${label}.skills lists "${name}" more than once`);
|
|
50
|
+
}
|
|
51
|
+
else
|
|
52
|
+
skills.push(name);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return prompt === undefined ? undefined : { prompt, skills };
|
|
57
|
+
}
|
|
58
|
+
/** Parses manifest data; structural checks only, no filesystem access. */
|
|
59
|
+
export function parsePackManifest(raw, path) {
|
|
60
|
+
const c = new Checker(path);
|
|
61
|
+
const root = expectRecord(c, raw, "pack");
|
|
62
|
+
if (root === undefined) {
|
|
63
|
+
c.fail("invalid-type", "pack manifest must be a mapping");
|
|
64
|
+
return { value: undefined, problems: c.problems };
|
|
65
|
+
}
|
|
66
|
+
requireKeys(c, root, "pack", ["name", "version", "pactwright", "capabilities", "agents"]);
|
|
67
|
+
rejectUnknownKeys(c, root, "pack", ["name", "version", "pactwright", "capabilities", "agents"]);
|
|
68
|
+
const name = expectString(c, root["name"], "pack.name");
|
|
69
|
+
if (name !== undefined && (name.length > 214 || !PACK_NAME_PATTERN.test(name))) {
|
|
70
|
+
c.fail("invalid-value", `pack.name must be a lowercase npm package name (optionally scoped), found "${name}"`);
|
|
71
|
+
}
|
|
72
|
+
const version = expectString(c, root["version"], "pack.version");
|
|
73
|
+
if (version !== undefined && !VERSION_PATTERN.test(version)) {
|
|
74
|
+
c.fail("invalid-value", `pack.version must be x.y.z, found "${version}"`);
|
|
75
|
+
}
|
|
76
|
+
const pactwright = expectString(c, root["pactwright"], "pack.pactwright");
|
|
77
|
+
if (pactwright !== undefined && !COMPAT_PATTERN.test(pactwright)) {
|
|
78
|
+
c.fail("invalid-value", `pack.pactwright must be x.y.z or ^x.y.z, found "${pactwright}"`);
|
|
79
|
+
}
|
|
80
|
+
// Prototype-less maps: keys are author-controlled, and a name like
|
|
81
|
+
// "constructor" must never resolve to an Object.prototype member.
|
|
82
|
+
const agents = Object.create(null);
|
|
83
|
+
const agentsRaw = expectRecord(c, root["agents"], "pack.agents");
|
|
84
|
+
if (agentsRaw !== undefined) {
|
|
85
|
+
for (const key of Object.keys(agentsRaw).sort()) {
|
|
86
|
+
if (!NAME_PATTERN.test(key)) {
|
|
87
|
+
c.fail("invalid-value", `pack.agents key "${key}" is not a valid agent name`);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const agent = parseAgent(c, agentsRaw[key], `pack.agents.${key}`);
|
|
91
|
+
if (agent !== undefined)
|
|
92
|
+
agents[key] = agent;
|
|
93
|
+
}
|
|
94
|
+
if (Object.keys(agentsRaw).length === 0) {
|
|
95
|
+
c.fail("missing-field", "pack.agents must declare at least one agent");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const capabilities = Object.create(null);
|
|
99
|
+
const capsRaw = expectRecord(c, root["capabilities"], "pack.capabilities");
|
|
100
|
+
if (capsRaw !== undefined) {
|
|
101
|
+
for (const capability of Object.keys(capsRaw).sort()) {
|
|
102
|
+
if (!CAPABILITY_PATTERN.test(capability)) {
|
|
103
|
+
c.fail("invalid-capability", `pack.capabilities "${capability}" is not a capability name`);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const agent = expectString(c, capsRaw[capability], `pack.capabilities.${capability}`);
|
|
107
|
+
if (agent === undefined)
|
|
108
|
+
continue;
|
|
109
|
+
if (agentsRaw !== undefined && !Object.hasOwn(agentsRaw, agent)) {
|
|
110
|
+
c.fail("unknown-agent", `pack.capabilities.${capability} names agent "${agent}", which pack.agents does not declare`);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
capabilities[capability] = agent;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (!c.ok || name === undefined || version === undefined || pactwright === undefined) {
|
|
117
|
+
return { value: undefined, problems: c.problems };
|
|
118
|
+
}
|
|
119
|
+
return { value: { name, version, pactwright, capabilities, agents }, problems: [] };
|
|
120
|
+
}
|
|
121
|
+
export function skillPath(dir, skill) {
|
|
122
|
+
return join(dir, SKILLS_DIR, `${skill}.md`);
|
|
123
|
+
}
|
|
124
|
+
function isNonEmptyFile(path) {
|
|
125
|
+
try {
|
|
126
|
+
return statSync(path).isFile() && statSync(path).size > 0;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Loads and validates the manifest at `<dir>/pack.yml`, then checks that
|
|
134
|
+
* every agent prompt and every referenced skill exists as a non-empty file.
|
|
135
|
+
*/
|
|
136
|
+
export function loadPackManifest(dir) {
|
|
137
|
+
const manifestPath = join(dir, PACK_MANIFEST_FILE);
|
|
138
|
+
if (!existsSync(manifestPath)) {
|
|
139
|
+
return {
|
|
140
|
+
value: undefined,
|
|
141
|
+
problems: [
|
|
142
|
+
{ code: "pack-not-found", message: `no ${PACK_MANIFEST_FILE} found`, path: manifestPath },
|
|
143
|
+
],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
const read = readYamlFile(manifestPath);
|
|
147
|
+
if (read.problems.length > 0)
|
|
148
|
+
return { value: undefined, problems: read.problems };
|
|
149
|
+
const parsed = parsePackManifest(read.value, manifestPath);
|
|
150
|
+
if (parsed.value === undefined)
|
|
151
|
+
return parsed;
|
|
152
|
+
const c = new Checker(manifestPath);
|
|
153
|
+
for (const [key, agent] of Object.entries(parsed.value.agents)) {
|
|
154
|
+
if (!isNonEmptyFile(join(dir, agent.prompt))) {
|
|
155
|
+
c.fail("missing-prompt", `pack.agents.${key}.prompt "${agent.prompt}" is not a non-empty file in the pack`);
|
|
156
|
+
}
|
|
157
|
+
for (const skill of agent.skills) {
|
|
158
|
+
if (!isNonEmptyFile(skillPath(dir, skill))) {
|
|
159
|
+
c.fail("missing-skill", `pack.agents.${key} uses skill "${skill}" but ${SKILLS_DIR}/${skill}.md is not a non-empty file in the pack`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return c.ok ? parsed : { value: undefined, problems: c.problems };
|
|
164
|
+
}
|
|
165
|
+
/** Reads a pack file with LF line endings, the bytes that are hashed. */
|
|
166
|
+
export function readPackFile(dir, relative) {
|
|
167
|
+
return readFileSync(join(dir, relative), "utf8").replace(/\r\n/g, "\n");
|
|
168
|
+
}
|