feat: Substack control surface — five nouns over stdlib HTTP and webglass (v0.10.0) - #5
Conversation
`/init` replaced the scaffold seed `CLAUDE.md` with a runtime prompt grounded in this repo, and the other three harness files were brought in line with it — each written for its own harness rather than restating the same text. - CLAUDE.md: command block (tests, single test, every lint/rubric/smoke gate), the cross-file CLI wiring (`_CliArgumentParser.error()` override, the `_json_hint` argv peek, the `parser_class` propagation trap, `CliError`, the stdout/stderr split, the catalog walked by `tests/test_cli.py`), a five-place checklist for adding a Substack noun, and `_PROMPT_FILE` vs `_RESIDENT_PROMPT`. - QWEN.md: the coding-session machinery for a Qwen Code session. - AGENTS.override.md: context only (identity stays in `.pi/SYSTEM.md`), and tells Pi's non-coding lane to report the scaffold gap rather than infer an implementation from the project description. - AGENTS.colleague.md: rewritten around colleague as the `/ask-colleague` second mind — per-verb expectations, the contracts a diff must respect, and the colleague#494 empty-skills caveat. - README.md: template-clone instructions replaced by this repo's own Status / CLI / Development sections. Fixes four drifts the docs carried: a Substack surface (posts, comments, statistics, subscribers) that does not exist on disk; a quickstart using `uv run substack-cli …` when `[project.scripts]` installs the binary as `substack`; a skill count of 11 against 19 on disk; and three files calling `CLAUDE.md` "the fullest write-up" while it was still the `/init` seed. Verified: 117 tests pass, `teken cli doctor . --strict` PASS, `harness-smoke --stage config` 6/6 PASS, scan-secrets clean, markdownlint clean, `devex pr lint` no violations. Qwen and Pi were asked "Who are you, and what can you do?" in this tree and both answered from their new files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VaCYnmwSaYL2DLxDtd1fTs
The Worktrees section claimed the vendored skill's fan-out example uses a shared `../worktrees/` path that must be overridden. It does not: the current `.claude/skills/assign-to-workforce/SKILL.md` mandates the same repo-named `.worktrees.<repo-name>` root this file does, and explicitly says "Never use a bare `../worktrees/`". The claim was inherited from culture-agent-template's CLAUDE.md and describes an older revision of the vendored file. Only the branch-name half of the override still applies (`agent/<task-id>`). Found by `ask-colleague review` on this PR. Its other two findings: the "eight skills originate in devague" phrasing is correct as written (docs/skill-sources.md lists exactly eight devague-origin rows, all re-broadcast via guildmaster — the direct-from-devague vendoring it recalled is marked superseded in that ledger), and the code-side "clonable template" strings are filed as #3 rather than folded into a doc-only PR. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VaCYnmwSaYL2DLxDtd1fTs
…ecision Six inline findings from the Qodo review on PR #2, each verified against disk before acting: - Mark the Substack surface `(planned)` at first mention in all four harness files and on the "Adding the Substack surface" heading, per this repo's own rule that anything ahead of disk carries the marker. - Note that `markdownlint-cli2` is an npm prerequisite, not something `uv sync` installs — CI installs it separately, so a clean checkout following the documented sequence stopped there. - Stop implying all four harnesses load the shared skill tree: the symlinks are shared wiring, but colleague 1.76.0 loads 0 of the 19 (colleague#494). The caveat was in AGENTS.colleague.md only; it is now in CLAUDE.md and QWEN.md where the "one tree, four loaders" claim was made. - Qualify the CliError contract. "Every failure raises CliError" contradicted two deliberate paths: `_CliArgumentParser.error()` emits a CliError then raises SystemExit, and `doctor` returns 1 for an unhealthy report rather than raising. Both are now named so neither gets "fixed" by a future change. - AGENTS.colleague.md claimed nothing from a read-only run reaches the asker's checkout. Tracked files, correct — but the run artifact is copied into the gitignored `.colleague/` dir for grading. Stated. - docs/skill-sources.md listed 17 rows while `.claude/skills/` holds 19: `recall` and `remember` were vendored without ledger entries. Added both (guildmaster, scope literal adapted), plus a header line accounting for all 19. Qodo read the gap as "guildmaster does not supply these two" — it does ship both; the defect was the missing rows, not the supplier. Also corrected the kit description in CLAUDE.md, QWEN.md and README.md: 19 skills is 17 guildmaster + ask-colleague direct from colleague, not a single "canonical guildmaster kit". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VaCYnmwSaYL2DLxDtd1fTs
Converged frame for the Substack control surface: post/feed/comment/ reaction/account nouns, webglass-cli as the runtime browser plane, public read verbs first, owner verbs gated on webglass authenticated sessions (agentculture/webglass-cli#17). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CH5jAgaWUmCiXBJ3nKWdUV
Rigorous pass (hard-to-reverse publish + security-sensitive session): webglass has no network lens (capture via Chrome-MCP now, lens asked in webglass-cli#17), two API hosts, markdown->ProseMirror is real work, no auto-retry on writes, partial-state reporting, three-state auth probe, untrusted third-party text, containment twins for every write verb, draft-first publish with --no-email for live proofs. One self-reported lapse (l1) confirmed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CH5jAgaWUmCiXBJ3nKWdUV
17 tasks in 7 waves covering all 56 spec targets: baseline, HTTP transport, webglass adapter, untrusted-text helper, Chrome capture; five nouns; post write side + markdown body; parser/learn/catalog wiring; docs, version bump, CI gates; public and owner live proofs. Owner proof (t18) is a follow-up risk on webglass-cli#17. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CH5jAgaWUmCiXBJ3nKWdUV
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CH5jAgaWUmCiXBJ3nKWdUV
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CH5jAgaWUmCiXBJ3nKWdUV
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CH5jAgaWUmCiXBJ3nKWdUV
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CH5jAgaWUmCiXBJ3nKWdUV
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CH5jAgaWUmCiXBJ3nKWdUV
# Conflicts: # substack_cli/substack/__init__.py
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CH5jAgaWUmCiXBJ3nKWdUV
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CH5jAgaWUmCiXBJ3nKWdUV
Captured 2026-09-13 in the owner's logged-in browser on jetsonailab.substack.com: drafts create/update/publish/schedule/ unschedule/unpublish/delete, comments create/reply/delete, post and comment reactions add/remove, public archive/post/comments reads, the subscription endpoint as the whoami source, and the substack.com reader feed. Unobserved paths are marked as such. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CH5jAgaWUmCiXBJ3nKWdUV
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CH5jAgaWUmCiXBJ3nKWdUV
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CH5jAgaWUmCiXBJ3nKWdUV
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CH5jAgaWUmCiXBJ3nKWdUV
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CH5jAgaWUmCiXBJ3nKWdUV
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CH5jAgaWUmCiXBJ3nKWdUV
# Conflicts: # substack_cli/substack/http.py
…own body builder Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CH5jAgaWUmCiXBJ3nKWdUV
# Conflicts: # substack_cli/substack/http.py # tests/fakes/webglass/webglass
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CH5jAgaWUmCiXBJ3nKWdUV
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CH5jAgaWUmCiXBJ3nKWdUV
The live public-read proof returned 403: Substack rejects urllib's default Python-urllib agent (a descriptive substack-cli/<version> agent is accepted; verified with curl). The GET loop also retried every HTTPError; it now retries only 429, 5xx and transport errors, per spec claim c38. Two tests that asserted four attempts on a 404 now assert one. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CH5jAgaWUmCiXBJ3nKWdUV
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CH5jAgaWUmCiXBJ3nKWdUV
… prompts Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CH5jAgaWUmCiXBJ3nKWdUV
Evidence stored as a text record with a header line (JSON body): scripts/scan-secrets.py fails any JSON-parsable file whose url keys point off localhost, and the t2 invariant forbids tracked *.json files naming substack.com. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CH5jAgaWUmCiXBJ3nKWdUV
Partial run: 16 of 17 plan tasks delivered, t18 (owner-half live proof) blocked on agentculture/webglass-cli#17. Deviations d1-d4, lapses l1-l15, validation ledger o1-o23/e1-e22/b1-b4 all adjudicated. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CH5jAgaWUmCiXBJ3nKWdUV
|
/agentic_review |
PR Summary by QodoAdd five-noun Substack control surface via HTTP and webglass
AI Description
Diagram
High-Level Assessment
Files changed (52)
|
Code Review by Qodo
1.
|
…nflicts) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CH5jAgaWUmCiXBJ3nKWdUV
…998832725, 3998832730, 3998832732, 3998832734, 3998832736, 3998832739) - 3998832718: `comment list` now sends the observed query (all_comments=true&sort=best_first, urlencoded) and flattens nested `children` replies depth-first, parents before replies, keeping parent_id/ancestor_path on each item. - 3998832722: reaction add/remove build their URL from http.publication_base(), so SUBSTACK_API_BASE reaches writes as it does reads. - 3998832725: `feed read` builds limit/cursor with urllib.parse.urlencode, so a cursor containing &, #, + or % can no longer forge or truncate the query. - 3998832730: run_webglass passes a finite timeout (120s, SUBSTACK_WEBGLASS_TIMEOUT; invalid -> CliError(1)) and maps TimeoutExpired to CliError(2) naming the variable. - 3998832732: http passes a finite timeout to opener.open (30s, SUBSTACK_HTTP_TIMEOUT; invalid -> CliError(1)); timeouts retry on GET and raise a single CliError(2) on writes. - 3998832734: UnicodeDecodeError/JSONDecodeError around response decoding on both the GET and write paths -> CliError(2) naming the URL, never retried. - 3998832736: an argparse usage/unknown-verb answer from webglass now maps to CliError(2) saying webglass-cli has no authenticated request verb yet, citing agentculture/webglass-cli#17. The verbs stay registered. - 3998832739: --body-json must decode to {"type": "doc", "content": [...]}; anything else is CliError(1) with the ProseMirror remediation. - 3998832738 (--send): behaviour unchanged by design; the help text and docstring now say the send:true path is unverified against the live API. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CH5jAgaWUmCiXBJ3nKWdUV
…8513, S3358, S5799, S9073, S9100) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CH5jAgaWUmCiXBJ3nKWdUV
…a composite assert (S9073) HTTPError derives from URLError, so catching both is redundant; the behaviour is unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CH5jAgaWUmCiXBJ3nKWdUV
… HTTP branch (S3776), merge a literal (S5799) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CH5jAgaWUmCiXBJ3nKWdUV
|



What
The v1 Substack control surface: five nouns (
account,post,comment,reaction,feed), 24 command paths, every verb--json. Public read verbs (post list/get,comment list,reaction list) run over stdlib HTTP with no session. Owner verbs (post publish/schedule/unpublish/delete,comment reply/delete,reaction add/remove,feed read,account whoami) go through the siblingwebglassbinary as a subprocess and needSUBSTACK_WEBGLASS_SESSION. Runtime dependencies stay[]; nothing imports a browser-automation library.Every endpoint the CLI calls was observed in the owner's logged-in browser on 2026-09-13 and is documented in
docs/api/substack-endpoints.md; unobserved paths are marked as such and do not ship. Publish is draft-first: without--sendonly a draft is created;--send --no-emailpublishes to the site without emailing subscribers;--sendalone emails every subscriber and warns on stderr first.How it was built
devague flow end to end:
/scope→/think→/challenge→/spec-to-plan→/assign-to-workforce(17 tasks, 7 waves, one worktree per task, TDD-gated merges) →/deviate→/validate-delivery→/summarize-delivery. Artifacts in this PR:docs/specs/2026-09-12-substack-domain-surface.mddocs/plans/2026-09-12-substack-domain-surface.md+ approved split…-split.mddocs/api/substack-endpoints.mddocs/plans/evidence/baseline-learn.txt,proof-public.txtdocs/deliveries/2026-09-12-substack-domain-surface.md— the review map: every plan task accounted for, four approved deviations (Workforce run: deviations from the substack-domain-surface plan (d1-d3) #4), 15 self-reported lapses, and a validation ledger with one recorded failureStatus: partial run
16 of 17 plan tasks delivered.
t18(owner-half live proof) is blocked: webglass-cli 0.8.3 cannot create an authenticated persistent session or issue a request from one (agentculture/webglass-cli#17). Owner verbs therefore exit 2 with a hint until that lands; the public half is proven live against two publication hosts.Known, recorded, not fixed here:
comment liston an unknown post exits 2 wherepost getandreaction listmap a 404 to exit 1 (deltab3).Gates
uv run pytest -n auto: 329 passed, 1 pre-existing skip; coverage 96 % (floor 60 enforced in CI)teken cli doctor . --strict, harness-smoke config: clean.pi/SYSTEM.md,doctor.py, vendored skills, publish workflow and Sonar config untouched (asserted bytests/test_repo_invariants.py)🤖 Generated with Claude Code
https://claude.ai/code/session_01CH5jAgaWUmCiXBJ3nKWdUV