@orchestraworks/cli 0.5.8 → 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 +1 -1
- package/dist/cli/api/approval.js +3 -3
- package/dist/cli/commands/outpost/local.js +3 -3
- package/dist/cli/commands/outpost/machine-names.d.ts +17 -0
- package/dist/cli/commands/outpost/machine-names.js +22 -0
- package/dist/cli/commands/outpost/up.js +7 -5
- package/dist/cli/commands/worker/enroll.d.ts +4 -3
- package/dist/cli/commands/worker/enroll.js +4 -3
- package/dist/cli/core/auth.d.ts +11 -1
- package/dist/cli/core/auth.js +21 -4
- package/dist/cli/core/session-body.d.ts +1 -1
- package/dist/cli/core/session-body.js +1 -1
- package/dist/cli/firstRun.js +2 -2
- package/dist/cli/main.js +4 -3
- package/dist/constructs/environment.d.ts +24 -11
- package/dist/constructs/environment.js +76 -12
- package/dist/constructs/secret.d.ts +17 -7
- package/dist/constructs/secret.js +24 -16
- package/dist/core/labels.d.ts +32 -6
- package/dist/core/labels.js +70 -8
- package/dist/engine/diff.js +38 -11
- package/dist/engine/gateway-key-reconcile.d.ts +1 -2
- package/dist/engine/gateway-key-reconcile.js +19 -15
- package/dist/engine/pipeline-reconcile.js +2 -2
- package/dist/engine/pipeline-workflow-reconcile.js +15 -9
- package/dist/engine/reconcile.js +48 -15
- package/dist/engine/refusals.d.ts +0 -5
- package/dist/engine/refusals.js +1 -5
- package/dist/engine/secrets-apply.d.ts +10 -14
- package/dist/engine/secrets-apply.js +12 -17
- package/dist/engine/workflow-reconcile.d.ts +1 -1
- package/dist/generated/cli-commands.json +170 -34
- package/dist/generated/providers.d.ts +40 -0
- package/dist/generated/providers.js +30 -0
- package/docs/REFERENCE.md +9 -7
- package/node_modules/@orchestraworks/client/dist/esm/models/ApiErrorDetail.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/esm/models/CreateEnvironmentRequest.d.ts +7 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/CreateEnvironmentRequest.js +3 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/CreateSecretRequest.d.ts +4 -3
- package/node_modules/@orchestraworks/client/dist/esm/models/Environment.d.ts +7 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/Environment.js +3 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentCredential.d.ts +38 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentCredential.js +45 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentProvisionSpec.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentProvisionSpecRequest.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentSnapshot.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/esm/models/InjectionRecipe.d.ts +6 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/InjectionRecipe.js +2 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/OAuthTokenRequest.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/esm/models/Secret.d.ts +1 -7
- package/node_modules/@orchestraworks/client/dist/esm/models/Secret.js +0 -4
- package/node_modules/@orchestraworks/client/dist/esm/models/UpdateEnvironmentRequest.d.ts +7 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/UpdateEnvironmentRequest.js +3 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/UpdateSecretRequest.d.ts +4 -3
- package/node_modules/@orchestraworks/client/dist/esm/models/index.d.ts +1 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/index.js +1 -0
- package/node_modules/@orchestraworks/client/dist/models/ApiErrorDetail.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/models/CreateEnvironmentRequest.d.ts +7 -0
- package/node_modules/@orchestraworks/client/dist/models/CreateEnvironmentRequest.js +3 -0
- package/node_modules/@orchestraworks/client/dist/models/CreateSecretRequest.d.ts +4 -3
- package/node_modules/@orchestraworks/client/dist/models/Environment.d.ts +7 -0
- package/node_modules/@orchestraworks/client/dist/models/Environment.js +3 -0
- package/node_modules/@orchestraworks/client/dist/models/EnvironmentCredential.d.ts +38 -0
- package/node_modules/@orchestraworks/client/dist/models/EnvironmentCredential.js +52 -0
- package/node_modules/@orchestraworks/client/dist/models/EnvironmentProvisionSpec.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/models/EnvironmentProvisionSpecRequest.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/models/EnvironmentSnapshot.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/models/InjectionRecipe.d.ts +6 -0
- package/node_modules/@orchestraworks/client/dist/models/InjectionRecipe.js +2 -0
- package/node_modules/@orchestraworks/client/dist/models/OAuthTokenRequest.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/models/Secret.d.ts +1 -7
- package/node_modules/@orchestraworks/client/dist/models/Secret.js +0 -4
- package/node_modules/@orchestraworks/client/dist/models/UpdateEnvironmentRequest.d.ts +7 -0
- package/node_modules/@orchestraworks/client/dist/models/UpdateEnvironmentRequest.js +3 -0
- package/node_modules/@orchestraworks/client/dist/models/UpdateSecretRequest.d.ts +4 -3
- package/node_modules/@orchestraworks/client/dist/models/index.d.ts +1 -0
- package/node_modules/@orchestraworks/client/dist/models/index.js +1 -0
- package/node_modules/@orchestraworks/client/package.json +1 -1
- package/package.json +4 -4
- package/skills/orchestra/SKILL.md +132 -18
- package/skills/orchestra-fdk/references/authoring.md +511 -20
- package/skills/orchestra-fdk/references/environment.md +49 -12
- package/skills/orchestra-fdk/references/integrations/code-repo.md +26 -5
- package/skills/orchestra-fdk/references/phases/1-discover.md +1 -1
- package/skills/orchestra-fdk/references/phases/2-plan.md +1 -1
- package/skills/orchestra-fdk/references/troubleshooting.md +3 -2
- package/templates/bug-fix/main.ts +7 -4
- package/templates/engineering-team/lib/environment.ts +6 -3
- package/templates/engineering-team/package.json +1 -1
- package/templates/todo-factory/main.ts +7 -4
- package/templates/todo-factory/package.json +1 -1
|
@@ -68,8 +68,10 @@ const factory = new Factory({ name: "acme-support" });
|
|
|
68
68
|
new Secret(factory, "openai-api-key", {
|
|
69
69
|
name: "OpenAI API key",
|
|
70
70
|
value: SecretValue.fromEnv("OPENAI_API_KEY_VALUE"), // read from env at apply — never in the plan
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
injection: {
|
|
72
|
+
headers: [{ name: "Authorization", value: "Bearer ${secret}" }], // how the edge presents it
|
|
73
|
+
hosts: ["api.openai.com"], // where
|
|
74
|
+
},
|
|
73
75
|
});
|
|
74
76
|
|
|
75
77
|
const runtime = new Environment(factory, "runtime", {
|
|
@@ -554,26 +556,29 @@ token the platform mints. The **Claude sign-in recipe** is the one to copy when
|
|
|
554
556
|
on a person's Claude subscription token rather than an API key:
|
|
555
557
|
|
|
556
558
|
```ts
|
|
557
|
-
new Secret(factory, "claude-sign-in", {
|
|
559
|
+
const claudeSignIn = new Secret(factory, "claude-sign-in", {
|
|
558
560
|
value: SecretValue.fromEnv("ORCHESTRA_FDK_CLAUDE_CODE_OAUTH_TOKEN"),
|
|
559
|
-
hosts: ["api.anthropic.com"],
|
|
560
561
|
injection: {
|
|
561
562
|
headers: [{ name: "authorization", value: "Bearer ${secret}" }],
|
|
562
563
|
removeHeaders: ["x-api-key"], // stripped before injection
|
|
564
|
+
hosts: ["api.anthropic.com"],
|
|
563
565
|
},
|
|
564
566
|
});
|
|
565
567
|
|
|
566
568
|
// …in the Environment's props:
|
|
567
|
-
env: { CLAUDE_CODE_OAUTH_TOKEN:
|
|
569
|
+
env: { CLAUDE_CODE_OAUTH_TOKEN: claudeSignIn, ANTHROPIC_API_KEY: "" },
|
|
568
570
|
egress: EgressPolicy.allowAll()
|
|
569
571
|
.allow("claude-sign-in", { hosts: ["api.anthropic.com"], priority: 300 }),
|
|
570
572
|
```
|
|
571
573
|
|
|
572
574
|
It injects `authorization` and nothing else. **Never inject an `anthropic-beta` header**: an injected
|
|
573
|
-
one replaces the harness's own list of beta flags, and every call then answers 400.
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
575
|
+
one replaces the harness's own list of beta flags, and every call then answers 400. A Secret as an `env`
|
|
576
|
+
value is written as its reference, `orchestra:secret/<id>`, with the id resolved at apply. In
|
|
577
|
+
`CLAUDE_CODE_OAUTH_TOKEN` it makes the harness sign in the subscription way and send its own list, and the
|
|
578
|
+
edge reads the reference where the token goes and swaps the real token in. The edge adds a credential only
|
|
579
|
+
to a request that names one, or that an environment default covers (`references/environment.md`, "Naming a
|
|
580
|
+
credential"). The empty `ANTHROPIC_API_KEY` covers the runtime image's placeholder key, which the harness
|
|
581
|
+
would otherwise send instead.
|
|
577
582
|
|
|
578
583
|
- Sources: `SecretValue.fromEnv(NAME)`, `SecretValue.fromCommandLine(label?)` and
|
|
579
584
|
`SecretValue.fromGatewayKey(key)`, plus the derived recipes `basicAuth`, `registryCredential` and
|
|
@@ -613,7 +618,7 @@ which the harness would otherwise send instead.
|
|
|
613
618
|
`GatewayKey` you declare in the same factory — `new GatewayKey(factory, "model-gateway-key", props)`,
|
|
614
619
|
whose only props are `slug`, `displayName`, `monthlyCreditCap` and `labels` — so apply mints the
|
|
615
620
|
key and writes the token it shows *exactly once* straight into this Secret, which the edge then
|
|
616
|
-
adds on its `hosts` like any other. It is legal only as the whole top-level value of exactly one Secret:
|
|
621
|
+
adds on its recipe's `hosts` like any other. It is legal only as the whole top-level value of exactly one Secret:
|
|
617
622
|
nesting it inside another recipe, or a key with zero or two Secrets, is a synth error, and there
|
|
618
623
|
is no importer because an existing key's token is unrecoverable. Rotation is new-slug-only.
|
|
619
624
|
- A **`ModelProviderKey`** is the other credential you can declare: your organization's own key for
|
|
@@ -648,11 +653,12 @@ which the harness would otherwise send instead.
|
|
|
648
653
|
suppresses it and none can supply it either, so a headless apply of a `fromCommandLine`
|
|
649
654
|
declaration refuses at *every* level and tells you to run from a terminal or change the source —
|
|
650
655
|
it never recommends a policy lever that could not clear it.
|
|
651
|
-
- Presenting a secret on the network: the `Secret` itself declares `
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
that passes `credential` or `credentials` is a synth error
|
|
655
|
-
nowhere.
|
|
656
|
+
- Presenting a secret on the network: the `Secret` itself declares an `injection` recipe — how: the
|
|
657
|
+
headers it sets, reading `${secret}`, or `${secret.<field>}` for a `SecretValue.fields` value; and
|
|
658
|
+
where: its `hosts`, each an exact host, `*.suffix`, or `*`. Egress rules carry no credential, so a rule
|
|
659
|
+
that passes `credential` or `credentials` is a synth error. A Secret with no `injection` is stored
|
|
660
|
+
only, and one whose recipe has no `hosts` is presented nowhere. The top-level `hosts` prop is
|
|
661
|
+
deprecated: it is moved into the recipe for one release.
|
|
656
662
|
|
|
657
663
|
## How the agents reach a model
|
|
658
664
|
|
|
@@ -668,9 +674,9 @@ on their own computer; run from your shell, its browser step hangs. Never read o
|
|
|
668
674
|
At apply, the shell exports it from that file into the variable the Secret reads. Declare the Claude
|
|
669
675
|
sign-in recipe (§ Secrets are recipes, never values): a `Secret`, an egress rule that allows
|
|
670
676
|
`api.anthropic.com` and sets `Authorization: Bearer ${secret}` with `x-api-key` removed, and, in the
|
|
671
|
-
environment's `env`, `CLAUDE_CODE_OAUTH_TOKEN
|
|
672
|
-
|
|
673
|
-
|
|
677
|
+
environment's `env`, `CLAUDE_CODE_OAUTH_TOKEN` set to that Secret and an empty `ANTHROPIC_API_KEY`.
|
|
678
|
+
Turn `enableModelGateway` off for the agent. The machine holds only the Secret's reference; the edge
|
|
679
|
+
adds the token. Copying the renewing credentials file instead is the fallback,
|
|
674
680
|
and follows the ChatGPT sign-in steps below.
|
|
675
681
|
- **Codex: a personal access token, where the workspace allows one.** A ChatGPT Business or Enterprise
|
|
676
682
|
member creates it at chatgpt.com/admin/access-tokens, once the workspace owner allows access tokens,
|
|
@@ -762,7 +768,7 @@ An environment's `egress` is a policy you build up from one of **two starting po
|
|
|
762
768
|
- **`EgressPolicy.allowAll()`** allows every host no rule refuses.
|
|
763
769
|
|
|
764
770
|
Rules fold onto either one the same way: a rule only opens or closes the network, a credential still goes
|
|
765
|
-
only to the hosts its own `hosts` cover, and a deny still refuses.
|
|
771
|
+
only to the hosts its own recipe's `hosts` cover, and a deny still refuses.
|
|
766
772
|
|
|
767
773
|
Most of what an agent needs to reach arrives ready-made as a **bundle** — a list of rules folded onto
|
|
768
774
|
the policy by one `applyPreset(...)` call — so **reach for the bundles first** and hand-write allow/deny
|
|
@@ -955,6 +961,491 @@ new Trigger(factory, "bug-intake", {
|
|
|
955
961
|
each to what you declared. A revert made in the app shows in `orchestra fdk diff` as the rules apply
|
|
956
962
|
would write back.
|
|
957
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
|
+
|
|
958
1449
|
## The six design principles
|
|
959
1450
|
|
|
960
1451
|
These decide the design questions authoring throws up. When something feels like it needs new API
|
|
@@ -1079,7 +1570,7 @@ orchestra gateway-keys create --slug ci-smoke --output-file key.json
|
|
|
1079
1570
|
export, a CI secret) suppresses the only prompt you would have seen, the command says so in one
|
|
1080
1571
|
line on **stderr**, naming the lever — stdout stays the response document.
|
|
1081
1572
|
- **Writing to a factory-managed resource warns.** `apply` stamps what it manages with an
|
|
1082
|
-
`
|
|
1573
|
+
`orchestra.managedBy` labels key naming the owning factory, and a write to a stamped resource says so:
|
|
1083
1574
|
the next `apply` reverts it. It is a warning, not a refusal — the fallback has to stay usable in an
|
|
1084
1575
|
emergency — and it is the CLI telling you this belongs in `main.ts`.
|
|
1085
1576
|
|