agent-contracts 0.7.0 → 0.7.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/README.md CHANGED
@@ -204,6 +204,15 @@ An **Artifact** defines the objects that move through the workflow:
204
204
  * required validations
205
205
  * visibility
206
206
 
207
+ ### Workflow
208
+
209
+ A **Workflow** defines a phase-level execution sequence:
210
+
211
+ * description
212
+ * entry conditions
213
+ * trigger
214
+ * ordered steps (handoff, validation, decision)
215
+
207
216
  ### Handoff
208
217
 
209
218
  A **Handoff** is a runtime delegation instance.
@@ -458,12 +467,13 @@ tasks:
458
467
  execution_steps:
459
468
  - id: read-specs
460
469
  action: "Read spec-md and design-docs"
461
- description: "Read all relevant specification and design documents"
470
+ reads_artifact: spec-md
462
471
  - id: implement
463
472
  action: "Implement changes in codebase"
473
+ produces_artifact: codebase
474
+ depends_on: [read-specs]
464
475
  - id: run-db-lint
465
476
  action: "Run db-lint"
466
- description: "Run database schema linter to check migrations"
467
477
  uses_tool: db-lint
468
478
  x-timeout: 120
469
479
  completion_criteria:
@@ -493,6 +503,33 @@ artifacts:
493
503
 
494
504
  ---
495
505
 
506
+ ## Example: Workflow definition
507
+
508
+ ````yaml
509
+ workflow:
510
+ specify:
511
+ description: "Externalize requirements — create spec.md from user stories"
512
+ entry_conditions:
513
+ - User story or feature request received
514
+ trigger: "User invokes /speckit.specify or asks to create a feature spec."
515
+ steps:
516
+ - type: handoff
517
+ handoff_kind: delegation
518
+ task: specify-feature
519
+ from_agent: main-architect
520
+ - type: validation
521
+ validation_id: spec-semantic-review
522
+ - type: decision
523
+ agent: main-architect
524
+ options:
525
+ - label: approve
526
+ next_workflow: plan
527
+ - label: revise
528
+ retry_from_step: 0
529
+ ````
530
+
531
+ ---
532
+
496
533
  ## Inheritance and merge operators
497
534
 
498
535
  `agent-contracts` supports shared base definitions with project-level overrides through `extends`.
@@ -686,8 +723,8 @@ Each `context` type provides a different rendering scope:
686
723
 
687
724
  * `relatedTasks` — tasks where `task.workflow` matches this phase
688
725
  * `relatedAgents` — agents from task `target_agent`, `allowed_from_agents`, step `from_agent`, and validation executors
689
- * `relatedTools` — tools from `can_execute_tools` of all related agents
690
- * `relatedArtifacts` — artifacts from `can_read_artifacts`, `can_write_artifacts`, and `input_artifacts`
726
+ * `relatedTools` — tools from `can_execute_tools` of all related agents, plus `uses_tool` in execution steps
727
+ * `relatedArtifacts` — artifacts from `can_read_artifacts`, `can_write_artifacts`, `input_artifacts`, plus `produces_artifact` and `reads_artifact` in execution steps
691
728
  * `relatedValidations` — validations referenced in workflow steps
692
729
 
693
730
  **`artifact` context** provides ownership and cross-reference data:
@@ -724,6 +761,7 @@ Templates can use these built-in helpers:
724
761
  | `or` | `{{#if (or a b)}}` | Boolean OR (variadic) |
725
762
  | `and` | `{{#if (and a b)}}` | Boolean AND (variadic) |
726
763
  | `gt` / `gte` / `lt` | `{{#if (gt a b)}}` | Numeric comparisons |
764
+ | `sequenceDiagram` | `{{{sequenceDiagram}}}` | Generate Mermaid sequence diagram from workflow context (requires `workflow` context) |
727
765
 
728
766
  ---
729
767
 
package/dist/cli.js CHANGED
@@ -59155,6 +59155,7 @@ var ExecutionStepSchema = z7.object({
59155
59155
  abort_on_failure: z7.boolean().optional(),
59156
59156
  uses_tool: z7.string().optional(),
59157
59157
  produces_artifact: z7.string().optional(),
59158
+ reads_artifact: z7.string().optional(),
59158
59159
  depends_on: z7.array(z7.string()).optional(),
59159
59160
  skip_condition: z7.string().optional(),
59160
59161
  wait_for_approval: z7.boolean().optional()
@@ -59225,6 +59226,7 @@ var WorkflowStepSchema = z10.discriminatedUnion("type", [
59225
59226
  WorkflowDecisionStepSchema
59226
59227
  ]);
59227
59228
  var WorkflowSchema = z10.object({
59229
+ description: z10.string().optional(),
59228
59230
  entry_conditions: z10.array(z10.string()).default([]),
59229
59231
  trigger: z10.string().optional(),
59230
59232
  steps: z10.array(WorkflowStepSchema)
@@ -59805,6 +59807,16 @@ var taskAgentBindingRule = {
59805
59807
  });
59806
59808
  }
59807
59809
  }
59810
+ if (step.reads_artifact) {
59811
+ if (!targetAgent.can_read_artifacts.includes(step.reads_artifact)) {
59812
+ diagnostics.push({
59813
+ ruleId: "task-agent-binding",
59814
+ severity: "error",
59815
+ path: `tasks.${taskId}.execution_steps`,
59816
+ message: `Task "${taskId}" step reads_artifact "${step.reads_artifact}" not in target agent "${task.target_agent}" can_read_artifacts`
59817
+ });
59818
+ }
59819
+ }
59808
59820
  }
59809
59821
  }
59810
59822
  return diagnostics;
@@ -60602,6 +60614,11 @@ function buildWorkflowContext(dsl, workflowId) {
60602
60614
  }
60603
60615
  }
60604
60616
  }
60617
+ for (const task of relatedTasks) {
60618
+ for (const step of task.execution_steps ?? []) {
60619
+ if (step.uses_tool) toolIds.add(step.uses_tool);
60620
+ }
60621
+ }
60605
60622
  const relatedTools = {};
60606
60623
  for (const id of toolIds) {
60607
60624
  if (dsl.tools[id]) relatedTools[id] = dsl.tools[id];
@@ -60609,6 +60626,10 @@ function buildWorkflowContext(dsl, workflowId) {
60609
60626
  const artifactIds = /* @__PURE__ */ new Set();
60610
60627
  for (const task of relatedTasks) {
60611
60628
  for (const artId of task.input_artifacts) artifactIds.add(artId);
60629
+ for (const step of task.execution_steps ?? []) {
60630
+ if (step.produces_artifact) artifactIds.add(step.produces_artifact);
60631
+ if (step.reads_artifact) artifactIds.add(step.reads_artifact);
60632
+ }
60612
60633
  }
60613
60634
  for (const id of agentIds) {
60614
60635
  const agent = dsl.agents[id];
@@ -60752,6 +60773,248 @@ function buildPerAgentContext(dsl, agent) {
60752
60773
  };
60753
60774
  }
60754
60775
 
60776
+ // src/renderer/sequence-diagram.ts
60777
+ function hashToColor(s2, saturation, lightness) {
60778
+ let hash = 0;
60779
+ for (const ch of s2) hash = (hash << 5) - hash + ch.charCodeAt(0) | 0;
60780
+ const h2 = (hash % 360 + 360) % 360;
60781
+ const c2 = (1 - Math.abs(2 * lightness - 1)) * saturation;
60782
+ const x3 = c2 * (1 - Math.abs(h2 / 60 % 2 - 1));
60783
+ const m2 = lightness - c2 / 2;
60784
+ let r3 = 0, g2 = 0, b3 = 0;
60785
+ if (h2 < 60) {
60786
+ r3 = c2;
60787
+ g2 = x3;
60788
+ } else if (h2 < 120) {
60789
+ r3 = x3;
60790
+ g2 = c2;
60791
+ } else if (h2 < 180) {
60792
+ g2 = c2;
60793
+ b3 = x3;
60794
+ } else if (h2 < 240) {
60795
+ g2 = x3;
60796
+ b3 = c2;
60797
+ } else if (h2 < 300) {
60798
+ r3 = x3;
60799
+ b3 = c2;
60800
+ } else {
60801
+ r3 = c2;
60802
+ b3 = x3;
60803
+ }
60804
+ return `rgb(${Math.round((r3 + m2) * 255)},${Math.round((g2 + m2) * 255)},${Math.round((b3 + m2) * 255)})`;
60805
+ }
60806
+ function sanitizeAlias(id) {
60807
+ return id.replace(/[^a-zA-Z0-9_]/g, "_");
60808
+ }
60809
+ function agentAlias(id, agent) {
60810
+ const words = agent.role_name.split(/\s+/);
60811
+ if (words.length === 1) return sanitizeAlias(words[0].slice(0, 4));
60812
+ return sanitizeAlias(words.map((w2) => w2[0]).join(""));
60813
+ }
60814
+ function collectReferencedIds(workflow, dsl, relatedTasks) {
60815
+ const agents = /* @__PURE__ */ new Set();
60816
+ const tools = /* @__PURE__ */ new Set();
60817
+ const artifacts = /* @__PURE__ */ new Set();
60818
+ const taskMap = /* @__PURE__ */ new Map();
60819
+ for (const t3 of relatedTasks) taskMap.set(t3.id, t3);
60820
+ for (const step of workflow.steps) {
60821
+ if (step.type === "handoff") {
60822
+ if (step.from_agent) agents.add(step.from_agent);
60823
+ if (step.task) {
60824
+ const task = taskMap.get(step.task);
60825
+ if (task) {
60826
+ agents.add(task.target_agent);
60827
+ for (const es of task.execution_steps ?? []) {
60828
+ collectExecutionStepIds(es, tools, artifacts);
60829
+ }
60830
+ }
60831
+ }
60832
+ } else if (step.type === "validation") {
60833
+ const val = dsl.validations[step.validation];
60834
+ if (val) {
60835
+ if (val.executor_type === "agent") agents.add(val.executor);
60836
+ else tools.add(val.executor);
60837
+ artifacts.add(val.target_artifact);
60838
+ }
60839
+ } else if (step.type === "decision") {
60840
+ }
60841
+ }
60842
+ return { agents, tools, artifacts };
60843
+ }
60844
+ function collectExecutionStepIds(es, tools, artifacts) {
60845
+ if (es.uses_tool) tools.add(es.uses_tool);
60846
+ if (es.produces_artifact) artifacts.add(es.produces_artifact);
60847
+ if (es.reads_artifact) artifacts.add(es.reads_artifact);
60848
+ }
60849
+ function buildParticipants(ids, dsl) {
60850
+ const participants = [];
60851
+ const usedAliases = /* @__PURE__ */ new Set();
60852
+ function uniqueAlias(preferred) {
60853
+ let alias = preferred;
60854
+ let i3 = 2;
60855
+ while (usedAliases.has(alias)) {
60856
+ alias = `${preferred}${i3}`;
60857
+ i3++;
60858
+ }
60859
+ usedAliases.add(alias);
60860
+ return alias;
60861
+ }
60862
+ for (const id of ids.agents) {
60863
+ const agent = dsl.agents[id];
60864
+ if (!agent) continue;
60865
+ const alias = uniqueAlias(agentAlias(id, agent));
60866
+ participants.push({ id, alias, label: agent.role_name, group: "agents" });
60867
+ }
60868
+ for (const id of ids.tools) {
60869
+ const tool = dsl.tools[id];
60870
+ if (!tool) continue;
60871
+ const alias = uniqueAlias(sanitizeAlias(id));
60872
+ participants.push({ id, alias, label: id, group: "toolchain" });
60873
+ }
60874
+ for (const id of ids.artifacts) {
60875
+ const art = dsl.artifacts[id];
60876
+ if (!art) continue;
60877
+ const alias = uniqueAlias(sanitizeAlias(id));
60878
+ participants.push({ id, alias, label: id, group: "artifacts" });
60879
+ }
60880
+ return participants;
60881
+ }
60882
+ function participantAlias(participants, id) {
60883
+ const p2 = participants.find((pp) => pp.id === id);
60884
+ return p2 ? p2.alias : sanitizeAlias(id);
60885
+ }
60886
+ function emitParticipants(participants, lines, indent) {
60887
+ const groups = {
60888
+ agents: [],
60889
+ toolchain: [],
60890
+ artifacts: []
60891
+ };
60892
+ for (const p2 of participants) groups[p2.group].push(p2);
60893
+ const groupConfig = [
60894
+ { key: "agents", label: "Agents", color: "rgb(200,220,255)" },
60895
+ { key: "toolchain", label: "Toolchain", color: "rgb(220,255,220)" },
60896
+ { key: "artifacts", label: "Artifacts", color: "rgb(255,230,210)" }
60897
+ ];
60898
+ for (const { key, label, color } of groupConfig) {
60899
+ const members = groups[key];
60900
+ if (members.length === 0) continue;
60901
+ lines.push(`${indent}box ${color} ${label}`);
60902
+ for (const p2 of members) {
60903
+ if (p2.alias === p2.label) {
60904
+ lines.push(`${indent}participant ${p2.alias}`);
60905
+ } else {
60906
+ lines.push(`${indent}participant ${p2.alias} as ${p2.label}`);
60907
+ }
60908
+ }
60909
+ lines.push(`${indent}end`);
60910
+ }
60911
+ }
60912
+ function emitHandoffStep(step, participants, dsl, relatedTasks, lines, indent) {
60913
+ const taskMap = /* @__PURE__ */ new Map();
60914
+ for (const t3 of relatedTasks) taskMap.set(t3.id, t3);
60915
+ if (!step.task) {
60916
+ if (step.from_agent) {
60917
+ const fromAlias2 = participantAlias(participants, step.from_agent);
60918
+ lines.push(`${indent}${fromAlias2}->>${fromAlias2}: ${step.handoff_kind}`);
60919
+ }
60920
+ return;
60921
+ }
60922
+ const task = taskMap.get(step.task);
60923
+ if (!task) return;
60924
+ const fromAlias = step.from_agent ? participantAlias(participants, step.from_agent) : null;
60925
+ const targetAlias = participantAlias(participants, task.target_agent);
60926
+ if (fromAlias) {
60927
+ lines.push(`${indent}${fromAlias}->>${targetAlias}: delegate ${step.task}`);
60928
+ }
60929
+ for (const es of task.execution_steps ?? []) {
60930
+ emitExecutionStep(es, targetAlias, participants, lines, indent);
60931
+ }
60932
+ if (fromAlias) {
60933
+ lines.push(`${indent}${targetAlias}-->>${fromAlias}: ${task.result_handoff}`);
60934
+ }
60935
+ }
60936
+ function emitExecutionStep(es, agentAlias2, participants, lines, indent) {
60937
+ if (es.reads_artifact) {
60938
+ const artAlias = participantAlias(participants, es.reads_artifact);
60939
+ lines.push(`${indent}${agentAlias2}->>${artAlias}: [R] ${es.action}`);
60940
+ }
60941
+ if (es.produces_artifact) {
60942
+ const artAlias = participantAlias(participants, es.produces_artifact);
60943
+ lines.push(`${indent}${agentAlias2}->>${artAlias}: [W] ${es.action}`);
60944
+ }
60945
+ if (es.uses_tool) {
60946
+ const toolAlias = participantAlias(participants, es.uses_tool);
60947
+ lines.push(`${indent}${agentAlias2}->>${toolAlias}: ${es.action}`);
60948
+ }
60949
+ if (!es.reads_artifact && !es.produces_artifact && !es.uses_tool) {
60950
+ lines.push(`${indent}${agentAlias2}->>${agentAlias2}: ${es.action}`);
60951
+ }
60952
+ }
60953
+ function emitValidationStep(step, participants, dsl, lastFromAgent, lines, indent) {
60954
+ const val = dsl.validations[step.validation];
60955
+ if (!val) return;
60956
+ const executorAlias = participantAlias(participants, val.executor);
60957
+ const artifactAlias = participantAlias(participants, val.target_artifact);
60958
+ const fromAlias = lastFromAgent ? participantAlias(participants, lastFromAgent) : null;
60959
+ if (fromAlias && fromAlias !== executorAlias) {
60960
+ lines.push(`${indent}${fromAlias}->>${executorAlias}: ${step.validation}`);
60961
+ }
60962
+ lines.push(`${indent}${executorAlias}->>${artifactAlias}: [R] ${val.target_artifact}`);
60963
+ if (fromAlias && fromAlias !== executorAlias) {
60964
+ lines.push(`${indent}${executorAlias}-->>${fromAlias}: results`);
60965
+ }
60966
+ }
60967
+ function emitDecisionStep(step, participants, lastFromAgent, lines, indent) {
60968
+ const branches = Object.entries(step.branches);
60969
+ if (branches.length === 0) return;
60970
+ const agentAlias2 = lastFromAgent ? participantAlias(participants, lastFromAgent) : null;
60971
+ for (let i3 = 0; i3 < branches.length; i3++) {
60972
+ const [key, values] = branches[i3];
60973
+ if (i3 === 0) {
60974
+ lines.push(`${indent}alt ${key}`);
60975
+ } else {
60976
+ lines.push(`${indent}else ${key}`);
60977
+ }
60978
+ if (agentAlias2) {
60979
+ lines.push(`${indent} Note over ${agentAlias2}: ${values.join(", ")}`);
60980
+ } else {
60981
+ lines.push(`${indent} Note right of ${participantAlias(participants, [.../* @__PURE__ */ new Set()].join(""))}: ${values.join(", ")}`);
60982
+ }
60983
+ }
60984
+ lines.push(`${indent}end`);
60985
+ }
60986
+ function generateSequenceDiagram(workflow, relatedTasks, dsl) {
60987
+ const ids = collectReferencedIds(workflow, dsl, relatedTasks);
60988
+ const participants = buildParticipants(ids, dsl);
60989
+ const lines = [];
60990
+ const indent = " ";
60991
+ lines.push("sequenceDiagram");
60992
+ emitParticipants(participants, lines, indent);
60993
+ lines.push("");
60994
+ lines.push(`${indent}rect ${hashToColor(workflow.id, 0.15, 0.95)}`);
60995
+ const firstP = participants[0];
60996
+ const lastP = participants[participants.length - 1];
60997
+ if (firstP && lastP) {
60998
+ const noteLabel = workflow.description ? `${workflow.id} \u2014 ${workflow.description}` : workflow.id;
60999
+ lines.push(`${indent}Note over ${firstP.alias},${lastP.alias}: ${noteLabel}`);
61000
+ }
61001
+ let lastFromAgent;
61002
+ for (const step of workflow.steps) {
61003
+ lines.push("");
61004
+ if (step.type === "handoff") {
61005
+ if (step.from_agent) lastFromAgent = step.from_agent;
61006
+ emitHandoffStep(step, participants, dsl, relatedTasks, lines, indent);
61007
+ } else if (step.type === "validation") {
61008
+ emitValidationStep(step, participants, dsl, lastFromAgent, lines, indent);
61009
+ } else if (step.type === "decision") {
61010
+ emitDecisionStep(step, participants, lastFromAgent, lines, indent);
61011
+ }
61012
+ }
61013
+ lines.push("");
61014
+ lines.push(`${indent}end`);
61015
+ return lines.join("\n");
61016
+ }
61017
+
60755
61018
  // src/renderer/renderer.ts
60756
61019
  Handlebars.registerHelper("eq", (a3, b3) => a3 === b3);
60757
61020
  Handlebars.registerHelper("notEmpty", (obj) => {
@@ -60913,6 +61176,16 @@ Handlebars.registerHelper("size", (obj) => {
60913
61176
  if (obj && typeof obj === "object") return Object.keys(obj).length;
60914
61177
  return 0;
60915
61178
  });
61179
+ Handlebars.registerHelper(
61180
+ "sequenceDiagram",
61181
+ function() {
61182
+ const workflow = this["workflow"];
61183
+ const relatedTasks = this["relatedTasks"];
61184
+ const dsl = this["dsl"];
61185
+ if (!workflow || !dsl) return "";
61186
+ return generateSequenceDiagram(workflow, relatedTasks ?? [], dsl);
61187
+ }
61188
+ );
60916
61189
  function getDslSection(dsl, context) {
60917
61190
  const sectionMap = {
60918
61191
  agent: dsl.agents,