@awebai/oats 0.29.4 → 0.30.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -6
- package/bin/oats.mjs +194 -50
- package/docs/capabilities.md +160 -171
- package/docs/capability-manifest.schema.json +6 -11
- 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 +97 -117
- 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 +77 -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 +83 -0
- package/docs/release-lane.md +82 -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.30.0.md +205 -0
- package/docs/release-notes/v0.30.1.md +123 -0
- package/docs/schedules.md +280 -363
- 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 +137 -215
- package/lib/automations.mjs +21 -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 +1 -1
- package/lib/resolve.mjs +30 -88
- package/lib/schedule.mjs +1 -1
- package/lib/teams-verbs.mjs +195 -0
- package/lib/teams.mjs +190 -0
- package/lib/triggers.mjs +2 -2
- package/lib/workspace.mjs +54 -147
- package/package-catalog.json +10 -16
- package/package.json +1 -3
- package/skills/oats-getting-started/SKILL.md +25 -13
- package/capabilities/oats-authoring/LICENSE +0 -21
- package/capabilities/oats-authoring/oats-package.json +0 -11
- package/capabilities/oats-authoring/oats.json +0 -12
- package/capabilities/oats-authoring/skills/integration-authoring/SKILL.md +0 -84
- package/capabilities/oats-authoring/skills/skill-craft/SKILL.md +0 -109
- package/capabilities/oats-authoring/skills/soul-craft/SKILL.md +0 -116
- package/capabilities/oats-aweb/bin/oats-aweb-binding.mjs +0 -11
- package/capabilities/oats-aweb/bin/oats-aweb.mjs +0 -1338
- package/capabilities/oats-aweb/injects/aweb.md +0 -47
- package/capabilities/oats-aweb/lib/binding-wire.mjs +0 -356
- package/capabilities/oats-aweb/lib/captured-execution.mjs +0 -91
- package/capabilities/oats-aweb/lib/captured-native.mjs +0 -91
- package/capabilities/oats-aweb/lib/grant-custody.mjs +0 -38
- package/capabilities/oats-aweb/lib/invocation-shape.mjs +0 -135
- package/capabilities/oats-aweb/lib/portable-binding.mjs +0 -146
- package/capabilities/oats-aweb/lib/session-readiness.mjs +0 -56
- package/capabilities/oats-aweb/lib/wake-receive.mjs +0 -56
- package/capabilities/oats-aweb/oats.json +0 -208
- package/capabilities/oats-aweb/skills/LICENSE +0 -21
- package/capabilities/oats-aweb/skills/VENDORED.md +0 -31
- package/capabilities/oats-aweb/skills/aweb-identity/SKILL.md +0 -201
- package/capabilities/oats-aweb/skills/aweb-messaging/SKILL.md +0 -161
- package/capabilities/oats-aweb/skills/aweb-messaging/references/messaging-scenarios.md +0 -61
- package/capabilities/oats-aweb/skills/aweb-team-membership/SKILL.md +0 -116
- package/capabilities/oats-aweb/skills/aweb-team-membership/references/team-membership-reference.md +0 -74
- package/capabilities/oats-aweb/skills/oats-aweb/SKILL.md +0 -216
- package/capabilities/oats-jira/bin/oats-jira.mjs +0 -40
- package/capabilities/oats-jira/injects/jira.md +0 -10
- package/capabilities/oats-jira/oats.json +0 -22
- package/capabilities/oats-jira/skills/jira-tasks/SKILL.md +0 -179
- package/capabilities/oats-linear/bin/oats-linear-hook.mjs +0 -34
- package/capabilities/oats-linear/bin/oats-linear.mjs +0 -344
- package/capabilities/oats-linear/injects/linear.md +0 -8
- package/capabilities/oats-linear/oats.json +0 -24
- package/capabilities/oats-linear/skills/linear-tasks/SKILL.md +0 -223
- package/capabilities/oats-okf/bin/oats-okf-binding.mjs +0 -14
- package/capabilities/oats-okf/bin/oats-okf.mjs +0 -209
- package/capabilities/oats-okf/injects/okf.md +0 -42
- package/capabilities/oats-okf/lib/binding-wire.mjs +0 -348
- package/capabilities/oats-okf/lib/captured-worker.mjs +0 -109
- package/capabilities/oats-okf/lib/config.mjs +0 -124
- package/capabilities/oats-okf/lib/consult.mjs +0 -518
- package/capabilities/oats-okf/lib/harvest-status.mjs +0 -88
- package/capabilities/oats-okf/lib/harvest-switch.mjs +0 -94
- package/capabilities/oats-okf/lib/inspection.mjs +0 -119
- package/capabilities/oats-okf/lib/invocation-context.mjs +0 -111
- package/capabilities/oats-okf/lib/invocation-shape.mjs +0 -135
- package/capabilities/oats-okf/lib/io.mjs +0 -118
- package/capabilities/oats-okf/lib/migration.mjs +0 -137
- package/capabilities/oats-okf/lib/okf-validate.mjs +0 -123
- package/capabilities/oats-okf/lib/portable-binding.mjs +0 -199
- package/capabilities/oats-okf/lib/source-contract.mjs +0 -46
- package/capabilities/oats-okf/lib/sources.mjs +0 -424
- package/capabilities/oats-okf/lib/stores.mjs +0 -473
- package/capabilities/oats-okf/lib/worker.mjs +0 -497
- package/capabilities/oats-okf/oats.json +0 -148
- package/capabilities/oats-okf/schemas/okf-base.schema.json +0 -46
- package/capabilities/oats-okf/schemas/okf-bindings.schema.json +0 -112
- package/capabilities/oats-okf/schemas/okf-portable-declaration.schema.json +0 -87
- package/capabilities/oats-okf/schemas/okf-portable-payload.schema.json +0 -113
- package/capabilities/oats-okf/schemas/okf-soul.schema.json +0 -37
- package/capabilities/oats-okf/skills/okf-consultation/SKILL.md +0 -144
- package/capabilities/oats-okf/skills/okf-consultation/references/consult.md +0 -86
- package/capabilities/oats-okf/skills/okf-instance-knowledge/SKILL.md +0 -104
- package/capabilities/oats-okf-harvest/bin/okf-harvest.mjs +0 -140
- package/capabilities/oats-okf-harvest/injects/harvester.md +0 -12
- package/capabilities/oats-okf-harvest/oats.json +0 -26
- package/capabilities/oats-okf-harvest/skills/knowledge-harvest/SKILL.md +0 -168
- package/capabilities/oats-okf-harvest/skills/knowledge-theory/SKILL.md +0 -192
- package/capabilities/oats-okf-harvest/skills/okf-authoring/SKILL.md +0 -151
- package/capabilities/oats-okf-harvest/skills/okf-authoring/scripts/okf-validate.mjs +0 -123
- package/capabilities/oats-okf-maintenance/bin/okf-maintenance.mjs +0 -170
- package/capabilities/oats-okf-maintenance/injects/maintainer.md +0 -12
- package/capabilities/oats-okf-maintenance/lib/provenance.mjs +0 -50
- package/capabilities/oats-okf-maintenance/oats.json +0 -21
- package/capabilities/oats-okf-maintenance/skills/knowledge-review/SKILL.md +0 -159
- package/capabilities/oats-okf-maintenance/skills/knowledge-theory/SKILL.md +0 -192
- package/capabilities/oats-okf-maintenance/skills/okf-authoring/SKILL.md +0 -151
- package/capabilities/oats-okf-maintenance/skills/okf-authoring/scripts/okf-validate.mjs +0 -123
- package/capabilities/oats-okf-maintenance/skills/okf-trigger-setup/SKILL.md +0 -146
- 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/resolve.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* lib/resolve.mjs — from a soul to an immutable resolution (module contract §3).
|
|
3
3
|
*
|
|
4
4
|
* Contract: docs/design/2026-09-23-workspace-module-contracts.md §3.
|
|
5
|
-
* Decision:
|
|
5
|
+
* Decision: https://github.com/awebai/oats-knowledge/blob/main/knowledge/nodes/oats-expert/decisions/workspace-model-v2.md (6, 12, 14, 16, 19–21).
|
|
6
6
|
*
|
|
7
7
|
* `resolveSoul(discovery, soulEntry, options)` turns a discovered soul into the
|
|
8
8
|
* exact set of modules an instance will be built from: which capability comes
|
|
@@ -25,13 +25,12 @@
|
|
|
25
25
|
*
|
|
26
26
|
* Composition order (soul wins; `off` removes):
|
|
27
27
|
* workspace.defaults.{knowledge,messaging,tasks} (slot defaults; a soul `none` drops them)
|
|
28
|
-
* ⊕ workspace.defaults.capabilities ⊕
|
|
29
|
-
* soul's
|
|
30
|
-
* entries → E_TEAM_CONFLICT naming both (teams contract 2026-09-25, decision 2).
|
|
28
|
+
* ⊕ workspace.defaults.capabilities ⊕ soul.capabilities. (Team labels compose nothing: 0.30 removed
|
|
29
|
+
* defaults.byTeam, so a soul's composition is the same for every person and machine.)
|
|
31
30
|
*
|
|
32
31
|
* Slot `none` (contract §3, post-0.25.0 rule): a soul's `<slot>: none` EMPTIES the slot — it drops the
|
|
33
32
|
* workspace's `defaults.<slot>` AND any capability of that layer the workspace defaults contributed
|
|
34
|
-
* (`defaults.capabilities
|
|
33
|
+
* (`defaults.capabilities`). A layer-bearing capability the SOUL ITSELF declares
|
|
35
34
|
* next to `none` is contradictory and stays E_SLOT_CONFLICT { reason: "none" } (spell `<cap>: off` to
|
|
36
35
|
* remove a default explicitly; drop the soul's own line to fill the slot).
|
|
37
36
|
*
|
|
@@ -45,7 +44,7 @@
|
|
|
45
44
|
* everything, while a preview can say WHAT changed since the previous instance (declarations | payload | both).
|
|
46
45
|
*
|
|
47
46
|
* Payloads (decision 14), later wins on scalars/arrays, objects deep-merge:
|
|
48
|
-
* workspace.messaging (messaging slot only
|
|
47
|
+
* workspace.messaging (messaging slot only)
|
|
49
48
|
* ⊕ soul.<slot> ⊕ local.settings[cap] ⊕ spawn.providers[cap]
|
|
50
49
|
*
|
|
51
50
|
* This module shells out to nothing. Remote access is injected (`remote`, default
|
|
@@ -58,6 +57,7 @@ import { readFileSync } from "node:fs";
|
|
|
58
57
|
import { posix } from "node:path";
|
|
59
58
|
import { oatsError as baseOatsError } from "./errors.mjs";
|
|
60
59
|
import * as defaultRemote from "./remote.mjs";
|
|
60
|
+
import { BY_TEAM_REMOVED, reportRows, soulKeyOf, soulTeams, teamModel } from "./teams.mjs";
|
|
61
61
|
import { bindRemote, packageProviding, readPackageManifests, validateLock } from "./packages.mjs";
|
|
62
62
|
import { settingValueProblems } from "./capability-contract.mjs";
|
|
63
63
|
|
|
@@ -150,14 +150,13 @@ export function payloadOrigins(layers) {
|
|
|
150
150
|
return origins;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
/** `byTeam`
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
|
|
157
|
-
const RESERVED_KEY = "byTeam";
|
|
153
|
+
/** `byTeam` was removed in 0.30 (team model v2): in any payload layer — the workspace's messaging, a soul's
|
|
154
|
+
* slot payload, local.settings[cap], spawn.providers[cap] — it would reach the provider verbatim as a dead
|
|
155
|
+
* key; refused at the layer's top level with E_WORKSPACE_SCHEMA reason "removed-key" and the path named. */
|
|
156
|
+
const REMOVED_KEY = "byTeam";
|
|
158
157
|
function assertNoReservedKey(value, path) {
|
|
159
|
-
if (isObject(value) && Object.hasOwn(value,
|
|
160
|
-
throw fail("E_WORKSPACE_SCHEMA", `${path}/${
|
|
158
|
+
if (isObject(value) && Object.hasOwn(value, REMOVED_KEY)) {
|
|
159
|
+
throw fail("E_WORKSPACE_SCHEMA", `${path}/${REMOVED_KEY}: ${BY_TEAM_REMOVED}`, { path: `${path}/${REMOVED_KEY}`, key: REMOVED_KEY, reason: "removed-key" });
|
|
161
160
|
}
|
|
162
161
|
}
|
|
163
162
|
|
|
@@ -333,46 +332,16 @@ const choiceOf = (value, path, via) => {
|
|
|
333
332
|
throw fail("E_WORKSPACE_SCHEMA", `${path} must be { from: <location> } or "off", got ${show(value)}`, { path, value });
|
|
334
333
|
};
|
|
335
334
|
|
|
336
|
-
/** A soul's team labels, primary first: `team` is a label or a list of distinct labels (teams
|
|
337
|
-
* contract 2026-09-25, decision 1). A discovery SoulEntry carries them as `labels`; an entry
|
|
338
|
-
* without them (a hand-built one) has its `team` as the only label. */
|
|
339
|
-
export function teamLabelsOf(soulEntry) {
|
|
340
|
-
if (Array.isArray(soulEntry?.labels)) return soulEntry.labels.filter((l) => typeof l === "string");
|
|
341
|
-
return typeof soulEntry?.team === "string" ? [soulEntry.team] : [];
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* The eligible teams of a soul (teams contract, decision 3): one entry per label, in soul order,
|
|
346
|
-
* `{ label, team, mapped, payload }`. `payload` is workspace.messaging's base ⊕ byTeam[label] when the
|
|
347
|
-
* workspace maps the label, else the base alone with `mapped: false`; `team` is the payload's team id
|
|
348
|
-
* when mapped (null otherwise). Kernel-owned and delivered BESIDE a provider's settings, never inside
|
|
349
|
-
* them; joining any of them is the messaging provider's explicit act. No label → [] (the workspace's default team only).
|
|
350
|
-
* Pure: resolveSoul validates every carried label's entry (reserved byTeam, hostOnly keys) first.
|
|
351
|
-
*/
|
|
352
|
-
export function teamsOf(workspace, labels) {
|
|
353
|
-
const messaging = isObject(workspace?.messaging) ? workspace.messaging : {};
|
|
354
|
-
const { byTeam, ...base } = messaging;
|
|
355
|
-
return (labels || []).map((label) => {
|
|
356
|
-
const mapped = isObject(byTeam) && Object.hasOwn(byTeam, label) && isObject(byTeam[label]);
|
|
357
|
-
const payload = mapped ? mergePayload(base, byTeam[label]) : mergePayload(base);
|
|
358
|
-
return { label, team: mapped && typeof payload.team === "string" ? payload.team : null, mapped, payload };
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
|
|
362
335
|
/**
|
|
363
336
|
* The ordered capability map of a soul BEFORE any lookup:
|
|
364
|
-
* slot defaults (dropped where the soul says `none`) ⊕ defaults.capabilities ⊕
|
|
365
|
-
* each label in order ⊕ soul.capabilities
|
|
337
|
+
* slot defaults (dropped where the soul says `none`) ⊕ defaults.capabilities ⊕ soul.capabilities
|
|
366
338
|
* → [{ name, from, via }] sorted by name; `off` removes the entry from every lower layer.
|
|
367
|
-
* `via` is one of "defaults.<slot>" | "defaults.capabilities" | "
|
|
368
|
-
* Two labels that give one capability different entries (`off` vs a location, or two locations) →
|
|
369
|
-
* E_TEAM_CONFLICT { capability, labels: [a, b] }; identical entries are not a conflict, and a capability
|
|
370
|
-
* the soul names itself is not one either (the soul's entry wins over both).
|
|
339
|
+
* `via` is one of "defaults.<slot>" | "defaults.capabilities" | "soul".
|
|
371
340
|
* `offs` (optional, feature desktop-facts): receives what the soul turned off — { name, reason: "off",
|
|
372
341
|
* overrides } for each capability its own `off` removed from a lower layer, and { name, reason: "slot-none",
|
|
373
342
|
* slot, overrides: "workspace" } for a slot default its `<slot>: none` dropped (`overrides`: fromOfVia vocabulary).
|
|
374
343
|
*/
|
|
375
|
-
export function composeCapabilities(workspace, soulDefinition, {
|
|
344
|
+
export function composeCapabilities(workspace, soulDefinition, { offs = null } = {}) {
|
|
376
345
|
const map = new Map();
|
|
377
346
|
const apply = (entries, via, path) => {
|
|
378
347
|
for (const [name, value] of Object.entries(entries || {})) {
|
|
@@ -392,22 +361,6 @@ export function composeCapabilities(workspace, soulDefinition, { team = null, la
|
|
|
392
361
|
apply(d, `defaults.${slot}`, `/defaults/${slot}`);
|
|
393
362
|
}
|
|
394
363
|
apply(defaults.capabilities, "defaults.capabilities", "/defaults/capabilities");
|
|
395
|
-
const byLabel = new Map(); // capability -> { label, choice } of the first label that set it
|
|
396
|
-
// The soul's own entry wins over every label, so a capability the soul names settles the conflict.
|
|
397
|
-
const soulNames = new Set(Object.keys(isObject(soulDefinition?.capabilities) ? soulDefinition.capabilities : {}));
|
|
398
|
-
for (const label of labels) {
|
|
399
|
-
if (!isObject(defaults.byTeam) || !Object.hasOwn(defaults.byTeam, label) || !isObject(defaults.byTeam[label])) continue;
|
|
400
|
-
const entries = defaults.byTeam[label].capabilities, path = `/defaults/byTeam/${label}/capabilities`;
|
|
401
|
-
for (const [name, value] of Object.entries(entries || {})) {
|
|
402
|
-
const choice = choiceOf(value, `${path}/${name}`, `defaults.byTeam.${label}`);
|
|
403
|
-
const seen = byLabel.get(name);
|
|
404
|
-
if (seen && !soulNames.has(name) && canonicalJson(seen.choice) !== canonicalJson(choice)) {
|
|
405
|
-
throw fail("E_TEAM_CONFLICT", `${name}: team labels ${show(seen.label)} and ${show(label)} give it different entries (${canonicalJson(seen.choice)} vs ${canonicalJson(choice)}) in defaults.byTeam — make them agree, or name ${name} in the soul`, { capability: name, labels: [seen.label, label], entries: [seen.choice, choice], paths: [`/defaults/byTeam/${seen.label}/capabilities/${name}`, `${path}/${name}`] });
|
|
406
|
-
}
|
|
407
|
-
if (!seen) byLabel.set(name, { label, choice });
|
|
408
|
-
}
|
|
409
|
-
apply(entries, `defaults.byTeam.${label}`, path);
|
|
410
|
-
}
|
|
411
364
|
apply(soulDefinition?.capabilities, "soul", "/capabilities");
|
|
412
365
|
return [...map.values()].sort((a, b) => byCodepoint(a.name, b.name));
|
|
413
366
|
}
|
|
@@ -599,17 +552,18 @@ export async function resolveSoul(discovery, soulEntry, { local = null, lock = n
|
|
|
599
552
|
const workspace = isObject(discovery?.workspace) ? discovery.workspace : null;
|
|
600
553
|
assertSoulDiscovered(discovery, soulEntry);
|
|
601
554
|
const definition = isObject(soulEntry.definition) ? soulEntry.definition : {};
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
|
|
555
|
+
const soul = { name: soulEntry.name, repoKey: soulEntry.repoKey, commit: soulEntry.commit ?? null, path: soulEntry.path ?? null };
|
|
556
|
+
// Team model v2: the soul's teams HERE (the committed shared teams + oats-local.yaml). An unknown
|
|
557
|
+
// label (E_TEAM_UNKNOWN) or an ineligible souls.default (E_TEAM_NOT_ELIGIBLE) refuses the soul.
|
|
558
|
+
const soulTeamsHere = soulTeams(teamModel(discovery?.standalone === true ? null : workspace, local, { workspaceKey: discovery?.key ?? null }), soulKeyOf(soulEntry));
|
|
605
559
|
|
|
606
560
|
// Standalone: the soul's own repo only, workspace defaults unknown (decision 10).
|
|
607
561
|
// What the soul turned off (feature desktop-facts): its own `off` over a lower layer, and a `<slot>: none`
|
|
608
562
|
// that dropped a workspace default below. Provenance, like slotsFrom.
|
|
609
563
|
const offs = [];
|
|
610
564
|
const declared = discovery?.standalone === true
|
|
611
|
-
? composeCapabilities(null, { ...definition, capabilities: soulEntry.capabilities ?? definition.capabilities }, {
|
|
612
|
-
: composeCapabilities(workspace, definition, {
|
|
565
|
+
? composeCapabilities(null, { ...definition, capabilities: soulEntry.capabilities ?? definition.capabilities }, { offs })
|
|
566
|
+
: composeCapabilities(workspace, definition, { offs });
|
|
613
567
|
const turnedOff = offs;
|
|
614
568
|
|
|
615
569
|
const modules = [];
|
|
@@ -727,21 +681,11 @@ export async function resolveSoul(discovery, soulEntry, { local = null, lock = n
|
|
|
727
681
|
// provider receives one merged payload without provenance and cannot enforce this itself.
|
|
728
682
|
const hostOnly = hostOnlyKeys(m.manifest);
|
|
729
683
|
const committed = (payload, path, origin) => { assertNoHostOnlyKey(payload, path, hostOnly, m.name); add(payload, origin); };
|
|
684
|
+
// The provider's settings are base ⊕ soul ⊕ host ⊕ spawn. Teams are not settings: they reach the
|
|
685
|
+
// provider beside them (OATS_DEFAULT_TEAM*, OATS_TEAMS).
|
|
730
686
|
if (m.layer === "messaging" && isObject(workspace?.messaging)) {
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
// label's base ⊕ byTeam[label] is delivered only in its `teams` entry (teamsOf → OATS_TEAMS),
|
|
734
|
-
// so settings.team is the default team a host, soul or spawn set. `byTeam` never reaches the
|
|
735
|
-
// provider, and the primary team's own payload may still not nest one.
|
|
736
|
-
const { byTeam, ...base } = workspace.messaging;
|
|
737
|
-
committed(base, "/messaging", { kind: "workspace", at: "oats-workspace.yaml#/messaging" });
|
|
738
|
-
// Every label's entry still reaches the provider, in OATS_TEAMS (teamsOf), so each one the soul
|
|
739
|
-
// carries — the primary's and every other — may nest no byTeam and carry no hostOnly key.
|
|
740
|
-
for (const label of labels) {
|
|
741
|
-
if (!isObject(byTeam) || !Object.hasOwn(byTeam, label) || !isObject(byTeam[label])) continue;
|
|
742
|
-
assertNoReservedKey(byTeam[label], `/messaging/byTeam/${label}`);
|
|
743
|
-
assertNoHostOnlyKey(byTeam[label], `/messaging/byTeam/${label}`, hostOnly, m.name);
|
|
744
|
-
}
|
|
687
|
+
assertNoReservedKey(workspace.messaging, "/messaging");
|
|
688
|
+
committed(workspace.messaging, "/messaging", { kind: "workspace", at: "oats-workspace.yaml#/messaging" });
|
|
745
689
|
}
|
|
746
690
|
if (m.layer && isObject(definition[m.layer])) { assertNoReservedKey(definition[m.layer], `/${m.layer}`); committed(definition[m.layer], `/${m.layer}`, { kind: "soul", at: `soul.yaml#/${m.layer}` }); }
|
|
747
691
|
if (Object.hasOwn(settings, m.name) && isObject(settings[m.name])) { assertNoReservedKey(settings[m.name], `/settings/${m.name}`); add(settings[m.name], { kind: "host", at: `oats-local.yaml#/settings/${m.name}` }); } // the host layer: hostOnly keys are legal here
|
|
@@ -805,23 +749,21 @@ export async function resolveSoul(discovery, soulEntry, { local = null, lock = n
|
|
|
805
749
|
const payloadRevision = revisionOf(payloads);
|
|
806
750
|
const revision = revisionOf({ declRevision, payloadRevision });
|
|
807
751
|
// payloadOrigins is provenance only: it is derived from the same inputs the two revisions already
|
|
808
|
-
// bind, so it does not enter either fingerprint. `teams` (
|
|
809
|
-
//
|
|
810
|
-
// single-label soul's revision is what it was.
|
|
752
|
+
// bind, so it does not enter either fingerprint. `teams` / `defaultTeam` (team model v2) are
|
|
753
|
+
// deployment-local messaging state, not composition: they stay out of both fingerprints too.
|
|
811
754
|
// `slotsFrom` (where each filled slot's capability came from) is provenance, like payloadOrigins: outside
|
|
812
755
|
// both fingerprints, so the same capability reached another way is not a composition change. So are
|
|
813
756
|
// `capabilitiesFrom` (the same per composed capability) and `turnedOff` (feature desktop-facts).
|
|
814
|
-
const teams =
|
|
757
|
+
const teams = reportRows(soulTeamsHere.teams), defaultTeam = soulTeamsHere.defaultTeam;
|
|
815
758
|
const capabilitiesFrom = Object.fromEntries(modules.map((m) => [m.name, fromOfVia(viaOf.get(m.name))]));
|
|
816
759
|
turnedOff.sort((a, b) => byCodepoint(a.name, b.name));
|
|
817
|
-
return deepFreeze({ ...decl, payloads, payloadOrigins: origins, teams, slotsFrom, capabilitiesFrom, turnedOff, declRevision, payloadRevision, revision });
|
|
760
|
+
return deepFreeze({ ...decl, payloads, payloadOrigins: origins, teams, defaultTeam, slotsFrom, capabilitiesFrom, turnedOff, declRevision, payloadRevision, revision });
|
|
818
761
|
}
|
|
819
762
|
|
|
820
763
|
/** Where a composed capability came from, as the Desktop names it (`layers.<layer>.from`): the soul's own
|
|
821
|
-
* entry → "soul"; defaults.<slot> or defaults.capabilities → "workspace"
|
|
764
|
+
* entry → "soul"; defaults.<slot> or defaults.capabilities → "workspace". */
|
|
822
765
|
export function fromOfVia(via) {
|
|
823
766
|
if (via === "soul") return "soul";
|
|
824
|
-
if (typeof via === "string" && via.startsWith("defaults.byTeam.")) return `team:${via.slice("defaults.byTeam.".length)}`;
|
|
825
767
|
if (typeof via === "string" && via.startsWith("defaults.")) return "workspace";
|
|
826
768
|
return null;
|
|
827
769
|
}
|
package/lib/schedule.mjs
CHANGED
|
@@ -507,7 +507,7 @@ export function childEnv() {
|
|
|
507
507
|
"OATS_DEPLOYMENT", "OATS_RESOLUTION", "OATS_CAPABILITY", "OATS_LAYER", "OATS_LEVEL", "OATS_META", "OATS_OPERATION",
|
|
508
508
|
"OATS_REPO", "OATS_BRANCH", "OATS_WORK", "OATS_KIND", "OATS_TASK",
|
|
509
509
|
"OATS_HARNESS", "OATS_PREVIOUS_HARNESS", "OATS_RUNTIME", "OATS_PREVIOUS_RUNTIME", "OATS_RETIRE_INTENT",
|
|
510
|
-
"OATS_TEAM_NAME", "OATS_TEAM_ID", "
|
|
510
|
+
"OATS_TEAM_NAME", "OATS_TEAM_SCOPE", "OATS_TEAM_ID", "OATS_TEAM_LABEL", "OATS_TEAM_LABELS", "OATS_DEFAULT_TEAM", "OATS_DEFAULT_TEAM_ID", "OATS_DEFAULT_TEAM_FROM", "OATS_TEAMS", "OATS_TEAMS_SOURCE", "OATS_TRIGGER_EVENT_FILE",
|
|
511
511
|
]) delete env[key];
|
|
512
512
|
return env;
|
|
513
513
|
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The team model v2 verbs (docs/desktop-cli-api.md, `oats teams`, `oats soul teams`): CONFIG ONLY — they
|
|
3
|
+
* never call a provider. Each reads the deployment's oats-local.yaml and the committed shared teams, and a
|
|
4
|
+
* mutation rewrites oats-local.yaml in place (comments and every other key kept), after validating the
|
|
5
|
+
* result: the local schema, and no label reference the write would leave unknown or ineligible.
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import YAML from "yaml";
|
|
9
|
+
import { parseConfigData } from "./config-data.mjs";
|
|
10
|
+
import { oatsError } from "./errors.mjs";
|
|
11
|
+
import { validateLocal } from "./workspace.mjs";
|
|
12
|
+
import { LOCAL_FILE, TEAM_ID_RE, pointerKey, soulTeams, teamModel, teamProblems, teamReferences } from "./teams.mjs";
|
|
13
|
+
import { writeFileAtomic } from "./packages.mjs";
|
|
14
|
+
|
|
15
|
+
export const TEAMS_API = 1, SOUL_TEAMS_API = 1;
|
|
16
|
+
const LABEL_RE = /^[a-z0-9][a-z0-9._-]*$/;
|
|
17
|
+
const isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
18
|
+
const byCodepoint = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
19
|
+
function fail(code, message, details) { const e = oatsError(code, message, details); e.details = details; return e; }
|
|
20
|
+
|
|
21
|
+
/** Whether a messaging layer is active in this deployment: the workspace fills the messaging slot. */
|
|
22
|
+
const messagingActive = (workspace) => isObject(workspace?.defaults?.messaging);
|
|
23
|
+
|
|
24
|
+
/** `oats teams --json`. `ctx`: { deployment, local, workspace (the committed file, or null), workspaceKey }. */
|
|
25
|
+
export function teamsDocument(ctx) {
|
|
26
|
+
const model = teamModel(ctx.workspace, ctx.local, { workspaceKey: ctx.workspaceKey });
|
|
27
|
+
const teams = [...model.labels.values()].sort((a, b) => byCodepoint(a.label, b.label))
|
|
28
|
+
.map((d) => ({ label: d.label, team: d.team, description: d.description, from: d.from, default: d.label === model.defaultTeam, at: d.at }));
|
|
29
|
+
return {
|
|
30
|
+
teamsApi: TEAMS_API, deployment: ctx.deployment, defaultTeam: model.defaultTeam, teams,
|
|
31
|
+
souls: { teams: structuredClone(model.souls.teams), default: structuredClone(model.souls.default) },
|
|
32
|
+
problems: teamProblems(model, { messaging: messagingActive(ctx.workspace) }),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** `oats soul teams <soul>|'*' --json`: `key` is the soul's key ("*" for every soul), `soul` its name. */
|
|
37
|
+
export function soulTeamsDocument(ctx, { soul, key }) {
|
|
38
|
+
const model = teamModel(ctx.workspace, ctx.local, { workspaceKey: ctx.workspaceKey });
|
|
39
|
+
const t = soulTeams(model, key);
|
|
40
|
+
const list = (v) => (Array.isArray(v) ? [...v] : []);
|
|
41
|
+
return {
|
|
42
|
+
soulTeamsApi: SOUL_TEAMS_API, soul, key, defaultTeam: t.defaultTeam, teams: t.teams,
|
|
43
|
+
local: { teams: list(model.souls.teams[key]), default: key === "*" ? null : model.souls.default[key] ?? null },
|
|
44
|
+
all: list(model.souls.teams["*"]),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* ───────────────────────────── writing oats-local.yaml ─────────────────── */
|
|
49
|
+
|
|
50
|
+
const clone = (v) => (v === undefined ? undefined : structuredClone(v));
|
|
51
|
+
/** The four team keys of a local value, as plain data. */
|
|
52
|
+
const teamState = (local) => ({
|
|
53
|
+
teams: clone(isObject(local?.teams) ? local.teams : {}),
|
|
54
|
+
defaultTeam: typeof local?.defaultTeam === "string" ? local.defaultTeam : null,
|
|
55
|
+
soulsTeams: clone(isObject(local?.souls?.teams) ? local.souls.teams : {}),
|
|
56
|
+
soulsDefault: clone(isObject(local?.souls?.default) ? local.souls.default : {}),
|
|
57
|
+
});
|
|
58
|
+
const modelOf = (ctx, local) => teamModel(ctx.workspace, local, { workspaceKey: ctx.workspaceKey });
|
|
59
|
+
/** The refusals a state carries (unknown / ineligible references), keyed so a write can tell new ones. */
|
|
60
|
+
const refusals = (ctx, local) => new Map(teamProblems(modelOf(ctx, local))
|
|
61
|
+
.filter((p) => p.code === "E_TEAM_UNKNOWN" || p.code === "E_TEAM_NOT_ELIGIBLE").map((p) => [`${p.code} ${p.at}`, p]));
|
|
62
|
+
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
63
|
+
const readLocal = (ctx) => {
|
|
64
|
+
const text = readFileSync(ctx.localPath, "utf8");
|
|
65
|
+
const doc = YAML.parseDocument(text, { keepSourceTokens: true });
|
|
66
|
+
if (doc.errors?.length) throw fail("E_WORKSPACE_SCHEMA", `${ctx.localPath}: ${doc.errors[0].message}`, { path: ctx.localPath });
|
|
67
|
+
return { text, doc, local: parseConfigData(text, { origin: { kind: "local", path: ctx.localPath } }).value };
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Edit `doc` from `before` to `next` SURGICALLY: only the entries that change are touched, so comments and
|
|
72
|
+
* styles on everything else (inline comments on sibling entries, flow sequences) are kept. A map or list
|
|
73
|
+
* left empty is removed, and so is an empty `souls`.
|
|
74
|
+
*/
|
|
75
|
+
function applyState(doc, before, next) {
|
|
76
|
+
const map = (path, from, to, entry) => {
|
|
77
|
+
for (const k of Object.keys(from)) if (!Object.hasOwn(to, k)) doc.deleteIn([...path, k]);
|
|
78
|
+
for (const [k, v] of Object.entries(to)) if (!same(from[k], v)) entry([...path, k], from[k], v);
|
|
79
|
+
const node = doc.getIn(path);
|
|
80
|
+
if (YAML.isMap(node) && node.items.length === 0) doc.deleteIn(path);
|
|
81
|
+
};
|
|
82
|
+
// A team definition, key by key (its own comments stay on untouched keys).
|
|
83
|
+
map(["teams"], before.teams, next.teams, (path, was, now) => {
|
|
84
|
+
if (!isObject(was) || !YAML.isMap(doc.getIn(path))) { doc.setIn(path, now); return; }
|
|
85
|
+
for (const k of Object.keys(was)) if (!Object.hasOwn(now, k)) doc.deleteIn([...path, k]);
|
|
86
|
+
for (const [k, v] of Object.entries(now)) if (!same(was[k], v)) doc.setIn([...path, k], v);
|
|
87
|
+
});
|
|
88
|
+
if (next.defaultTeam !== before.defaultTeam) { if (next.defaultTeam === null) doc.deleteIn(["defaultTeam"]); else doc.setIn(["defaultTeam"], next.defaultTeam); }
|
|
89
|
+
// A soul's list, item by item (its style — flow or block — and the other items' comments stay).
|
|
90
|
+
map(["souls", "teams"], before.soulsTeams, next.soulsTeams, (path, was, now) => {
|
|
91
|
+
const seq = doc.getIn(path, true);
|
|
92
|
+
if (!Array.isArray(was) || !YAML.isSeq(seq)) { doc.setIn(path, now); return; }
|
|
93
|
+
seq.items = seq.items.filter((item) => now.includes(YAML.isScalar(item) ? item.value : item));
|
|
94
|
+
for (const label of now) if (!was.includes(label)) seq.items.push(doc.createNode(label));
|
|
95
|
+
});
|
|
96
|
+
map(["souls", "default"], before.soulsDefault, next.soulsDefault, (path, _was, now) => doc.setIn(path, now));
|
|
97
|
+
const souls = doc.get("souls");
|
|
98
|
+
if (YAML.isMap(souls) && souls.items.length === 0) doc.delete("souls");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Rewrite oats-local.yaml. `mutate(state, model)` edits the plain `state` (teamState) and may throw a
|
|
103
|
+
* refusal; both come from the file AS IT IS NOW (re-read here, never the command's startup snapshot), so
|
|
104
|
+
* two verbs running together cannot drop each other's change: the write is compare-and-swap — when the
|
|
105
|
+
* file changed after this read, the edit is redone on the new content (a few times, then E_LOCAL_CHANGED).
|
|
106
|
+
* `check(local)` may refuse the result before anything is written. → { changed, local } (the value now).
|
|
107
|
+
*/
|
|
108
|
+
function editLocal(ctx, mutate, { check } = {}) {
|
|
109
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
110
|
+
const { text, doc, local: current } = readLocal(ctx);
|
|
111
|
+
const before = teamState(current);
|
|
112
|
+
const next = teamState(current);
|
|
113
|
+
mutate(next, modelOf(ctx, current));
|
|
114
|
+
if (same(next, before)) return { changed: false, local: current };
|
|
115
|
+
applyState(doc, before, next);
|
|
116
|
+
const out = doc.toString();
|
|
117
|
+
const local = parseConfigData(out, { origin: { kind: "local", path: ctx.localPath } }).value;
|
|
118
|
+
const problems = validateLocal(local);
|
|
119
|
+
if (problems.length) throw fail("E_WORKSPACE_SCHEMA", `the rewritten oats-local.yaml would be invalid (${problems.map((p) => `${p.path || "/"}: ${p.message}`).join("; ")}); nothing was written`, { problems });
|
|
120
|
+
const had = refusals(ctx, current);
|
|
121
|
+
for (const [k, p] of refusals(ctx, local)) if (!had.has(k)) throw fail(p.code, `${p.message}; nothing was written`, Object.fromEntries(Object.entries(p).filter(([key]) => !["code", "severity", "message", "fix"].includes(key))));
|
|
122
|
+
if (check) check(local);
|
|
123
|
+
if (readFileSync(ctx.localPath, "utf8") !== text) continue; // changed under us: redo on the new content
|
|
124
|
+
writeFileAtomic(ctx.localPath, out);
|
|
125
|
+
return { changed: true, local };
|
|
126
|
+
}
|
|
127
|
+
throw fail("E_LOCAL_CHANGED", `${ctx.localPath} kept changing while this edit ran; nothing was written — run it again`, { path: ctx.localPath });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const assertLabel = (label) => {
|
|
131
|
+
if (typeof label !== "string" || !LABEL_RE.test(label)) throw fail("E_BAD_ARGS", `${JSON.stringify(label)} is not a team label (lowercase letters, digits, and . _ - after the first)`, { label });
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/** `oats teams add <label> --team <id> [--description <d>]` → { changed, local }. */
|
|
135
|
+
export function teamsAdd(ctx, label, { team, description } = {}) {
|
|
136
|
+
assertLabel(label);
|
|
137
|
+
if (typeof team !== "string" || !team.trim()) throw fail("E_BAD_ARGS", "oats teams add needs --team <provider team id>", { label });
|
|
138
|
+
if (!TEAM_ID_RE.test(team.trim())) throw fail("E_BAD_ARGS", `${JSON.stringify(team)} is not a team id: it must match ${TEAM_ID_RE.source} (a letter or digit first; no whitespace or control characters; at most 256 characters)`, { label, team });
|
|
139
|
+
return editLocal(ctx, (s, model) => {
|
|
140
|
+
if (model.labels.has(label)) {
|
|
141
|
+
const from = model.labels.get(label).from;
|
|
142
|
+
throw fail("E_TEAM_EXISTS", `team ${label} is already declared (${from === "shared" ? "shared, in oats-workspace.yaml" : "local, in oats-local.yaml"})${from === "local" ? ": remove it first (`oats teams remove`) to redefine it" : ""}`, { label, from });
|
|
143
|
+
}
|
|
144
|
+
s.teams[label] = { team: team.trim(), ...(typeof description === "string" && description ? { description } : {}) };
|
|
145
|
+
// The first team added becomes the deployment's default.
|
|
146
|
+
if (s.defaultTeam === null) s.defaultTeam = label;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** `oats teams remove <label>`: a LOCAL team nothing references (no cascade). */
|
|
151
|
+
export function teamsRemove(ctx, label) {
|
|
152
|
+
return editLocal(ctx, (s, model) => {
|
|
153
|
+
if (!model.local.has(label)) {
|
|
154
|
+
if (model.shared.has(label)) throw fail("E_TEAM_SHARED", `team ${label} is shared (declared in oats-workspace.yaml): it is edited by a PR to that file, never removed here`, { label, at: model.shared.get(label).at });
|
|
155
|
+
throw fail("E_TEAM_UNKNOWN", `team ${JSON.stringify(label)} is not declared`, { label });
|
|
156
|
+
}
|
|
157
|
+
// A label also declared in the committed file still resolves after the local copy goes (the
|
|
158
|
+
// collision's fix): its references stay valid, so only a purely local label must be unreferenced.
|
|
159
|
+
const usedBy = model.shared.has(label) ? [] : teamReferences(model, label);
|
|
160
|
+
if (usedBy.length) throw fail("E_TEAM_IN_USE", `team ${label} is still referenced (${usedBy.join(", ")}): remove the references first (\`oats teams default\`, \`oats soul teams … --remove ${label}\`)`, { label, usedBy });
|
|
161
|
+
delete s.teams[label];
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** `oats teams default <label>`: a label of either file. */
|
|
166
|
+
export function teamsDefault(ctx, label) {
|
|
167
|
+
return editLocal(ctx, (s, model) => {
|
|
168
|
+
if (!model.labels.has(label)) throw fail("E_TEAM_UNKNOWN", `team ${JSON.stringify(label)} is not declared: \`oats teams add\` it first`, { label });
|
|
169
|
+
s.defaultTeam = label;
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* `oats soul teams <soul>|'*' [--add a,b] [--remove a,b] [--default <l> | --clear-default]`.
|
|
175
|
+
* `key` is the soul's key ("*" for every soul). → { changed, local }.
|
|
176
|
+
*/
|
|
177
|
+
export function soulTeamsEdit(ctx, key, { add = [], remove = [], setDefault = null, clearDefault = false } = {}) {
|
|
178
|
+
if (key === "*" && (setDefault !== null || clearDefault)) throw fail("E_BAD_ARGS", "a default is per soul: --default / --clear-default need a soul, not '*' (the deployment's default is `oats teams default`)", { soul: key });
|
|
179
|
+
if (setDefault !== null && clearDefault) throw fail("E_BAD_ARGS", "choose --default <label> or --clear-default, not both");
|
|
180
|
+
// --default must name one of the soul's teams after the write.
|
|
181
|
+
const check = setDefault === null ? undefined : (local) => {
|
|
182
|
+
try { soulTeams(modelOf(ctx, local), key); }
|
|
183
|
+
catch (e) { if (e.code === "E_TEAM_NOT_ELIGIBLE") throw fail("E_TEAM_NOT_ELIGIBLE", `${e.message}; nothing was written`, { soul: key, label: setDefault, at: `${LOCAL_FILE}#/souls/default/${pointerKey(key)}` }); throw e; }
|
|
184
|
+
};
|
|
185
|
+
return editLocal(ctx, (s, model) => {
|
|
186
|
+
for (const label of [...add, ...remove, ...(setDefault !== null ? [setDefault] : [])]) {
|
|
187
|
+
if (!model.labels.has(label)) throw fail("E_TEAM_UNKNOWN", `team ${JSON.stringify(label)} is not declared: \`oats teams add\` it first`, { label });
|
|
188
|
+
}
|
|
189
|
+
const list = Array.isArray(s.soulsTeams[key]) ? s.soulsTeams[key] : [];
|
|
190
|
+
const next = [...list.filter((l) => !remove.includes(l)), ...add.filter((l) => !list.includes(l) && !remove.includes(l))].filter((l, i, a) => a.indexOf(l) === i);
|
|
191
|
+
if (next.length) s.soulsTeams[key] = next; else delete s.soulsTeams[key];
|
|
192
|
+
if (clearDefault) delete s.soulsDefault[key];
|
|
193
|
+
if (setDefault !== null) s.soulsDefault[key] = setDefault;
|
|
194
|
+
}, { check });
|
|
195
|
+
}
|
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
|
@@ -483,8 +483,8 @@ export function tickTriggers(ws, { now = new Date(), io, dryRun = false, ctx = n
|
|
|
483
483
|
for (const e of entries) {
|
|
484
484
|
const id = e.id;
|
|
485
485
|
// A workspace trigger another host runs is not this tick's business; one NAMED for this host
|
|
486
|
-
// that it cannot run (another gh account) is reported every tick until someone fixes it.
|
|
487
|
-
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; }
|
|
488
488
|
if (e.invalid) { if (e.placement.enabledHere) rec(id, "invalid", { error: e.invalid.message }); continue; }
|
|
489
489
|
if (!e.placement.runsHere) continue;
|
|
490
490
|
const def = runDef(e);
|