npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@gonovus-ai/spec-flow-cli

v0.6.0

Published

SpecFlow supported command-line client

Readme

SpecFlow CLI

specflow auth login authorizes one independently revocable CLI Session with the User's current SpecFlow permissions. There is no Workspace, Project or action chooser. New Workspaces and Projects are usable after creation without another login; membership changes, Role reductions, User disablement and Session revocation remain effective immediately. Account security and management of other Sessions remain Web-only.

specflow auth login
specflow workspace list

Existing explicitly restricted Sessions keep their approved scope. Log in again without --grants to use the normal User-permission mode. User API Keys retain explicit resource grants and do not inherit personal account capabilities.

The byte-only work-item asset put --reservation-file <path> --file <path> accepts signed S3 and Azure BlockBlob reservations, including reservations from map screen image-reserve. Save reservation JSON in an owner-only file and keep its URL out of logs. Uploading bytes alone does not attach an asset; complete the matching task or Screen finalization command separately.

The current CLI contract is 0.6.0, coordinated with the official Skill's exact pin. It breaks 0.2.0 callers: Spec, Capability, Glossary, Proposal and authoring-history commands address shared knowledge with --workspace-id instead of --project-id, project directory is replaced by spec list, Session grants separate WorkspaceKnowledge and ProjectTasks targets, and Workspace read-only Tokens replace Project Tokens. Failures also carry the public error.code, the HTTP error.status and optional field-level error.issues, and Workspace-scoped Product Map commands are new. Upgrade the Skill and the CLI together.

@gonovus-ai/spec-flow-cli uses OAuth Device Authorization to create an independent seven-day Better Auth CLI Session. It supports the Node.js 24 runtime line; Node.js 22 and unverified future major versions are intentionally unsupported.

npm install --global @gonovus-ai/[email protected]
specflow auth login
specflow auth status
specflow auth logout

The published package's default service origin comes from the GitHub repository Actions variable SPEC_FLOW_DEFAULT_API_URL, embedded at build time. Both authentication and resource commands resolve --server first, then runtime SPEC_FLOW_API_URL, then that packaged default. Keep the same origin for login and subsequent commands. Credentials are isolated by origin; a different environment requires its own login.

Changing the repository variable requires publishing a new CLI version; existing installations keep their packaged default. The release workflow rejects a missing, empty or invalid origin. Skill instructions do not need a deployment address.

Local builds without SPEC_FLOW_DEFAULT_API_URL have no implicit service target; pass --server or set runtime SPEC_FLOW_API_URL. To build a local package with a default, provide SPEC_FLOW_DEFAULT_API_URL to the CLI build command. The packed installation smoke uses an isolated .invalid test origin when no build value is provided; it never substitutes a deployment URL into source.

After login, resource commands reuse the stored CLI Session and emit structured JSON on stdout. Existing-Project commands always require an explicit --project-id:

SPEC_FLOW_API_URL=https://specflow.example specflow workspace list
SPEC_FLOW_API_URL=https://specflow.example specflow workspace create --name "Core Platform"
SPEC_FLOW_API_URL=https://specflow.example specflow project list
SPEC_FLOW_API_URL=https://specflow.example specflow project list --workspace-id <workspace-id>
SPEC_FLOW_API_URL=https://specflow.example specflow project list --status Archived
SPEC_FLOW_API_URL=https://specflow.example specflow project list --status Trashed
SPEC_FLOW_API_URL=https://specflow.example specflow project create --name "Payments" --workspace-id <workspace-id>
SPEC_FLOW_API_URL=https://specflow.example specflow project archive --project-id <project-id> --expected-version '"1"'
SPEC_FLOW_API_URL=https://specflow.example specflow project restore --project-id <project-id> --expected-version '"2"'
SPEC_FLOW_API_URL=https://specflow.example specflow project trash --project-id <project-id> --expected-version '"2"'
SPEC_FLOW_API_URL=https://specflow.example specflow spec list --workspace-id <workspace-id>
SPEC_FLOW_API_URL=https://specflow.example specflow spec read --workspace-id <workspace-id> --spec-id <spec-id>
SPEC_FLOW_API_URL=https://specflow.example specflow spec current --workspace-id <workspace-id> --spec-id <spec-id>
SPEC_FLOW_API_URL=https://specflow.example specflow spec revision --workspace-id <workspace-id> --spec-id <spec-id> --revision-id <revision-id>
SPEC_FLOW_API_URL=https://specflow.example specflow spec changelog --workspace-id <workspace-id> --spec-id <spec-id>
SPEC_FLOW_API_URL=https://specflow.example specflow spec edit --workspace-id <workspace-id> --spec-id <spec-id> --content "$(cat payments.md)"
SPEC_FLOW_API_URL=https://specflow.example specflow spec assign-scenario-ids --workspace-id <workspace-id> --spec-id <spec-id>
SPEC_FLOW_API_URL=https://specflow.example specflow spec publish --workspace-id <workspace-id> --spec-id <spec-id>
SPEC_FLOW_API_URL=https://specflow.example specflow workspace glossary edit --workspace-id <workspace-id> --entries '[{"id":"payment","canonicalTerm":"Payment","definition":"A completed transfer.","usageNote":null}]'
SPEC_FLOW_API_URL=https://specflow.example specflow proposal create-spec --workspace-id <workspace-id> --spec-id <spec-id> --baseline-content "$(cat payments.md)" --proposed-content "$(cat payments.md)" --agent-revision <revision> --agent-model <model> --source-reference-ids '["source-id"]'
SPEC_FLOW_API_URL=https://specflow.example specflow proposal list --workspace-id <workspace-id> --target-kind Spec --spec-id <spec-id> --status Open
SPEC_FLOW_API_URL=https://specflow.example specflow proposal apply --workspace-id <workspace-id> --proposal-id <proposal-id> --change-ids '["hunk-1"]'
SPEC_FLOW_API_URL=https://specflow.example specflow proposal discard --workspace-id <workspace-id> --proposal-id <proposal-id>
SPEC_FLOW_API_URL=https://specflow.example specflow authoring-history --workspace-id <workspace-id>

Every official CLI service request sends the versioned @gonovus-ai/spec-flow-cli/<version> User-Agent. Diagnostics use stderr, and stable Skill Errors use documented reason and exit-status categories.

Optional Task resource links

Use work-item resource-link list --project-id PAY --work-item-id PAY-7 to read current authorized references. Add a stable Spec with add --kind spec --spec-id PAY-S8 --workspace-id <workspace-id> in the Task’s owning Workspace. Add a document or other locator with add --kind document|other --title <name> --url <http-or-https-url>. Every add requires --idempotency-key <uuid> and the owning Project/Task flags. Retain that key for an ambiguous retry of the same input.

Unlink with remove --link-id <uuid> --expected-version <opaque-version> --idempotency-key <uuid> and the Project/Task flags. The target and Task remain independent. spec work-items --workspace-id <workspace-id> --spec-id <id> [--limit <n>] [--cursor <opaque-cursor>] reads paged authorized reverse Task references. These commands read current metadata without pinning a Revision, importing content, or changing Task completion or Spec publication. External locator names are user-authored Task data; opening a destination uses that destination's authorization.

Machine contract

Resource-command success writes exactly one newline-terminated JSON object to stdout. stderr is newline-delimited JSON (NDJSON): zero or more diagnostics use { "diagnostic": { "level": "warning", "message": string } }. These command/result pairs are the supported schemas:

| Command | Success object | | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | workspace list | { workspaces: WorkspaceSummary[] } | | workspace create | { workspace: WorkspaceSummary } | | workspace member list --workspace-id <id> | { members: WorkspaceMember[] } | | workspace member search --workspace-id <id> --query <q> | { candidates: WorkspaceDirectoryCandidate[] } | | workspace member add --workspace-id <id> --user-id <userId> --role <role> | { member: WorkspaceMember } | | workspace member set-role --workspace-id <id> --user-id <userId> --role <role> --expected-version <v> | { member: WorkspaceMember } | | workspace member remove --workspace-id <id> --user-id <userId> --expected-version <v> | { removed: { userId: string } } | | workspace role list --workspace-id <id> | { roles: WorkspaceRole[] } | | workspace role create --workspace-id <id> --name <name> --statements <json> | { role: WorkspaceRole } | | workspace role update --workspace-id <id> --name <name> --statements <json> --expected-version <v> | { role: WorkspaceRole } | | workspace role delete --workspace-id <id> --name <name> --expected-version <v> | { deleted: { name: string } } | | project list [--status Active\|Archived\|Trashed] [--workspace-id <workspace-id>] | { projects: ProjectSummary[] } | | project create | { project: ProjectSummary } | | project archive | { project: ProjectSummary } | | project restore | { project: ProjectSummary } | | project trash | { project: ProjectSummary } | | project context | { project: ProjectContext, publishedGlossary: PublishedGlossary \| null } | | spec list | { capabilities: Capability[], permissions: Permissions, specs: SpecSummary[] } | | spec search | { nextCursor: string \| null, results: SearchResult[], totalCount: number } | | work-item resource-link list | { links: WorkItemResourceLink[], canCreateLink: boolean, canReadSpecs: boolean } | | work-item resource-link add | { linkId: string, link: WorkItemResourceLink \| null, replayed: boolean } | | work-item resource-link remove | { deleted: true, replayed: boolean } | | spec work-items | { workItems: SpecWorkItemReference[], nextCursor: string \| null, canReadWorkItems: boolean } | | spec read | { currentRevision: SpecRevision \| null, permissions: Permissions, spec: SpecDetails } | | spec current | { revision: SpecRevision, spec: SpecSummary } | | spec revision | { revision: SpecRevision } | | spec changelog | { entries: SpecChangelogEntry[] } | | spec create | { spec: SpecDetails } | | spec update | { spec: SpecDetails } | | spec draft | { spec: SpecDetails } | | spec edit | { draft: { draftSessionId: string, content: string, specId: string } } | | spec assign-scenario-ids | { assignedScenarioIds: string[], draft: { draftSessionId: string, content: string, specId: string } } | | spec publish | { revision: SpecRevision, spec: SpecSummary } | | spec state | { spec: SpecSummary } | | spec rollback | { revision: SpecRevision, spec: SpecSummary } | | spec delete | { deleted: { specId: string } } | | glossary read | { currentRevision: GlossaryRevision \| null, draftSession: GlossaryDraftSession \| null, permissions: Permissions } | | glossary changelog | { entries: GlossaryChangelogEntry[] } | | glossary revision | { revision: GlossaryRevision } | | glossary draft | { glossary: GlossaryDetails } | | glossary edit | { draft: { draftSessionId: string, entries: GlossaryEntry[] } } | | glossary publish | { revision: GlossaryRevision } | | glossary rollback | { revision: GlossaryRevision } | | proposal create-spec\|create-glossary | { proposal: DraftProposal } | | proposal list | { proposals: DraftProposal[] } | | proposal apply | { mergeOutcome: "Applied", proposal: DraftProposal } | | proposal discard | { proposal: DraftProposal } | | authoring-history | { operations: AuthoringOperation[] } | | capability create | { capability: Capability } | | capability update | { capability: Capability } | | capability delete | { deleted: { capabilityId: string } } | | token issue | { credential: string, token: ProjectReadToken } | | token list | { tokens: ProjectReadToken[] } | | token revoke | { revoked: { tokenId: string } } | | review request | { review: ManualReviewRecord } | | review read | { review: ManualReviewRecord } | | finding list | { findings: ReviewFinding[], nextCursor: string \| null, permissions: FindingPermissions } | | finding read | { finding: ReviewFinding, permissions: FindingPermissions } | | finding create | { finding: ReviewFinding, permissions: FindingPermissions } | | finding confirm\|dismiss\|resolve | { finding: ReviewFinding, permissions: FindingPermissions } |

The named JSON shapes are:

type WorkItemResourceLink = {
  id: string;
  version: string; // opaque ETag
  createdAt: string;
  target:
    | {
        kind: "spec";
        projectId: string;
        specId: string;
        title: string;
        state: "Draft" | "Ready" | "Unavailable";
        unavailabilityReason: string | null;
      }
    | { kind: "document" | "other"; title: string; url: string };
};
type SpecWorkItemReference = {
  linkId: string;
  projectId: string;
  workItemId: string;
  title: string;
  status: string;
};
type WorkspaceSummary = {
  capabilities: {
    canReadMembers: boolean;
    canReadRoles: boolean;
  };
  id: string;
  name: string;
  role: string;
};
type WorkspaceMember = {
  accessStatus: "Enabled" | "Disabled";
  email: string | null;
  image: string | null;
  name: string;
  role: string;
  userId: string;
  version: string; // opaque ETag
};
type WorkspaceDirectoryCandidate = {
  eligible: boolean;
  ineligibleReason: string | null;
  issuer: string;
  name: string;
  subject: string;
  userId: string | null;
};
type WorkspaceRole = {
  name: string;
  statements: Record<string, string[]>;
  version: string; // opaque ETag
};
type ProjectSummary = {
  effectiveCapabilities: {
    canAdministerGitHubConnection: boolean;
    canAdministerProject: boolean;
    canEditProject: boolean;
    canReadProject: boolean;
  };
  specLanguage: string;
  id: string;
  name: string;
  role: string;
  status: "Active" | "Archived" | "Trashed";
  version: string; // opaque ETag
  workspaceId: string;
  workspaceName: string;
};
type ProjectContext = {
  specLanguage: string;
  id: string;
  name: string;
  status: "Active" | "Archived";
  version: string; // opaque ETag
};
type GlossaryEntry = {
  canonicalTerm: string;
  definition: string;
  id: string;
  usageNote: string | null;
};
type PublishedGlossary = {
  entries: GlossaryEntry[];
  id: string;
  publishedAt: string; // ISO 8601 date-time
};
type Capability = {
  id: string;
  name: string;
  parentId: string | null;
  version: string; // opaque ETag, for example: "\"1\""
};
type Permissions = { canEdit: boolean };
type FindingPermissions = { canWrite: boolean };
type SourceReference = {
  externalRevision?: string;
  id: string;
  section?: string;
  title: string;
  url: string;
};
type SpecSummary = {
  capabilityId: string;
  featureName: string;
  id: string;
  sourceReferences: SourceReference[];
  state: "Draft" | "Ready" | "Unavailable";
  unavailabilityReason: string | null;
  version: string; // opaque ETag
};
type SpecDetails = SpecSummary & {
  draftSession: {
    documentName: string;
    id: string;
    rootName: "spec-outline";
  } | null;
};
type SpecRevision = {
  content: string;
  id: string;
  publishedAt: string; // ISO 8601 date-time
};
type CapabilityPathEntry = { id: string; name: string };
type SearchResult = {
  capabilityPath: CapabilityPathEntry[];
  spec: SpecSummary;
};
type SourceReferenceInput = Omit<SourceReference, "id">;
type SpecChangelogEntry = {
  acceptedAt: string; // ISO 8601 date-time
  action:
    | "Published"
    | "RolledBack"
    | "StateChanged"
    | "MetadataChanged"
    | "FindingCreated"
    | "FindingConfirmed"
    | "FindingDismissed"
    | "FindingResolved"
    | "FindingStale";
  actorSessionId: string;
  actorUserId: string;
  changes: {
    content: { from: string | null; to: string } | null;
    finding: {
      findingId: string;
      status: "Confirmed" | "Dismissed" | "Proposed" | "Resolved" | "Stale";
    } | null;
    metadata: {
      capabilityId?: { from: string; to: string };
      sourceReferences?: {
        from: SourceReferenceInput[];
        to: SourceReferenceInput[];
      };
    } | null;
    state: {
      from: "Draft" | "Ready" | "Unavailable";
      fromUnavailabilityReason: string | null;
      to: "Draft" | "Ready" | "Unavailable";
      toUnavailabilityReason: string | null;
    } | null;
  };
  id: string;
  previousRevisionId: string | null;
  revisionId: string | null;
};
type GlossaryRevision = {
  entries: GlossaryEntry[];
  id: string;
  publishedAt: string; // ISO 8601 date-time
};
type GlossaryDraftSession = {
  documentName: string;
  entriesName: "entries";
  id: string;
};
type GlossaryDetails = {
  currentRevision: GlossaryRevision | null;
  draftSession: GlossaryDraftSession;
};
type GlossaryChangelogEntry = {
  acceptedAt: string; // ISO 8601 date-time
  action: "Published" | "RolledBack";
  actorSessionId: string;
  actorUserId: string;
  changes: {
    added: GlossaryEntry[];
    removed: GlossaryEntry[];
    updated: { from: GlossaryEntry; to: GlossaryEntry }[];
  };
  id: string;
  previousRevisionId: string | null;
  revisionId: string;
};
type ProjectReadToken = {
  createdAt: string; // ISO 8601 date-time
  expiresAt: string | null; // ISO 8601 date-time
  id: string;
  issuer: { id: string; name: string };
  lastUsedAt: string | null; // ISO 8601 date-time
  name: string;
  revokedAt: string | null; // ISO 8601 date-time
};
type FindingType =
  | "Contradiction"
  | "Conflict"
  | "Duplicate"
  | "Ambiguity"
  | "Terminology"
  | "Unverifiable";
type ManualReviewRecord = {
  agentRevision: string;
  completedAt: string | null; // ISO 8601 date-time
  createdAt: string; // ISO 8601 date-time
  draft: { contentHash: string; draftSessionId: string; specId: string };
  enabledFindingTypes: FindingType[];
  failureCategory:
    | "InvalidOutput"
    | "ProjectInactive"
    | "TerminalExecutionFailure"
    | "Timeout"
    | null;
  findingIds: string[];
  glossaryRevisionId: string | null;
  id: string;
  knownFindingIds: string[];
  mastraExecutionVersion: string;
  model: string | null;
  projectId: string;
  requestedBy: { sessionId: string; userId: string };
  reviewContractVersion: string;
  startedAt: string | null; // ISO 8601 date-time
  status:
    | "Queued"
    | "Running"
    | "ProposedFindings"
    | "Superseded"
    | "Cancelled"
    | "Failed";
  trigger: "ManualDraft";
  updatedAt: string; // ISO 8601 date-time
};
type FindingReference = {
  kind: "Feature" | "Rule" | "Background" | "Scenario" | "Examples" | "Step";
  text: string;
};
type FindingTarget = {
  binding:
    | { kind: "Revision"; revisionId: string }
    | {
        contentHash: string;
        draftSessionId: string;
        kind: "WorkingDraftSnapshot";
      };
  references: FindingReference[];
  specId: string;
};
type FindingEvidence =
  | { incompatibleClaims: [string, string]; kind: "Contradiction" }
  | { incompatibleRequirements: [string, string]; kind: "Conflict" }
  | { duplicatedBehavior: string; kind: "Duplicate" }
  | {
      interpretations: [
        { behavior: string; meaning: string },
        { behavior: string; meaning: string },
      ];
      kind: "Ambiguity";
    }
  | {
      glossaryEntryIds: string[];
      issue: "MissingDefinition" | "GlossaryConflict" | "MultipleMeanings";
      kind: "Terminology";
      term: string;
    }
  | { kind: "Unverifiable"; missingCriterion: string };
type ReviewFinding = {
  affectedScenarios: string[];
  author: { sessionId: string; userId: string };
  conclusion: string;
  createdAt: string; // ISO 8601 date-time
  evidence: FindingEvidence;
  explanation: string;
  glossaryReferences: {
    entry: GlossaryEntry;
    entryId: string;
    revisionId: string;
  }[];
  id: string;
  projectId: string;
  reviewRecordId: string | null;
  status: "Proposed" | "Confirmed" | "Dismissed" | "Resolved" | "Stale";
  targets: FindingTarget[];
  type: FindingType;
  updatedAt: string; // ISO 8601 date-time
  version: string; // opaque ETag
};

A failed resource command writes nothing to stdout. After any diagnostic lines, the final stderr line is exactly { "error": { "message": string, "reason": SkillErrorReason, "retryAfter"?: string, "retryable": boolean, "diagnostics"?: SkillErrorDiagnostic[] } } where type SkillErrorDiagnostic = { column?: number; line?: number; message: string; reason: string }. diagnostics is present whenever the server reports positioned source problems: Spec Publish and Rollback rejections from the Spec Content Publish Gate carry one entry per feature-count, language-mismatch, parse-error, scenario-required, or scenario-id-* reason, spec assign-scenario-ids carries the same shape when the Working Draft source is invalid, and other Invalid Operation rejections may carry it too. SkillErrorReason is Authentication Failed, Project Not Accessible, Permission Denied, Resource Not Found, Resource Changed, Invalid Operation, or Temporary Failure. Exit status 2 covers authentication, validation, inaccessible/not-found resources, conflicts, and invalid operations; 3 is permission denial; 4 is a Temporary Failure, whose retryable field determines whether automatic retry is safe. Exit status 0 is success. Exit status 1 is reserved for startup/runtime failure before a resource-command contract can run.

HTTP 429 is a retryable Temporary Failure. When the server supplies Retry-After, the CLI copies its exact value to retryAfter and does not retry the request automatically. Generated --help is a separate controlled success path: top-level help includes authentication and every resource group, nested help includes the selected group's operations and options, and all help writes to stdout, exits zero, and does not resolve credentials or call the service.

ProjectSummary.effectiveCapabilities incorporates lifecycle status. Archived Projects always report canEditProject: false, including for Admins and Editors; an Archived Admin still reports canAdministerProject: true for Restore and access reduction. A Trashed Project reports canReadProject: false even for its Admin: it appears only in the explicit --status Trashed list, and every read or write path rejects it; its only accepted mutation is project restore, which lands it on Archived, so reaching Active again takes a second explicit restore.

Workspace membership is the only source of Project access. ProjectSummary.role is the caller's Role in the owning Workspace, and workspace create grants the creator the Owner Workspace Role. project create requires the owning Workspace's ID and at least the Editor Role inside it.

workspace member and workspace role administer that source. member search queries the bound identity directory and reports each candidate's eligibility; member add provisions the selected User — including before first login — and member set-role/member remove require the Membership's opaque version as --expected-version. role commands manage the Workspace's statement sets: --statements takes a JSON object mapping resources to action arrays, a Role never grants statements the caller does not hold, Owner is immutable, and a Role still assigned to a member cannot be deleted. Role updates and deletes require the Role's version as --expected-version.

project list stays stateless: pass --workspace-id <workspace-id> — from workspace list or project create output — to select one Workspace and list only its Projects, combinable with --status. The CLI keeps no implicit current Workspace. An unknown or inaccessible Workspace returns an empty list rather than revealing that it exists, and a CLI Session approved for a Project subset only ever returns Projects inside that scope, even when the filter names a Workspace that contains no scoped Project.

Every non-idempotent create, Draft-session, Publish, Rollback, token issuance, Finding disposition, Manual Review request, and Project lifecycle command performs at most one fetch-transport retry. It clones the request and reuses the same Idempotency-Key; an HTTP response, including a response whose body cannot be decoded, is never dispatched again. Archive, Restore, and Trash require the opaque Project version returned by discovery or context. If neither transport attempt yields a usable response, the final Temporary Failure has retryable: false and directs the caller to inspect the resource or Changelog before deciding whether another logical mutation is appropriate.

Token issuance returns the plaintext credential only for the first committed response. If a same-key retry confirms the token with credential: null, the CLI exits nonzero, prints no success payload, and directs the caller to list or revoke the token before deliberately issuing another.

Source Reference URLs accept only HTTP or HTTPS and must not contain URL userinfo (username or password). Keep external credentials in the Agent's own authorized source-reading tool; never embed them in SpecFlow metadata.

The repository-owned official Skill is skills/specflow/SKILL.md. It pins this exact CLI release, requires explicit User confirmation immediately before Spec or Glossary Publish and Rollback, and does not send proof of that confirmation to the CLI or API.

The CLI stores its Session credential in macOS Keychain, Windows Credential Manager, or Linux Secret Service. It does not print or persist the credential in environment variables. If the system store is unavailable on a POSIX platform, local-file storage is used only after --allow-file-fallback is explicitly selected; the CLI warns and enforces directory mode 0700 and file mode 0600.

Work Item creation labels

specflow work-item create --project-id PAY --title "Retry alert" --label <definitionId> sets existing Workspace labels in the same transaction as creation. Repeat --label for multiple labels; duplicate IDs are ignored. The response includes workItem.labels. Missing, foreign-Workspace, copy, or deleting labels reject the entire creation with INVALID_WORK_ITEM_LABEL.

Work Item JSON exchange

specflow work-item export --project-id PAY --status done > tasks.json
specflow work-item import --project-id OPS --file tasks.json --idempotency-key release-tasks-1

Export supports the same selection flags as work-item list and returns a JSON { project: { name, slug, description, exportedAt }, tasks: [...] } document on stdout. Each task contains title, description, status, priority, startDate, dueDate, userId, and labels: [{ name, color }]. Dates are ISO strings or null. Only readable tasks from the explicit Project are included. An export over 1000 tasks or 1 MiB fails; narrow the selection instead of expecting a truncated document.

Import accepts a JSON file with a tasks array, at most 1000 tasks and 1 MiB. File Project metadata is descriptive; --project-id always selects the target. The server applies ordinary creation and assignment authorization, membership, workflow, default Custom Field, and identity rules. Unknown statuses become planned; unknown priorities become no-priority; absent or empty priority becomes low. Labels are exported but not imported, matching pinned Kaneo; nonempty imported labels produce an explicit warning.

Import stdout contains importedAt, target project, and results with total, successful, failed, and indexed tasks. Each row has success and warnings; successful rows include workItemId and opaque version, while failed rows include an error code. If any row fails, the CLI retains this complete receipt on stdout, emits a nonretryable Invalid Operation on stderr, and exits 2. Correct and import only failed rows with a new key. Successful rows and business refusals commit together; an unexpected database error rolls back the whole batch.

Retry the same file with the same --idempotency-key after an interrupted request. Within the existing 24-hour replay window it returns the original receipt without duplicating tasks. A changed task payload with that key conflicts. Read Tokens may export their single Project; they cannot import. No CSV or legacy migration format is provided.

Task relations

Relations link two tasks in the same Workspace, including across Projects. subtask means source parent → target child; blocks means source blocker → blocked target; related is symmetric. Reading either endpoint returns both task identities and their current Projects. Self-links and duplicate pairs of the same relation type in either direction are rejected; longer cycles are allowed, as in the pinned Kaneo contract. These links are separate from Spec/document links.

specflow work-item relation list --project-id PAY --work-item-id PAY-7
specflow work-item relation create --project-id PAY --work-item-id PAY-7 \
  --target-project-id OPS --target-work-item-id OPS-2 --type blocks \
  --idempotency-key relation-command-1
specflow work-item relation delete --project-id OPS --work-item-id OPS-2 \
  --peer-project-id PAY --relation-id <relation-id> --expected-version '"1"' \
  --idempotency-key remove-relation-command-1

Keep the same command key and inputs after an interrupted response. Receipts contain only the relation identifier; a create replay returns relation: null when the relation has been deleted or its current endpoints are outside the original authority. It never restores a deleted link. After a task transfer, reread to obtain the endpoint's new task identity and Project before removing its relation. Both endpoints require current task update permission for writes; Archived endpoints are read-only. Lists omit inaccessible peers and their counts. Project Read Tokens can read only relations wholly inside their single Project and cannot create or delete relations.

Task comments and activity

Read comments oldest first, or read the mixed discussion/activity feed newest first. Both commands return an opaque nextCursor; pass it back with --cursor. Pages accept --limit from 1 through 100.

specflow work-item comment list --project-id PAY --work-item-id PAY-7 --limit 20
specflow work-item discussion --project-id PAY --work-item-id PAY-7 --limit 20
specflow work-item comment create --project-id PAY --work-item-id PAY-7 \
  --content '**Review** the rollout' --idempotency-key <uuid>
specflow work-item comment update --project-id PAY --work-item-id PAY-7 \
  --comment-id <comment-uuid> --content 'Updated review' \
  --expected-version '"1"' --idempotency-key <uuid>
specflow work-item comment delete --project-id PAY --work-item-id PAY-7 \
  --comment-id <comment-uuid> --expected-version '"2"' --idempotency-key <uuid>

Comment mutations require a CLI Session, current task update permission, and a UUID command key. Only the comment's author may edit or delete it, including when another Member is an Admin. Reuse the same command key and inputs after a lost response; receipts retain only the comment identifier and resolve its current content. A replay after deletion returns comment: null.

Take the opaque comment version from the read result. An edit or deletion against an older version reports Resource Changed; reread before deciding whether to resubmit. Project Read Tokens may read comments and activity under WorkItemRead, and cannot mutate comments. Activities are produced by the server's actual task operations; there is no activity creation command.

Time recording removal (0.6.0)

Version 0.6.0 removes the deprecated work-item time-entry command hierarchy and its API operations. Scripts using those commands must stop invoking them; there is no transition command. The official Skill did not use these commands and now pins CLI 0.6.0. Historical activity and notifications remain readable.

Personal notifications

Use the existing User Session; a Project Read Token cannot access this inbox.

specflow notification list
specflow notification read --notification-id <uuid> --idempotency-key <uuid>
specflow notification read-all --idempotency-key <uuid>
specflow notification clear --idempotency-key <uuid>
specflow notification preferences get
specflow notification preferences update --expected-version <version> --preferences '{"taskCommentEnabled":false}' --idempotency-key <uuid>
specflow notification create --type info --title "Review ready" --message "Evidence attached" --idempotency-key <uuid>

Task or Workspace creation targets use paired --related-entity-id and --related-entity-type task|workspace options; Task identifiers are current public IDs such as PAY-7. Optional --event-data accepts a bounded JSON object. Manual creation is for the current User only and returns a null notification when the category is muted or the target is inaccessible.

The feed returns the latest 50 currently accessible entries and their unread count. A read command returns the fresh target after a transfer. Reuse identical inputs and the same UUID key after an ambiguous write outcome, particularly for read-all and clear, so a retry cannot alter later arrivals. A CLI Session restricted to Projects can read only admitted targets; read-all, clear, and preferences require an unrestricted Session. Preference reads return an opaque version; saves require --expected-version. A stale save returns Resource Changed and requires a deliberate reread and reapply. Preferences accept partial assignment/comment/status/due booleans and dueDateReminderMinutes from 5 to 43200. No command schedules reminders or configures external delivery channels.

Public task sharing

specflow project sharing read --project-id PAY
specflow project sharing set --project-id PAY --public true \
  --expected-version '"1"' --idempotency-key <uuid>
specflow project sharing set --project-id PAY --public false \
  --expected-version '"2"' --idempotency-key <uuid>

Use the sharing read's opaque version, which is independent of the Project's lifecycle version. Changes require a CLI Session with current project:share permission on an Active Project. Reuse the same UUID key, boolean and expected version after a lost response; a replay returns its original command result without changing current sharing. Read the settings again to obtain the current state. A stale fresh command requires a reread and an explicit decision. Project Read Tokens cannot manage sharing.

Public sharing exposes only Task/workflow fields and finalized assets currently referenced by Task descriptions. It creates no Workspace membership and grants no Spec, Glossary, resource association, comment or private attachment access. Archive and Trash close sharing; Restore does not reopen it. The anonymous Web entry is /public/projects/<PROJECT>.

User API Keys in the current checkout

Use an authenticated CLI Session to manage the current User’s explicitly scoped keys. Creating a key saves its one-time secret directly to the origin-scoped system credential store; stdout contains metadata and credentialStored, never the secret. The warned owner-only file fallback remains opt-in with --allow-file-fallback. These commands describe the repository implementation; a published npm release must be verified separately.

specflow --server https://specflow.example api-key create --name "Task automation" --grants '[{"scope":"ProjectTasks","resourceId":"<project-id>","statements":{"workItem":["read","create"]}}]' --expires-at never --idempotency-key <uuid>
specflow --server https://specflow.example api-key list --limit 50
specflow --server https://specflow.example api-key list --cursor <nextCursor>
specflow --server https://specflow.example --api-key-id <key-id> work-item list --project-id PAY
specflow --server https://specflow.example api-key revoke --key-id <key-id> --expected-version '"1"' --idempotency-key <uuid>

An expiry can instead be an explicit UTC ISO timestamp. Creation requires explicit Workspace or Project targets and resource-to-actions grant statements. Spec/Glossary publish is a separate grant; unknown or ungranted scopes fail closed. Every business request checks current User eligibility, membership, Role, explicit key scope and lifecycle. Global account/Workspace and credential-management routes require a User Session.

Keep the same creation command ID and intent when recovering an uncertain response. A replay cannot redisclose a lost secret; revoke that key before deliberately issuing a replacement. Saving failure triggers a versioned compensating revoke; an uncertain revoke reports the key ID for operator reconciliation. Revocation removes only the selected local key slot after the server confirms success. Session credentials stay independent.

SPEC_FLOW_API_KEY accepts an explicitly provided key for CI. Do not place raw secrets in command arguments or use multiple credential selectors. An absent selected key never falls back to the Session. API Keys and Project Read Tokens cannot manage credentials.

Explicit GitHub import in the current checkout

specflow github connection status --project-id PAY
specflow github import status --project-id PAY
specflow github import run --project-id PAY --expected-version '"1"' --idempotency-key <uuid>
specflow github import run --project-id PAY --expected-version '"1"' --idempotency-key <new-uuid> --run-id <saved-run-id>

Use the connected repository's observed version. Starting and reading import progress require a real User Session with current GitHub connection management permission on an Active Project. Each page separately checks the connection's current delegated API Key and the Task/label operations it needs. Passing --api-key-id cannot grant connection management.

The command processes bounded batches until completion, retrying rate limits up to five times with the same command identity. Stdout contains the final run identity and new/updated/skipped counts. A failed batch leaves committed progress intact; retry a lost initial response with the same UUID and identical options. Once a run ID is known, resume it explicitly. A completed explicit run returns its saved counts; starting without a run ID after completion begins a new import.

Import reads open Issues, copies ordinary labels and non-bot historical comments, and associates matching Pull Requests. Historical comments remain read-only. Explicit import refreshes existing linked Tasks without applying webhook workflow rules; PR association does not move a Task. The Project settings Web surface offers the same start/resume capability. Closing it stops future batches; an in-flight batch may still commit before the pause takes effect.

Specs may be created without --capability-id. They appear in Unassigned and retain the same editing and publication contract. To move a Spec to Unassigned, use specflow spec update --workspace-id <id> --spec-id <id> --capability-id none --expected-version <version>. Capability deletion rejects child Capabilities; deleting a leaf moves its Specs to Unassigned and records their placement changes.

Connect 1–50 repositories atomically with one idempotency key:

specflow github connection connect-batch --project-id PROJECT_ID \
  --repository owner/tasks:10:20 --repository owner/mobile:10:21 \
  --outbound-repository-id 20 --idempotency-key UUID

Repeat --repository owner/name:installationId:repositoryId for each repository. Use --no-outbound to explicitly select none. Without either option, only the first repository of the Project's first batch becomes outbound; later batches only create from GitHub. A Project with an outbound repository cannot nominate another during connection. Workspace binding conflicts reject the whole batch. A real Web/CLI Session with current Workspace settings permission and personal GitHub administrator access to every repository is required. The single-repository github connection connect command remains available.

Spec responsibility, Scenario discussions and audit pages (0.5.0)

Version 0.5.0 adds these commands compatibly to 0.4.0 (ADR 0065). The Skill source pins the matching minor version; publishing remains a separate release. All commands emit the existing JSON result on stdout; --json is accepted explicitly. Failures keep the existing structured stderr and exit categories.

specflow spec owner read --workspace-id W --spec-id PAY-S1
specflow spec owner candidates --workspace-id W --spec-id PAY-S1
specflow spec owner assign --workspace-id W --spec-id PAY-S1 --user-id USER --expected-version '"1"' --idempotency-key UUID
specflow spec owner clear --workspace-id W --spec-id PAY-S1 --expected-version '"2"' --idempotency-key UUID
specflow spec discussion targets --workspace-id W --spec-id PAY-S1 --context-kind Published
specflow spec discussion list --workspace-id W --spec-id PAY-S1 --context-kind Published --scenario-id PAY-S1-1 --limit 30
specflow spec discussion comments --workspace-id W --spec-id PAY-S1 --thread-id THREAD_UUID --limit 30
specflow spec discussion start --workspace-id W --spec-id PAY-S1 --scenario-id PAY-S1-1 --context-hash HASH --content 'Question' --idempotency-key UUID
specflow spec discussion reply --workspace-id W --spec-id PAY-S1 --thread-id THREAD_UUID --context-kind WorkingDraft --context-hash HASH --content 'Reply' --idempotency-key UUID
specflow spec discussion edit-comment --workspace-id W --spec-id PAY-S1 --thread-id THREAD_UUID --comment-id COMMENT_UUID --content 'Edited' --expected-version '"1"' --idempotency-key UUID
specflow spec discussion delete-comment --workspace-id W --spec-id PAY-S1 --thread-id THREAD_UUID --comment-id COMMENT_UUID --expected-version '"2"' --idempotency-key UUID
specflow spec discussion resolve --workspace-id W --spec-id PAY-S1 --thread-id THREAD_UUID --expected-version '"3"' --idempotency-key UUID
specflow spec discussion reopen --workspace-id W --spec-id PAY-S1 --thread-id THREAD_UUID --expected-version '"4"' --idempotency-key UUID
specflow project rename --project-id PROJECT_UUID --name 'Delivery' --expected-version '"1"' --idempotency-key UUID
specflow project audit-log --project-id PROJECT_UUID --limit 50
specflow workspace audit-log --workspace-id W --limit 50

Owner reads return { owner, version }, candidates return { candidates }, and assignment/clear return { version }. Responsibility grants no additional access; only currently eligible Workspace members can be assigned. Clearing is explicit.

Discussion targets return { targets, targetsAvailable, currentContext, canComment }. Obtain the allocated Scenario ID and its context.hash there; pass the exact hash as --context-hash to start/reply. Every discussion command accepts --context-kind Published|WorkingDraft (default Published). Never use a Published hash for different Working Draft content. SPEC_DISCUSSION_CONTEXT_CHANGED retains its public code and maps to Invalid Operation; deliberately reread the selected context before submitting a new intent. Edits preserve original comment context, and deletion retains a tombstone. Only authors edit/delete their comments; initiators or current Spec editors resolve/reopen threads. Use comments[].version for comment writes and threads[].version for status writes; they are independent. Discussion writes return { threadId, commentId, replayed }.

Thread, comment and audit reads return one bounded page and nextCursor; repeat with --cursor <nextCursor> and the same scope/view/filter to continue. Discussion pages have threads or comments; audit pages have events including original source and actor attribution. Audit access is checked separately for the Project or Workspace. Rename returns the existing { project } lifecycle result; it retains Project identity and rejects ordinary writes to Archived Projects.

All new writes require an explicit UUID idempotency key. A bounded transport retry reuses the same input, observed version, credential and key; retain that key after an ambiguous result. Stale versions map to Resource Changed. The CLI neither fetches a replacement version nor changes the requested context automatically. Workspace read Tokens cannot use these private operations. Current Role, eligibility, credential and lifecycle rules remain enforced by the existing service endpoints.

Shared task views, statistics, colors and personal tasks (ZTA-875/876/878)

specflow work-item view list --project-id PROJECT
specflow work-item view save --project-id PROJECT --input '{"id":"UUID","name":"Mine","layout":"list","groupedByStatus":false,"filters":{"assignees":[],"customFields":{},"dueDates":[],"labels":[],"priorities":[],"search":"","sortBy":"number","sortOrder":"asc","statuses":[]}}'
specflow work-item view save --project-id PROJECT --input '<complete view JSON without version>' --expected-version '"1"'
specflow work-item statistics --project-id PROJECT --query '{"view":"list","assignee":"USER_ID","priority":"high,urgent"}'
specflow workspace my-work-items --workspace-id WORKSPACE --limit 50 [--cursor CURSOR]
specflow work-item-custom-field create --project-id PROJECT --name Risk --type dropdown --option High --option Low --option-colors '["danger",null]'
specflow work-item-custom-field option-colors --project-id PROJECT --field-id FIELD --colors '["warning",null]' --expected-version '"1"' --idempotency-key UUID
specflow work-item-custom-field option-colors --project-id PROJECT --field-id FIELD --clear --expected-version '"2"' --idempotency-key UUID

Views retain the existing board/list layout and complete filters, including optional repositoryConnectionId. Save omits the expected version only for a new ID. Identical immediate retries use the same ID, content and observed version; the server owns replay and stale-version behavior. No view Idempotency-Key is invented. Statistics returns the server aggregate, including trends at overview.timeline; there is no client page counting. Query accepts existing server selection keys: view, repositoryConnectionId, assignee, customFields, due, dueAfter, dueBefore, label, priority, search, sortBy, sortOrder, startAfter, startBefore, status. CSV and customFields JSON strings retain server semantics. Personal tasks returns total, Project groups and nextCursor from the current User's authorized unfinished tasks in Active Projects; retain the server cursor. Colors align one-to-one with dropdown options; null entries clear individual colors, --clear clears all. The server validates color values, lengths and field type. Assistant can discover views, statistics/trends, option colors and my unfinished Work Items. View and color writes wait for approval and retain version preconditions.

Local Screen image files (ZTA-881 CLI)

specflow map screen image-upload --workspace-id WORKSPACE --screen-id SCREEN --file ./image.png --content-type image/png --expected-version '"1"' --idempotency-key RESERVE_KEY --finalize-key FINALIZE_KEY
specflow map screen image-download --workspace-id WORKSPACE --screen-id SCREEN --asset-id ASSET_UUID --output-file ./download.png

Upload reserves against the observed Screen version, validates the returned fresh HTTP(S) reservation, PUTs bytes without Session credentials or redirects, and finalizes using a separate retained key and the same version. Local files must be nonempty regular files of at most 10 MiB; server MIME/size checks still apply. Retain both phase keys and identical inputs after failures. Once finalized, a new reservation attempt with the old reserve key returns upload-expired; reread the Screen to resolve an ambiguous completion. Existing image-reserve/image-finalize/ image-remove commands remain available for explicit phase control. Download accepts only authorized Workspace/Screen/asset identities, never an arbitrary URL, and creates a new owner-only file without overwriting existing files. Assistant image handling is outside this CLI slice.

Global search uses the server's cross-resource search and current authority:

specflow search-global --query 'PAY-12' --limit 20
specflow search-global --query 'payment' --limit 20 --cursor '<nextCursor>'

The JSON response preserves object kind, ID, owning Workspace/Project, totalCount and nextCursor. Continue with the same query until nextCursor is null. The server has no type filter. specflow spec search retains its published-Spec semantics.

Work Item automation (ZTA-911, CLI 0.6.0)

Existing commands keep full JSON by default. Use --output summary for Work Item create/read/update/assign/list and relation create/list to omit descriptions while retaining IDs, versions, permissions, cursors and replay receipts. Grouped pages are summarized too. Summary does not fetch further pages. relation: null remains null. Unsupported summary commands reject before sending a mutation.

Work Item url is the server-authorized SpecFlow page URL; githubIssue.url remains the external GitHub URL. Do not construct links from the API origin. An older server that omits url still yields a successful business result with one stderr warning.

specflow auth whoami
specflow work-item create --project-id PROJECT --title 'Prepare release' --assignee me --description-file ./release.md --output summary
cat ./release.md | specflow work-item update --project-id PROJECT --work-item-id GON-78 --expected-version '"3"' --description-file - --output summary
specflow work-item subtask link --project-id PROJECT --parent GON-78 --child GON-79 --child-project-id OTHER_PROJECT --idempotency-key KEY

auth whoami returns { user: { id }, credentialKind } for the selected Session or User API Key (session or api-key). --assignee me on create/assign resolves that same credential once, without printing the intermediate identity. Read-only Tokens cannot resolve me; filters and bulk assignment accept User IDs only. Explicit credential failure never falls back to another credential. Older servers without the identity route require an upgrade or an explicit User ID.

Descriptions accept exactly one of inline --description, --description-file (regular UTF-8 file, or - for explicit piped stdin), and update's --clear-description (null). Omission leaves update unchanged and preserves create's default. No shell expansion or Markdown parsing occurs. Empty/whitespace content, invalid UTF-8, nonregular files and input above 80,000 bytes are rejected locally; text must contain 1–20,000 UTF-16 code units after trimming. Interactive stdin is rejected: pipe the text or select a file.

Subtask source is always parent and target is child. subtask link defaults to summary; --project-id owns the parent and --child-project-id defaults to it. The existing relation create --type subtask remains available:

specflow work-item relation create --project-id PROJECT --work-item-id GON-78 --target-project-id PROJECT --target-work-item-id GON-79 --type subtask --idempotency-key KEY

Retain the same relation key and body after an uncertain response; full/summary never change server intent. Create retains its automatically generated key within one invocation. Transport retries reuse that key and body once; version conflicts never fetch a newer version and overwrite automatically. Honor retryAfter on 429. LOCAL_OUTPUT_FAILED means local formatting/output failed after possible server success: read the resource before deliberately retrying. It never permits a blind write retry. Diagnostics stay on stderr and stdout remains business JSON.