weftens 0.1.4 → 0.1.5

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.
@@ -0,0 +1,66 @@
1
+ # Weftens — Architecture
2
+
3
+ How the code is organized. For what Weftens *is* and how it's positioned, see
4
+ the positioning doc kept outside this repo; this file is the technical map.
5
+
6
+ ## The shape
7
+
8
+ ```
9
+ weftens send "<request>"
10
+
11
+
12
+ agent-router.js ── deterministic keyword routing → one agent (+ why)
13
+
14
+
15
+ invoke.js ── runs the chosen agent by one of three paths:
16
+ ├── core-in-the-loop a deterministic agent-cores package runs on an exported
17
+ │ report and returns a checkable change sheet. No model.
18
+ ├── content-pipeline the `content` lead runs the content-org media pipeline.
19
+ └── model-produce a seat's .md is the system prompt, run through the user's
20
+ own AI CLI (provider.js) — their subscription, no API key.
21
+ ```
22
+
23
+ Richy and PIO have a second, older path of their own (`weftens ask`/`chat`/`route`): a
24
+ Claude-driven tool loop and a deterministic synthesizer (`agent.js`, `weftens.js`,
25
+ `router.js`, `synthesize.js`) that join representation + operations into one answer. The
26
+ front door routes to them; they also run standalone.
27
+
28
+ ## Files
29
+
30
+ | File | Role |
31
+ |---|---|
32
+ | `src/registry.js` | The agent directory — one entry per agent (tier, team, core, seat, routeHints). Source of truth for which agents exist. |
33
+ | `src/agent-router.js` | The **front-door** router. Deterministic, explainable keyword match over the registry. |
34
+ | `src/invoke.js` | Runs a routed agent (core CLI / content pipeline / model-produce). Degrades to a plain note; never fabricates. |
35
+ | `src/provider.js` | Bring-your-own-model. Shells the user's chosen AI CLI (config, not code). |
36
+ | `src/router.js` | The **Richy/PIO intent** classifier (representation vs operations). A sub-router, not the front door. |
37
+ | `src/weftens.js` | The Richy+PIO orchestrator — gathers from both over their wire contracts and synthesizes one answer. |
38
+ | `src/agent.js` | The Claude-driven Richy+PIO conversational agent (SDK tool loop). |
39
+ | `src/synthesize.js` | Deterministic join of representation + operations. |
40
+ | `src/richy-client.js`, `src/pio-client.js` | HTTP clients for the live Richy / PIO services (with fixtures for offline runs). |
41
+ | `bin/weftens.js` | The CLI: `list`, `send`, `--route`, and the older `ask`/`chat`/`route`. |
42
+
43
+ ## Design rules
44
+
45
+ - **Routing is deterministic and explainable.** A request matches an agent by declared keywords; the
46
+ CLI shows which matched. No model call to route — the routing layer runs with no API key.
47
+ - **Bring-your-own-model.** Model-backed agents run on the user's own AI subscription via their CLI.
48
+ No key is stored, nobody is billed through Weftens.
49
+ - **Propose-only where it matters.** The ad/SEO cores propose changes and write nothing to a live account.
50
+ - **Portable.** Paths resolve relative to the install (`WEFTENS_WORKSPACE`, `WEFTENS_AGENTS_DIR`), not
51
+ to any one machine.
52
+ - **Honest degradation.** When a step can't run (no input, missing provider CLI), invoke returns a plain
53
+ reason — never a fabricated result.
54
+
55
+ ## Invariants (do not break)
56
+
57
+ - Each agent stays independently runnable; the front door routes to them, it does not absorb them.
58
+ - Richy ↔ PIO integration is a PII-free wire contract, never repo coupling.
59
+ - PIO's wire-shape (`grantengine_session` cookie, `x-grantengine-*` headers) is frozen.
60
+ - The privacy spine — sanitized exports, human review gates, receipts — is shared across all names.
61
+
62
+ ## Naming
63
+
64
+ Weftens is the company; **Richy** and **PIO** are named agents under it (not merged into one
65
+ orchestrator). The naming history and business strategy live in the memory files and `WEFTENS_PICTURE.md`,
66
+ not here. The repo is public and the package is on npm (2026-07-19). This file is the technical map, not a gate.
package/CHANGELOG.md ADDED
@@ -0,0 +1,63 @@
1
+ # Changelog
2
+
3
+ ## 0.1.2
4
+
5
+ Applied an outside code review. Four defects, each reproduced before it was fixed.
6
+
7
+ - **The SDK and the CLI disagreed.** Specialist handling — resolving which organization a request
8
+ is about, mapping it to an intent, calling the orchestrator — was implemented in the CLI only.
9
+ `weftens("audit our listings")` from the published package returned a note saying the specialist
10
+ couldn't be invoked, while the same request through the CLI did the real work. Every test drove
11
+ the CLI, so nothing caught it. That logic moved into `invoke()`, client construction moved to
12
+ `src/specialists.js`, and `index.js` now exports the renderer. `test/sdk.test.js` drives the
13
+ entry point that actually ships.
14
+ - **Every execution failure exited 0.** A missing input file, an absent provider CLI, a specialist
15
+ with no organization — all reported success. The tool wasn't scriptable, and the CI step meant to
16
+ prove a fresh clone works couldn't fail: with `agent-cores/` missing entirely it stayed green.
17
+ - **The renderer threw on malformed input**, in the one place in the stack that must not — every
18
+ layer upstream fails soft with a plain note. Now guarded, and unit-tested as the pure function it
19
+ is (19 tests, 22ms, versus four subprocesses to assert on a string builder). It also hardcoded a
20
+ dollar sign while parsing reports denominated in the account's currency; currency now comes from
21
+ the report, or the amount prints bare.
22
+ - **The routing confidence threshold was tuned rather than derived.** The constant meant a request
23
+ silently needed three weak signals while the comment claimed two, and it refused *"review the
24
+ site"* — an ordinary sentence at a door whose whole pitch is plain language. Five entries in the
25
+ weak-signal list matched nothing in the registry at all. A new registry-invariants suite makes
26
+ both impossible to reintroduce, and it immediately found a real one: `instagram` was claimed by
27
+ both the paid-social and organic-social seats, a permanent tie decided by scoring accident.
28
+
29
+ Also: `engines` relaxed to Node >=20 (nothing required 22; the core-test runner is now portable
30
+ instead of depending on a Node 21 glob), `exports` gained `./package.json`, the bin got its exec
31
+ bit, and CI grew to eight jobs — Ubuntu and Windows across Node 20/22/24, plus a job that installs
32
+ the actual packed tarball, because a git clone and an `npm install` are different artifacts and
33
+ only one is what users get.
34
+
35
+ 207 tests.
36
+
37
+ ## 0.1.1
38
+
39
+ Rewrote the front page for someone landing on it cold, and made the output on it real.
40
+
41
+ Change sheets were emitted as raw JSON — correct for a program, useless to the person deciding
42
+ whether to act. `src/format.js` renders one as plain text: rank, what to change, where, why, what
43
+ it's worth, and what could go wrong. `--json` still returns the raw object for piping.
44
+
45
+ Two honesty fixes, now enforced by tests. "Recoverable" became "flagged": the tool measures spend
46
+ against a threshold, it does not establish that the money comes back — and the top-ranked line is
47
+ a keyword that *converted*, so the old wording contradicted its own risk note on the same screen.
48
+ "Never touches your ad account" was true of the seven deterministic auditors and stated as a
49
+ property of all 27 seats; it is now scoped where it belongs, alongside what is finished, what is
50
+ model-backed, what is a client for a service not in this repo, and what is still a placeholder.
51
+
52
+ ## 0.1.0
53
+
54
+ First working release. A deterministic router over 27 agent seats, seven of them deterministic
55
+ auditors that read an exported report and produce a ranked, propose-only change sheet with no
56
+ model involved. Model-backed seats run on an AI CLI the user already has and is logged into — no
57
+ API key, nobody billed through Weftens.
58
+
59
+ Packaging fixes in the same release: the end-to-end test resolved `agent-cores/` from the repo's
60
+ *parent*, so the suite passed only on a machine with a sibling checkout and failed in every fresh
61
+ clone. The test runner also globbed the `node --test` core suites and reported nine phantom
62
+ failures. Added the MIT license — a "free tool" with no license is unusable inside a company — a
63
+ `files` allowlist, and a README whose claims match what was actually run.
package/README.md CHANGED
@@ -23,7 +23,7 @@ wrong if you're wrong.
23
23
  That is the actual output of the sample export included in this repo, produced by:
24
24
 
25
25
  npm i weftens
26
- npx weftens send paid-search audit --input agent-cores/paid-search/test/fixtures/search-terms.csv
26
+ npx weftens send paid-search audit --input node_modules/weftens/agent-cores/paid-search/test/fixtures/search-terms.csv
27
27
 
28
28
  **Same file in, same change sheet out, every time.** The ad and SEO auditors are arithmetic, not
29
29
  a model — nothing is generated, so nothing can be invented, and two runs can be diffed to prove
@@ -57,7 +57,7 @@ Free, MIT licensed, no account, no billing.
57
57
  ## Install
58
58
 
59
59
  ```
60
- git clone git@github.com:Weftens/weftens.git
60
+ git clone https://github.com/Weftens/weftens.git
61
61
  cd weftens
62
62
  npm install
63
63
  npm test
@@ -140,7 +140,7 @@ CLI prints which words matched, so you always see why a request went where it di
140
140
  ## Honest status
141
141
 
142
142
  - **Verified:** deterministic routing, all seven auditor cores end-to-end, both specialist legs
143
- (Richy/PIO) through the door, and BYOM model-produce via `claude`. **207 tests pass — 103 unit/CLI
143
+ (Richy/PIO) through the door, and BYOM model-produce via `claude`. **217 tests pass — 113 unit/CLI
144
144
  (vitest) + 104 core (`node --test`)**, confirmed in a fresh clone at an unrelated path, not just in
145
145
  the author's workspace.
146
146
  - **Richy and PIO are interfaces here, not implementations — read this before you install.**
@@ -153,10 +153,18 @@ CLI prints which words matched, so you always see why a request went where it di
153
153
  never mistake it for a finding about a real business. Everything else in this repo — all seven
154
154
  deterministic auditors, the router, the CLI, the SDK — runs entirely on what you just cloned.
155
155
  `weftens send richy --org "Acme Co" "audit our listings"` shows you exactly this.
156
+ - **A file it cannot read is never reported as clean.** Hand an auditor the wrong export and it
157
+ says so — *"could not read this as the report this agent expects… this is NOT a finding that your
158
+ account is clean"* — and exits non-zero. Six of the seven cores are covered by this; `content-ops`
159
+ reports item counts with no monetary values, so the generic check cannot distinguish a wrong file
160
+ from a real inventory. Treat a `content-ops` run on an unexpected file with suspicion.
161
+ - **`ask` and `chat` are the exception to "no API key".** Those two commands drive a conversational
162
+ orchestrator and need `ANTHROPIC_API_KEY`. Everything else — all routing, every deterministic
163
+ auditor, and every model-backed seat via your own CLI — needs no key.
156
164
  - **The router refuses rather than guesses.** Generic words alone ("site", "listing", "people") no
157
165
  longer dispatch — a confident wrong route in front of a customer costs more than a reprompt.
158
166
  - **Licensed MIT.** Free to use, including commercially.
159
- - **Published:** `npm i weftens` installs the real thing (`weftens@0.1.4`, MIT). Verified from the
167
+ - **Published:** `npm i weftens` installs the real thing (`weftens@0.1.5`, MIT). Verified from the
160
168
  registry by a clean third-party install — `npx weftens list` prints the registry, and a
161
169
  deterministic core runs a real audit from inside `node_modules`. (`0.0.1` was an empty name-holder;
162
170
  ignore it.)
@@ -16,7 +16,6 @@ Experience" / "Ecommerce Marketing Account Director" (GH 5160651007 / 5118461007
16
16
  own acquisition channels, plan and run campaign tests, manage budget pacing, lifecycle/email
17
17
  programs, CRO, report against CAC/LTV/pipeline.
18
18
 
19
-
20
19
  ## What this agent does when run
21
20
  1. **Growth planning for real assets** — channel strategy, campaign concepts, test roadmaps, and
22
21
  funnel diagnostics for Machines Rule, client properties, or PIO's tenant work. Output is a plan
@@ -24,11 +23,7 @@ programs, CRO, report against CAC/LTV/pipeline.
24
23
  2. **Drill the ladder** — Tier-1 crash-week coaching (cert order, practice-campaign design for the
25
24
  $50 runs, platform walk-throughs), and Tier-2 concept drills (pacing scenarios, "ROAS dropped
26
25
  40% overnight — what do you check first" style).
27
- 3. **Interview prep against live JDs** — fetch a current posting, map her true experience to its
28
- bullets, build the 2–3 campaign stories WITH numbers that hold the room, rehearse the probes
29
- (tools hands-on, budget scale, what was your number). Truth rule: stories are real; numbers are
30
- real; "certified" only when certified. No invented quota history — that claim family is probed
31
- and reference-checked.
26
+
32
27
  4. **Audit-and-propose on live channels** — reads exports/analytics and proposes changes. Composes
33
28
  with the audit seats: paid-search (Google Ads), amazon-ppc, supply-verify. PROPOSE-ONLY: a
34
29
  human applies changes in any live account; no spend moved by the agent.
@@ -42,12 +37,4 @@ programs, CRO, report against CAC/LTV/pipeline.
42
37
 
43
38
  ## Boundaries
44
39
  1. Propose-only on anything live: ad accounts, budgets, email sends to real lists.
45
- 2. Job-hunt outputs follow the HONEST_CLAIMS scope rule — move fast, but resume/interview claims
46
- stay true (held = provable; the forward-looking framing lives in the skills file, not on the
47
- resume).
48
- 3. This seat drills and prepares the owner for the paid seat; interviews and the job are performed by
49
- her.
50
40
 
51
- Good output: a channel plan a hiring manager would recognize as the job, or a drill that leaves
52
- her able to answer a real JD bullet with a true story and a number. Bad output: generic growth
53
- listicles, invented metrics, or plans with no hypothesis.
@@ -55,8 +55,7 @@ this seat exists partly to build them.
55
55
  notes — the writing surface of security engineering.
56
56
 
57
57
  ## Boundaries
58
- - The paid AI-trainer gig is human-performed work — the platform pays for the owner's judgment; this
59
- agent prepares and drills her, it does not do the gig for her.
58
+
60
59
  - Defensive and educational scope only: evaluation, explanation, documentation, and defensive
61
60
  patterns. No offensive tooling beyond what understanding published material requires.
62
61
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: seo-content
3
- description: Content & entity SEO seat — the strategy-and-authority half of heavy SEO. Run on keyword/GSC/Semrush/Ahrefs data, a competitor set, or a topic to decide what a site should rank and be cited for, then turn that into briefs the content team produces. Owns keyword and entity strategy, answer-surface visibility, and rank/traffic/citation measurement. Consumes Richy's resolution audit and the technical seat's clean site; produces gated briefs, never publishes. Anchors to the content/strategy half of the Senior SEO/AEO Specialist (UPG) and SEO & Website Optimization Specialist (BrandMinded) postings.
3
+ description: Content & entity SEO seat — the strategy-and-authority half of heavy SEO. Run on keyword/GSC/Semrush/Ahrefs data, a competitor set, or a topic to decide what a site should rank and be cited for, then turn that into briefs the content team produces. Owns keyword and entity strategy, answer-surface visibility, and rank/traffic/citation measurement. Consumes Richy's resolution audit and the technical seat's clean site; produces gated briefs, never publishes.
4
4
  ---
5
5
 
6
6
  You are the content and entity SEO seat — you decide what the site should win and hand the content
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: seo-technical
3
- description: Technical SEO seat — the site-plumbing half of heavy SEO. Run on a crawl, GSC export, log-file sample, or a live URL to find what stops machines from crawling, indexing, and rendering a site correctly, and to write schema/structured data that earns rich results and AI-answer grounding. Diagnoses and proposes; site and DNS changes are the owner's/developer's to apply. Anchors to the technical half of the Senior SEO/AEO Specialist (UPG) and SEO & Website Optimization Specialist (BrandMinded) postings.
3
+ description: Technical SEO seat — the site-plumbing half of heavy SEO. Run on a crawl, GSC export, log-file sample, or a live URL to find what stops machines from crawling, indexing, and rendering a site correctly, and to write schema/structured data that earns rich results and AI-answer grounding. Diagnoses and proposes; site and DNS changes are the owner's/developer's to apply.
4
4
  ---
5
5
 
6
6
  You are the technical SEO seat — the plumbing under the search surface. Richy tells you how the
package/bin/weftens.js CHANGED
@@ -15,7 +15,7 @@
15
15
  import { readFileSync } from "node:fs";
16
16
  import readline from "node:readline";
17
17
  import { orchestrate } from "../src/weftens.js";
18
- import { isChangeSheet, formatChangeSheet } from "../src/format.js";
18
+ import { isChangeSheet, formatChangeSheet, looksUnparsed, unparsedNotice } from "../src/format.js";
19
19
  import { askWeftens } from "../src/agent.js";
20
20
  import { buildSpecialistClients } from "../src/specialists.js";
21
21
  import { route } from "../src/agent-router.js";
@@ -153,6 +153,13 @@ async function cmdSend(args, { routeOnly } = {}) {
153
153
  if (outcome.result !== undefined) {
154
154
  // A change sheet renders for a human by default — the whole point of one is that a
155
155
  // person can read a line and decide. `--json` returns the raw object for piping.
156
+ if (!asJson && looksUnparsed(outcome.result)) {
157
+ process.stdout.write(`
158
+ ${unparsedNotice(outcome.result)}
159
+ `);
160
+ process.exitCode = 1;
161
+ return;
162
+ }
156
163
  if (!asJson && isChangeSheet(outcome.result)) {
157
164
  process.stdout.write(`\n${formatChangeSheet(outcome.result)}\n`);
158
165
  } else {
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "weftens",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
- "description": "Weftens \u2014 one front door that routes a marketing/representation request to the right agent and runs it on your own AI subscription (bring-your-own-model). Usable as a CLI or an SDK.",
5
+ "description": "Weftens one front door that routes a marketing/representation request to the right agent and runs it on your own AI subscription (bring-your-own-model). Usable as a CLI or an SDK.",
6
6
  "main": "index.js",
7
7
  "exports": {
8
8
  ".": "./index.js",
@@ -23,6 +23,8 @@
23
23
  "providers.json.example",
24
24
  "README.md",
25
25
  "LICENSE",
26
+ "CHANGELOG.md",
27
+ "ARCHITECTURE.md",
26
28
  "!**/.preflight",
27
29
  "!**/.preflight/**"
28
30
  ],
@@ -39,7 +41,8 @@
39
41
  "vitest": "^2.0.0"
40
42
  },
41
43
  "dependencies": {
42
- "@anthropic-ai/sdk": "^0.110.0"
44
+ "@anthropic-ai/sdk": "^0.110.0",
45
+ "weftens": "^0.1.4"
43
46
  },
44
47
  "repository": {
45
48
  "type": "git",
package/src/format.js CHANGED
@@ -35,6 +35,80 @@ export function isChangeSheet(result) {
35
35
  return Boolean(result && typeof result === "object" && Array.isArray(result.changes));
36
36
  }
37
37
 
38
+
39
+ // A core that was handed the WRONG report does not usually error — it parses the rows, finds none of
40
+ // the columns it wants, and returns a well-formed sheet with zero changes. Rendered normally that
41
+ // reads as "your account is clean", which is the most expensive lie this tool could tell: a user
42
+ // exports the wrong file and is told there is nothing to fix. Five of seven cores did exactly this.
43
+ //
44
+ // The signal is that rows were read but every money/volume figure came back zero or null. A real
45
+ // account under audit always has spend. An account with genuinely zero activity is indistinguishable
46
+ // — and for that one, "this doesn't look like the right export, or there was no activity" is also
47
+ // the truthful answer.
48
+ // Two different kinds of number, and conflating them is what let four cores slip past the first two
49
+ // versions of this check. ROW fields count what was READ — a non-zero row count proves the file was
50
+ // opened, not that it was understood. VALUE fields are what the report is FOR. The signal for "wrong
51
+ // file" is: rows were read, and every value came back zero.
52
+ const ROW_FIELDS = ["rows", "terms", "placements", "urls", "pages", "items", "searchTermRows", "campaignRows"];
53
+ const VALUE_FIELDS = [
54
+ "totalCost", "totalSpend", "totalClicks", "totalImpressions", "totalConversions",
55
+ "totalResults", "spend", "cost", "clicks", "impressions", "conversions", "sales", "orders",
56
+ "flaggedSpend", "reclaimableSpend", "totalViews", "views",
57
+ ];
58
+
59
+ export function looksUnparsed(result) {
60
+ if (!result || typeof result !== "object") return false;
61
+ if (Array.isArray(result.changes) && result.changes.length > 0) return false;
62
+ // Cores don't share one result shape: some report totals under `account`, others under `summary`,
63
+ // others at the top level. Check wherever the volume figures actually live rather than assuming a
64
+ // shape — assuming `account` is why four of the five cores slipped past the first version of this.
65
+ // Every core names its totals block differently — `account`, `summary`, `totals` — and the first
66
+ // two versions of this guard each missed the cores they didn't happen to name. Scan any one-level
67
+ // block that carries volume fields instead of guessing at keys; a new core is covered for free.
68
+ const blocks = [result, ...Object.values(result).filter((v) => v && typeof v === "object" && !Array.isArray(v))];
69
+ const zero = (v) => v === 0 || v === null || v === undefined;
70
+
71
+ // Strongest signal first: a core that reports it read ZERO rows read nothing at all. Whatever the
72
+ // file was, it was not this report.
73
+ for (const b of blocks) {
74
+ for (const f of ROW_FIELDS) if (b[f] === 0) return true;
75
+ }
76
+
77
+ for (const b of blocks) {
78
+ const values = VALUE_FIELDS.filter((f) => f in b && typeof b[f] !== "object");
79
+ if (values.length === 0) continue;
80
+ if (!values.every((f) => zero(b[f]))) continue;
81
+ // Every value is zero. If rows were read, the file parsed as text but not as this report.
82
+ // If no row count is reported either, an all-zero result is still not a clean bill of health.
83
+ return true;
84
+ }
85
+ return false;
86
+ }
87
+
88
+ /** Row count from wherever a core reports it — used only to make the refusal message concrete. */
89
+ function rowsRead(result) {
90
+ if (!result || typeof result !== "object") return null;
91
+ const blocks = [result, ...Object.values(result).filter((v) => v && typeof v === "object" && !Array.isArray(v))];
92
+ for (const b of blocks) {
93
+ for (const f of ROW_FIELDS) if (typeof b[f] === "number" && b[f] > 0) return b[f];
94
+ }
95
+ return null;
96
+ }
97
+
98
+
99
+ /** The one message for "this file did not parse", shared by every core shape. */
100
+ export function unparsedNotice(result) {
101
+ const rows = rowsRead(result);
102
+ return [
103
+ "Could not read this as the report this agent expects.",
104
+ typeof rows === "number" && rows > 0
105
+ ? ` ${rows} row(s) were read, but none of the expected columns (spend, clicks, results) had values.`
106
+ : " No rows with the expected columns were found.",
107
+ " This is NOT a finding that your account is clean — it means the file did not parse.",
108
+ " Check you exported the right report for this agent, then run it again.",
109
+ ].join("\n");
110
+ }
111
+
38
112
  /**
39
113
  * Render a change sheet as plain text. Never invents a field: anything missing is simply
40
114
  * not printed, so a core that doesn't produce risk/evidence still renders cleanly.
@@ -50,6 +124,9 @@ export function formatChangeSheet(result) {
50
124
  const changes = result.changes.filter((c) => c && typeof c === "object");
51
125
  const dropped = result.changes.length - changes.length;
52
126
 
127
+ // Refuse to present an unreadable file as a clean bill of health.
128
+ if (looksUnparsed(result)) return unparsedNotice(result);
129
+
53
130
  const flagged = money(result.wastedSpendRecoverable, currency);
54
131
  const header = [`${changes.length} proposed change${changes.length === 1 ? "" : "s"}`];
55
132
  // "flagged", not "recoverable": the tool measured spend against a threshold, it did not