@awebai/oats 0.22.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/LICENSE +21 -0
- package/README.md +380 -0
- package/bin/oats.mjs +3294 -0
- package/capabilities/oats-authoring/oats.json +12 -0
- package/capabilities/oats-aweb/bin/oats-aweb.mjs +297 -0
- package/capabilities/oats-aweb/injects/aweb.md +55 -0
- package/capabilities/oats-aweb/oats.json +47 -0
- package/capabilities/oats-aweb/package.json +20 -0
- package/capabilities/oats-jira/bin/oats-jira.mjs +40 -0
- package/capabilities/oats-jira/injects/jira.md +10 -0
- package/capabilities/oats-jira/oats.json +22 -0
- package/capabilities/oats-jira/package.json +25 -0
- package/capabilities/oats-jira/skills/jira-tasks/SKILL.md +176 -0
- package/capabilities/oats-linear/README.md +234 -0
- package/capabilities/oats-linear/bin/oats-linear-hook.mjs +32 -0
- package/capabilities/oats-linear/bin/oats-linear.mjs +344 -0
- package/capabilities/oats-linear/injects/linear.md +8 -0
- package/capabilities/oats-linear/oats.json +24 -0
- package/capabilities/oats-linear/package.json +29 -0
- package/capabilities/oats-linear/skills/linear-tasks/SKILL.md +217 -0
- package/capabilities/oats-linear/test/oats-linear.test.mjs +168 -0
- package/capabilities/oats-okf/agents/memory-harvest.md +25 -0
- package/capabilities/oats-okf/bin/oats-okf.mjs +283 -0
- package/capabilities/oats-okf/injects/okf.md +62 -0
- package/capabilities/oats-okf/oats.json +20 -0
- package/capabilities/oats-okf/package.json +22 -0
- package/capabilities/oats-okf/skills/memory-harvest/SKILL.md +100 -0
- package/capabilities/oats-okf/skills/okf/SKILL.md +107 -0
- package/capabilities/oats-okf/skills/okf/scripts/okf-validate.mjs +123 -0
- package/capabilities/oats-review/agents/reviewer/AGENTS.md +53 -0
- package/capabilities/oats-review/agents/reviewer/soul.yaml +6 -0
- package/capabilities/oats-review/injects/review.md +68 -0
- package/capabilities/oats-review/oats.json +10 -0
- package/capabilities/oats-review/skills/code-review/SKILL.md +44 -0
- package/capabilities/oats-review/skills/security-review/SKILL.md +59 -0
- package/docs/capabilities.md +505 -0
- package/docs/capability-manifest.schema.json +223 -0
- package/docs/configuration.md +482 -0
- package/docs/conventions.md +73 -0
- package/docs/design/desktop-ux-plan.md +362 -0
- package/docs/design/package-engine-contract.md +813 -0
- package/docs/design/package-runtime-api.md +414 -0
- package/docs/desktop-cli-api.md +89 -0
- package/docs/desktop-succession.md +51 -0
- package/docs/desktop.md +187 -0
- package/docs/implementation.md +282 -0
- package/docs/integrations.md +123 -0
- package/docs/knowledge-theory.md +97 -0
- package/docs/knowledge.md +139 -0
- package/docs/layers.md +110 -0
- package/docs/migration-from-oas.md +122 -0
- package/docs/oats-config.schema.json +140 -0
- package/docs/oats-lock.schema.json +175 -0
- package/docs/oats-package.schema.json +129 -0
- package/docs/packages.md +442 -0
- package/docs/release-lane.md +132 -0
- package/docs/release-notes/v0.18.2.md +79 -0
- package/docs/release-notes/v0.18.3.md +63 -0
- package/docs/release-notes/v0.18.4.md +65 -0
- package/docs/release-notes/v0.18.5.md +66 -0
- package/docs/release-notes/v0.18.6.md +87 -0
- package/docs/release-notes/v0.19.0.md +186 -0
- package/docs/release-notes/v0.19.1.md +24 -0
- package/docs/release-notes/v0.19.2.md +32 -0
- package/docs/release-notes/v0.19.3.md +16 -0
- package/docs/release-notes/v0.19.4.md +12 -0
- package/docs/release-notes/v0.20.0.md +89 -0
- package/docs/release-notes/v0.22.0.md +180 -0
- package/docs/souls-and-instances.md +344 -0
- package/injects/framework-workspace.md +7 -0
- package/injects/instance-boundary.md +43 -0
- package/injects/local-soul.md +19 -0
- package/injects/oats.md +11 -0
- package/injects/work-attached.md +16 -0
- package/injects/work-checkout.md +12 -0
- package/injects/work-workspace.md +22 -0
- package/injects/work-worktree.md +13 -0
- package/lib/core.mjs +6792 -0
- package/lib/packages.mjs +1375 -0
- package/lib/tmux-config.mjs +35 -0
- package/package-catalog.json +44 -0
- package/package.json +55 -0
- package/packages/record/README.md +151 -0
- package/packages/record/bin/capture.mjs +256 -0
- package/packages/record/bin/recall.mjs +102 -0
- package/packages/record/bin/setup.mjs +281 -0
- package/packages/record/bin/turn-record.mjs +47 -0
- package/packages/record/docs/turn-record-sot.md +350 -0
- package/packages/record/lib/canonical.mjs +151 -0
- package/packages/record/lib/capture-aw.mjs +179 -0
- package/packages/record/lib/capture-cc.mjs +322 -0
- package/packages/record/lib/formats.mjs +296 -0
- package/packages/record/lib/ignore.mjs +155 -0
- package/packages/record/lib/index-db.mjs +524 -0
- package/packages/record/lib/project-aweb.mjs +251 -0
- package/packages/record/lib/segments.mjs +186 -0
- package/packages/record/lib/store.mjs +447 -0
- package/packages/record/lib/tags.mjs +114 -0
- package/packages/record/package.json +45 -0
- package/packages/record/test/vectors/README.md +34 -0
- package/packages/record/test/vectors/aweb-projection-v1.json +231 -0
- package/packages/record/test/vectors/journal-merge-v1.json +261 -0
- package/packages/record/test/vectors/turn-id-v1.json +152 -0
- package/packages/record/test/vectors/validate.mjs +391 -0
- package/skills/integration-authoring/SKILL.md +76 -0
- package/skills/oats/SKILL.md +151 -0
- package/skills/oats-config/SKILL.md +162 -0
- package/skills/oats-getting-started/SKILL.md +159 -0
- package/skills/oats-packages/SKILL.md +180 -0
- package/skills/oats-support/SKILL.md +79 -0
- package/skills/skill-craft/SKILL.md +109 -0
- package/skills/soul-craft/SKILL.md +109 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"capability": "oats.authoring",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"compatibility": { "oats": ">=0.6.2" },
|
|
5
|
+
"description": "Additive framework-authoring guidance for capability packages, agent skills, and souls.",
|
|
6
|
+
"requires": [],
|
|
7
|
+
"skills": [
|
|
8
|
+
"../../skills/integration-authoring",
|
|
9
|
+
"../../skills/skill-craft",
|
|
10
|
+
"../../skills/soul-craft"
|
|
11
|
+
]
|
|
12
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* oats-aweb — OATS messaging-provider hooks for aweb.
|
|
4
|
+
*
|
|
5
|
+
* Invoked by the OATS kernel at instance lifecycle events (hook contract):
|
|
6
|
+
* oats-aweb spawn mint a team-scoped aweb identity for the instance
|
|
7
|
+
* oats-aweb retire gracefully self-delete it (BEFORE the home dir is removed)
|
|
8
|
+
* oats-aweb roster list the aweb team's members — the cross-machine directory
|
|
9
|
+
* of live instances (alias = instance name) and humans
|
|
10
|
+
* oats-aweb setup guided onboarding: check the aw CLI, initialize the team
|
|
11
|
+
* scope's aweb workspace, create/join the team
|
|
12
|
+
*
|
|
13
|
+
* Env contract (set by the kernel):
|
|
14
|
+
* OATS_EVENT spawn|retire
|
|
15
|
+
* OATS_INSTANCE instance name (used as the aweb alias)
|
|
16
|
+
* OATS_HOME instance home dir (cwd is also set to it)
|
|
17
|
+
* OATS_CONTEXT resolution context dir (the soul's repo / agents root parent)
|
|
18
|
+
* OATS_WORKSPACE the agents root's parent — the team boundary
|
|
19
|
+
* OATS_SETTINGS JSON of the provider's `settings:` block
|
|
20
|
+
* OATS_TEAM_NAME/OATS_TEAM_ID/OATS_TEAM_SCOPE resolved config `team:` block (may be empty)
|
|
21
|
+
* OATS_META JSON persisted from this hook's previous spawn output (retire only)
|
|
22
|
+
*
|
|
23
|
+
* Output (spawn, stdout JSON):
|
|
24
|
+
* { "meta": {...persisted to instance.json + OATS_META at retire},
|
|
25
|
+
* "brief": "one-line TASK.md briefing line", "warning": "non-fatal problem" }
|
|
26
|
+
*
|
|
27
|
+
* EXIT CODE IS THE CONTRACT for the spawn hook. This capability declares
|
|
28
|
+
* `spawn` as required (oats.json), so a nonzero exit fails the spawn and rolls it
|
|
29
|
+
* back. Messaging is the whole point of the capability: an instance whose
|
|
30
|
+
* identity was never minted believes it can be woken by mail and cannot, which
|
|
31
|
+
* is worse than not starting. So identity failures exit nonzero, while genuinely
|
|
32
|
+
* advisory problems (the Claude channel plugin, a team-name mismatch) stay
|
|
33
|
+
* warnings on exit 0.
|
|
34
|
+
*
|
|
35
|
+
* On a fatal path, ALWAYS emit any metadata gathered so far before exiting: the
|
|
36
|
+
* kernel feeds it to the retire hook to compensate partial state, and an
|
|
37
|
+
* identity joined moments before the failure must still be deletable.
|
|
38
|
+
*/
|
|
39
|
+
import { execFileSync } from "node:child_process";
|
|
40
|
+
import { existsSync, statSync } from "node:fs";
|
|
41
|
+
import { join, dirname, resolve, delimiter } from "node:path";
|
|
42
|
+
|
|
43
|
+
/** Run a command as ARGV — never a shell string. Team ids, aliases, instance
|
|
44
|
+
* names and invite tokens all flow through here; quoting them correctly is a
|
|
45
|
+
* property of one helper staying correct forever, while argv removes the class.
|
|
46
|
+
* This hook is a REQUIRED spawn hook, so it gates every spawn, which is reason
|
|
47
|
+
* enough not to rely on quoting. */
|
|
48
|
+
const run = (argv, cwd, timeout = 45000, { secrets = [], secretSafe = false } = {}) => {
|
|
49
|
+
try {
|
|
50
|
+
return execFileSync(argv[0], argv.slice(1), { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout }).trim();
|
|
51
|
+
} catch (e) {
|
|
52
|
+
// execFileSync puts the WHOLE ARGV in e.message ("Command failed: aw team
|
|
53
|
+
// join <token> …"). This hook's failures are reported by the kernel and land
|
|
54
|
+
// in CLI/Desktop logs, so a failed join disclosed a still-valid team invite
|
|
55
|
+
// token to anyone reading them (reviewer-aggregate2). Rebuild the error from
|
|
56
|
+
// the command name and the status — never the argv.
|
|
57
|
+
//
|
|
58
|
+
// secretSafe drops the child's OUTPUT as well. Scrubbing known strings is not
|
|
59
|
+
// enough for a command that MINTS a secret: `aw team invite` can print a
|
|
60
|
+
// freshly-created token to stderr while failing, and at that point the caller
|
|
61
|
+
// has no value to scrub because the token is exactly what it never received
|
|
62
|
+
// (reviewer-1a6e82e). For those commands, status plus fixed context is all
|
|
63
|
+
// the diagnosis anyone gets.
|
|
64
|
+
const scrub = (t) => secrets.filter(Boolean).reduce((acc, sec) => acc.split(sec).join("<redacted>"), String(t ?? ""));
|
|
65
|
+
const where = [argv[0], argv[1], argv[2]].filter((a) => a && !secrets.includes(a) && !a.startsWith("-")).join(" ");
|
|
66
|
+
const why = secretSafe ? "" : (scrub(e.stderr).trim() || (e.status === undefined ? String(e.code || "failed") : ""));
|
|
67
|
+
const err = new Error(`${where} failed${e.status === undefined ? "" : ` (exit ${e.status})`}${why ? `: ${why}` : ""}${secretSafe ? " (output withheld: this command handles credentials)" : ""}`);
|
|
68
|
+
err.status = e.status;
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
/** JSON.parse whose failure never quotes the input. Node includes an excerpt of
|
|
73
|
+
* the malformed text in a SyntaxError, which for these commands IS the credential
|
|
74
|
+
* (reviewer-1a6e82e). */
|
|
75
|
+
const parseSecretJson = (text, what) => {
|
|
76
|
+
try { return JSON.parse(text); }
|
|
77
|
+
catch { throw new Error(`${what} returned output that is not valid JSON (withheld: this command handles credentials)`); }
|
|
78
|
+
};
|
|
79
|
+
/** Is a command on PATH? Resolved in-process rather than by running
|
|
80
|
+
* `command -v`, which is a SHELL BUILTIN — spawning it as a program depends on
|
|
81
|
+
* a /usr/bin/command binary that many systems do not ship, and its absence
|
|
82
|
+
* would read as "aw is missing" on every such host. */
|
|
83
|
+
function onPath(cmd) {
|
|
84
|
+
for (const dir of String(process.env.PATH || "").split(delimiter)) {
|
|
85
|
+
if (!dir) continue;
|
|
86
|
+
try { const st = statSync(join(dir, cmd)); if (st.isFile() && (st.mode & 0o111)) return true; } catch { /* keep looking */ }
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
const out = (o, code = 0) => { process.stdout.write(JSON.stringify(o) + "\n"); process.exit(code); };
|
|
91
|
+
const warn = (m) => out({ warning: `oats-aweb: ${String(m).slice(0, 300)}` });
|
|
92
|
+
/** Fatal for a REQUIRED spawn hook: emit metadata for compensation, then exit
|
|
93
|
+
* nonzero so the kernel rolls the spawn back. `meta` carries whatever external
|
|
94
|
+
* state already exists (e.g. a joined identity) so retire can undo it. */
|
|
95
|
+
const fatal = (m, meta) => out({ ...(meta ? { meta } : {}), warning: `oats-aweb: ${String(m).slice(0, 300)}` }, 1);
|
|
96
|
+
|
|
97
|
+
const event = process.env.OATS_EVENT || process.argv[2];
|
|
98
|
+
const instance = process.env.OATS_INSTANCE;
|
|
99
|
+
const home = process.env.OATS_HOME || process.cwd();
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The aweb root (minting authority). BOUNDED candidates — the deployment's team
|
|
103
|
+
* scope (from config `team:`) is the natural home; we never walk past the
|
|
104
|
+
* workspace to the laptop root (a `.aw` there would be a different team;
|
|
105
|
+
* minting into it would be a silent cross-team leak):
|
|
106
|
+
* 1. the declared team scope (OATS_TEAM_SCOPE)
|
|
107
|
+
* 2. the instance home itself
|
|
108
|
+
* 3. the git repo root containing the home (if any)
|
|
109
|
+
* 4. the resolution context (the soul's target repo) and its git repo root
|
|
110
|
+
* 5. the workspace root (OATS_WORKSPACE — e.g. ~/lfx)
|
|
111
|
+
* First candidate with a `.aw` wins; none → no minting.
|
|
112
|
+
*/
|
|
113
|
+
function gitRootOf(startDir) {
|
|
114
|
+
let d = resolve(startDir);
|
|
115
|
+
while (true) {
|
|
116
|
+
if (existsSync(join(d, ".git"))) return d;
|
|
117
|
+
const parent = dirname(d);
|
|
118
|
+
if (parent === d) return undefined;
|
|
119
|
+
d = parent;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function awebRoot() {
|
|
123
|
+
const candidates = [];
|
|
124
|
+
const push = (p) => { if (p && !candidates.includes(resolve(p))) candidates.push(resolve(p)); };
|
|
125
|
+
push(process.env.OATS_TEAM_SCOPE);
|
|
126
|
+
push(home);
|
|
127
|
+
push(gitRootOf(home));
|
|
128
|
+
push(process.env.OATS_CONTEXT);
|
|
129
|
+
if (process.env.OATS_CONTEXT) push(gitRootOf(process.env.OATS_CONTEXT));
|
|
130
|
+
push(process.env.OATS_WORKSPACE);
|
|
131
|
+
for (const c of candidates) if (existsSync(join(c, ".aw"))) return c;
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Team memberships from `aw team list --json`. The current CLI returns
|
|
136
|
+
* `memberships`; older output used `teams`. Spawn resolution and `oats aweb
|
|
137
|
+
* setup` MUST read this the same way — they drifted once, and because spawn
|
|
138
|
+
* treats "no matching membership" as fatal, the stale reader turned every
|
|
139
|
+
* name-only team config into a blocked spawn (reviewer-602627c). */
|
|
140
|
+
const teamMemberships = (listed) => listed?.memberships || listed?.teams || [];
|
|
141
|
+
const teamIdsOf = (listed) => teamMemberships(listed).map((m) => m.team_id || m.id || m);
|
|
142
|
+
|
|
143
|
+
const AW_INSTALL = "install the aw CLI first — see https://aweb.ai/docs (or `oats aweb setup` for guided onboarding)";
|
|
144
|
+
const isCommand = ["roster", "setup"].includes(event);
|
|
145
|
+
if (!onPath("aw")) {
|
|
146
|
+
if (isCommand) { console.error(`oats aweb ${event}: aw CLI not on PATH — ${AW_INSTALL}`); process.exit(1); }
|
|
147
|
+
if (event === "spawn") fatal(`aw CLI not on PATH, so no identity could be minted and this instance would have no messaging — ${AW_INSTALL}`);
|
|
148
|
+
warn(`aw CLI not on PATH — no identity minted; ${AW_INSTALL}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (event === "spawn") {
|
|
152
|
+
let minted; // external identity, once `aw team join` succeeds
|
|
153
|
+
const root = awebRoot();
|
|
154
|
+
if (!root) fatal(`no initialized aweb root (.aw) among the bounded candidates (home, its git repo, context repo, workspace ${process.env.OATS_WORKSPACE || "?"}), so no identity could be minted and this instance would have no messaging — run \`oats aweb setup\` for guided onboarding`);
|
|
155
|
+
try {
|
|
156
|
+
// Team correctness: the config's `team:` block wins (id, then name), else the
|
|
157
|
+
// root's active team. ALWAYS pass --team-id explicitly — never inherit whatever
|
|
158
|
+
// team happens to be active at mint time — and verify the joined cert matches.
|
|
159
|
+
// The instance name IS the discoverable alias (the team roster doubles as the
|
|
160
|
+
// cross-machine instance directory).
|
|
161
|
+
let team = process.env.OATS_TEAM_ID || process.env.OATS_TEAM_NAME;
|
|
162
|
+
if (!team) team = JSON.parse(run(["aw", "team", "list", "--json"], root)).active_team;
|
|
163
|
+
if (!team) fatal("cannot determine target team (no config team block, no active team at root), so no identity could be minted — set a team: block in oats-config.yaml, or activate a team at the aweb root");
|
|
164
|
+
// A bare team name (no namespace) resolves against the root's memberships.
|
|
165
|
+
if (!team.includes(":")) {
|
|
166
|
+
const teams = JSON.parse(run(["aw", "team", "list", "--json"], root));
|
|
167
|
+
const match = teamIdsOf(teams).filter((tid) => String(tid).startsWith(`${team}:`));
|
|
168
|
+
if (match.length === 1) team = match[0];
|
|
169
|
+
else if (match.length > 1) fatal(`team name "${team}" is ambiguous at ${root}: ${match.join(", ")}, so no identity could be minted — set team.id in oats-config.yaml`);
|
|
170
|
+
else fatal(`no membership matching team "${team}" at ${root}, so no identity could be minted — join or create it first (aweb-team-membership skill), or set team.id`);
|
|
171
|
+
}
|
|
172
|
+
// Both of these carry the invite token — one mints it, the other spends it —
|
|
173
|
+
// so neither their output nor their diagnostics may reach a log.
|
|
174
|
+
const inv = parseSecretJson(run(["aw", "team", "invite", "--team-id", team, "--json"], root, 45000, { secretSafe: true }), "aw team invite");
|
|
175
|
+
if (!inv?.token || typeof inv.token !== "string") fatal("aw team invite returned no usable token, so no identity could be minted");
|
|
176
|
+
const raw = parseSecretJson(run(["aw", "team", "join", inv.token, "--name", instance, "--json"], home, 45000, { secrets: [inv.token], secretSafe: true }), "aw team join");
|
|
177
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) fatal("aw team join returned no usable result, so no identity could be minted", minted);
|
|
178
|
+
// The RESPONSE is not a safe place to take strings from. Suppressing the
|
|
179
|
+
// failure paths does nothing if a successful reply is copied into meta and
|
|
180
|
+
// the briefing verbatim: a response echoing the invite token back as the
|
|
181
|
+
// alias would print it twice, on exit 0 (reviewer-a6aa1c5). Accept a field
|
|
182
|
+
// only if it is a plausible value of its own kind and is not carrying the
|
|
183
|
+
// token; otherwise fall back to what WE asked for, which is always known.
|
|
184
|
+
const clean = (v) => (typeof v === "string" && v.trim() && !v.includes(inv.token) ? v.trim() : undefined);
|
|
185
|
+
const joined = {
|
|
186
|
+
alias: (() => { const a = clean(raw.alias); return a && /^[a-z0-9][a-z0-9._-]{0,127}$/i.test(a) ? a : instance; })(),
|
|
187
|
+
// Team ids are "<name>:<domain>"; anything else is not one, and the
|
|
188
|
+
// requested team is the honest fallback.
|
|
189
|
+
team_id: (() => { const t = clean(raw.team_id); return t && /^[^\s:]+:[^\s:]+$/.test(t) ? t : team; })(),
|
|
190
|
+
};
|
|
191
|
+
// External state now exists. Record it immediately so any later failure can
|
|
192
|
+
// still report it for compensation.
|
|
193
|
+
minted = { team: joined.team_id, alias: joined.alias };
|
|
194
|
+
run(["aw", "init", "--do-not-touch-agents-md"], home);
|
|
195
|
+
const alias = joined.alias;
|
|
196
|
+
const mismatch = joined.team_id !== team
|
|
197
|
+
? ` [WARNING: joined ${joined.team_id}, expected ${team}]` : "";
|
|
198
|
+
// Runtime integration: for Claude Code sessions the aweb-channel plugin
|
|
199
|
+
// carries real-time push events. This hook does NOT install it. The plugin
|
|
200
|
+
// is a DECLARED runtime requirement (oats.json), consented once at
|
|
201
|
+
// `oats install` and verified by the kernel before spawn — installing it here
|
|
202
|
+
// would mutate the operator's Claude configuration without asking, inside a
|
|
203
|
+
// spawn, which is exactly the silent host mutation the consent gate exists
|
|
204
|
+
// to prevent. By the time this runs the kernel has already proven the plugin
|
|
205
|
+
// is present and enabled, so contributing the flag is safe.
|
|
206
|
+
const launch = (process.env.OATS_RUNTIME || "") === "claude"
|
|
207
|
+
? { claude: "--dangerously-load-development-channels plugin:aweb-channel@awebai-marketplace" }
|
|
208
|
+
: undefined;
|
|
209
|
+
const channelWarning = undefined;
|
|
210
|
+
out({
|
|
211
|
+
meta: { team: joined.team_id, alias },
|
|
212
|
+
brief: `Comms: you have an aweb identity — alias "${alias}" on team ${joined.team_id}.${mismatch} Use \`aw mail\`/\`aw chat\` for messaging (see the aweb-messaging skill); coordination stays in your deployment's task layer.`,
|
|
213
|
+
...(launch ? { launch } : {}),
|
|
214
|
+
...(mismatch ? { warning: `oats-aweb: team mismatch — joined ${joined.team_id}, expected ${team}` } : channelWarning ? { warning: channelWarning } : {}),
|
|
215
|
+
});
|
|
216
|
+
} catch (e) {
|
|
217
|
+
// A join may already have created a REMOTE identity before the failure.
|
|
218
|
+
// Hand it back as meta so the kernel's compensation can delete it — losing
|
|
219
|
+
// it here would strand a roster entry no one owns.
|
|
220
|
+
fatal(`identity minting failed: ${e.message || e}`, minted);
|
|
221
|
+
}
|
|
222
|
+
} else if (event === "retire") {
|
|
223
|
+
const meta = JSON.parse(process.env.OATS_META || "{}");
|
|
224
|
+
// No alias means the spawn hook never reported an identity: nothing exists to
|
|
225
|
+
// undo, which is completion. An alias WITH no local `.aw` is the opposite —
|
|
226
|
+
// the remote record exists and its key is gone, so the self-delete cannot be
|
|
227
|
+
// authenticated and the cleanup is incomplete, not vacuous (reviewer-602627c).
|
|
228
|
+
if (!meta.alias) out({ meta: { retired: false, reason: "nothing-to-delete" } });
|
|
229
|
+
if (!existsSync(join(home, ".aw"))) {
|
|
230
|
+
out({ meta: { retired: false, reason: "no-local-identity-key" }, warning: `oats-aweb: alias "${meta.alias}" was minted but ${join(home, ".aw")} is gone, so the remote record cannot be self-deleted and will linger until stale` }, 1);
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
// Self-delete from inside the home, authenticated by its own key — a remote
|
|
234
|
+
// delete would 409 until the server marks the workspace stale.
|
|
235
|
+
run(["aw", "workspace", "delete", meta.alias], home);
|
|
236
|
+
out({ meta: { retired: true } });
|
|
237
|
+
} catch (e) {
|
|
238
|
+
// Exit nonzero: during a required-hook rollback this is the signal that
|
|
239
|
+
// compensation did NOT complete, so the spawn is not reported as cleanly
|
|
240
|
+
// rolled back while a remote identity still exists.
|
|
241
|
+
out({ meta: { retired: false, reason: "self-delete-failed" }, warning: `oats-aweb: self-delete failed (the remote record will linger until stale): ${e.message || e}` }, 1);
|
|
242
|
+
}
|
|
243
|
+
} else if (event === "roster") {
|
|
244
|
+
// Cross-machine directory: every OATS-spawned instance joins the team with
|
|
245
|
+
// alias = instance name, so the team's member roster lists live instances
|
|
246
|
+
// wherever they run (plus human members). Local liveness comes from
|
|
247
|
+
// `oats status --team`; this is the network view.
|
|
248
|
+
const root = awebRoot();
|
|
249
|
+
if (!root) { console.error("oats aweb roster: no initialized aweb root (.aw) found"); process.exit(1); }
|
|
250
|
+
const team = process.env.OATS_TEAM_ID || process.env.OATS_TEAM_NAME || JSON.parse(run(["aw", "team", "list", "--json"], root)).active_team;
|
|
251
|
+
if (!team) { console.error("oats aweb roster: cannot determine team (no config team block, no active team)"); process.exit(1); }
|
|
252
|
+
const teamFlag = team.includes(":") ? ["--team-id", team] : ["--team", team];
|
|
253
|
+
const r = JSON.parse(run(["aw", "id", "team", "members", ...teamFlag, "--json"], root, 60000));
|
|
254
|
+
if (process.argv.includes("--json")) { console.log(JSON.stringify(r, null, 2)); process.exit(0); }
|
|
255
|
+
console.log(`aweb team ${r.team_id || team} — member roster (cross-machine):`);
|
|
256
|
+
const members = r.members || [];
|
|
257
|
+
if (!members.length) console.log(" (no member certificates visible from this workspace)");
|
|
258
|
+
for (const m of members) console.log(` ${m.alias || m.name || m.did || JSON.stringify(m)}`);
|
|
259
|
+
console.log("\nAliases minted by OATS are instance names; message one with `aw mail send --to <alias> --subject \"...\" --body \"...\"`.");
|
|
260
|
+
process.exit(0);
|
|
261
|
+
} else if (event === "setup") {
|
|
262
|
+
// Guided onboarding — idempotent, prints what it finds and the one next step.
|
|
263
|
+
const scope = process.env.OATS_TEAM_SCOPE || process.cwd();
|
|
264
|
+
const teamName = process.env.OATS_TEAM_NAME;
|
|
265
|
+
const teamId = process.env.OATS_TEAM_ID;
|
|
266
|
+
console.log(`aweb onboarding — team scope: ${scope}${teamName ? `, config team: ${teamName}${teamId ? ` (${teamId})` : ""}` : ""}\n`);
|
|
267
|
+
if (!teamName) {
|
|
268
|
+
console.log("1. Declare your team in the deployment scope's oats-config.yaml first:");
|
|
269
|
+
console.log(" team:\n name: <your-team>\n then re-run `oats aweb setup` from there.");
|
|
270
|
+
process.exit(0);
|
|
271
|
+
}
|
|
272
|
+
if (!existsSync(join(scope, ".aw"))) {
|
|
273
|
+
console.log(`No aweb workspace at the team scope yet. Initialize it (interactive — creates or connects an aweb account):`);
|
|
274
|
+
console.log(` cd ${scope} && aw init`);
|
|
275
|
+
console.log(" First time on aweb? `aw init` walks you through creating a hosted aweb.ai account.");
|
|
276
|
+
console.log(" Own your domain? Use `aw init --byod` (see the aweb-team-membership skill).");
|
|
277
|
+
process.exit(0);
|
|
278
|
+
}
|
|
279
|
+
let teams = { memberships: [] };
|
|
280
|
+
try { teams = JSON.parse(run(["aw", "team", "list", "--json"], scope)); } catch { /* fall through */ }
|
|
281
|
+
const want = teamId || teamName;
|
|
282
|
+
const match = teamIdsOf(teams).find((tid) => String(tid) === want || String(tid).startsWith(`${want}:`));
|
|
283
|
+
if (match) {
|
|
284
|
+
console.log(`✓ aweb workspace initialized and member of ${match}.`);
|
|
285
|
+
if (teams.active_team && teams.active_team !== match) console.log(` Note: active team is ${teams.active_team}; instances join ${match} explicitly, but consider \`aw team switch ${match}\`.`);
|
|
286
|
+
console.log(" Done — spawned instances will join this team automatically (alias = instance name).");
|
|
287
|
+
console.log(" Roster: `oats aweb roster` · local: `oats status --team`");
|
|
288
|
+
} else {
|
|
289
|
+
console.log(`Workspace initialized, but no membership matching "${want}".`);
|
|
290
|
+
console.log(` Create the team: cd ${scope} && aw team create ${teamName}`);
|
|
291
|
+
console.log(" Or join an existing one: get an invite token from a member, then `aw team join <token>`");
|
|
292
|
+
console.log(" (details: aweb-team-membership skill)");
|
|
293
|
+
}
|
|
294
|
+
process.exit(0);
|
|
295
|
+
} else {
|
|
296
|
+
warn(`unknown event "${event}" (expected spawn|retire)`);
|
|
297
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
## Messaging: aweb
|
|
2
|
+
|
|
3
|
+
Your messaging layer is **aweb**. You have (or will be minted) a team-scoped
|
|
4
|
+
aweb identity — alias = your instance name — on your deployment's team (see
|
|
5
|
+
`instance.json` / your TASK.md briefing for the team).
|
|
6
|
+
|
|
7
|
+
**Load the skills at the right moments — do not work from memory:**
|
|
8
|
+
|
|
9
|
+
- **Before your first `aw mail`/`aw chat` of a session**, load the
|
|
10
|
+
**aweb-messaging** skill. It is the
|
|
11
|
+
playbook for sending, replying, and chat etiquette.
|
|
12
|
+
- **When an aweb channel event awakens you**, read the injected event
|
|
13
|
+
metadata first, then load the **aweb-messaging** skill ("Read the event
|
|
14
|
+
first" section) before responding — continue the existing conversation,
|
|
15
|
+
never
|
|
16
|
+
start a new thread when a `message_id`/`conversation_id` is provided.
|
|
17
|
+
- For team/roster/certificate questions, load **aweb-team-membership**; for
|
|
18
|
+
identity/key questions, load **aweb-identity**.
|
|
19
|
+
- If a command errors or a flag looks wrong, re-read the skill or run
|
|
20
|
+
`aw <cmd> --help` — never invent flags.
|
|
21
|
+
|
|
22
|
+
Quick crib (the skill has the full craft; run from your instance home):
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
aw mail inbox # UNREAD mail only
|
|
26
|
+
aw mail inbox --show-all # full history (read mail is not lost)
|
|
27
|
+
aw mail send --to <alias> --subject "..." --body "..." # recipient needs --to
|
|
28
|
+
aw mail send --to <alias> --subject "..." --body-file <f> # markdown/backticks
|
|
29
|
+
aw mail reply <message-id> --body "..." # reply on an existing thread
|
|
30
|
+
aw chat send --to <alias> --body "..." # synchronous back-and-forth
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Flags are exactly these — there is no positional recipient and no
|
|
34
|
+
`--reply-to`; when unsure run `aw mail send --help`, don't guess. For any
|
|
35
|
+
body longer than a sentence (or containing quotes/backticks/newlines),
|
|
36
|
+
write it to a temp file and use `--body-file` — inline `--body` shell
|
|
37
|
+
escaping is a recurring failure.
|
|
38
|
+
|
|
39
|
+
Aliases are instance names (e.g. `dev-coordinator-1`). Discovery:
|
|
40
|
+
`oats status --team` lists this machine's live instances; `oats aweb roster`
|
|
41
|
+
lists the aweb team across machines.
|
|
42
|
+
|
|
43
|
+
**Never sleep, poll, or busy-wait for another agent's reply.** Send your
|
|
44
|
+
message, finish your turn, and go idle — the aweb channel awakens your
|
|
45
|
+
session the moment mail or chat arrives (you saw `✓ aweb connected` at
|
|
46
|
+
startup). A `sleep N; aw mail inbox` loop burns tokens, delays the reply,
|
|
47
|
+
and adds nothing. An empty `aw mail inbox` means no UNREAD mail — not that
|
|
48
|
+
messages were lost.
|
|
49
|
+
|
|
50
|
+
If messaging fails or your identity is missing, `oats aweb setup` diagnoses
|
|
51
|
+
the deployment's aweb state and prints the next step (report it to your
|
|
52
|
+
human rather than re-onboarding yourself).
|
|
53
|
+
|
|
54
|
+
Messaging only: task coordination lives in your deployment's task layer, and
|
|
55
|
+
`aw task`/`work`/`lock`/`roles` are not part of this integration.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"capability": "oats.aweb",
|
|
3
|
+
"command": "aweb",
|
|
4
|
+
"version": "1.8.0",
|
|
5
|
+
"compatibility": {
|
|
6
|
+
"oats": ">=0.6.2"
|
|
7
|
+
},
|
|
8
|
+
"layer": "messaging",
|
|
9
|
+
"description": "Messaging layer via aweb: per-instance team identities + native aw mail/chat skills + cross-machine team roster.",
|
|
10
|
+
"requires": [
|
|
11
|
+
{
|
|
12
|
+
"command": "aw",
|
|
13
|
+
"why": "identity minting at spawn, self-delete at retire, and all agent messaging",
|
|
14
|
+
"install": "https://aweb.ai/docs (aw CLI)"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"runtime": "pi",
|
|
18
|
+
"package": "npm:@awebai/pi",
|
|
19
|
+
"why": "the aweb channel extension for pi sessions \u2014 real-time mail/chat awakenings; without it a pi instance can send with `aw` but is never woken by incoming messages",
|
|
20
|
+
"install": "https://aweb.ai/docs (installed into pi with `pi install npm:@awebai/pi`)"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"runtime": "claude",
|
|
24
|
+
"package": "aweb-channel@awebai-marketplace",
|
|
25
|
+
"marketplace": "awebai/claude-plugins",
|
|
26
|
+
"why": "the aweb channel plugin for Claude Code sessions \u2014 real-time mail/chat awakenings; without it a Claude instance can send with `aw` but is never woken by incoming messages",
|
|
27
|
+
"install": "https://aweb.ai/docs (installs the awebai marketplace and the aweb-channel plugin into Claude Code)"
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"skills": [
|
|
31
|
+
"node_modules/@awebai/pi/skills/aweb-messaging",
|
|
32
|
+
"node_modules/@awebai/pi/skills/aweb-team-membership",
|
|
33
|
+
"node_modules/@awebai/pi/skills/aweb-identity"
|
|
34
|
+
],
|
|
35
|
+
"inject": "injects/aweb.md",
|
|
36
|
+
"commands": {
|
|
37
|
+
"roster": "bin/oats-aweb.mjs roster",
|
|
38
|
+
"setup": "bin/oats-aweb.mjs setup"
|
|
39
|
+
},
|
|
40
|
+
"hooks": {
|
|
41
|
+
"spawn": {
|
|
42
|
+
"command": "bin/oats-aweb.mjs spawn",
|
|
43
|
+
"required": true
|
|
44
|
+
},
|
|
45
|
+
"retire": "bin/oats-aweb.mjs retire"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@awebai/oats-aweb",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "OATS messaging provider: per-instance aweb identities (mint at spawn, self-delete at retire), cross-machine roster, guided setup, and agents-md injection",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"oats",
|
|
8
|
+
"aweb"
|
|
9
|
+
],
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/awebai/oats",
|
|
13
|
+
"directory": "capabilities/oats-aweb"
|
|
14
|
+
},
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"type": "module",
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@awebai/pi": "^0.2.1"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* oats-jira — OATS tasks-provider hook for Jira.
|
|
4
|
+
*
|
|
5
|
+
* Invoked by the OATS kernel at instance lifecycle events (hook contract):
|
|
6
|
+
* oats-jira spawn surface the instance's Jira identity (label) and the
|
|
7
|
+
* deployment's site/project in TASK.md — advisory only,
|
|
8
|
+
* no Jira calls, nothing to mint or clean up.
|
|
9
|
+
*
|
|
10
|
+
* Env contract (set by the kernel):
|
|
11
|
+
* OATS_EVENT spawn
|
|
12
|
+
* OATS_INSTANCE instance name (its Jira label is agent-<instance>)
|
|
13
|
+
* OATS_SETTINGS JSON of the provider's `settings:` block ({ site?, project? })
|
|
14
|
+
*
|
|
15
|
+
* Output (stdout JSON): { "meta": {...}, "brief": "...", "warning": "..." }
|
|
16
|
+
* Exit code is advisory: the kernel treats hook failure as a warning, never a block.
|
|
17
|
+
*/
|
|
18
|
+
const out = (o) => { process.stdout.write(JSON.stringify(o) + "\n"); process.exit(0); };
|
|
19
|
+
const warn = (m) => out({ warning: `oats-jira: ${String(m).slice(0, 300)}` });
|
|
20
|
+
|
|
21
|
+
const event = process.env.OATS_EVENT || process.argv[2];
|
|
22
|
+
const instance = process.env.OATS_INSTANCE;
|
|
23
|
+
const settings = JSON.parse(process.env.OATS_SETTINGS || "{}");
|
|
24
|
+
|
|
25
|
+
if (event === "spawn") {
|
|
26
|
+
const label = `agent-${instance}`;
|
|
27
|
+
const site = settings.site;
|
|
28
|
+
const project = settings.project;
|
|
29
|
+
const where = site && project ? `project ${project} on ${site}`
|
|
30
|
+
: site ? `site ${site} (project unset — ask your human or check oats doctor)`
|
|
31
|
+
: project ? `project ${project} (site unset — ask your human or check oats doctor)`
|
|
32
|
+
: `your deployment's Jira (site/project not configured — ask your human, or set capabilities.oats.jira.<target>.settings in oats-config.yaml)`;
|
|
33
|
+
out({
|
|
34
|
+
meta: { label, ...(site ? { site } : {}), ...(project ? { project } : {}) },
|
|
35
|
+
brief: `Tasks: Jira — ${where}. Your Jira identity is the label "${label}" (never the assignee field). Load the jira-tasks skill before touching tickets.`,
|
|
36
|
+
...(site && project ? {} : { warning: `oats-jira: settings incomplete (site: ${site || "unset"}, project: ${project || "unset"}) — set capabilities.oats.jira.<target>.settings.{site,project} in oats-config.yaml` }),
|
|
37
|
+
});
|
|
38
|
+
} else {
|
|
39
|
+
warn(`unknown event "${event}" (expected spawn)`);
|
|
40
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
## Tasks: Jira
|
|
2
|
+
|
|
3
|
+
Your tasks layer is **Jira** (via the `acli` CLI). Work traces up to epics;
|
|
4
|
+
you are identified by the label `agent-<your-instance-alias>` and an `Agent:`
|
|
5
|
+
line in descriptions — never by the assignee field. Load the **jira-tasks**
|
|
6
|
+
skill before reading your work queue, joining an epic's roster, posting
|
|
7
|
+
progress, transitioning status, or creating stories/tasks. Your Jira site and
|
|
8
|
+
project come from your deployment's settings (see your TASK.md briefing or
|
|
9
|
+
the skill). Tasks only: status and outcomes live in Jira; conversation lives
|
|
10
|
+
in your deployment's messaging layer.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"capability": "oats.jira",
|
|
3
|
+
"command": "jira",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"compatibility": { "oats": ">=0.6.2" },
|
|
6
|
+
"layer": "tasks",
|
|
7
|
+
"description": "Tasks layer via Jira: acli-based epic/story/task protocol, agent roster in epic descriptions, label-based agent identity.",
|
|
8
|
+
"requires": [
|
|
9
|
+
{
|
|
10
|
+
"command": "acli",
|
|
11
|
+
"why": "all Jira operations (search, view, create, transition, comment)",
|
|
12
|
+
"install": "https://developer.atlassian.com/cloud/acli/guides/install-acli/"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"skills": [
|
|
16
|
+
"skills"
|
|
17
|
+
],
|
|
18
|
+
"inject": "injects/jira.md",
|
|
19
|
+
"hooks": {
|
|
20
|
+
"spawn": "bin/oats-jira.mjs spawn"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@awebai/oats-jira",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OATS tasks provider: Jira via acli — epic/story/task protocol, agent roster, label-based agent identity, and agents-md injection",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"oats",
|
|
8
|
+
"jira"
|
|
9
|
+
],
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/awebai/oats",
|
|
13
|
+
"directory": "capabilities/oats-jira"
|
|
14
|
+
},
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"type": "module",
|
|
17
|
+
"bin": {
|
|
18
|
+
"oats-jira": "./bin/oats-jira.mjs"
|
|
19
|
+
},
|
|
20
|
+
"pi": {
|
|
21
|
+
"skills": [
|
|
22
|
+
"./skills"
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
}
|