agent-contracts 0.11.1 → 0.11.3
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/README.md +39 -1
- package/dist/cli.js +75 -26
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +17 -9
- package/dist/index.js +77 -28
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -297,7 +297,7 @@ Design regressions become testable.
|
|
|
297
297
|
* **Structured handoff definitions** with formal JSON Schema and `allOf` composition
|
|
298
298
|
* **Reusable schema components** via `components.schemas` and JSON Pointer `$ref`
|
|
299
299
|
* **Artifact ownership and lifecycle modeling**
|
|
300
|
-
* **Config-driven prompt rendering**
|
|
300
|
+
* **Config-driven prompt rendering** with `skip_empty` support for conditional file generation
|
|
301
301
|
* **Variable substitution** via `${vars.xxx}` in DSL values
|
|
302
302
|
* **Inheritance with merge operators via `extends`**
|
|
303
303
|
* **Guardrail definitions** for cross-cutting process constraints
|
|
@@ -865,6 +865,44 @@ This lets you generate static outputs for:
|
|
|
865
865
|
|
|
866
866
|
all from the same resolved DSL.
|
|
867
867
|
|
|
868
|
+
### Render target options
|
|
869
|
+
|
|
870
|
+
Each entry in `renders` supports these fields:
|
|
871
|
+
|
|
872
|
+
| Field | Type | Required | Description |
|
|
873
|
+
|-------|------|----------|-------------|
|
|
874
|
+
| `template` | string | yes | Path to Handlebars template |
|
|
875
|
+
| `context` | string | yes | Context type (see below) |
|
|
876
|
+
| `output` | string | yes | Output file path (supports `{<context>.id}` placeholder) |
|
|
877
|
+
| `include` | string[] | no | Only render these entity IDs (not with `system`) |
|
|
878
|
+
| `exclude` | string[] | no | Skip these entity IDs (not with `system`) |
|
|
879
|
+
| `skip_empty` | boolean | no | When `true`, if the rendered output is empty or whitespace-only, the file is **not written**. If the file already exists, it is **deleted**. |
|
|
880
|
+
|
|
881
|
+
#### `skip_empty` usage
|
|
882
|
+
|
|
883
|
+
`skip_empty` is useful when a single template applies to all entities of a context type, but only some entities produce meaningful output.
|
|
884
|
+
|
|
885
|
+
For example, when using `context: tool` to generate per-tool scripts, tools without an `x-script` property would produce empty files. With `skip_empty: true`, those files are simply not created:
|
|
886
|
+
|
|
887
|
+
````yaml
|
|
888
|
+
renders:
|
|
889
|
+
- template: ./templates/tool-script.sh.hbs
|
|
890
|
+
context: tool
|
|
891
|
+
output: ./output/scripts/{tool.id}.sh
|
|
892
|
+
skip_empty: true
|
|
893
|
+
````
|
|
894
|
+
|
|
895
|
+
````handlebars
|
|
896
|
+
{{!-- tool-script.sh.hbs --}}
|
|
897
|
+
{{#if tool.x-script}}
|
|
898
|
+
{{{tool.x-script}}}
|
|
899
|
+
{{/if}}
|
|
900
|
+
````
|
|
901
|
+
|
|
902
|
+
Tools with `x-script` get a generated script file; tools without it produce no file at all.
|
|
903
|
+
|
|
904
|
+
`skip_empty` also works with `render --check` (drift detection): when the expected output is empty, the check expects the file to **not exist** and reports drift if it does.
|
|
905
|
+
|
|
868
906
|
### Available context types
|
|
869
907
|
|
|
870
908
|
Each `context` type provides a different rendering scope:
|
package/dist/cli.js
CHANGED
|
@@ -50498,17 +50498,18 @@ import { z as z3 } from "zod";
|
|
|
50498
50498
|
var CommandRegexMatcherSchema = z3.object({
|
|
50499
50499
|
type: z3.literal("command_regex"),
|
|
50500
50500
|
pattern: z3.string()
|
|
50501
|
-
});
|
|
50501
|
+
}).passthrough();
|
|
50502
50502
|
var ContentRegexMatcherSchema = z3.object({
|
|
50503
50503
|
type: z3.literal("content_regex"),
|
|
50504
50504
|
pattern: z3.string(),
|
|
50505
50505
|
file_glob: z3.string().optional(),
|
|
50506
50506
|
exclude_glob: z3.string().optional()
|
|
50507
|
-
});
|
|
50507
|
+
}).passthrough();
|
|
50508
50508
|
var FileGlobMatcherSchema = z3.object({
|
|
50509
50509
|
type: z3.literal("file_glob"),
|
|
50510
|
-
pattern: z3.string()
|
|
50511
|
-
|
|
50510
|
+
pattern: z3.string(),
|
|
50511
|
+
exclude_glob: z3.string().optional()
|
|
50512
|
+
}).passthrough();
|
|
50512
50513
|
var MatcherSchema = z3.discriminatedUnion("type", [
|
|
50513
50514
|
CommandRegexMatcherSchema,
|
|
50514
50515
|
ContentRegexMatcherSchema,
|
|
@@ -50525,7 +50526,8 @@ var BindingOutputSchema = z3.object({
|
|
|
50525
50526
|
inline_template: z3.string().optional(),
|
|
50526
50527
|
mode: z3.enum(["write", "patch"]).default("write"),
|
|
50527
50528
|
group_by: z3.string().optional(),
|
|
50528
|
-
executable: z3.boolean().optional()
|
|
50529
|
+
executable: z3.boolean().optional(),
|
|
50530
|
+
skip_empty: z3.boolean().optional()
|
|
50529
50531
|
}).passthrough().refine(
|
|
50530
50532
|
(data) => !(data.template && data.inline_template),
|
|
50531
50533
|
{ message: "template and inline_template are mutually exclusive" }
|
|
@@ -50859,7 +50861,8 @@ var RenderTargetSchema = z14.object({
|
|
|
50859
50861
|
context: ContextTypeSchema,
|
|
50860
50862
|
output: z14.string(),
|
|
50861
50863
|
include: z14.array(z14.string()).optional(),
|
|
50862
|
-
exclude: z14.array(z14.string()).optional()
|
|
50864
|
+
exclude: z14.array(z14.string()).optional(),
|
|
50865
|
+
skip_empty: z14.boolean().optional()
|
|
50863
50866
|
}).refine(
|
|
50864
50867
|
(data) => !(data.include && data.exclude),
|
|
50865
50868
|
{ message: "include and exclude are mutually exclusive" }
|
|
@@ -53280,7 +53283,7 @@ var lintCommand = new Command3("lint").description("Run semantic lint rules on r
|
|
|
53280
53283
|
import { Command as Command4 } from "commander";
|
|
53281
53284
|
|
|
53282
53285
|
// src/renderer/renderer.ts
|
|
53283
|
-
import { readFile as readFile4, writeFile, mkdir } from "fs/promises";
|
|
53286
|
+
import { readFile as readFile4, writeFile, mkdir, unlink } from "fs/promises";
|
|
53284
53287
|
import { dirname as dirname5 } from "path";
|
|
53285
53288
|
import Handlebars from "handlebars";
|
|
53286
53289
|
|
|
@@ -54396,6 +54399,15 @@ function buildEntityContext(dsl, context, entityId) {
|
|
|
54396
54399
|
async function loadTemplate(templatePath) {
|
|
54397
54400
|
return readFile4(templatePath, "utf8");
|
|
54398
54401
|
}
|
|
54402
|
+
function isEffectivelyEmpty(output) {
|
|
54403
|
+
return output.trim().length === 0;
|
|
54404
|
+
}
|
|
54405
|
+
async function removeIfExists(filePath) {
|
|
54406
|
+
try {
|
|
54407
|
+
await unlink(filePath);
|
|
54408
|
+
} catch {
|
|
54409
|
+
}
|
|
54410
|
+
}
|
|
54399
54411
|
async function renderFromConfig(dsl, renderTargets) {
|
|
54400
54412
|
const outputFiles = [];
|
|
54401
54413
|
for (const target of renderTargets) {
|
|
@@ -54404,6 +54416,10 @@ async function renderFromConfig(dsl, renderTargets) {
|
|
|
54404
54416
|
if (target.context === "system") {
|
|
54405
54417
|
const ctx = buildSystemContext(dsl);
|
|
54406
54418
|
const output = compiled(ctx);
|
|
54419
|
+
if (target.skip_empty && isEffectivelyEmpty(output)) {
|
|
54420
|
+
await removeIfExists(target.output);
|
|
54421
|
+
continue;
|
|
54422
|
+
}
|
|
54407
54423
|
await mkdir(dirname5(target.output), { recursive: true });
|
|
54408
54424
|
await writeFile(target.output, output, "utf8");
|
|
54409
54425
|
outputFiles.push(target.output);
|
|
@@ -54415,6 +54431,10 @@ async function renderFromConfig(dsl, renderTargets) {
|
|
|
54415
54431
|
const ctx = buildEntityContext(dsl, target.context, entityId);
|
|
54416
54432
|
const output = compiled(ctx);
|
|
54417
54433
|
const outputPath = expandOutputPath(target.output, target.context, entityId);
|
|
54434
|
+
if (target.skip_empty && isEffectivelyEmpty(output)) {
|
|
54435
|
+
await removeIfExists(outputPath);
|
|
54436
|
+
continue;
|
|
54437
|
+
}
|
|
54418
54438
|
await mkdir(dirname5(outputPath), { recursive: true });
|
|
54419
54439
|
await writeFile(outputPath, output, "utf8");
|
|
54420
54440
|
outputFiles.push(outputPath);
|
|
@@ -54423,6 +54443,26 @@ async function renderFromConfig(dsl, renderTargets) {
|
|
|
54423
54443
|
}
|
|
54424
54444
|
return outputFiles;
|
|
54425
54445
|
}
|
|
54446
|
+
async function fileExists2(filePath) {
|
|
54447
|
+
try {
|
|
54448
|
+
await readFile4(filePath);
|
|
54449
|
+
return true;
|
|
54450
|
+
} catch {
|
|
54451
|
+
return false;
|
|
54452
|
+
}
|
|
54453
|
+
}
|
|
54454
|
+
function checkExpectedVsExisting(expected, outputPath, skipEmpty, diffs) {
|
|
54455
|
+
if (skipEmpty && isEffectivelyEmpty(expected)) {
|
|
54456
|
+
return fileExists2(outputPath).then((exists) => {
|
|
54457
|
+
if (exists) diffs.push(outputPath);
|
|
54458
|
+
});
|
|
54459
|
+
}
|
|
54460
|
+
return readFile4(outputPath, "utf8").then((existing) => {
|
|
54461
|
+
if (existing !== expected) diffs.push(outputPath);
|
|
54462
|
+
}).catch(() => {
|
|
54463
|
+
diffs.push(outputPath);
|
|
54464
|
+
});
|
|
54465
|
+
}
|
|
54426
54466
|
async function checkDriftFromConfig(dsl, renderTargets) {
|
|
54427
54467
|
const diffs = [];
|
|
54428
54468
|
for (const target of renderTargets) {
|
|
@@ -54431,12 +54471,7 @@ async function checkDriftFromConfig(dsl, renderTargets) {
|
|
|
54431
54471
|
if (target.context === "system") {
|
|
54432
54472
|
const ctx = buildSystemContext(dsl);
|
|
54433
54473
|
const expected = compiled(ctx);
|
|
54434
|
-
|
|
54435
|
-
const existing = await readFile4(target.output, "utf8");
|
|
54436
|
-
if (existing !== expected) diffs.push(target.output);
|
|
54437
|
-
} catch {
|
|
54438
|
-
diffs.push(target.output);
|
|
54439
|
-
}
|
|
54474
|
+
await checkExpectedVsExisting(expected, target.output, target.skip_empty, diffs);
|
|
54440
54475
|
} else {
|
|
54441
54476
|
const section = getDslSection(dsl, target.context);
|
|
54442
54477
|
const allIds = Object.keys(section);
|
|
@@ -54445,12 +54480,7 @@ async function checkDriftFromConfig(dsl, renderTargets) {
|
|
|
54445
54480
|
const ctx = buildEntityContext(dsl, target.context, entityId);
|
|
54446
54481
|
const expected = compiled(ctx);
|
|
54447
54482
|
const outputPath = expandOutputPath(target.output, target.context, entityId);
|
|
54448
|
-
|
|
54449
|
-
const existing = await readFile4(outputPath, "utf8");
|
|
54450
|
-
if (existing !== expected) diffs.push(outputPath);
|
|
54451
|
-
} catch {
|
|
54452
|
-
diffs.push(outputPath);
|
|
54453
|
-
}
|
|
54483
|
+
await checkExpectedVsExisting(expected, outputPath, target.skip_empty, diffs);
|
|
54454
54484
|
}
|
|
54455
54485
|
}
|
|
54456
54486
|
}
|
|
@@ -54608,7 +54638,7 @@ var checkCommand = new Command5("check").description("Run full pipeline: resolve
|
|
|
54608
54638
|
import { Command as Command6 } from "commander";
|
|
54609
54639
|
|
|
54610
54640
|
// src/guardrail-generator/generator.ts
|
|
54611
|
-
import { readFile as readFile5, writeFile as writeFile2, mkdir as mkdir2, chmod } from "fs/promises";
|
|
54641
|
+
import { readFile as readFile5, writeFile as writeFile2, mkdir as mkdir2, chmod, unlink as unlink2 } from "fs/promises";
|
|
54612
54642
|
import { resolve as resolve6, dirname as dirname6 } from "path";
|
|
54613
54643
|
import Handlebars2 from "handlebars";
|
|
54614
54644
|
|
|
@@ -54782,6 +54812,7 @@ async function generateGuardrails(options) {
|
|
|
54782
54812
|
});
|
|
54783
54813
|
continue;
|
|
54784
54814
|
}
|
|
54815
|
+
const shouldSkipEmpty = outputDef.skip_empty === true;
|
|
54785
54816
|
if (outputDef.group_by) {
|
|
54786
54817
|
const groupField = outputDef.group_by;
|
|
54787
54818
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -54801,6 +54832,15 @@ async function generateGuardrails(options) {
|
|
|
54801
54832
|
const compiled = Handlebars2.compile(templateContent, { noEscape: true });
|
|
54802
54833
|
const output = compiled(groupCtx);
|
|
54803
54834
|
const groupTarget = resolve6(targetPath, groupKey);
|
|
54835
|
+
if (shouldSkipEmpty && output.trim().length === 0) {
|
|
54836
|
+
if (!dryRun) {
|
|
54837
|
+
try {
|
|
54838
|
+
await unlink2(groupTarget);
|
|
54839
|
+
} catch {
|
|
54840
|
+
}
|
|
54841
|
+
}
|
|
54842
|
+
continue;
|
|
54843
|
+
}
|
|
54804
54844
|
if (!dryRun) {
|
|
54805
54845
|
await mkdir2(dirname6(groupTarget), { recursive: true });
|
|
54806
54846
|
await writeFile2(groupTarget, output, "utf8");
|
|
@@ -54813,14 +54853,23 @@ async function generateGuardrails(options) {
|
|
|
54813
54853
|
} else {
|
|
54814
54854
|
const compiled = Handlebars2.compile(templateContent, { noEscape: true });
|
|
54815
54855
|
const output = compiled(ctx);
|
|
54816
|
-
if (
|
|
54817
|
-
|
|
54818
|
-
|
|
54819
|
-
|
|
54820
|
-
|
|
54856
|
+
if (shouldSkipEmpty && output.trim().length === 0) {
|
|
54857
|
+
if (!dryRun) {
|
|
54858
|
+
try {
|
|
54859
|
+
await unlink2(targetPath);
|
|
54860
|
+
} catch {
|
|
54861
|
+
}
|
|
54862
|
+
}
|
|
54863
|
+
} else {
|
|
54864
|
+
if (!dryRun) {
|
|
54865
|
+
await mkdir2(dirname6(targetPath), { recursive: true });
|
|
54866
|
+
await writeFile2(targetPath, output, "utf8");
|
|
54867
|
+
if (outputDef.executable) {
|
|
54868
|
+
await chmod(targetPath, 493);
|
|
54869
|
+
}
|
|
54821
54870
|
}
|
|
54871
|
+
outputFiles.push(targetPath);
|
|
54822
54872
|
}
|
|
54823
|
-
outputFiles.push(targetPath);
|
|
54824
54873
|
}
|
|
54825
54874
|
}
|
|
54826
54875
|
}
|