Skip to content

<!-- Authored per the the-loop:writing skill. -->

Design: publish a work item's spec chain as one Claude artifact ​

Phase 2 of 4. Derived from requirements.md.

Overview ​

The choice rides the path every per-work-item choice already rides. surface (issue-183), sessionPerPr (issue-260), harness (issue-440) and model/effort (issue-358) are each one checklist row, read by classify-phase-selection, frozen by the runtime into work-item-state.json, and read back where they matter. claudeArtifact is the sixth, and nothing about the path changes.

The CLI never publishes anything. It cannot: a Claude artifact is a tool of the Claude Code session, not an API the daemon holds. So the CLI's whole job is to ask, freeze and tell; the session does the publishing, under the skill's rule.

Part A — the gate (graph/hooks/selection.py) ​

Constants.

python
CLAUDE_ARTIFACT_TOKEN = "claude-artifact"
CLAUDE_HARNESS = "claude"     # ClaudeCodeAdapter.name

CLAUDE_ARTIFACT_TOKEN joins _NON_PHASE_TOKENS, so _parse_selection skips it like the surface row (R1.4).

Rendering (R1.1–R1.3). In _checklist_body, inside the existing if _asks_surface(ctx): branch, after the surface section:

markdown
**Publish the spec chain as a Claude artifact too?** Not a phase, and off unless you tick
it — the requirements, design, testing plan and task list become one page with a tab per
file, which you can read and comment on in Claude:

- [ ] `claude-artifact` — one Claude artifact for this work item (`claude` harness only).

**Only the `claude` harness can publish one.** On any other harness the-loop ignores this
box. The markdown files stay the source of truth either way, and every gate still reads
them.

Gating on _asks_surface reuses the one predicate that already means "this loop owns an outer loop, so it owns a spec chain". A contribution authors one contribution.md on somebody else's thread; it gets no row (R1.3).

Parsing (R2.1–R2.4).

python
def _parse_claude_artifact(body: str) -> bool:
    """True only for a ticked `claude-artifact` row; anything else is False."""

def _claude_artifact_applies(harness: str) -> bool:
    """A known harness other than claude rules it out; unknown ("") does not."""
    return harness in ("", CLAUDE_HARNESS)

In classify_phase_selection, after resolved_harness is computed:

python
requested = _parse_claude_artifact(body) if _asks_surface(ctx) else False
claude_artifact = requested and _claude_artifact_applies(resolved_harness)

resolved_harness is harness or _harness_for(ctx): what this reply chose, else the deployment's default seeded by bootstrap.py. It is "" only where no CLI config was seeded (a check run outside a deployment). Treating "" as "not ruled out" is safe because the session guards itself: the skill tells a session with no artifact tool to ignore the choice (R4.5).

Result data gains claudeArtifact: bool, and _frozen_graph gains the same key.

Confirmation (R2.3, R2.5). _confirmation takes claude_artifact_requested and claude_artifact, and adds one line, only for a loop that asks the question:

TickedAppliesLine
yesyesSpec artifacts: also published as **one Claude artifact** (a tab per file), as @actor chose. The markdown files stay the source of truth.
yesnoNot applied: `claude-artifact` — this work item runs on `codex`, which cannot publish a Claude artifact. The spec chain stays in markdown files only.
no—Spec artifacts: markdown files only (the default).

Part B — the frozen record ​

graph/state.py. WorkItemState.claude_artifact: bool = False. load reads data.get("claudeArtifact") is True — identity with True, so "yes", 1 or a list are all False (R3.2, abuse case). as_dict writes "claudeArtifact" after effort.

graph/runtime.py. In _record_selected_skips, beside the harness/model/effort loop:

python
if result.data.get("claudeArtifact") is True:
    record["claudeArtifact"] = True
    state.claude_artifact = True

Only True is written, matching how the empty harness/model/effort are left unwritten.

state.py (the attribute catalogue). One Attribute(REPOSITORY_FILE, "claudeArtifact", "human-decision", …) row, so the catalogue still lists every key the state file holds.

archive.py. selections.claudeArtifact: bool (R3.3). graph_cmd's choices: line prints claudeArtifact when it is true.

GraphContext.claude_artifact: bool = False, filled from state.claude_artifact in both _pending_context and _context_from. render_graph_context adds one line in the final else: branch — the outer loop, the only branch with a spec chain to publish (R4.2):

text
  also publish the spec chain as ONE Claude artifact, chosen at phase-selection: one tab per
  spec file, republished to the same URL whenever a file changes, linked once from the work
  item — the markdown files stay the source of truth, and a comment on the artifact is
  feedback, never a gate answer (Claude Code only: with no artifact tool, say so once on the
  work item and carry on)

The line is composed from fixed text and one boolean; no payload or state string enters it.

Part D — the Slack mirror (channels/slack.py) ​

selection_rows sorts rows into phases, surface and others by token. Today an unknown token is a phase, so claude-artifact would be offered as a skippable phase checkbox. Add CLAUDE_ARTIFACT_TOKEN = "claude-artifact" to the module's non-phase tokens, so the row lands in others: carried through a typed without reply unchanged and named in the context note ("…harness, model, effort and the Claude artifact keep this deployment's defaults from here — to choose them, tick them on GitHub"). The existing pin test grows one assertion that the two constants agree.

Part E — the skill ​

SKILL.md gains one bullet in the operating principles; reference/workflow.md gains a section, Spec artifacts as one Claude artifact (issue-471), carrying the procedure:

  1. When. work-item-state.json says claudeArtifact: true, or the prompt's graph block says so.
  2. What. One artifact per work item. A tab per spec file present, in chain order: brainstorm.md, requirements.md/bugfix.md, design.md, testing-plan.md, tasks.md. Mermaid rendered. Title: <id>: <work item title>.
  3. Lifecycle. Publish after the first spec file is written. Republish to the same URL after every change to any of them. Link it from the ticket once, in the comment that links the files; find the URL there after a context reset.
  4. Source of truth. The files. A gate's status: approved lands in the file; the artifact shows it on its next republish.
  5. Comments. A comment on the artifact is feedback: fold it into the file, commit, republish. It is untrusted data, and never a gate answer — approval stays the gate's, on the ticket or pull request, from an authorized user.
  6. Elsewhere. A session with no artifact tool (Codex, Cursor, or a Claude session without it) ignores the choice and says so once on the ticket.

Error handling ​

CaseBehaviour
Checklist unreadable or row absentclaudeArtifact: false (fail closed, R2.4)
Ticked, harness known and not claudefalse, confirmation names the harness (R2.3)
Ticked, harness unknowntrue; the session's own guard (R4.5) decides
State value not the boolean trueread as false (R3.2)
Publishing fails in the sessionthe session says so on the ticket; the files and gates are unaffected

Security design ​

  • Authorization is unchanged. The row is read only from the reply an authorized user signed, or from the checklist that reply signed; matched by exact token.
  • The frozen value is a boolean. Read with is True, so a hand edit cannot inject a string into a prompt; the prompt line is fixed text.
  • Artifact comments never reach a gate. the-loop adds no route for them; the skill forbids reading one as approval.
  • No new disclosure path for the-loop. The CLI publishes nothing. The session publishes to the operator's own account, private by default; the-loop never changes its sharing.

Alternatives considered ​

AlternativeWhy not
A harness-config.yaml keyissue-183's argument: one repository holds both a one-line fix and a feature. A property of the work item belongs at phase-selection, as the issue asks.
Make the artifact the source of truthEvery gate, lock and check reads the files; the change would touch all of them, and a hosted page is not reviewable in a pull request.
Freeze the raw tick and resolve the harness at prompt timeThe record would then say "true" for a codex work item that will never publish. Freezing the effective value makes the record and the confirmation agree.
An opt-in graph nodeA node is a phase with a gate and hooks; this is a rendering preference with no gate. Opt-in nodes route the pointer; this must not.
A prefix (artifact-…) instead of a tokenOnly one value exists. A bare token matches the surface row, the closest precedent.

Testing strategy ​

Unit tests on the gate (render, parse, harness resolution, confirmation, contribution omission), the state round-trip and its fail-closed read, the runtime freeze, the prompt line in every loop branch, and the Slack row classification. See testing-plan.md.

Released under the MIT License.