@orchestraworks/cli 0.6.0 → 0.7.0

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
@@ -20,7 +20,7 @@ The full guide to every construct, secrets, connections and troubleshooting is [
20
20
  ```bash
21
21
  npm install -g @orchestraworks/cli # the CLI, on your path as `orchestra`
22
22
  npm install @orchestraworks/cli # the current release
23
- npm install @orchestraworks/cli@0.6.0 # this exact release
23
+ npm install @orchestraworks/cli@0.7.0 # this exact release
24
24
  ```
25
25
 
26
26
  Then `import { Factory, Agent, … } from "@orchestraworks/cli"` normally — the package `exports` entry resolves
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orchestraworks/client",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Orchestra public API TypeScript client",
5
5
  "author": "Living Computers",
6
6
  "repository": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orchestraworks/cli",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "description": "The Orchestra command line.",
6
6
  "license": "Apache-2.0",
@@ -109,13 +109,13 @@
109
109
  },
110
110
  "dependencies": {
111
111
  "@agentclientprotocol/sdk": "1.3.0",
112
- "@orchestraworks/client": "0.6.0",
113
- "@orchestraworks/worker": "0.0.142",
112
+ "@orchestraworks/client": "0.7.0",
113
+ "@orchestraworks/worker": "0.0.143",
114
114
  "commander": "^14.0.0",
115
115
  "liquidjs": "^10.28.0"
116
116
  },
117
117
  "optionalDependencies": {
118
- "@orchestraworks/local": "0.0.142"
118
+ "@orchestraworks/local": "0.0.143"
119
119
  },
120
120
  "bundleDependencies": [
121
121
  "@orchestraworks/client"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: orchestra
3
- description: Use when you need to understand or explain an Orchestra concept - organization, role, agent, version, environment, machine, network rules, secret, reaching a model (subscription, model gateway and gateway key, own key), session, pipeline, work item, workflow rules, trigger, skill, file, plan, or the factory as code - or how the orchestra command line works, what the platform cannot do yet, and where an agent's knowledge belongs. A reference to read, not a list of steps.
3
+ description: Use when you need to understand or explain an Orchestra concept - organization, role, agent, version, environment, machine, network rules, secret, reaching a model (subscription, model gateway and gateway key, own key), session, pipeline, work item and its check gates, workflow rules and the judge, trigger, skill, file, plan, or the factory as code - or how the orchestra command line works, what the platform cannot do yet, and where an agent's knowledge belongs. A reference to read, not a list of steps.
4
4
  ---
5
5
 
6
6
  # Orchestra: the ideas behind the platform
@@ -120,8 +120,9 @@ reaches the program on its standard input. It starts like any agent:
120
120
  to the agent. A schedule trigger targets a pipeline, never an agent directly.
121
121
 
122
122
  A `script` agent cannot use skills or model settings (the platform refuses both with a 400), and it cannot
123
- judge, read or write prose the way a model does. A turn runs its whole program again, so the program must be
124
- safe to run twice. It is billed as an agent: **Settings › Plan & usage** shows the plan.
123
+ read, write or judge prose the way a model does. It can be a judge of a fixed rule, such as "the description
124
+ has a Release notes section": the program decides and writes the check itself (section 11). A turn runs its
125
+ whole program again, so the program must be safe to run twice. It is billed as an agent: **Settings › Plan & usage** shows the plan.
125
126
 
126
127
  **Cannot yet.**
127
128
  - Change an agent's harness after it is created. Make a new agent instead.
@@ -445,12 +446,90 @@ it, puts the code bundle's file id in it, and assigns it to an Engineer Agent.
445
446
 
446
447
  **Cannot yet.** Declare items in the factory code. Create them with `orchestra work-items create`.
447
448
 
449
+ ### Checks and gates
450
+
451
+ <!-- Sources, on develop at b155aff3a8: docs/architecture/work-plane.md:197-205 (a reopen clears every check;
452
+ one vote per writer per key; counts; nothing declares a key ahead), :274-277 (approval as an IAM-restricted
453
+ key; isAssignee; passUsers), :480-486 (what requires reads), :490-493 (POST /decisions writes nothing), :575
454
+ (one key per grant), :634 (.result is the latest write; .writer.* is the latest writer; counts read 0 when
455
+ absent), :656-657 (setCheck, clearCheck; the author holds the key at save), :673 (body {result, summary?,
456
+ evidence?}; owner and admin write every key);
457
+ docs/architecture/iam.md:48-56 (statement shape), :1055 (no managed role but owner and admin holds the
458
+ action); cli/src/constructs/pipeline.ts:218-228 (allowCheck; apply writes no IAM);
459
+ cli/src/engine/pipeline-reconcile.ts:441-455 (the printed notice); cli/src/generated/cli-commands.json,
460
+ operationId setWorkItemCheck (set-check flags: --result, --summary; evidence only through --body);
461
+ platform/src/main/kotlin/ai/livingcomputers/softwarefactory/workplane/controller/WorkplaneWorkItemController.kt:267
462
+ (the item path takes an id or a slug). -->
463
+
464
+ **What it's for.** A gate keeps an item out of a stage until the work passed a test, a review or a person's
465
+ approval.
466
+
467
+ **What it is.** A *check* is a named key on an item, such as `ci`, `review` or `approval`. Each writer has
468
+ one *vote* on a key: `PASS`, `FAIL` or `SKIP`, with an optional summary and evidence links. A writer's new
469
+ vote replaces its own earlier vote. Nobody declares a key ahead: a stage's `requires` names the keys it
470
+ reads. A *gate* is a stage's `requires` list. The platform reads it when something moves the item into the
471
+ stage, and refuses the move when a condition is false. A gate never asks anybody: it reads the stored votes.
472
+
473
+ A stage's `requires` can read these fields of a key:
474
+
475
+ | Field | Value |
476
+ |---|---|
477
+ | `checks.<key>.result` | The result of the latest vote: `PASS`, `FAIL` or `SKIP` |
478
+ | `checks.<key>.pass`, `.fail` | How many writers voted `PASS`, `FAIL` (0 when nobody voted) |
479
+ | `checks.<key>.passUsers`, `.passAgents` | How many people, agents, voted `PASS` |
480
+ | `checks.<key>.writer.isAssignee` | Whether the writer of the latest vote held the item when it voted |
481
+
482
+ `.writer.isAssignee` reads the latest vote only. With two approvers, it cannot prove that neither of them
483
+ is the assignee.
484
+
485
+ **Who can write a key.** IAM decides, one key at a time: the action `work-items:setWorkItemCheck` on the
486
+ resource `work-items/*/checks/<key>`. Owner and admin write every key. No other built-in role holds the
487
+ action, so each agent, service account or group of people that votes needs a policy for its key. A
488
+ policy is a list of statements, such as:
489
+
490
+ ```json
491
+ { "effect": "allow", "actions": ["work-items:setWorkItemCheck"], "resources": ["work-items/*/checks/ci"] }
492
+ ```
493
+
494
+ In the factory code, `pipeline.allowCheck(agent, "review")` names that grant. Apply does not write it: it
495
+ prints the statement for an administrator to attach to the agent's membership.
496
+
497
+ **How a vote is written.**
498
+
499
+ - The command: `orchestra work-items set-check <item> <key> --result PASS --summary "…"`. `<item>` is the
500
+ item's id or its slug, such as `bug-12`. Evidence links go in a JSON file passed with `--body @vote.json`.
501
+ - The API: `PUT /work-items/{id}/checks/{key}` with `{result, summary?, evidence?}`.
502
+ - A workflow rule: `setCheck` writes the rule's own vote, and `clearCheck` removes every vote on a key. The
503
+ person who applies the rule must hold the key.
504
+ - A judge: a rule's `evaluate` asks an agent to vote (section 11).
505
+
506
+ **A reopen clears every check.** A move from a closed stage (`DONE` or `CANCELED`) back to an open one
507
+ removes every vote on every key, before the target stage's gate is read.
508
+
509
+ **Not a check writer.** `POST /decisions` reads the text typed into the console's "+ Create" box and
510
+ proposes the fields of a new item. It writes nothing, and it never writes a check.
511
+
512
+ **Recipes.** The orchestra-fdk skill's `references/authoring.md`, "Check gates", has five complete
513
+ examples: a model judge, a CI result, a script judge, a two-person approval and a decision model judge.
514
+
448
515
  ## 11. Workflow rules and the judge
449
516
 
450
517
  <!-- Sources, on develop at 19459f9ebb: docs/architecture/multi-agent-workflows.md:18-30 (a workflow is a named,
451
518
  versioned list of rules for one pipeline); docs/architecture/work-plane.md:364-370 (workflow entries pick which
452
519
  version runs on which items), :266-279 (evaluate asks a judge to decide one check; refused when the judge is the
453
- assignee), :573 (rule actions); AGENTS.md "API Conventions" (no runs, no graph engine). -->
520
+ assignee), :573 (rule actions); AGENTS.md "API Conventions" (no runs, no graph engine). Judge facts, on develop
521
+ at b155aff3a8: docs/architecture/work-plane.md:279-285 (the judge writes under its own grant, else 403; a gate
522
+ never calls a judge; a failed judge turn writes no SESSION_FAILED, so an inStageFor rule re-runs evaluate), :658
523
+ (evaluate starts or wakes the judge's session); platform/src/main/kotlin/ai/livingcomputers/softwarefactory/
524
+ workplane/service/WorkItemSessionService.kt:188-208 (the turn's text names the item, the criterion and the PUT
525
+ line); worker/script-harness/index.js:682-717 (a script turn's exit code sets the session outcome, not a check).
526
+ A decision model judge, on 3664271a69: https://docs.typesafe.ai/api.md and platform/src/main/kotlin/ai/
527
+ livingcomputers/softwarefactory/integration/typesafe/TypeSafeDecisionClient.kt (choice and noul questions);
528
+ docs/architecture/work-plane.md:496-498 (a decision model picks among options and writes no text), :562-564 (the
529
+ platform's own calls run on its key). The session lookup, on 20b09bc114: WorkItemSessionService.kt:332-392
530
+ (converse finds the judge's link on the item, else startConversation starts a session with a link to that item
531
+ alone); docs/architecture/work-plane.md:193-194 (session keeps the items a link names); docs/architecture/
532
+ fwp.md:1060-1063 (ORCHESTRA_SESSION_ID on every harness process, the script's command included). -->
454
533
 
455
534
  **What it's for.** Rules do the routine moves, so people and agents do not have to: assign the next agent, move
456
535
  an item, ask for a review.
@@ -463,6 +542,25 @@ what it does: move, assign, create an item, or evaluate. There are no workflow r
463
542
  the judge's session on the item. The judge writes the check, and a stage that requires it then lets the item in.
464
543
  The item's own assignee cannot be its judge.
465
544
 
545
+ A judge can also be a script agent that asks a *decision model*, such as TypeSafe's Jev, with the customer's own
546
+ key. That model picks one of a fixed list of options, or gives the probability of yes, and writes no text. The
547
+ recipe is in section 10's Recipes.
548
+
549
+ **What a judge must know.**
550
+
551
+ - The judge's turn names the item, the key and the criterion, and the `PUT /work-items/<id>/checks/<key>`
552
+ call to make. The judge must make that call itself, for example with `orchestra work-items set-check`.
553
+ The platform does not read the judge's reply or its session outcome into the check.
554
+ - The judge's session is on that one item: a judge of two items has two sessions. A program can find its
555
+ item with `orchestra work-items list --session "$ORCHESTRA_SESSION_ID"` and read none of the turn's text.
556
+ - The judge needs its own grant on the key (section 10). Without it, its write gets a 403, and the gate
557
+ stays shut.
558
+ - A rule that names the item's assignee as the judge is refused, with a note in the item's feed.
559
+ - A failed judge turn writes no `SESSION_FAILED` entry, so no failure rule sees it. To recover, add a rule
560
+ on `inStageFor` that runs `evaluate` again while the check is still absent.
561
+ - A gate does not start a judge. Something must run `evaluate` before the move, usually a rule on entering
562
+ the stage before the gated one.
563
+
466
564
  **How it relates.** Rules move items through stages (section 9) and write to checks (section 10).
467
565
 
468
566
  **In the starter.** None yet. Agents and people move items by hand.
@@ -961,6 +961,491 @@ new Trigger(factory, "bug-intake", {
961
961
  each to what you declared. A revert made in the app shows in `orchestra fdk diff` as the rules apply
962
962
  would write back.
963
963
 
964
+ ## Check gates
965
+
966
+ A *gate* is a stage's `requires` list. It keeps an item out of the stage until its checks say so. A
967
+ *check* is a named key on an item. Each writer has one vote on it: `PASS`, `FAIL` or `SKIP`, with an
968
+ optional summary and evidence links. The orchestra skill's section 10, "Checks and gates", explains the
969
+ model. This section is the setup.
970
+
971
+ **Set up a gate in four steps.**
972
+
973
+ 1. **Pick a key**, such as `ci`, `review` or `approval`. Nothing declares it ahead.
974
+ 2. **Write the gate.** Add conditions over the key to the stage's `requires`. A condition is
975
+ `{ field, op, value }`, and a count takes a number value: `{ field: "checks.approval.passUsers", op:
976
+ "gte", value: 2 }`. A stage reads `checks.<key>.result`, the counts `.pass`, `.fail`, `.passUsers` and
977
+ `.passAgents`, and `.writer.isAssignee`, which describes the latest vote only.
978
+ 3. **Grant the key to each writer.** The action is `work-items:setWorkItemCheck` on the resource
979
+ `work-items/*/checks/<key>`, one key per grant. Only owner and admin hold it by default. For an agent,
980
+ `pipeline.allowCheck(agent, "<key>")` declares the grant, and apply prints the statement for an
981
+ administrator to attach. Apply writes no IAM.
982
+ 4. **Make something vote.** A judge agent that a rule's `evaluate` asks, a CI job or a person runs
983
+ `orchestra work-items set-check <item> <key> --result PASS|FAIL|SKIP --summary "…"`. `<item>` is the
984
+ item's id or its slug, such as `feat-12`. Evidence links go in a JSON file passed with
985
+ `--body @vote.json`. A rule can also vote with `setCheck`, or clear every vote with `clearCheck`; the
986
+ person who applies that rule must hold the key.
987
+
988
+ Before you rely on a gate, read the orchestra skill's section 11, "What a judge must know", and the
989
+ reopen rule in its section 10. The examples follow those rules: a rule runs `evaluate` on entering the
990
+ stage before the gated one, the judge votes with `orchestra work-items set-check`, and an `inStageFor`
991
+ rule asks again while the check is absent.
992
+
993
+ The examples build on the `factory` app and the import line of § The construct model. Example 3 runs on its
994
+ `runtime` environment, and Examples 1 and 5 declare their own. Each example imports only the names that
995
+ section and the earlier examples do not import, so all of them paste into one `main.ts`.
996
+
997
+ ### Example 1: a model judge on an OpenAI model
998
+
999
+ A `codex` agent judges the `review` key through the model gateway. Its environment, `codexRuntime`, differs
1000
+ from `runtime` in two places:
1001
+
1002
+ - **`env` sets `INITIAL_AGENT_MODE: "agent-full-access"`.** Codex opens each session with a command
1003
+ sandbox that has no network. Without this mode, the judge cannot reach the platform to vote. The
1004
+ egress policy still decides which hosts the judge may reach.
1005
+ - **The egress policy carries the gateway rule**, `routeModelsViaGateway`, with the gateway key's secret.
1006
+ An agent whose model settings turn on the model gateway needs it.
1007
+
1008
+ ```ts
1009
+ import {
1010
+ GatewayKey, Pipeline, Workflow, check, clearCheck, entered, evaluate, inStageFor, move, rule,
1011
+ } from "@orchestraworks/cli";
1012
+
1013
+ const gatewayKey = new Secret(factory, "model-gateway-key", {
1014
+ value: SecretValue.fromGatewayKey(new GatewayKey(factory, "model-gateway")),
1015
+ injection: {
1016
+ headers: [{ name: "Authorization", value: "Bearer ${secret}" }],
1017
+ removeHeaders: ["x-api-key"],
1018
+ hosts: ["models.orchestraworks.ai"],
1019
+ },
1020
+ });
1021
+
1022
+ const codexRuntime = new Environment(factory, "codex-runtime", {
1023
+ name: "Codex judge environment",
1024
+ image: Image.fromTag("acme/support-agent:1.0.0"),
1025
+ platform: { os: "linux", arch: "amd64" },
1026
+ resources: { cpu: "2", memory: "4G" },
1027
+ env: { INITIAL_AGENT_MODE: "agent-full-access" },
1028
+ egress: EgressPolicy.denyAll()
1029
+ .applyPreset(egressPreset.softwareDevelopment())
1030
+ .routeModelsViaGateway({ key: gatewayKey }),
1031
+ });
1032
+
1033
+ const reviewer = new Agent(factory, "reviewer", {
1034
+ environment: codexRuntime,
1035
+ harness: "codex",
1036
+ modelSettings: { model: "gpt-6.1-sol", enableModelGateway: true },
1037
+ instructions: `You judge work items. You never change the code or the item.
1038
+
1039
+ When a turn asks you to set a check:
1040
+ 1. Read the item: orchestra work-items get <item id>
1041
+ 2. Decide against the criterion the turn gives.
1042
+ 3. Record the decision. Only this command counts; your reply is not read:
1043
+ orchestra work-items set-check <item id> <key> --result PASS --summary "<one line: why>"
1044
+ Use --result FAIL when the work does not meet the criterion. Use the item id and key the turn names.`,
1045
+ });
1046
+
1047
+ const features = new Pipeline(factory, "features", {
1048
+ itemSlugPrefix: "feat",
1049
+ stages: [
1050
+ { slug: "building", category: "IN_PROGRESS" },
1051
+ { slug: "review", category: "IN_PROGRESS" },
1052
+ { slug: "done", category: "DONE", requires: [{ field: "checks.review.result", op: "is", value: "PASS" }] },
1053
+ ],
1054
+ }).allowCheck(reviewer, "review");
1055
+
1056
+ const askReviewer = evaluate({
1057
+ agent: reviewer,
1058
+ check: "review",
1059
+ criterion: "The change does what the item asks, its tests pass, and it adds no unrelated change.",
1060
+ });
1061
+
1062
+ new Workflow(factory, "features-review", {
1063
+ pipeline: features,
1064
+ description: "Asks the reviewer agent to judge each feature in Review and moves the item on its vote.",
1065
+ rules: [
1066
+ rule({
1067
+ name: "Clear the old vote and ask the reviewer on entering Review",
1068
+ when: entered("review"),
1069
+ then: [clearCheck("review"), askReviewer],
1070
+ }),
1071
+ rule({
1072
+ name: "Ask the reviewer again after an hour with no vote",
1073
+ when: inStageFor("review", "1h"),
1074
+ if: { field: "checks.review.result", op: "isAbsent" },
1075
+ then: [askReviewer],
1076
+ }),
1077
+ rule({ name: "Move a passed review to Done", when: check("review").is("PASS"), then: [move("done")] }),
1078
+ rule({ name: "Send a failed review back to Building", when: check("review").is("FAIL"), then: [move("building")] }),
1079
+ ],
1080
+ });
1081
+ ```
1082
+
1083
+ The first rule clears the vote of an earlier stay, so the second rule sees an absent check. A rule that
1084
+ writes or clears a check needs its author to hold the key, so the person who applies this workflow holds
1085
+ `review`, as owner and admin do. An `inStageFor` rule fires once in each stay in the stage. For more
1086
+ tries, add a second rule with a longer duration.
1087
+
1088
+ **Attach the judge's grant before its first vote.** `allowCheck` only prints the policy statement, and a
1089
+ judge without it gets a 403. An administrator attaches it with the policy and membership commands of
1090
+ Example 2: write the policy for the `review` key, and take the membership id from the `membershipId` field
1091
+ that `orchestra agents get <agent id>` answers. The agent's membership already holds the policies the
1092
+ platform attached at its create, so keep them in `attach.json`.
1093
+
1094
+ ### Example 2: a CI result from GitHub Actions
1095
+
1096
+ The CI job runs the tests and votes on the `ci` key as a service account. The FDK neither creates a
1097
+ service account nor writes a policy, so the setup is four commands, run once by an administrator:
1098
+
1099
+ ```bash
1100
+ orchestra service-accounts create --name github-ci # note its id, svc_…, and its membershipId
1101
+ orchestra policies create --body @ci-check-policy.json # note its id, pol_…
1102
+ orchestra organization-memberships update <membership id> --body @attach.json
1103
+ orchestra api-keys create --principal-type SERVICE_ACCOUNT --principal-id <svc id> --name github-ci \
1104
+ --output-file github-ci-key.json
1105
+ ```
1106
+
1107
+ `ci-check-policy.json`:
1108
+
1109
+ ```json
1110
+ {
1111
+ "name": "Write the ci check",
1112
+ "document": {
1113
+ "statements": [
1114
+ { "effect": "allow", "actions": ["work-items:setWorkItemCheck"], "resources": ["work-items/*/checks/ci"] }
1115
+ ]
1116
+ }
1117
+ }
1118
+ ```
1119
+
1120
+ `attach.json` replaces the membership's whole list of directly attached policies. Read the list first with
1121
+ `orchestra organization-memberships get <membership id>` and keep each entry in it:
1122
+
1123
+ ```json
1124
+ { "policyAssignments": [{ "policyId": "<pol id>", "allTeams": true }] }
1125
+ ```
1126
+
1127
+ The platform shows a key's token once, in the answer to its create, and stores only its digest. The CLI
1128
+ masks the token on the screen, so the last command writes the whole answer to `github-ci-key.json`. Store
1129
+ its `token` field as the repository secret `CHECKS_CI_KEY`, then delete the file. The gate:
1130
+
1131
+ ```ts
1132
+ { slug: "merged", category: "DONE", requires: [{ field: "checks.ci.result", op: "is", value: "PASS" }] }
1133
+ ```
1134
+
1135
+ The job must know the item. This example uses a team convention: the branch name starts with the item's
1136
+ slug, such as `feat-12-export-csv`. Use whatever convention your team keeps, such as a line in the pull
1137
+ request body.
1138
+
1139
+ ```yaml
1140
+ name: tests
1141
+ on: pull_request
1142
+ jobs:
1143
+ test:
1144
+ runs-on: ubuntu-latest
1145
+ steps:
1146
+ - uses: actions/checkout@v4
1147
+ - id: test
1148
+ run: npm ci && npm test
1149
+ - name: Vote on the work item's ci check
1150
+ if: always()
1151
+ env:
1152
+ ORCHESTRA_BASE_URL: https://api.orchestraworks.ai
1153
+ ORCHESTRA_API_TOKEN: ${{ secrets.CHECKS_CI_KEY }}
1154
+ HEAD_REF: ${{ github.head_ref }}
1155
+ TEST_OUTCOME: ${{ steps.test.outcome }}
1156
+ run: |
1157
+ item=$(printf '%s' "$HEAD_REF" | grep -oE '^feat-[0-9]+' || true)
1158
+ if [ -z "$item" ]; then echo "The branch name names no work item"; exit 0; fi
1159
+ result=FAIL
1160
+ if [ "$TEST_OUTCOME" = success ]; then result=PASS; fi
1161
+ run_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
1162
+ printf '{"evidence":["%s"]}' "$run_url" > vote.json
1163
+ npx --yes @orchestraworks/cli work-items set-check "$item" ci \
1164
+ --result "$result" --summary "Tests: $TEST_OUTCOME" --body @vote.json
1165
+ ```
1166
+
1167
+ Each run replaces the account's earlier vote, so a fix pushed after a `FAIL` turns the check to `PASS`.
1168
+ `ORCHESTRA_BASE_URL` is required: the CLI never sends a token to an address it did not read from you.
1169
+
1170
+ A GitHub event cannot write this check without the CI change. The GitHub connection delivers only
1171
+ `issues`, `issue_comment` and `pull_request` events, and none of them carries a test result.
1172
+
1173
+ ### Example 3: a script judge
1174
+
1175
+ A `script` agent's `instructions` are a Bash program, so it judges a fixed rule with no model. Each turn's
1176
+ text reaches the program on standard input, and this program reads none of it. `evaluate` runs the judge in a
1177
+ session on that one item (the orchestra skill's section 11). So the program lists the items of its own
1178
+ session, `ORCHESTRA_SESSION_ID`, decides, and votes with `orchestra`. With no item or more than one, it stops
1179
+ with no vote. The machine image ships `orchestra` and `jq`. `orchestra` sends the agent's own token on each
1180
+ platform call, so the program holds no credential. The command also gets `PLATFORM_BASE_URL` and
1181
+ `ORCHESTRA_ORGANIZATION_ID`.
1182
+
1183
+ ```ts
1184
+ import { changed } from "@orchestraworks/cli";
1185
+
1186
+ const notesJudge = new Agent(factory, "notes-judge", {
1187
+ environment: runtime,
1188
+ harness: "script",
1189
+ instructions: `set -euo pipefail
1190
+ items=$(orchestra work-items list --session "$ORCHESTRA_SESSION_ID" --output json)
1191
+ if [ "$(printf '%s' "$items" | jq '.data | length')" != 1 ]; then
1192
+ echo "This session is not on exactly one work item." >&2
1193
+ exit 1
1194
+ fi
1195
+ id=$(printf '%s' "$items" | jq -r '.data[0].id')
1196
+ description=$(printf '%s' "$items" | jq -r '.data[0].description // ""')
1197
+ if printf '%s' "$description" | grep -q '^## Release notes'; then
1198
+ orchestra work-items set-check "$id" notes --result PASS --summary "The description has a Release notes section."
1199
+ else
1200
+ orchestra work-items set-check "$id" notes --result FAIL --summary "Add a '## Release notes' section to the description."
1201
+ fi`,
1202
+ });
1203
+
1204
+ const releases = new Pipeline(factory, "releases", {
1205
+ itemSlugPrefix: "rel",
1206
+ stages: [
1207
+ { slug: "drafting", category: "TODO" },
1208
+ { slug: "review", category: "IN_PROGRESS", requires: [{ field: "checks.notes.result", op: "is", value: "PASS" }] },
1209
+ { slug: "released", category: "DONE" },
1210
+ ],
1211
+ }).allowCheck(notesJudge, "notes");
1212
+
1213
+ const askNotesJudge = evaluate({
1214
+ agent: notesJudge,
1215
+ check: "notes",
1216
+ criterion: "The description has a Release notes section.",
1217
+ });
1218
+
1219
+ new Workflow(factory, "releases-notes", {
1220
+ pipeline: releases,
1221
+ description: "Asks the notes judge to check each release's description while it is drafted.",
1222
+ rules: [
1223
+ rule({ name: "Ask the notes judge on entering Drafting", when: entered("drafting"), then: [askNotesJudge] }),
1224
+ rule({
1225
+ name: "Ask the notes judge again after ten minutes with no vote",
1226
+ when: inStageFor("drafting", "10m"),
1227
+ if: { field: "checks.notes.result", op: "isAbsent" },
1228
+ then: [askNotesJudge],
1229
+ }),
1230
+ rule({ name: "Ask the notes judge again on a recheck label", when: changed("labels.recheck"), then: [askNotesJudge] }),
1231
+ ],
1232
+ });
1233
+ ```
1234
+
1235
+ - **The exit code sets the session outcome, not the check.** Exit 0 after any vote, `FAIL` included.
1236
+ Exit non-zero only when the program could not vote; that turn fails, and the `inStageFor` rule asks
1237
+ again.
1238
+ - **The judge does not see later edits.** A `FAIL` stays until a new turn asks again. So the last rule
1239
+ asks again when a person sets or changes the `recheck` label after a fix.
1240
+ - **A turn runs the whole program again**, so it must be safe to run twice. A second vote replaces the
1241
+ first, so this program is.
1242
+ - **Attach the `notes` grant** the way Example 1 attaches its judge's grant.
1243
+ - **The program reads only the description.** The item's JSON also holds the checks' summaries, and the
1244
+ `FAIL` summary names the section, so a search of the whole JSON would find it there.
1245
+
1246
+ ### Example 4: approval by two people
1247
+
1248
+ The `approval` key is granted to people through a role. The gate asks for two people's `PASS`, no `FAIL`,
1249
+ and a latest voter who does not hold the item.
1250
+
1251
+ 1. Create the policy with `orchestra policies create --body @approval-policy.json`, the same shape as in
1252
+ example 2 with the resource `work-items/*/checks/approval`. The FDK names a policy by id; it does not
1253
+ create one.
1254
+ 2. Declare the role in code:
1255
+
1256
+ ```ts
1257
+ import { Policy, Role } from "@orchestraworks/cli";
1258
+
1259
+ const approver = new Role(factory, "approver", {
1260
+ name: "Approver",
1261
+ policies: [Policy.fromId(factory, "pol_…")], // the policy from step 1
1262
+ });
1263
+ ```
1264
+
1265
+ 3. Give each approver the role at the pipeline's team, or at all teams:
1266
+ `orchestra team-memberships create --membership-id <person's membership id> --team-id <team id>
1267
+ --role-id <role id>`. A member holds one role per scope, so a person who already holds a role at that
1268
+ scope needs one custom role that lists both sets of policies. The FDK declares team memberships for
1269
+ agents and service accounts only, not for people.
1270
+ 4. Write the gate:
1271
+
1272
+ ```ts
1273
+ {
1274
+ slug: "released",
1275
+ category: "DONE",
1276
+ requires: [
1277
+ { field: "checks.approval.passUsers", op: "gte", value: 2 },
1278
+ { field: "checks.approval.fail", op: "is", value: 0 },
1279
+ { field: "checks.approval.writer.isAssignee", op: "is", value: false },
1280
+ ],
1281
+ }
1282
+ ```
1283
+
1284
+ An approver votes with `orchestra work-items set-check rel-7 approval --result PASS --summary "…"`.
1285
+ `passUsers` counts people only, so an agent's vote does not count toward the two. One `FAIL` holds the item
1286
+ until that person votes again.
1287
+
1288
+ **The limit of the self-approval guard.** `.writer.isAssignee` reads the latest vote only. If the assignee
1289
+ votes first and a second person votes last, the guard passes. With two approvers, it cannot prove that
1290
+ neither of them holds the item. Keep the assignee out of the approver role when that matters.
1291
+
1292
+ ### Example 5: a decision model judge (Jev)
1293
+
1294
+ <!-- Sources, on 74243daf6c: https://docs.typesafe.ai/api.md (the endpoint, the noul and choice questions and
1295
+ answers, a choice's criteria map, back off on 429 and 529); platform/src/main/kotlin/ai/livingcomputers/
1296
+ softwarefactory/integration/typesafe/TypeSafeDecisionClient.kt (that shape as encoded; a choice answer needs a
1297
+ string choice and a number confidence; one attempt) and platform/src/test/resources/typesafe/
1298
+ decision-transcript.json (every field name); platform/…/config/DecisionsProperties.kt:31 (a pinned version, never
1299
+ jev-latest: a new version changes the confidences); docs/architecture/work-plane.md:490-498, 562-564 (POST
1300
+ /decisions writes nothing; the platform's calls, on its key); platform/…/workplane/service/
1301
+ WorkItemSessionService.kt:332-392 (a judge's session is linked to one item); platform/…/common/api/Page.kt:11-16
1302
+ (rows under `data`); cli/src/constructs/environment.ts:717-722 and docs/architecture/egress.md:919-921, 1364-1371
1303
+ (an environment credential is added to a request to its hosts that names none, whatever the agent);
1304
+ cli/src/engine/reconcile.ts:368 and secrets-apply.ts:254-257 (apply reads the variable from its own environment);
1305
+ docs/architecture/commercial-plans-and-credits.md:268, 432 (the individual plan refuses a Secret create);
1306
+ cli/src/constructs/rules.ts:149-152 (entered fires on every entry); agents/runtime/image/Dockerfile:43 (curl, jq);
1307
+ cli/src/constructs/pipeline.ts:31-35 (`next` lists the stage slugs a move may go to);
1308
+ docs/architecture/work-plane.md:650 (a rule's move is judged against `next`); platform/…/workplane/model/
1309
+ WorkItem.kt:344 (a create with no stage lands in the first stage); platform/…/workplane/service/
1310
+ WorkplaneWorkItemService.kt:372 (a move to the item's own stage does nothing); execve(2) (one argument holds at
1311
+ most 128 KiB on Linux) and curl(1) `--data-binary` (`@-` reads the body from standard input as it is). -->
1312
+
1313
+ A *decision model* answers by picking, not by writing: one option from a fixed list, or the probability of yes.
1314
+ TypeSafe's Jev is one. Here a `script` agent asks Jev if a request is ready to build. Jev picks `PASS` or `FAIL`
1315
+ with a confidence, and the agent votes on `ready`.
1316
+
1317
+ - **Bring your own TypeSafe key.** The platform calls Jev only to read the text typed into the console's
1318
+ "+ Create" box, with its own key. That call cannot write a check, and no agent gets that key. Each call this
1319
+ judge makes bills your TypeSafe account.
1320
+ - **Set `TYPESAFE_API_KEY` in the shell that runs `orchestra fdk apply`.** Apply reads the key from that variable
1321
+ and writes it to the Secret. On the `individual` plan, apply refuses a Secret: see the orchestra skill's
1322
+ section 6, "Secret".
1323
+ - **The program never holds the key.** The Secret's recipe sets `Authorization: Bearer` on `api.typesafe.ai`. The
1324
+ environment lists the Secret in `credentials`. So the edge adds the key to each request that any program on
1325
+ this environment's machines sends to `api.typesafe.ai`, and no program holds it. Use this environment for the
1326
+ judge only.
1327
+ - **The request and the answer follow TypeSafe's published reference**, https://docs.typesafe.ai/api.md. The
1328
+ request is `{model, state, questions}`. The program asks one `choice` question, whose `criteria` map each
1329
+ option to a description: here `PASS` and `FAIL`, each described from the criterion. The answer gives the
1330
+ picked option, its confidence and each option's probability. For example: `{"answers": {"ready": {"type":
1331
+ "choice", "choice": "PASS", "confidence": 0.88, "probabilities": {"PASS": 0.94, "FAIL": 0.06}}}}`. The image
1332
+ ships `curl` beside `orchestra` and `jq`.
1333
+
1334
+ ```ts
1335
+ const typesafeKey = new Secret(factory, "typesafe-api-key", {
1336
+ value: SecretValue.fromEnv("TYPESAFE_API_KEY"),
1337
+ injection: {
1338
+ headers: [{ name: "Authorization", value: "Bearer ${secret}" }],
1339
+ hosts: ["api.typesafe.ai"],
1340
+ },
1341
+ });
1342
+
1343
+ const decisionRuntime = new Environment(factory, "decision-runtime", {
1344
+ name: "Decision model judge environment",
1345
+ image: Image.fromTag("acme/support-agent:1.0.0"),
1346
+ platform: { os: "linux", arch: "amd64" },
1347
+ resources: { cpu: "2", memory: "4G" },
1348
+ credentials: [{ credential: typesafeKey }],
1349
+ egress: EgressPolicy.denyAll()
1350
+ .applyPreset(egressPreset.softwareDevelopment())
1351
+ .allow("typesafe-api", { hosts: ["api.typesafe.ai"], priority: 300 }),
1352
+ });
1353
+
1354
+ const readyCriterion = "The description says what to build and how to tell that it is done.";
1355
+ const minConfidence = 0.8;
1356
+ const unsureResult: "PASS" | "FAIL" = "FAIL";
1357
+
1358
+ const readyJudge = new Agent(factory, "ready-judge", {
1359
+ environment: decisionRuntime,
1360
+ harness: "script",
1361
+ instructions: `set -euo pipefail
1362
+ items=$(orchestra work-items list --session "$ORCHESTRA_SESSION_ID" --output json)
1363
+ if [ "$(printf '%s' "$items" | jq '.data | length')" != 1 ]; then
1364
+ echo "This session is not on exactly one work item." >&2
1365
+ exit 1
1366
+ fi
1367
+ id=$(printf '%s' "$items" | jq -r '.data[0].id')
1368
+ body=$(printf '%s' "$items" | jq --arg criterion '${readyCriterion}' '{
1369
+ model: "jev-1.13.0",
1370
+ state: (.data[0] | {title, description: (.description // "")}),
1371
+ questions: {ready: {
1372
+ type: "choice",
1373
+ instructions: ("Is this true of the work item? " + $criterion),
1374
+ criteria: {PASS: ("True: " + $criterion), FAIL: ("Not true: " + $criterion)}
1375
+ }}
1376
+ }')
1377
+ answer=$(printf '%s' "$body" |
1378
+ curl -sS --fail --max-time 30 -H 'content-type: application/json' --data-binary @- https://api.typesafe.ai/v1/systemone)
1379
+ choice=$(printf '%s' "$answer" | jq -er '.answers.ready.choice | select(. == "PASS" or . == "FAIL")')
1380
+ confidence=$(printf '%s' "$answer" | jq -e '.answers.ready.confidence | numbers')
1381
+ if jq -en --argjson c "$confidence" '$c >= ${minConfidence}' > /dev/null; then
1382
+ result=$choice
1383
+ summary="The decision model picked $choice with confidence $confidence."
1384
+ else
1385
+ result=${unsureResult}
1386
+ summary="The decision model was unsure: it picked $choice with confidence $confidence."
1387
+ fi
1388
+ orchestra work-items set-check "$id" ready --result "$result" --summary "$summary"`,
1389
+ });
1390
+
1391
+ const requests = new Pipeline(factory, "requests", {
1392
+ itemSlugPrefix: "req",
1393
+ stages: [
1394
+ { slug: "drafting", category: "TODO", next: ["inbox"] },
1395
+ { slug: "inbox", category: "TODO" },
1396
+ { slug: "building", category: "IN_PROGRESS", requires: [{ field: "checks.ready.result", op: "is", value: "PASS" }] },
1397
+ { slug: "done", category: "DONE" },
1398
+ ],
1399
+ }).allowCheck(readyJudge, "ready");
1400
+
1401
+ const askReadyJudge = evaluate({ agent: readyJudge, check: "ready", criterion: readyCriterion });
1402
+
1403
+ new Workflow(factory, "requests-ready", {
1404
+ pipeline: requests,
1405
+ description: "Asks the decision model judge if each request in Inbox is ready, and sends it back on a FAIL.",
1406
+ rules: [
1407
+ rule({
1408
+ name: "Clear the old vote and ask the ready judge on entering Inbox",
1409
+ when: entered("inbox"),
1410
+ then: [clearCheck("ready"), askReadyJudge],
1411
+ }),
1412
+ rule({
1413
+ name: "Ask the ready judge again after ten minutes with no vote",
1414
+ when: inStageFor("inbox", "10m"),
1415
+ if: { field: "checks.ready.result", op: "isAbsent" },
1416
+ then: [askReadyJudge],
1417
+ }),
1418
+ rule({ name: "Send a request that is not ready back to Drafting", when: check("ready").is("FAIL"), then: [move("drafting")] }),
1419
+ ],
1420
+ });
1421
+ ```
1422
+
1423
+ - **The rules work as Example 1's**, with Drafting and Inbox in place of Building and Review. By default, a new
1424
+ request lands in Drafting, the first stage. Move it to Inbox to ask the judge. No rule moves a passed
1425
+ request: a person moves it to Building.
1426
+ - **A PASS holds until the request next enters Inbox.** A request in Drafting can move only to Inbox, so it
1427
+ reaches Building only through Inbox. Each entry into Inbox clears the vote and asks again. After a fix or an
1428
+ edit, move the request back to Inbox to ask again. A move to the stage the request is in does nothing. So
1429
+ from Inbox, move it to Drafting first.
1430
+ - **The program reads nothing from the turn.** It finds the item as Example 3 does, and the key and the
1431
+ criterion are fixed in it. So no text from the turn reaches the question. The item's title and description
1432
+ go in `state`. A long description can be longer than the limit for one command-line argument. So the
1433
+ program gives the item's JSON and the TypeSafe body to `jq` and `curl` on standard input. The cost is one
1434
+ judge for each criterion. One constant gives the criterion to the rule and to the program. Keep single
1435
+ quotes out of it, because the program quotes it in them.
1436
+ - **The vote.** At a confidence of `minConfidence` or more, the vote is Jev's pick, `answers.ready.choice`. Below
1437
+ it, the vote is `unsureResult`. The summary gives the pick and the confidence, and says when Jev was unsure.
1438
+ - **`FAIL` as the unsure result fails closed.** An unsure answer keeps the gate shut, and the `FAIL` rule sends
1439
+ the request back to Drafting, as in Example 1. Set `unsureResult` to `PASS` only where a wrong pass costs
1440
+ little.
1441
+ - **Pin the model version**, `jev-1.13.0` here. Never use `jev-latest`: a new version changes the confidences,
1442
+ and `minConfidence` then means something else. Choose `minConfidence` from Jev's answers on your own items.
1443
+ - **One call, no retry.** TypeSafe asks a caller to back off on 429 and 529. On an HTTP error or no answer in
1444
+ 30 seconds, the program exits non-zero with no vote. It does the same when `choice` is not exactly `PASS` or
1445
+ `FAIL`, or `confidence` is not a number. The `inStageFor` rule then asks again, as in Example 1.
1446
+ - **Attach the `ready` grant** the way Example 1 attaches its judge's grant.
1447
+ - **Example 3's warnings on the exit code and on running twice hold here too.**
1448
+
964
1449
  ## The six design principles
965
1450
 
966
1451
  These decide the design questions authoring throws up. When something feels like it needs new API
@@ -9,7 +9,7 @@
9
9
  "typecheck": "tsc -p tsconfig.json"
10
10
  },
11
11
  "dependencies": {
12
- "@orchestraworks/cli": "^0.6.0"
12
+ "@orchestraworks/cli": "^0.7.0"
13
13
  },
14
14
  "devDependencies": {
15
15
  "@types/node": "^22.10.0",
@@ -9,7 +9,7 @@
9
9
  "typecheck": "tsc -p tsconfig.json"
10
10
  },
11
11
  "dependencies": {
12
- "@orchestraworks/cli": "^0.6.0"
12
+ "@orchestraworks/cli": "^0.7.0"
13
13
  },
14
14
  "devDependencies": {
15
15
  "@types/node": "^22.10.0",