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 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 (handoff, validation, decision)
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` (handoff steps only) — defines a conditional retry loop with `condition`, `fix_task`, and optional `revalidate_task`
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: handoff
606
- handoff_kind: delegation
636
+ - type: delegate
607
637
  task: specify-feature
608
638
  from_agent: main-architect
609
639
  - type: validation
610
- validation_id: spec-semantic-review
640
+ validation: spec-semantic-review
611
641
  - type: decision
612
- agent: main-architect
613
- options:
614
- - label: approve
615
- next_workflow: plan
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: z11.string(),
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
- const data = config?.vars ? substituteVars(result.data, config.vars) : result.data;
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 = [];