agent-contracts 0.11.4 → 0.11.5
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 +79 -11
- package/dist/cli.js +118 -5
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +34 -4
- package/dist/index.js +117 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -221,12 +221,13 @@ A **Workflow** defines a phase-level execution sequence:
|
|
|
221
221
|
* `entry_conditions`
|
|
222
222
|
* `trigger`
|
|
223
223
|
* `external_participants` — actors/participants outside the agent system (e.g., User, external advisory)
|
|
224
|
-
* ordered steps (
|
|
224
|
+
* ordered steps (delegate, gate, validation, decision)
|
|
225
225
|
|
|
226
226
|
Workflow steps support additional properties:
|
|
227
227
|
|
|
228
228
|
* `group` — consecutive steps with the same group are rendered as `par` (parallel) blocks in sequence diagrams
|
|
229
|
-
* `retry` (
|
|
229
|
+
* `retry` (delegate steps) — defines a conditional retry loop with `condition`, `fix_task`, and optional `revalidate_task`
|
|
230
|
+
* `routing_key` (decision steps) — the field that determines branch selection. The legacy field `on` is still accepted but deprecated due to YAML 1.1 reserved word collision
|
|
230
231
|
|
|
231
232
|
### Guardrail
|
|
232
233
|
|
|
@@ -305,6 +306,9 @@ Design regressions become testable.
|
|
|
305
306
|
* **Software bindings** (DI) for tool-specific guardrail implementation (Cursor, Git, GitHub)
|
|
306
307
|
* **Guardrail generation** from DSL + policy + bindings via `generate guardrails`
|
|
307
308
|
* **Flexible file splitting** via `$ref` (replacement), `$refs` (import + deep-merge), and JSON Pointer `$ref` (in-document)
|
|
309
|
+
* **YAML safety linting** for reserved word collision detection across YAML 1.1/1.2
|
|
310
|
+
* **`x-extensions` declarations** for documenting project-specific custom extension fields
|
|
311
|
+
* **`resolve --expand-defaults`** to materialize all Zod schema defaults in output
|
|
308
312
|
* **JSON Schema for editor support and external tooling**
|
|
309
313
|
* **CI-friendly workflow checks**
|
|
310
314
|
|
|
@@ -342,6 +346,16 @@ guardrails: {}
|
|
|
342
346
|
guardrail_policies: {}
|
|
343
347
|
components:
|
|
344
348
|
schemas: {}
|
|
349
|
+
|
|
350
|
+
# Optional: declare project-specific x-* extensions
|
|
351
|
+
x-extensions:
|
|
352
|
+
x-flags:
|
|
353
|
+
type: array
|
|
354
|
+
items: string
|
|
355
|
+
description: "CLI flags for tool commands"
|
|
356
|
+
x-check-script:
|
|
357
|
+
type: string
|
|
358
|
+
description: "Path to hook check script"
|
|
345
359
|
````
|
|
346
360
|
|
|
347
361
|
This makes definitions easy to merge, extend, and reference by stable identifiers.
|
|
@@ -572,6 +586,23 @@ tasks:
|
|
|
572
586
|
`x-` prefixed custom properties work at any nesting level — including inside
|
|
573
587
|
`execution_steps`, `rules`, `workflow.steps`, and other nested objects.
|
|
574
588
|
|
|
589
|
+
### `x-extensions` declarations
|
|
590
|
+
|
|
591
|
+
Projects can optionally declare their custom `x-*` extension fields in the DSL using `x-extensions`. This makes extensions discoverable and self-documenting:
|
|
592
|
+
|
|
593
|
+
````yaml
|
|
594
|
+
x-extensions:
|
|
595
|
+
x-flags:
|
|
596
|
+
type: array
|
|
597
|
+
items: string
|
|
598
|
+
description: "CLI flags for tool commands"
|
|
599
|
+
x-check-script:
|
|
600
|
+
type: string
|
|
601
|
+
description: "Path to hook check script"
|
|
602
|
+
````
|
|
603
|
+
|
|
604
|
+
Each key must start with `x-` (validated at schema level). The `type`, `items`, and `description` fields describe the expected shape. This declaration is informational today; future versions may validate `x-*` field values against their declared schemas.
|
|
605
|
+
|
|
575
606
|
---
|
|
576
607
|
|
|
577
608
|
## Example: Artifact definition
|
|
@@ -602,21 +633,20 @@ workflow:
|
|
|
602
633
|
- User story or feature request received
|
|
603
634
|
trigger: "User invokes /speckit.specify or asks to create a feature spec."
|
|
604
635
|
steps:
|
|
605
|
-
- type:
|
|
606
|
-
handoff_kind: delegation
|
|
636
|
+
- type: delegate
|
|
607
637
|
task: specify-feature
|
|
608
638
|
from_agent: main-architect
|
|
609
639
|
- type: validation
|
|
610
|
-
|
|
640
|
+
validation: spec-semantic-review
|
|
611
641
|
- type: decision
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
- label: revise
|
|
617
|
-
retry_from_step: 0
|
|
642
|
+
routing_key: evidence-gate-verdict.verdict
|
|
643
|
+
branches:
|
|
644
|
+
PASS: [plan]
|
|
645
|
+
REVISE: [specify-feature]
|
|
618
646
|
````
|
|
619
647
|
|
|
648
|
+
Decision steps use `routing_key` to specify the field that determines branching. The legacy `on` field is still accepted but deprecated — see [YAML safety](#yaml-safety) below.
|
|
649
|
+
|
|
620
650
|
---
|
|
621
651
|
|
|
622
652
|
## Example: Handoff type definition
|
|
@@ -819,10 +849,19 @@ npx agent-contracts
|
|
|
819
849
|
The `[path]` argument defaults to `agent-contracts.yaml` in the current directory.
|
|
820
850
|
If `-c` / `--config` is specified, the DSL path from the config file is used.
|
|
821
851
|
|
|
852
|
+
#### `resolve` options
|
|
853
|
+
|
|
854
|
+
| Option | Description |
|
|
855
|
+
|--------|-------------|
|
|
856
|
+
| `--format <text\|json>` | Output format (default: `text`) |
|
|
857
|
+
| `--expand-defaults` | Expand all Zod default values in output. Fields like `required_validations: []`, `tags: []`, and `can_read_artifacts: []` are written explicitly instead of being silently applied by schema defaults. |
|
|
858
|
+
| `-c, --config <path>` | Path to `agent-contracts.config.yaml` |
|
|
859
|
+
|
|
822
860
|
### Common usage
|
|
823
861
|
|
|
824
862
|
````bash
|
|
825
863
|
agent-contracts resolve
|
|
864
|
+
agent-contracts resolve --expand-defaults --format json
|
|
826
865
|
agent-contracts validate
|
|
827
866
|
agent-contracts lint --strict
|
|
828
867
|
agent-contracts render -c agent-contracts.config.yaml
|
|
@@ -1153,6 +1192,34 @@ Checks:
|
|
|
1153
1192
|
|
|
1154
1193
|
Custom properties with `x-` prefix are allowed on any object in the DSL — top-level entities (agents, tasks, artifacts, …), nested objects (rules, execution steps, workflow steps, …), and the root DSL itself.
|
|
1155
1194
|
|
|
1195
|
+
### YAML safety
|
|
1196
|
+
|
|
1197
|
+
The DSL is expressed in YAML, which introduces risks from YAML 1.1's implicit type coercion. The `yaml-reserved-key-safety` lint rule warns when reserved words appear in positions that may be misinterpreted by non-1.2 parsers.
|
|
1198
|
+
|
|
1199
|
+
The most notable case is the `on` field in decision steps. In YAML 1.1, bare `on` as a mapping key is interpreted as boolean `true`. While `agent-contracts` uses a YAML 1.2 parser internally, DSL consumers (CI tools, editors, other parsers) may use YAML 1.1 parsers.
|
|
1200
|
+
|
|
1201
|
+
To address this:
|
|
1202
|
+
|
|
1203
|
+
* Decision steps now support `routing_key` as the preferred field name (replacing `on`)
|
|
1204
|
+
* The legacy `on` field is still accepted for backward compatibility but triggers a lint warning
|
|
1205
|
+
* Branch keys like `yes`, `no`, `true`, `false` also trigger warnings
|
|
1206
|
+
|
|
1207
|
+
````yaml
|
|
1208
|
+
# Preferred — safe across all YAML versions
|
|
1209
|
+
- type: decision
|
|
1210
|
+
routing_key: evidence-gate-verdict.verdict
|
|
1211
|
+
branches:
|
|
1212
|
+
PASS: [release]
|
|
1213
|
+
FAIL: [fix-violations]
|
|
1214
|
+
|
|
1215
|
+
# Deprecated — works but triggers yaml-reserved-key-safety warning
|
|
1216
|
+
- type: decision
|
|
1217
|
+
on: evidence-gate-verdict.verdict
|
|
1218
|
+
branches:
|
|
1219
|
+
PASS: [release]
|
|
1220
|
+
FAIL: [fix-violations]
|
|
1221
|
+
````
|
|
1222
|
+
|
|
1156
1223
|
### Reference integrity
|
|
1157
1224
|
|
|
1158
1225
|
Checks:
|
|
@@ -1174,6 +1241,7 @@ Checks:
|
|
|
1174
1241
|
* prerequisite readability
|
|
1175
1242
|
* artifact ownership — `produces_artifact`/`reads_artifact` in execution steps vs. artifact producers/editors/consumers
|
|
1176
1243
|
* tool commands — `commands[].reads`/`commands[].writes` reference valid artifacts and align with `output_artifacts`
|
|
1244
|
+
* YAML safety — warns when YAML 1.1 reserved words (`on`, `yes`, `no`, `true`, `false`, etc.) are used in positions where they may be misinterpreted by non-1.2 parsers
|
|
1177
1245
|
* naming/style issues through Spectral rules
|
|
1178
1246
|
|
|
1179
1247
|
---
|
package/dist/cli.js
CHANGED
|
@@ -50728,7 +50728,9 @@ var WorkflowValidationStepSchema = z11.object({
|
|
|
50728
50728
|
var WorkflowDecisionStepSchema = z11.object({
|
|
50729
50729
|
type: z11.literal("decision"),
|
|
50730
50730
|
description: z11.string().optional(),
|
|
50731
|
-
on
|
|
50731
|
+
/** @deprecated Use `routing_key` instead. `on` is kept for backward compatibility. */
|
|
50732
|
+
on: z11.string().optional(),
|
|
50733
|
+
routing_key: z11.string().optional(),
|
|
50732
50734
|
branches: z11.record(z11.string(), z11.array(z11.string())),
|
|
50733
50735
|
group: z11.string().optional()
|
|
50734
50736
|
}).passthrough();
|
|
@@ -50751,6 +50753,11 @@ var WorkflowSchema = z11.object({
|
|
|
50751
50753
|
var ComponentsSchema = z12.object({
|
|
50752
50754
|
schemas: z12.record(z12.string(), z12.record(z12.string(), z12.any())).default({})
|
|
50753
50755
|
}).passthrough();
|
|
50756
|
+
var XExtensionDeclSchema = z12.object({
|
|
50757
|
+
type: z12.string(),
|
|
50758
|
+
items: z12.string().optional(),
|
|
50759
|
+
description: z12.string().optional()
|
|
50760
|
+
});
|
|
50754
50761
|
var DslSchema = z12.object({
|
|
50755
50762
|
version: z12.literal(1),
|
|
50756
50763
|
extends: z12.string().optional(),
|
|
@@ -50765,7 +50772,8 @@ var DslSchema = z12.object({
|
|
|
50765
50772
|
policies: z12.record(z12.string(), PolicySchema).default({}),
|
|
50766
50773
|
guardrails: z12.record(z12.string(), GuardrailSchema).default({}),
|
|
50767
50774
|
guardrail_policies: z12.record(z12.string(), GuardrailPolicySchema).default({}),
|
|
50768
|
-
components: ComponentsSchema.default({ schemas: {} })
|
|
50775
|
+
components: ComponentsSchema.default({ schemas: {} }),
|
|
50776
|
+
"x-extensions": z12.record(z12.string(), XExtensionDeclSchema).optional()
|
|
50769
50777
|
}).passthrough();
|
|
50770
50778
|
|
|
50771
50779
|
// src/schema/json-schema-utils.ts
|
|
@@ -51620,6 +51628,13 @@ function substituteVars(data, vars) {
|
|
|
51620
51628
|
return walk(data, vars);
|
|
51621
51629
|
}
|
|
51622
51630
|
|
|
51631
|
+
// src/resolver/expand-defaults.ts
|
|
51632
|
+
function expandDefaults(data) {
|
|
51633
|
+
const result = DslSchema.safeParse(data);
|
|
51634
|
+
if (!result.success) return data;
|
|
51635
|
+
return JSON.parse(JSON.stringify(result.data));
|
|
51636
|
+
}
|
|
51637
|
+
|
|
51623
51638
|
// src/config/binding-merger.ts
|
|
51624
51639
|
function isRecord3(v2) {
|
|
51625
51640
|
return typeof v2 === "object" && v2 !== null && !Array.isArray(v2);
|
|
@@ -51760,12 +51775,15 @@ ${issues}`,
|
|
|
51760
51775
|
// src/cli/commands/resolve.ts
|
|
51761
51776
|
import { stringify } from "yaml";
|
|
51762
51777
|
var DIR_DEFAULT = "agent-contracts.yaml";
|
|
51763
|
-
var resolveCommand = new Command("resolve").description("Resolve DSL (load + merge extends) and output YAML").argument("[dir]", "Path to agent-contracts.yaml", DIR_DEFAULT).option("-c, --config <path>", "Path to agent-contracts.config.yaml").option("--format <format>", "Output format (text|json)", "text").action(async (dir, opts) => {
|
|
51778
|
+
var resolveCommand = new Command("resolve").description("Resolve DSL (load + merge extends) and output YAML").argument("[dir]", "Path to agent-contracts.yaml", DIR_DEFAULT).option("-c, --config <path>", "Path to agent-contracts.config.yaml").option("--format <format>", "Output format (text|json)", "text").option("--expand-defaults", "Expand all Zod default values in output", false).action(async (dir, opts) => {
|
|
51764
51779
|
try {
|
|
51765
51780
|
const config = await loadConfig(opts.config);
|
|
51766
51781
|
const dslPath = resolveDslPath(dir, DIR_DEFAULT, config);
|
|
51767
51782
|
const result = await resolve4(dslPath);
|
|
51768
|
-
|
|
51783
|
+
let data = config?.vars ? substituteVars(result.data, config.vars) : result.data;
|
|
51784
|
+
if (opts.expandDefaults) {
|
|
51785
|
+
data = expandDefaults(data);
|
|
51786
|
+
}
|
|
51769
51787
|
if (opts.format === "json") {
|
|
51770
51788
|
process.stdout.write(JSON.stringify(data, null, 2) + "\n");
|
|
51771
51789
|
} else {
|
|
@@ -51844,6 +51862,47 @@ function checkCustomPropsRecursive(data, schema, path) {
|
|
|
51844
51862
|
}
|
|
51845
51863
|
return [];
|
|
51846
51864
|
}
|
|
51865
|
+
function checkXExtensionsKeys(data) {
|
|
51866
|
+
const extensions = data["x-extensions"];
|
|
51867
|
+
if (typeof extensions !== "object" || extensions === null) return [];
|
|
51868
|
+
const diagnostics = [];
|
|
51869
|
+
for (const key of Object.keys(extensions)) {
|
|
51870
|
+
if (!key.startsWith("x-")) {
|
|
51871
|
+
diagnostics.push({
|
|
51872
|
+
path: `x-extensions.${key}`,
|
|
51873
|
+
message: `Extension key "${key}" must start with "x-" prefix.`,
|
|
51874
|
+
code: "x-extension-key-prefix"
|
|
51875
|
+
});
|
|
51876
|
+
}
|
|
51877
|
+
}
|
|
51878
|
+
return diagnostics;
|
|
51879
|
+
}
|
|
51880
|
+
function checkDecisionStepRoutingKey(data) {
|
|
51881
|
+
const workflow = data["workflow"];
|
|
51882
|
+
if (typeof workflow !== "object" || workflow === null) return [];
|
|
51883
|
+
const diagnostics = [];
|
|
51884
|
+
for (const [wfKey, wf] of Object.entries(
|
|
51885
|
+
workflow
|
|
51886
|
+
)) {
|
|
51887
|
+
if (typeof wf !== "object" || wf === null) continue;
|
|
51888
|
+
const steps = wf["steps"];
|
|
51889
|
+
if (!Array.isArray(steps)) continue;
|
|
51890
|
+
for (let i3 = 0; i3 < steps.length; i3++) {
|
|
51891
|
+
const step = steps[i3];
|
|
51892
|
+
if (typeof step !== "object" || step === null) continue;
|
|
51893
|
+
const s2 = step;
|
|
51894
|
+
if (s2["type"] !== "decision") continue;
|
|
51895
|
+
if (s2["routing_key"] === void 0 && s2["on"] === void 0) {
|
|
51896
|
+
diagnostics.push({
|
|
51897
|
+
path: `workflow.${wfKey}.steps[${i3}]`,
|
|
51898
|
+
message: 'Decision step requires "routing_key" (or deprecated "on"). Prefer "routing_key".',
|
|
51899
|
+
code: "decision-missing-routing-key"
|
|
51900
|
+
});
|
|
51901
|
+
}
|
|
51902
|
+
}
|
|
51903
|
+
}
|
|
51904
|
+
return diagnostics;
|
|
51905
|
+
}
|
|
51847
51906
|
function validateSchema(data) {
|
|
51848
51907
|
const result = DslSchema.safeParse(data);
|
|
51849
51908
|
if (!result.success) {
|
|
@@ -51858,6 +51917,14 @@ function validateSchema(data) {
|
|
|
51858
51917
|
if (customPropDiagnostics.length > 0) {
|
|
51859
51918
|
return { success: false, diagnostics: customPropDiagnostics };
|
|
51860
51919
|
}
|
|
51920
|
+
const decisionDiagnostics = checkDecisionStepRoutingKey(data);
|
|
51921
|
+
if (decisionDiagnostics.length > 0) {
|
|
51922
|
+
return { success: false, diagnostics: decisionDiagnostics };
|
|
51923
|
+
}
|
|
51924
|
+
const xExtDiagnostics = checkXExtensionsKeys(data);
|
|
51925
|
+
if (xExtDiagnostics.length > 0) {
|
|
51926
|
+
return { success: false, diagnostics: xExtDiagnostics };
|
|
51927
|
+
}
|
|
51861
51928
|
return { success: true, data: result.data, diagnostics: [] };
|
|
51862
51929
|
}
|
|
51863
51930
|
|
|
@@ -52583,6 +52650,51 @@ var guardrailPolicyCoverageRule = {
|
|
|
52583
52650
|
}
|
|
52584
52651
|
};
|
|
52585
52652
|
|
|
52653
|
+
// src/linter/rules/yaml-reserved-key-safety.ts
|
|
52654
|
+
var YAML_11_RESERVED_KEYS = /* @__PURE__ */ new Set([
|
|
52655
|
+
"on",
|
|
52656
|
+
"off",
|
|
52657
|
+
"yes",
|
|
52658
|
+
"no",
|
|
52659
|
+
"true",
|
|
52660
|
+
"false",
|
|
52661
|
+
"y",
|
|
52662
|
+
"n"
|
|
52663
|
+
]);
|
|
52664
|
+
var yamlReservedKeySafetyRule = {
|
|
52665
|
+
id: "yaml-reserved-key-safety",
|
|
52666
|
+
description: "Warns when YAML 1.1 reserved words are used as field values in positions where they may be misinterpreted by non-1.2 parsers",
|
|
52667
|
+
run(dsl) {
|
|
52668
|
+
const diagnostics = [];
|
|
52669
|
+
for (const [wfKey, wf] of Object.entries(dsl.workflow)) {
|
|
52670
|
+
for (let i3 = 0; i3 < wf.steps.length; i3++) {
|
|
52671
|
+
const step = wf.steps[i3];
|
|
52672
|
+
if (step.type !== "decision") continue;
|
|
52673
|
+
if (step.on !== void 0 && step.routing_key === void 0) {
|
|
52674
|
+
diagnostics.push({
|
|
52675
|
+
ruleId: "yaml-reserved-key-safety",
|
|
52676
|
+
severity: "warning",
|
|
52677
|
+
path: `workflow.${wfKey}.steps[${i3}].on`,
|
|
52678
|
+
message: 'Field name "on" is a YAML 1.1 reserved word and may be interpreted as boolean true by some parsers. Use "routing_key" instead.'
|
|
52679
|
+
});
|
|
52680
|
+
}
|
|
52681
|
+
const branchKeys = Object.keys(step.branches);
|
|
52682
|
+
for (const key of branchKeys) {
|
|
52683
|
+
if (YAML_11_RESERVED_KEYS.has(key.toLowerCase())) {
|
|
52684
|
+
diagnostics.push({
|
|
52685
|
+
ruleId: "yaml-reserved-key-safety",
|
|
52686
|
+
severity: "warning",
|
|
52687
|
+
path: `workflow.${wfKey}.steps[${i3}].branches.${key}`,
|
|
52688
|
+
message: `Branch key "${key}" is a YAML 1.1 reserved word and may be interpreted as a boolean by some parsers. Consider quoting or renaming.`
|
|
52689
|
+
});
|
|
52690
|
+
}
|
|
52691
|
+
}
|
|
52692
|
+
}
|
|
52693
|
+
}
|
|
52694
|
+
return diagnostics;
|
|
52695
|
+
}
|
|
52696
|
+
};
|
|
52697
|
+
|
|
52586
52698
|
// src/linter/linter.ts
|
|
52587
52699
|
var builtinRules = [
|
|
52588
52700
|
validationCoverageRule,
|
|
@@ -52591,7 +52703,8 @@ var builtinRules = [
|
|
|
52591
52703
|
mergeIntegrityRule,
|
|
52592
52704
|
artifactOwnershipRule,
|
|
52593
52705
|
toolCommandsRule,
|
|
52594
|
-
guardrailPolicyCoverageRule
|
|
52706
|
+
guardrailPolicyCoverageRule,
|
|
52707
|
+
yamlReservedKeySafetyRule
|
|
52595
52708
|
];
|
|
52596
52709
|
function lint(dsl, rules = builtinRules) {
|
|
52597
52710
|
const diagnostics = [];
|