@awebai/oats 0.29.3 → 0.30.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 +12 -6
- package/bin/oats.mjs +203 -54
- package/capabilities/oats-aweb/bin/oats-aweb.mjs +538 -204
- package/capabilities/oats-aweb/injects/aweb.md +1 -1
- package/capabilities/oats-aweb/lib/binding-wire.mjs +31 -22
- package/capabilities/oats-aweb/oats.json +5 -12
- package/capabilities/oats-aweb/skills/VENDORED.md +4 -4
- package/capabilities/oats-aweb/skills/aweb-team-membership/SKILL.md +1 -1
- package/capabilities/oats-aweb/skills/oats-aweb/SKILL.md +83 -13
- package/capabilities/oats-code-review/injects/reviewer.md +26 -0
- package/capabilities/oats-code-review/oats.json +16 -0
- package/capabilities/oats-code-review/skills/adversarial-review/SKILL.md +66 -0
- package/capabilities/oats-code-review/skills/review-dev-docs/SKILL.md +30 -0
- package/capabilities/oats-code-review/skills/security-review/SKILL.md +56 -0
- package/capabilities/oats-code-review/skills/simplification-review/SKILL.md +34 -0
- package/capabilities/oats-developer/injects/developer.md +38 -0
- package/capabilities/oats-developer/oats.json +17 -0
- package/capabilities/oats-developer/skills/execution-strategy/SKILL.md +43 -0
- package/capabilities/oats-developer/skills/maintain-dev-docs/SKILL.md +47 -0
- package/capabilities/oats-developer/skills/run-the-review-loop/SKILL.md +65 -0
- package/capabilities/oats-developer/skills/understand-the-spec/SKILL.md +37 -0
- package/capabilities/oats-developer/skills/worktrees/SKILL.md +36 -0
- package/capabilities/oats-engineering-expert/injects/expert.md +37 -0
- package/capabilities/oats-engineering-expert/oats.json +17 -0
- package/capabilities/oats-engineering-expert/skills/coordinate-developers/SKILL.md +37 -0
- package/capabilities/oats-engineering-expert/skills/coordinate-experts/SKILL.md +52 -0
- package/capabilities/oats-engineering-expert/skills/land-your-prs/SKILL.md +50 -0
- package/capabilities/oats-engineering-expert/skills/plan-and-spec/SKILL.md +53 -0
- package/capabilities/oats-engineering-expert/skills/verify-developer-work/SKILL.md +49 -0
- package/capabilities/oats-okf/bin/oats-okf.mjs +16 -9
- package/capabilities/oats-okf/lib/binding-wire.mjs +47 -15
- package/capabilities/oats-okf/lib/config.mjs +2 -1
- package/capabilities/oats-okf/lib/consult.mjs +26 -4
- package/capabilities/oats-okf/lib/harvest-switch.mjs +16 -3
- package/capabilities/oats-okf/lib/inspection.mjs +26 -7
- package/capabilities/oats-okf/lib/io.mjs +9 -1
- package/capabilities/oats-okf/lib/sources.mjs +34 -3
- package/capabilities/oats-okf/lib/stores.mjs +8 -6
- package/capabilities/oats-okf/lib/worker.mjs +7 -17
- package/capabilities/oats-okf/oats.json +6 -3
- package/capabilities/oats-okf-harvest/bin/okf-harvest.mjs +2 -2
- package/capabilities/oats-okf-harvest/oats.json +3 -3
- package/capabilities/oats-okf-harvest/skills/knowledge-harvest/SKILL.md +6 -6
- package/capabilities/oats-okf-maintenance/bin/okf-maintenance.mjs +37 -16
- package/capabilities/oats-okf-maintenance/injects/maintainer.md +1 -1
- package/capabilities/oats-okf-maintenance/lib/provenance.mjs +6 -1
- package/capabilities/oats-okf-maintenance/oats.json +2 -2
- package/capabilities/oats-okf-maintenance/skills/knowledge-review/SKILL.md +17 -2
- package/capabilities/oats-okf-maintenance/skills/okf-trigger-setup/SKILL.md +11 -23
- package/capabilities/oats-workspace-experts/injects/oats-experts.md +26 -0
- package/capabilities/oats-workspace-experts/oats.json +9 -0
- package/docs/capabilities.md +160 -171
- package/docs/capability-manifest.schema.json +7 -10
- package/docs/configuration.md +213 -64
- package/docs/design/2026-09-16-knowledge-capability-contract.md +36 -50
- package/docs/design/2026-09-23-workspace-module-contracts.md +377 -544
- package/docs/design/2026-09-26-okf-knowledge-operations.md +132 -357
- package/docs/design/2026-09-27-team-model-v2.md +116 -0
- package/docs/design/2026-09-28-automations-trust.md +38 -0
- package/docs/design/2026-09-28-soul-launch-preference.md +63 -0
- package/docs/design/HISTORY.md +65 -0
- package/docs/design/README.md +23 -54
- package/docs/desktop-cli-api.md +1787 -1777
- package/docs/desktop.md +30 -91
- package/docs/execution-targets.md +146 -292
- package/docs/first-team.md +31 -17
- package/docs/implementation.md +76 -288
- package/docs/integrations.md +118 -320
- package/docs/knowledge-capability-authoring.md +25 -52
- package/docs/knowledge-reference/acceptance.md +3 -3
- package/docs/knowledge-reference/adoption.md +1 -1
- package/docs/knowledge-reference/harvester.md +2 -2
- package/docs/knowledge-reference/package-craft.md +3 -3
- package/docs/knowledge-reference/provider-mapping.md +3 -6
- package/docs/knowledge-reference/reader-capture.md +3 -3
- package/docs/knowledge-theory.md +62 -166
- package/docs/knowledge.md +225 -404
- package/docs/layers.md +42 -97
- package/docs/oats-local.schema.json +58 -5
- package/docs/oats-membership.schema.json +1 -8
- package/docs/oats-package.schema.json +5 -5
- package/docs/oats-workspace.schema.json +8 -22
- package/docs/official-catalog.md +25 -28
- package/docs/packages.md +45 -63
- package/docs/plans/0.30-close-out.md +61 -0
- package/docs/release-lane.md +77 -0
- package/docs/release-notes/oats-framework-v1.1.3.md +10 -8
- package/docs/release-notes/v0.19.0.md +48 -147
- package/docs/release-notes/v0.19.1.md +2 -3
- package/docs/release-notes/v0.19.3.md +2 -15
- package/docs/release-notes/v0.20.0.md +0 -15
- package/docs/release-notes/v0.22.0.md +71 -138
- package/docs/release-notes/v0.22.1.md +42 -90
- package/docs/release-notes/v0.22.10.md +1 -1
- package/docs/release-notes/v0.22.11.md +1 -47
- package/docs/release-notes/v0.22.12.md +4 -13
- package/docs/release-notes/v0.22.13.md +1 -42
- package/docs/release-notes/v0.22.14.md +3 -11
- package/docs/release-notes/v0.22.15.md +1 -46
- package/docs/release-notes/v0.22.16.md +6 -8
- package/docs/release-notes/v0.22.18.md +1 -99
- package/docs/release-notes/v0.22.19.md +3 -14
- package/docs/release-notes/v0.22.2.md +6 -15
- package/docs/release-notes/v0.22.3.md +0 -1
- package/docs/release-notes/v0.22.4.md +1 -14
- package/docs/release-notes/v0.22.5.md +2 -12
- package/docs/release-notes/v0.22.6.md +0 -3
- package/docs/release-notes/v0.23.0.md +9 -25
- package/docs/release-notes/v0.23.1.md +9 -25
- package/docs/release-notes/v0.23.2.md +2 -4
- package/docs/release-notes/v0.24.0.md +56 -97
- package/docs/release-notes/v0.24.1.md +7 -11
- package/docs/release-notes/v0.24.10.md +34 -45
- package/docs/release-notes/v0.24.11.md +12 -20
- package/docs/release-notes/v0.24.12.md +35 -48
- package/docs/release-notes/v0.24.13.md +34 -41
- package/docs/release-notes/v0.24.2.md +9 -13
- package/docs/release-notes/v0.24.3.md +7 -11
- package/docs/release-notes/v0.24.4.md +6 -6
- package/docs/release-notes/v0.24.5.md +6 -10
- package/docs/release-notes/v0.24.6.md +2 -5
- package/docs/release-notes/v0.24.7.md +46 -75
- package/docs/release-notes/v0.24.8.md +58 -96
- package/docs/release-notes/v0.24.9.md +38 -54
- package/docs/release-notes/v0.25.0.md +59 -76
- package/docs/release-notes/v0.25.1.md +57 -81
- package/docs/release-notes/v0.25.2.md +51 -70
- package/docs/release-notes/v0.25.3.md +11 -13
- package/docs/release-notes/v0.25.4.md +9 -13
- package/docs/release-notes/v0.25.5.md +3 -5
- package/docs/release-notes/v0.25.6.md +20 -29
- package/docs/release-notes/v0.25.7.md +5 -7
- package/docs/release-notes/v0.25.8.md +26 -39
- package/docs/release-notes/v0.26.0.md +175 -646
- package/docs/release-notes/v0.27.0.md +4 -5
- package/docs/release-notes/v0.27.1.md +4 -6
- package/docs/release-notes/v0.27.2.md +1 -1
- package/docs/release-notes/v0.28.0.md +57 -124
- package/docs/release-notes/v0.29.0.md +89 -208
- package/docs/release-notes/v0.29.1.md +1 -1
- package/docs/release-notes/v0.29.2.md +3 -4
- package/docs/release-notes/v0.29.4.md +90 -0
- package/docs/release-notes/v0.30.0.md +205 -0
- package/docs/schedules.md +280 -349
- package/docs/servers.md +99 -117
- package/docs/soul.schema.json +2 -9
- package/docs/souls-and-instances.md +145 -158
- package/docs/workspaces.md +132 -215
- package/lib/automations.mjs +28 -6
- package/lib/core.mjs +226 -74
- package/lib/instance-events.mjs +1 -1
- package/lib/instance-inspect.mjs +109 -34
- package/lib/instance-lifecycle.mjs +14 -1
- package/lib/instance-resolution.mjs +26 -27
- package/lib/launch-preference.mjs +87 -0
- package/lib/materialize.mjs +3 -3
- package/lib/packages.mjs +2 -5
- package/lib/resolve.mjs +29 -87
- package/lib/schedule.mjs +32 -18
- package/lib/teams-verbs.mjs +195 -0
- package/lib/teams.mjs +190 -0
- package/lib/triggers.mjs +53 -17
- package/lib/workspace.mjs +54 -147
- package/package-catalog.json +9 -15
- package/package.json +1 -1
- package/skills/oats-getting-started/SKILL.md +25 -13
- package/capabilities/oats-review/injects/review.md +0 -69
- package/capabilities/oats-review/oats.json +0 -10
- package/capabilities/oats-review/skills/code-review/SKILL.md +0 -44
- package/capabilities/oats-review/skills/security-review/SKILL.md +0 -59
- package/docs/conventions.md +0 -90
- package/docs/design/2026-09-07-architecture-reassessment.md +0 -131
- package/docs/design/2026-09-07-desktop-souls-capabilities.md +0 -50
- package/docs/design/2026-09-07-mobile-agent-management-proposal.md +0 -228
- package/docs/design/2026-09-08-expert-assisted-deployment-proposal.md +0 -558
- package/docs/design/2026-09-13-knowledge-and-memory-direction.md +0 -744
- package/docs/design/2026-09-13-knowledge-implementation.md +0 -127
- package/docs/design/2026-09-13-knowledge-location-contract.md +0 -340
- package/docs/design/2026-09-14-artifact-retention-contract.md +0 -190
- package/docs/design/2026-09-14-portable-souls-and-git-workspaces.md +0 -708
- package/docs/design/2026-09-14-portable-souls-contract-amendments.md +0 -85
- package/docs/design/2026-09-14-portable-souls-explainer.md +0 -750
- package/docs/design/2026-09-15-captured-dispatch.md +0 -127
- package/docs/design/2026-09-15-captured-resolution-records.md +0 -143
- package/docs/design/2026-09-15-package-preparation.md +0 -100
- package/docs/design/2026-09-15-portable-data-contract.md +0 -121
- package/docs/design/2026-09-15-portable-declarations.md +0 -189
- package/docs/design/2026-09-15-portable-souls-handoff.md +0 -150
- package/docs/design/2026-09-15-portable-souls-implementation.md +0 -417
- package/docs/design/2026-09-15-selection-lock-and-approval.md +0 -122
- package/docs/design/2026-09-15-source-observation.md +0 -119
- package/docs/design/2026-09-16-captured-admission.md +0 -77
- package/docs/design/2026-09-16-captured-helper-dispatch.md +0 -105
- package/docs/design/2026-09-16-captured-launch-inputs.md +0 -42
- package/docs/design/2026-09-16-command-profile-preparation.md +0 -86
- package/docs/design/2026-09-16-fresh-install-first-rollout.md +0 -47
- package/docs/design/2026-09-16-fresh-operator-walkthrough.md +0 -282
- package/docs/design/2026-09-16-messaging-capability-contract.md +0 -59
- package/docs/design/2026-09-16-portable-migration-evidence.md +0 -158
- package/docs/design/2026-09-16-portable-onboarding.md +0 -179
- package/docs/design/2026-09-16-prepare-request-transport.md +0 -26
- package/docs/design/2026-09-16-provider-binding-codecs.md +0 -98
- package/docs/design/2026-09-16-provider-binding-wire.md +0 -274
- package/docs/design/2026-09-17-capability-helper-input-contract.md +0 -95
- package/docs/design/2026-09-17-captured-backend-parity.md +0 -53
- package/docs/design/2026-09-17-captured-native-start.md +0 -58
- package/docs/design/2026-09-17-portable-boundary-hookup.md +0 -19
- package/docs/design/2026-09-17-portable-boundary-resources.md +0 -52
- package/docs/design/2026-09-17-public-captured-start.md +0 -108
- package/docs/design/2026-09-17-public-prepare-request.md +0 -90
- package/docs/design/2026-09-18-captured-pi-host.md +0 -205
- package/docs/design/2026-09-18-first-cut-release-checklist.md +0 -131
- package/docs/design/2026-09-18-herdr-protocol-compatibility.md +0 -60
- package/docs/design/2026-09-20-redesign-program-board.md +0 -142
- package/docs/design/2026-09-20-workspace-and-portable-adoption-plan.md +0 -289
- package/docs/design/2026-09-20-workspace-onboarding-public.md +0 -207
- package/docs/design/2026-09-22-desktop-parity-seams.md +0 -58
- package/docs/design/2026-09-23-simplified-workspace-model.md +0 -711
- package/docs/design/2026-09-23-workspace-v2-implementation-plan.md +0 -65
- package/docs/design/2026-09-24-desktop-phase-f-boundary.md +0 -242
- package/docs/design/2026-09-24-phase-d-plan.md +0 -305
- package/docs/design/2026-09-25-teams-contract.md +0 -258
- package/docs/design/2026-09-26-desktop-design-brief-architecture.md +0 -241
- package/docs/design/desktop-ux-plan.md +0 -362
- package/docs/design/launch-configurations.md +0 -168
- package/docs/design/okf-mirror-provenance.md +0 -105
- package/docs/design/operations-contract.md +0 -141
- package/docs/oats-member.schema.json +0 -38
- package/skills/integration-authoring/SKILL.md +0 -84
- package/skills/oats-support/SKILL.md +0 -79
- package/skills/skill-craft/SKILL.md +0 -109
- package/skills/soul-craft/SKILL.md +0 -116
package/lib/teams.mjs
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Team model v2 (docs/design/2026-09-27-team-model-v2.md, option B), pure.
|
|
3
|
+
*
|
|
4
|
+
* Where teams live:
|
|
5
|
+
* - the committed oats-workspace.yaml `teams.<label> = { description?, team? }`: SHARED teams (edited
|
|
6
|
+
* by PR). A shared team without `team` is declared but not yet created (unmapped);
|
|
7
|
+
* - the deployment's oats-local.yaml `teams.<label> = { team, description? }`: LOCAL teams, plus
|
|
8
|
+
* `defaultTeam: <label>`, `souls.teams: { "*" | <soul key>: [labels] }` and
|
|
9
|
+
* `souls.default: { <soul key>: <label> }`.
|
|
10
|
+
* A soul key is the soul's bare name, or `<package>/<soul>` for a package soul.
|
|
11
|
+
*
|
|
12
|
+
* Resolution: labels = shared ∪ local (a label in both is `team-label-collision`; the SHARED definition
|
|
13
|
+
* wins). defaultOf(soul) = souls.default[soul] ?? defaultTeam; a soul's teams = {defaultOf} ∪
|
|
14
|
+
* souls.teams["*"] ∪ souls.teams[soul]. An undeclared label the soul reaches is E_TEAM_UNKNOWN; a
|
|
15
|
+
* souls.default outside the soul's teams is E_TEAM_NOT_ELIGIBLE.
|
|
16
|
+
*
|
|
17
|
+
* Rows (docs/desktop-cli-api.md, Team model v2): TeamRow { label, team, default, from: shared|local },
|
|
18
|
+
* the default first, then by label. Reports carry unmapped rows (team null); OATS_TEAMS and
|
|
19
|
+
* instance.json carry mapped rows only. DefaultTeam { label, team, from: deployment|soul } | null.
|
|
20
|
+
*/
|
|
21
|
+
import { oatsError } from "./errors.mjs";
|
|
22
|
+
|
|
23
|
+
const isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
24
|
+
const str = (v) => (typeof v === "string" && v ? v : null);
|
|
25
|
+
const labelsOf = (v) => (Array.isArray(v) ? v.filter((l) => typeof l === "string") : []);
|
|
26
|
+
const byCodepoint = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
27
|
+
export const pointerKey = (k) => String(k).replace(/~/g, "~0").replace(/\//g, "~1");
|
|
28
|
+
function fail(code, message, details) {
|
|
29
|
+
const e = oatsError(code, message, details);
|
|
30
|
+
e.details = details;
|
|
31
|
+
return e;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const LOCAL_FILE = "oats-local.yaml";
|
|
35
|
+
/** A provider team id, as both schemas' `teams.*.team` pattern: the kernel's SAFETY rule only (never
|
|
36
|
+
* `-`-led, so never an option; no whitespace or control characters; bounded). The messaging provider
|
|
37
|
+
* validates its own id shape. Keep equal to docs/oats-{local,workspace}.schema.json. */
|
|
38
|
+
export const TEAM_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:@/+-]{0,255}$/;
|
|
39
|
+
/** What 0.30 removed, named by the schema problems that replace them. */
|
|
40
|
+
export const TEAM_MEMBERSHIP_MOVED = "team membership is local since 0.30: `oats soul teams`";
|
|
41
|
+
export const BY_TEAM_REMOVED = "byTeam was removed in 0.30: a team's provider id is teams.<label>.team (oats-workspace.yaml for a shared team, oats-local.yaml for a local one)";
|
|
42
|
+
|
|
43
|
+
const UNMAPPED_FIX = "its owner runs `oats aweb setup`, then commits the id";
|
|
44
|
+
|
|
45
|
+
/** The deployment's teams from the committed workspace and oats-local.yaml (either may be null). */
|
|
46
|
+
export function teamModel(workspace, local, { workspaceKey = null } = {}) {
|
|
47
|
+
const shared = new Map(), localTeams = new Map();
|
|
48
|
+
for (const [label, def] of Object.entries(isObject(workspace?.teams) ? workspace.teams : {})) {
|
|
49
|
+
shared.set(label, { label, team: str(def?.team), description: str(def?.description), from: "shared",
|
|
50
|
+
at: `${workspaceKey ? `${workspaceKey}:` : ""}oats-workspace.yaml#/teams/${pointerKey(label)}` });
|
|
51
|
+
}
|
|
52
|
+
for (const [label, def] of Object.entries(isObject(local?.teams) ? local.teams : {})) {
|
|
53
|
+
localTeams.set(label, { label, team: str(def?.team), description: str(def?.description), from: "local", at: `${LOCAL_FILE}#/teams/${pointerKey(label)}` });
|
|
54
|
+
}
|
|
55
|
+
const souls = isObject(local?.souls) ? local.souls : {};
|
|
56
|
+
return {
|
|
57
|
+
shared, local: localTeams,
|
|
58
|
+
labels: new Map([...localTeams, ...shared]), // the committed definition wins a collision
|
|
59
|
+
defaultTeam: str(local?.defaultTeam),
|
|
60
|
+
souls: { teams: isObject(souls.teams) ? souls.teams : {}, default: isObject(souls.default) ? souls.default : {} },
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The key a soul has in souls.teams / souls.default. */
|
|
65
|
+
export function soulKeyOf(soulEntry) {
|
|
66
|
+
return typeof soulEntry?.package === "string" && typeof soulEntry.qualifiedName === "string" ? soulEntry.qualifiedName : soulEntry.name;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const unknown = (label, at) => fail("E_TEAM_UNKNOWN", `team ${JSON.stringify(label)} is not declared (${at}): declare it with \`oats teams add\`, or in oats-workspace.yaml teams: for a shared team`, { label, at });
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* One soul's teams here (key "*": the deployment default + souls.teams["*"]).
|
|
73
|
+
* → { key, defaultTeam: DefaultTeam | null, teams: [TeamRow + via] } | throws E_TEAM_UNKNOWN / E_TEAM_NOT_ELIGIBLE.
|
|
74
|
+
* `via` ⊂ ["default", "*", "soul"], in that order: why the soul has the team.
|
|
75
|
+
*/
|
|
76
|
+
export function soulTeams(model, key) {
|
|
77
|
+
const known = (label, at) => { if (!model.labels.has(label)) throw unknown(label, at); };
|
|
78
|
+
if (model.defaultTeam !== null) known(model.defaultTeam, `${LOCAL_FILE}#/defaultTeam`);
|
|
79
|
+
const star = labelsOf(model.souls.teams["*"]);
|
|
80
|
+
star.forEach((l, i) => known(l, `${LOCAL_FILE}#/souls/teams/*/${i}`));
|
|
81
|
+
const own = key === "*" ? [] : labelsOf(model.souls.teams[key]);
|
|
82
|
+
own.forEach((l, i) => known(l, `${LOCAL_FILE}#/souls/teams/${pointerKey(key)}/${i}`));
|
|
83
|
+
const override = key === "*" ? null : str(model.souls.default[key]);
|
|
84
|
+
if (override !== null) {
|
|
85
|
+
const at = `${LOCAL_FILE}#/souls/default/${pointerKey(key)}`;
|
|
86
|
+
known(override, at);
|
|
87
|
+
if (override !== model.defaultTeam && !star.includes(override) && !own.includes(override)) {
|
|
88
|
+
throw fail("E_TEAM_NOT_ELIGIBLE", `souls.default.${key} is ${JSON.stringify(override)}, which is not one of ${key}'s teams here — add it first (\`oats soul teams ${key} --add ${override}\`)`, { soul: key, label: override, at });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const defaultLabel = override ?? model.defaultTeam;
|
|
92
|
+
const via = new Map();
|
|
93
|
+
const add = (label, why) => { const v = via.get(label) ?? []; if (!v.includes(why)) v.push(why); via.set(label, v); };
|
|
94
|
+
if (defaultLabel !== null) add(defaultLabel, "default");
|
|
95
|
+
for (const l of star) add(l, "*");
|
|
96
|
+
for (const l of own) add(l, "soul");
|
|
97
|
+
const teams = [...via].map(([label, v]) => {
|
|
98
|
+
const d = model.labels.get(label);
|
|
99
|
+
return { label, team: d.team, default: label === defaultLabel, from: d.from, via: v };
|
|
100
|
+
}).sort((a, b) => (b.default - a.default) || byCodepoint(a.label, b.label));
|
|
101
|
+
const defaultTeam = defaultLabel === null ? null : { label: defaultLabel, team: model.labels.get(defaultLabel).team, from: override !== null ? "soul" : "deployment" };
|
|
102
|
+
return { key, defaultTeam, teams };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** TeamRows as the reports carry them (unmapped included, no `via`). */
|
|
106
|
+
export const reportRows = (teams) => (teams || []).map(({ label, team, default: d, from }) => ({ label, team, default: d, from }));
|
|
107
|
+
/** TeamRows as OATS_TEAMS and instance.json carry them: mapped only. */
|
|
108
|
+
export const envRows = (teams) => reportRows(teams).filter((r) => r.team !== null);
|
|
109
|
+
|
|
110
|
+
/** Every reference to `label` in oats-local.yaml, in written order (`oats teams remove` refuses on any). */
|
|
111
|
+
export function teamReferences(model, label) {
|
|
112
|
+
const refs = [];
|
|
113
|
+
if (model.defaultTeam === label) refs.push("defaultTeam");
|
|
114
|
+
for (const [key, list] of Object.entries(model.souls.teams)) if (labelsOf(list).includes(label)) refs.push(`souls.teams:${key}`);
|
|
115
|
+
for (const [key, l] of Object.entries(model.souls.default)) if (l === label) refs.push(`souls.default:${key}`);
|
|
116
|
+
return refs;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const collisionProblem = (label, shared, local) => ({
|
|
120
|
+
code: "team-label-collision", label, severity: "warning",
|
|
121
|
+
shared: { team: shared.team, description: shared.description, at: shared.at },
|
|
122
|
+
local: { team: local.team, description: local.description, at: local.at },
|
|
123
|
+
message: `team ${label} is declared in both oats-workspace.yaml (shared) and oats-local.yaml (local); the shared definition wins`,
|
|
124
|
+
fix: "rename the local label in oats-local.yaml",
|
|
125
|
+
});
|
|
126
|
+
const unmappedProblem = (d, isDefault) => isDefault
|
|
127
|
+
? { code: "team-unmapped", label: d.label, default: true, severity: "failure", at: d.at, message: `the default team ${d.label} has no provider id yet`, fix: `${UNMAPPED_FIX}; or choose another default with \`oats teams default\`` }
|
|
128
|
+
: { code: "team-unmapped", label: d.label, default: false, severity: "warning", at: d.at, message: `shared team ${d.label} has no provider id yet`, fix: UNMAPPED_FIX };
|
|
129
|
+
const unconfiguredProblem = () => ({ code: "E_TEAM_UNCONFIGURED", severity: "failure", message: "no teams configured: run `oats aweb setup`", fix: "run `oats aweb setup` (it creates the teams and sets the default), or `oats teams add <label> --team <id>`" });
|
|
130
|
+
const refusalProblem = (e) => ({ code: e.code, ...e.details, severity: "failure", message: e.message,
|
|
131
|
+
fix: e.code === "E_TEAM_UNKNOWN" ? "declare the team (`oats teams add`), or remove the reference" : "add the label to the soul's teams (`oats soul teams … --add`), or clear its default (`--clear-default`)" });
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Readiness problems. Without `key`: the deployment's (`oats teams`): collisions, unmapped shared
|
|
135
|
+
* teams (a failure when it is `defaultTeam`), every unknown reference, every ineligible
|
|
136
|
+
* souls.default. With `key`: only what concerns that soul (`default` marks ITS default).
|
|
137
|
+
* `messaging`: a messaging layer is active, so no default is E_TEAM_UNCONFIGURED.
|
|
138
|
+
*/
|
|
139
|
+
export function teamProblems(model, { key = null, messaging = false } = {}) {
|
|
140
|
+
const problems = [];
|
|
141
|
+
if (key !== null) {
|
|
142
|
+
let t;
|
|
143
|
+
try { t = soulTeams(model, key); }
|
|
144
|
+
catch (e) { if (e.code === "E_TEAM_UNKNOWN" || e.code === "E_TEAM_NOT_ELIGIBLE") return [refusalProblem(e)]; throw e; }
|
|
145
|
+
for (const r of t.teams) if (model.shared.has(r.label) && model.local.has(r.label)) problems.push(collisionProblem(r.label, model.shared.get(r.label), model.local.get(r.label)));
|
|
146
|
+
for (const r of t.teams) if (r.team === null) problems.push(unmappedProblem(model.labels.get(r.label), r.default));
|
|
147
|
+
if (messaging && t.defaultTeam === null) problems.push(unconfiguredProblem());
|
|
148
|
+
return problems;
|
|
149
|
+
}
|
|
150
|
+
const labels = [...model.labels.keys()].sort(byCodepoint);
|
|
151
|
+
for (const label of labels) if (model.shared.has(label) && model.local.has(label)) problems.push(collisionProblem(label, model.shared.get(label), model.local.get(label)));
|
|
152
|
+
for (const label of labels) { const d = model.labels.get(label); if (d.team === null) problems.push(unmappedProblem(d, label === model.defaultTeam)); }
|
|
153
|
+
const known = (label, at) => { if (!model.labels.has(label)) problems.push(refusalProblem(unknown(label, at))); };
|
|
154
|
+
if (model.defaultTeam !== null) known(model.defaultTeam, `${LOCAL_FILE}#/defaultTeam`);
|
|
155
|
+
for (const [k, list] of Object.entries(model.souls.teams)) labelsOf(list).forEach((l, i) => known(l, `${LOCAL_FILE}#/souls/teams/${pointerKey(k)}/${i}`));
|
|
156
|
+
for (const [k, l] of Object.entries(model.souls.default)) if (typeof l === "string") known(l, `${LOCAL_FILE}#/souls/default/${pointerKey(k)}`);
|
|
157
|
+
for (const [k, l] of Object.entries(model.souls.default)) {
|
|
158
|
+
if (typeof l !== "string" || !model.labels.has(l)) continue;
|
|
159
|
+
try { soulTeams(model, k); } catch (e) { if (e.code === "E_TEAM_NOT_ELIGIBLE" && e.details.soul === k) problems.push(refusalProblem(e)); else if (e.code !== "E_TEAM_UNKNOWN") throw e; }
|
|
160
|
+
}
|
|
161
|
+
if (messaging && model.defaultTeam === null) problems.push(unconfiguredProblem());
|
|
162
|
+
return problems;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The provider environment (docs/desktop-cli-api.md, The provider environment) from a soul's teams
|
|
167
|
+
* `{ defaultTeam, teams, source: "live"|"recorded" }`, or null when they are not known. A name that
|
|
168
|
+
* does not apply is `undefined`, so a child process never inherits an ambient value. The pre-0.30
|
|
169
|
+
* names are always unset.
|
|
170
|
+
*/
|
|
171
|
+
export function teamsEnv(t) {
|
|
172
|
+
const known = isObject(t) && Array.isArray(t.teams);
|
|
173
|
+
const d = known && isObject(t.defaultTeam) ? t.defaultTeam : null;
|
|
174
|
+
return {
|
|
175
|
+
OATS_DEFAULT_TEAM: d?.label ?? undefined,
|
|
176
|
+
OATS_DEFAULT_TEAM_ID: d?.team ?? undefined,
|
|
177
|
+
OATS_DEFAULT_TEAM_FROM: d?.from ?? undefined,
|
|
178
|
+
OATS_TEAMS: known ? JSON.stringify(envRows(t.teams)) : undefined,
|
|
179
|
+
OATS_TEAMS_SOURCE: known && (t.source === "live" || t.source === "recorded") ? t.source : undefined,
|
|
180
|
+
OATS_TEAM_LABEL: undefined, OATS_TEAM_LABELS: undefined, OATS_TEAM_ID: undefined,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** A home's spawn-time teams (instance.json `teams` / `defaultTeam`) → { teams, defaultTeam }; teams null
|
|
185
|
+
* when the home recorded none in this shape (spawned before 0.30: its teams are unknown, never guessed). */
|
|
186
|
+
export function recordedTeams(meta) {
|
|
187
|
+
const rows = Array.isArray(meta?.teams) && meta.teams.every((r) => isObject(r) && typeof r.label === "string" && typeof r.default === "boolean") ? meta.teams : null;
|
|
188
|
+
const d = isObject(meta?.defaultTeam) && typeof meta.defaultTeam.label === "string" ? meta.defaultTeam : null;
|
|
189
|
+
return { teams: rows, defaultTeam: rows ? d : null };
|
|
190
|
+
}
|
package/lib/triggers.mjs
CHANGED
|
@@ -28,8 +28,8 @@ import { spawnSync } from "node:child_process";
|
|
|
28
28
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
29
29
|
import { dirname, join } from "node:path";
|
|
30
30
|
import { fileURLToPath } from "node:url";
|
|
31
|
-
import { readDefinitions, writeDefinitions, stateDir, withScopeLock, childEnv, parseEnvelopeText, schedulerStatus } from "./schedule.mjs";
|
|
32
|
-
import { automationError, baseRow, localEntry, localId, qualifiedId, soulOriginOf } from "./automations.mjs";
|
|
31
|
+
import { readDefinitions, writeDefinitions, stateDir, withScopeLock, childEnv, parseEnvelopeText, schedulerStatus, readRegistry } from "./schedule.mjs";
|
|
32
|
+
import { MODEL_RE, automationError, baseRow, localEntry, localId, parseOwner, qualifiedId, soulOriginOf } from "./automations.mjs";
|
|
33
33
|
import { lockedPackageRef } from "./packages.mjs";
|
|
34
34
|
|
|
35
35
|
export const TRIGGER_API = 1;
|
|
@@ -121,7 +121,7 @@ export function validateTrigger(def) {
|
|
|
121
121
|
if (!Array.isArray(teams) || teams.some((t) => typeof t !== "string" || !LABEL_RE.test(t)) || new Set(teams).size !== teams.length) throw invalid("spawn.teams", "a list of distinct team labels");
|
|
122
122
|
if (sp.launchConfig !== undefined && (typeof sp.launchConfig !== "string" || !LAUNCH_CONFIG_RE.test(sp.launchConfig))) throw invalid("spawn.launchConfig", "the name of a launch configuration on the running host (oats-local.yaml launch-configs)");
|
|
123
123
|
if (sp.harness !== undefined && !HARNESSES.has(sp.harness)) throw invalid("spawn.harness", "pi, claude or codex");
|
|
124
|
-
if (sp.model !== undefined && (typeof sp.model !== "string" || !sp.model
|
|
124
|
+
if (sp.model !== undefined && (typeof sp.model !== "string" || !MODEL_RE.test(sp.model))) throw invalid("spawn.model", "a model id: a letter or digit, then letters, digits and . _ : / @ + - [ ] (at most 128 characters)");
|
|
125
125
|
if (sp.yolo !== undefined && typeof sp.yolo !== "boolean") throw invalid("spawn.yolo", "boolean");
|
|
126
126
|
if (sp.backend !== undefined && !BACKENDS.has(sp.backend)) throw invalid("spawn.backend", "tmux or herdr");
|
|
127
127
|
const cc = def.concurrency === undefined ? {} : def.concurrency;
|
|
@@ -207,7 +207,12 @@ export function triggerKind({ lock = null, remote = null } = {}) {
|
|
|
207
207
|
parseBody: parseTriggerBody,
|
|
208
208
|
expand: async (a) => {
|
|
209
209
|
const raw = a.source.from ? await packageTriggerTemplate(lock, a.source.from, a.source.set, { id: a.name, remote }) : { ...a.source.definition };
|
|
210
|
-
|
|
210
|
+
const def = validateTrigger({ ...raw, id: a.name, kind: "trigger", enabled: true });
|
|
211
|
+
// The owner check asks gh on the OWNER's host, and the poll runs on the REPO's host: they must be
|
|
212
|
+
// one GitHub host, or polling would act as whoever is logged in there (re-review B #7).
|
|
213
|
+
const owner = parseOwner(a.owner), repo = parseRepo(def.on.repo);
|
|
214
|
+
if (owner && repo && owner.host.toLowerCase() !== repo.host) throw Object.assign(triggerError("E_TRIGGER_INVALID", `owner ${a.owner} is on ${owner.host}, but on.repo ${repo.key} is on ${repo.host}: a trigger acts as its owner on the repository's own GitHub host`), { field: "owner" });
|
|
215
|
+
return def;
|
|
211
216
|
},
|
|
212
217
|
};
|
|
213
218
|
}
|
|
@@ -265,8 +270,28 @@ export function readTriggerState(ws) {
|
|
|
265
270
|
let doc;
|
|
266
271
|
try { doc = JSON.parse(readFileSync(file, "utf8")); } catch { doc = null; }
|
|
267
272
|
if (!isObject(doc) || !isObject(doc.triggers)) return { version: 1, triggers: {} };
|
|
273
|
+
return carryUnqualifiedState(doc);
|
|
274
|
+
}
|
|
275
|
+
/** 0.28 kept a local trigger's state under its bare id, with event keys `<id>:…`; 0.29 qualifies
|
|
276
|
+
* both as `local/<id>`. Carried on read (re-review B #2): without it the first 0.29 tick saw no
|
|
277
|
+
* PR state, so every open matching PR emitted `opened` and spawned again. A bare id already
|
|
278
|
+
* present in qualified form keeps the qualified state. */
|
|
279
|
+
function carryUnqualifiedState(doc) {
|
|
280
|
+
for (const id of Object.keys(doc.triggers)) {
|
|
281
|
+
if (id.includes("/")) continue;
|
|
282
|
+
const qid = localId(id), old = doc.triggers[id];
|
|
283
|
+
delete doc.triggers[id];
|
|
284
|
+
if (doc.triggers[qid] || !isObject(old)) continue;
|
|
285
|
+
const rekey = (map) => Object.fromEntries(Object.entries(isObject(map) ? map : {}).map(([k, v]) => {
|
|
286
|
+
const key = k.startsWith(`${id}:`) ? `${qid}${k.slice(id.length)}` : k;
|
|
287
|
+
return [key, isObject(v) && "key" in v ? { ...v, key, ...(v.trigger === id ? { trigger: qid } : {}) } : v];
|
|
288
|
+
}));
|
|
289
|
+
doc.triggers[qid] = { ...old, prs: isObject(old.prs) ? old.prs : {}, fired: rekey(old.fired), pending: rekey(old.pending) };
|
|
290
|
+
}
|
|
268
291
|
return doc;
|
|
269
292
|
}
|
|
293
|
+
/** Whether an instance's recorded trigger id is `id`: a 0.28 home records a local trigger's bare id. */
|
|
294
|
+
const sameTrigger = (recorded, id) => recorded === id || (typeof recorded === "string" && !recorded.includes("/") && id === localId(recorded));
|
|
270
295
|
function writeTriggerState(ws, st) {
|
|
271
296
|
const file = statePath(ws);
|
|
272
297
|
mkdirSync(dirname(file), { recursive: true });
|
|
@@ -290,7 +315,7 @@ export function liveTriggerInstances(ws, id) {
|
|
|
290
315
|
if (!e.isDirectory()) continue;
|
|
291
316
|
const home = join(instances, e.name);
|
|
292
317
|
let meta; try { meta = JSON.parse(readFileSync(join(home, "instance.json"), "utf8")); } catch { continue; }
|
|
293
|
-
if (isObject(meta?.trigger) && meta.trigger.id
|
|
318
|
+
if (isObject(meta?.trigger) && (id === null || sameTrigger(meta.trigger.id, id))) out.push({ instance: meta.instance ?? e.name, home, repo: meta.trigger.repo, number: meta.trigger.number, event: meta.trigger.event, key: meta.trigger.key ?? null });
|
|
294
319
|
}
|
|
295
320
|
}
|
|
296
321
|
return out;
|
|
@@ -359,6 +384,9 @@ export function foldPoll(def, ts, poll, now) {
|
|
|
359
384
|
for (const e of eventsFor(def, ts.prs[pr.number], pr)) {
|
|
360
385
|
const key = eventKey(def, pr, e);
|
|
361
386
|
if (ts.fired[key] || ts.pending[key]) continue;
|
|
387
|
+
// A newer push supersedes a pending `synchronize` for an older head of the same PR: only
|
|
388
|
+
// the newest head is reviewed (re-review B #6).
|
|
389
|
+
if (e.event === "synchronize") for (const [k, p] of Object.entries(ts.pending)) if (p.number === pr.number && p.event === "synchronize") delete ts.pending[k];
|
|
362
390
|
ts.pending[key] = { trigger: def.qid ?? def.id, source: def.on.source, repo: def.on.repo, number: pr.number, url: pr.url, event: e.event, headSha: pr.headSha, labels: pr.labels, observedAt: now.toISOString(), key };
|
|
363
391
|
added.push(key);
|
|
364
392
|
}
|
|
@@ -384,7 +412,7 @@ function runOats(ws, argv, io) {
|
|
|
384
412
|
/** What `oats spawn <soul> --preview` says about the soul: its qualified resolution and the
|
|
385
413
|
* capability filling its messaging slot. → { ok, result | error } */
|
|
386
414
|
export function previewSoul(ws, def, io) {
|
|
387
|
-
const r = runOats(ws, ["spawn", def.spawn.soul,
|
|
415
|
+
const r = runOats(ws, ["spawn", def.spawn.soul, `--dir=${ws}`, "--preview", "--json"], io);
|
|
388
416
|
if (!r.envelope || typeof r.envelope.ok !== "boolean") return { ok: false, error: { code: "E_SPAWN_FAILED", message: r.timedOut ? "spawn --preview timed out" : `spawn --preview answered no envelope${r.stderr ? `: ${r.stderr.trim().split("\n").pop()}` : ""}` } };
|
|
389
417
|
if (!r.envelope.ok) return { ok: false, error: { code: r.envelope.error?.code ?? "E_SPAWN_FAILED", message: r.envelope.error?.message ?? "spawn --preview failed" } };
|
|
390
418
|
const modules = Array.isArray(r.envelope.result?.modules) ? r.envelope.result.modules : [];
|
|
@@ -396,17 +424,18 @@ export function previewSoul(ws, def, io) {
|
|
|
396
424
|
export function spawnForEvent(ws, def, ev, io) {
|
|
397
425
|
const fields = { repo: ev.repo, number: ev.number, url: ev.url, event: ev.event, headSha: ev.headSha, trigger: def.id };
|
|
398
426
|
const purpose = renderTemplate(def.spawn.purpose, fields);
|
|
399
|
-
|
|
427
|
+
// Every value travels as ONE `--flag=value` token, so no value can be read as a flag of its own.
|
|
428
|
+
const argv = ["spawn", def.spawn.soul, `--dir=${ws}`, `--purpose=${purpose}`, "--json"];
|
|
400
429
|
if (def.spawn.teams.length) {
|
|
401
430
|
const pv = previewSoul(ws, def, io);
|
|
402
431
|
if (!pv.ok) throw triggerError(pv.error.code, pv.error.message);
|
|
403
432
|
if (!pv.messaging) throw triggerError("E_TRIGGER_TEAMS", `trigger ${def.id}: spawn.teams [${def.spawn.teams.join(", ")}] needs a messaging capability, and soul ${def.spawn.soul} resolves none`, { details: { soul: def.spawn.soul, teams: def.spawn.teams } });
|
|
404
433
|
argv.push("--provider", pv.messaging, `join=${def.spawn.teams.join(",")}`);
|
|
405
434
|
}
|
|
406
|
-
if (def.spawn.launchConfig) argv.push(
|
|
407
|
-
if (def.spawn.harness) argv.push(
|
|
408
|
-
if (def.spawn.model) argv.push(
|
|
409
|
-
if (def.spawn.backend) argv.push(
|
|
435
|
+
if (def.spawn.launchConfig) argv.push(`--launch-config=${def.spawn.launchConfig}`);
|
|
436
|
+
if (def.spawn.harness) argv.push(`--harness=${def.spawn.harness}`);
|
|
437
|
+
if (def.spawn.model) argv.push(`--model=${def.spawn.model}`);
|
|
438
|
+
if (def.spawn.backend) argv.push(`--backend=${def.spawn.backend}`);
|
|
410
439
|
if (def.spawn.yolo === true) argv.push("--yolo"); else if (def.spawn.yolo === false) argv.push("--no-yolo");
|
|
411
440
|
if (io?.noLaunch === true) argv.push("--no-launch");
|
|
412
441
|
// The task and the event travel as private files, never in argv.
|
|
@@ -416,7 +445,7 @@ export function spawnForEvent(ws, def, ev, io) {
|
|
|
416
445
|
const taskFile = `${stem}.md`, eventFile = `${stem}.event.json`;
|
|
417
446
|
writeFileSync(eventFile, JSON.stringify(ev, null, 2) + "\n", { mode: 0o600 });
|
|
418
447
|
writeFileSync(taskFile, renderTemplate(def.spawn.task, fields).trimEnd() + triggerBlock(def, ev, "<home>/.oats/trigger-event.json"), { mode: 0o600 });
|
|
419
|
-
argv.push(
|
|
448
|
+
argv.push(`--task-file=${taskFile}`, `--trigger-event=${eventFile}`);
|
|
420
449
|
let r;
|
|
421
450
|
try { r = runOats(ws, argv, io); }
|
|
422
451
|
finally { for (const f of [taskFile, eventFile]) { try { rmSync(f, { force: true }); } catch { /* best effort */ } } }
|
|
@@ -438,18 +467,24 @@ function heldBy(def, ev, live) {
|
|
|
438
467
|
* whose poll is not due is not polled; pending events (a spawn that failed, or was held by
|
|
439
468
|
* concurrency) are retried whenever the trigger polls. `dryRun` reads only.
|
|
440
469
|
* → considered rows { workspace, trigger, action, … } */
|
|
441
|
-
export function tickTriggers(ws, { now = new Date(), io, dryRun = false, ctx = null } = {}) {
|
|
470
|
+
export function tickTriggers(ws, { now = new Date(), io, dryRun = false, ctx = null, reg = null, wsList = null } = {}) {
|
|
442
471
|
let entries;
|
|
443
472
|
try { entries = triggerEntries(ws, ctx); } catch (e) { return [{ workspace: ws, action: "error", error: e.message }]; }
|
|
444
473
|
if (!entries.length) return [];
|
|
474
|
+
// The host cap (registry triggersMaxConcurrent; absent = unbounded) counts every trigger-spawned
|
|
475
|
+
// live home in the host's scopes, and nothing a schedule runs (re-review B #5).
|
|
476
|
+
let hostCap;
|
|
477
|
+
try { hostCap = (reg || readRegistry()).triggersMaxConcurrent; } catch (e) { return [{ workspace: ws, action: "error", error: e.message }]; }
|
|
478
|
+
const scopes = [...new Set([...(wsList || []), ws])];
|
|
479
|
+
let hostLive = hostCap ? scopes.reduce((n, w) => n + liveTriggerInstances(w, null).length, 0) : 0;
|
|
445
480
|
const st = readTriggerState(ws);
|
|
446
481
|
const considered = [];
|
|
447
482
|
const rec = (id, action, extra = {}) => considered.push({ workspace: ws, trigger: id, action, ...extra });
|
|
448
483
|
for (const e of entries) {
|
|
449
484
|
const id = e.id;
|
|
450
485
|
// A workspace trigger another host runs is not this tick's business; one NAMED for this host
|
|
451
|
-
// that it cannot run (another gh account) is reported every tick until someone fixes it.
|
|
452
|
-
if (e.placement.reason) { if (e.placement.reason
|
|
486
|
+
// that it cannot run (another gh account, or not trusted here) is reported every tick until someone fixes it.
|
|
487
|
+
if (e.placement.reason) { if (["owner-mismatch", "untrusted"].includes(e.placement.reason) && e.placement.enabledHere) rec(id, "not-here", { reason: e.placement.reason, detail: e.placement.detail }); continue; }
|
|
453
488
|
if (e.invalid) { if (e.placement.enabledHere) rec(id, "invalid", { error: e.invalid.message }); continue; }
|
|
454
489
|
if (!e.placement.runsHere) continue;
|
|
455
490
|
const def = runDef(e);
|
|
@@ -471,15 +506,16 @@ export function tickTriggers(ws, { now = new Date(), io, dryRun = false, ctx = n
|
|
|
471
506
|
const queue = Object.values(work.pending).sort((a, b) => a.observedAt.localeCompare(b.observedAt) || a.key.localeCompare(b.key));
|
|
472
507
|
if (!queue.length) rec(id, "polled", { prs: work.lastPoll.prs, matching: work.lastPoll.matching });
|
|
473
508
|
for (const ev of queue) {
|
|
474
|
-
const held = heldBy(def, ev, live);
|
|
509
|
+
const held = heldBy(def, ev, live) ?? (hostCap && hostLive >= hostCap ? `host triggersMaxConcurrent ${hostCap} reached (${hostLive} live)` : null);
|
|
475
510
|
if (held) { rec(id, "held", { key: ev.key, reason: held }); continue; }
|
|
476
|
-
if (dryRun) { rec(id, "would-fire", { key: ev.key, event: ev.event, number: ev.number, url: ev.url }); live.push({ repo: ev.repo, number: ev.number }); continue; }
|
|
511
|
+
if (dryRun) { rec(id, "would-fire", { key: ev.key, event: ev.event, number: ev.number, url: ev.url }); live.push({ repo: ev.repo, number: ev.number }); hostLive++; continue; }
|
|
477
512
|
try {
|
|
478
513
|
const run = spawnForEvent(ws, def, ev, io);
|
|
479
514
|
work.fired[ev.key] = { at: now.toISOString(), instance: run.instance ?? null, home: run.home ?? null, event: ev.event, number: ev.number };
|
|
480
515
|
delete work.pending[ev.key];
|
|
481
516
|
work.lastFiredAt = now.toISOString();
|
|
482
517
|
live.push({ instance: run.instance, home: run.home, repo: ev.repo, number: ev.number });
|
|
518
|
+
hostLive++;
|
|
483
519
|
rec(id, "fired", { key: ev.key, instance: run.instance ?? null, home: run.home ?? null });
|
|
484
520
|
} catch (e) {
|
|
485
521
|
work.lastError = { at: now.toISOString(), code: e.code || "E_SPAWN_FAILED", message: e.message, key: ev.key };
|