confpub-cli 0.5.0__tar.gz → 0.6.0__tar.gz

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.
Files changed (51) hide show
  1. confpub_cli-0.6.0/FEEDBACK.md +197 -0
  2. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/PKG-INFO +1 -1
  3. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/confpub/__init__.py +1 -1
  4. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/confpub/applier.py +2 -0
  5. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/confpub/cli.py +17 -2
  6. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/confpub/confluence.py +5 -5
  7. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/confpub/guide.py +17 -1
  8. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/confpub/manifest.py +6 -1
  9. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/confpub/planner.py +2 -0
  10. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/confpub/publish.py +2 -0
  11. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/confpub/puller.py +7 -5
  12. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/confpub/validator.py +7 -2
  13. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/confpub/verifier.py +12 -2
  14. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/tests/test_guide.py +1 -1
  15. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/tests/test_puller.py +3 -1
  16. confpub_cli-0.5.0/FEEDBACK.md +0 -226
  17. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/.github/workflows/publish.yml +0 -0
  18. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/.gitignore +0 -0
  19. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/LICENSE +0 -0
  20. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/PRD.md +0 -0
  21. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/README.md +0 -0
  22. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/confpub/assets.py +0 -0
  23. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/confpub/config.py +0 -0
  24. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/confpub/converter.py +0 -0
  25. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/confpub/envelope.py +0 -0
  26. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/confpub/errors.py +0 -0
  27. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/confpub/lockfile.py +0 -0
  28. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/confpub/output.py +0 -0
  29. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/confpub/py.typed +0 -0
  30. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/confpub/reverse_converter.py +0 -0
  31. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/confpub.lock +0 -0
  32. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/pyproject.toml +0 -0
  33. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/tests/__init__.py +0 -0
  34. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/tests/conftest.py +0 -0
  35. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/tests/test_applier.py +0 -0
  36. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/tests/test_assets.py +0 -0
  37. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/tests/test_config.py +0 -0
  38. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/tests/test_confluence.py +0 -0
  39. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/tests/test_converter.py +0 -0
  40. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/tests/test_envelope.py +0 -0
  41. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/tests/test_errors.py +0 -0
  42. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/tests/test_integration.py +0 -0
  43. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/tests/test_lockfile.py +0 -0
  44. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/tests/test_manifest.py +0 -0
  45. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/tests/test_output.py +0 -0
  46. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/tests/test_planner.py +0 -0
  47. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/tests/test_publish.py +0 -0
  48. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/tests/test_reverse_converter.py +0 -0
  49. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/tests/test_validator.py +0 -0
  50. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/tests/test_verifier.py +0 -0
  51. {confpub_cli-0.5.0 → confpub_cli-0.6.0}/uv.lock +0 -0
@@ -0,0 +1,197 @@
1
+ # confpub-cli v0.5.0 — Blind Test Feedback
2
+
3
+ Tested by: Claude Code (Opus 4.6) on 2026-03-01, Windows 11, via `uvx confpub-cli`.
4
+
5
+ ---
6
+
7
+ ## Summary
8
+
9
+ confpub v0.5.0 is a well-designed, agent-friendly CLI. The structured JSON envelope, clear error taxonomy, and comprehensive `guide` command make it straightforward for an LLM agent to drive programmatically. The full publish/pull/delete lifecycle works correctly, and round-trip Markdown fidelity is excellent. This review covers what works well, what could be improved, and specific bugs encountered.
10
+
11
+ ---
12
+
13
+ ## What Works Well
14
+
15
+ ### Structured JSON Envelope
16
+ Every command returns the same `{ ok, command, target, result, warnings, errors, metrics }` shape. This is the single most important design decision for agent consumption — no output parsing required.
17
+
18
+ ### Error Taxonomy
19
+ Typed error codes (`ERR_VALIDATION_*`, `ERR_AUTH_*`, `ERR_IO_*`, `ERR_CONFLICT_*`) with `retryable` and `suggested_action` fields make programmatic error handling trivial. Exit codes are consistent with the documented schema.
20
+
21
+ ### `guide` Command
22
+ The machine-readable schema is comprehensive: command metadata, flags, safety annotations, concurrency rules, error codes, and auth precedence. The `--section` flag works for top-level keys (`commands`, `auth`, `error_codes`, `concurrency`).
23
+
24
+ ### Markdown Round-Trip Fidelity
25
+ Published a file with headings, bullet lists, tables, blockquotes, and fenced code blocks. Pulled it back — the Markdown was virtually identical (only trivial whitespace differences in table alignment `| --- |` vs `|---------|`). This is a strong result.
26
+
27
+ ### Publish Lifecycle
28
+ - `page.publish` with `--dry-run` correctly reports what would happen before writing.
29
+ - Re-publishing an existing page updates it in-place with version increment.
30
+ - `--backup` flag saves the previous HTML to `.confpub-backup-{id}.html`.
31
+ - `--title` override works as expected.
32
+ - `page.delete` cleanly removes the page.
33
+
34
+ ### Recursive Pull + Manifest Generation
35
+ `page pull --recursive --manifest` correctly traverses child pages and generates a well-structured `confpub.yaml` manifest. The flat layout manifest correctly represents the page hierarchy with `children:` nesting.
36
+
37
+ ### Plan Workflow
38
+ The `plan create` / `plan validate` / `plan apply --dry-run` workflow functions correctly. Plans include fingerprints for stale-state detection, which is a nice safety feature.
39
+
40
+ ### Lock File
41
+ `confpub.lock` tracks page IDs and versions, providing local state awareness across commands.
42
+
43
+ ### Auth & Config
44
+ `auth inspect` and `config inspect` return clear, useful information. Token masking in `config inspect` is a good security practice.
45
+
46
+ ---
47
+
48
+ ## Bugs
49
+
50
+ ### 1. Stderr Message Leaks on Expected "Not Found" Lookups
51
+ **Severity: Medium**
52
+
53
+ When publishing a new page (or doing a dry-run), the CLI prints `Can't find '<title>' page on ...` to stderr before the JSON output. This happens because the tool checks whether the page exists first. For a *new* page, not finding it is the expected path — not an error. The message is confusing, especially during `--dry-run` where the user explicitly wants to preview a creation.
54
+
55
+ `--quiet` suppresses it, but agents shouldn't need `--quiet` for expected behavior.
56
+
57
+ **Suggestion:** Only emit this message at `--verbose` level, or suppress it when the subsequent operation succeeds.
58
+
59
+ ### 2. Relative Paths Fail for `plan validate` and `plan apply`
60
+ **Severity: Medium**
61
+
62
+ ```
63
+ uvx confpub-cli plan validate --plan plan-test/test-plan.json
64
+ # ERR_IO_FILE_NOT_FOUND: Plan file not found: plan-test/test-plan.json
65
+
66
+ uvx confpub-cli plan validate --plan "C:/Users/.../test-plan.json"
67
+ # Works fine
68
+ ```
69
+
70
+ Relative paths resolve correctly for `page publish` (the FILE argument) but not for `--plan` in plan commands. This is likely a `Path.resolve()` vs `Path()` issue.
71
+
72
+ ### 3. `page inspect` `webui` Field Inconsistent Format
73
+ **Severity: Low**
74
+
75
+ - With `--space SD --title "..."`: returns relative URL `/spaces/SD/pages/327981/Test+Page`
76
+ - With `--page-id 327981`: returns absolute URL `https://...atlassian.net/wiki/spaces/SD/pages/327981/Test+Page`
77
+
78
+ Should be consistent — preferably always absolute, since the agent may not know the base URL.
79
+
80
+ ### 4. `ERR_AUTH_FORBIDDEN` for Nonexistent Space
81
+ **Severity: Low**
82
+
83
+ ```
84
+ uvx confpub-cli page inspect --space FAKESPACE --title "Test"
85
+ # ERR_AUTH_FORBIDDEN: "Permission denied (get_page)"
86
+ # suggested_action: "escalate"
87
+ ```
88
+
89
+ A nonexistent space key returns a permission error rather than a "space not found" validation error. The `suggested_action` is `"escalate"` but the guide says ERR_AUTH_FORBIDDEN should suggest `"reauth"`. An agent following the guide would incorrectly attempt to re-authenticate.
90
+
91
+ ### 5. `guide --section` Does Not List Valid Sections on Error
92
+ **Severity: Low**
93
+
94
+ ```
95
+ uvx confpub-cli guide --section search
96
+ # ERR_VALIDATION_REQUIRED: "Unknown guide section: search"
97
+ ```
98
+
99
+ The error doesn't tell you what the valid sections are. Adding `"valid_sections": ["commands", "auth", "error_codes", "concurrency", "compatibility"]` to the error details would save a round-trip.
100
+
101
+ ### 6. Nested Layout Manifest Uses Ambiguous `file: index.md` for All Pages
102
+ **Severity: Medium**
103
+
104
+ When using `--layout nested`, every page gets `file: index.md` in the manifest without a directory prefix:
105
+
106
+ ```yaml
107
+ pages:
108
+ - title: confpub v0.3.0 Blind Test Report
109
+ file: index.md
110
+ children:
111
+ - title: What Works Well
112
+ file: index.md
113
+ - title: Bugs and Issues
114
+ file: index.md
115
+ ```
116
+
117
+ These are all `index.md` — a plan created from this manifest would have no way to distinguish them. The file paths should include the relative directory (e.g., `confpub-v030-blind-test-report/index.md`).
118
+
119
+ ### 7. Nested Layout Doesn't Actually Nest Directories
120
+ **Severity: Low**
121
+
122
+ The `--layout nested` option creates a flat set of directories rather than truly nesting children inside parents:
123
+
124
+ ```
125
+ pulled-nested/
126
+ confpub-v030-blind-test-report/index.md # parent
127
+ what-works-well/index.md # child (not nested inside parent dir)
128
+ bugs-and-issues/index.md # child
129
+ full-test-matrix/index.md # child
130
+ ```
131
+
132
+ Expected behavior for "nested" layout would place children inside the parent directory.
133
+
134
+ ### 8. `--layout nested` Generates Manifest Without `--manifest` Flag
135
+ **Severity: Low**
136
+
137
+ Using `page pull --layout nested` generates a `confpub.yaml` even without the `--manifest` flag. The flat layout correctly requires `--manifest` to generate one.
138
+
139
+ ---
140
+
141
+ ## Suggestions (Not Bugs)
142
+
143
+ ### Add `page.inspect --format markdown`
144
+ Currently `page inspect` returns raw Confluence storage XML. An option to return the reverse-converted Markdown would be useful for quick content review without pulling to a file.
145
+
146
+ ### Add `search --type page` Default for Agent Usage
147
+ Agents almost always want pages, not attachments or space entities. Consider a default or a `--pages-only` shorthand.
148
+
149
+ ### Document `confpub.lock` in `guide`
150
+ The lock file is created implicitly but isn't documented in the guide schema. Agents should know it exists and what it tracks.
151
+
152
+ ### `ERR_IO_FILE_NOT_FOUND` Suggestion for Missing Source
153
+ When the source file doesn't exist, `retryable: true` with `suggested_action: "retry"` is misleading — a file that doesn't exist won't appear on retry. Consider `retryable: false` with `suggested_action: "fix_input"` for local file-not-found errors (vs. transient network errors).
154
+
155
+ ---
156
+
157
+ ## Test Matrix
158
+
159
+ | Command | Flags Tested | Result | Notes |
160
+ |---|---|---|---|
161
+ | `guide` | (none), `--section commands`, `--section auth`, `--section error_codes` | Pass | `--section` works for top-level keys |
162
+ | `guide --section` | invalid section name | Pass (with note) | Error lacks valid section list |
163
+ | `auth inspect` | (none) | Pass | |
164
+ | `config inspect` | (none) | Pass | Token correctly masked |
165
+ | `space list` | (none) | Pass | |
166
+ | `page list` | `--space` | Pass | |
167
+ | `page inspect` | `--space --title`, `--page-id` | Pass | webui format inconsistency |
168
+ | `page publish` | `--dry-run`, `--backup`, `--title` | Pass | stderr leak on new page |
169
+ | `page publish` (update) | `--backup` | Pass | Version incremented correctly |
170
+ | `page pull` | `--output`, `--force`, `--manifest` | Pass | |
171
+ | `page pull` | `--recursive`, `--layout flat` | Pass | |
172
+ | `page pull` | `--recursive`, `--layout nested` | Pass (with notes) | Manifest ambiguity, not truly nested |
173
+ | `page delete` | `--space --title` | Pass | |
174
+ | `search` | `--space`, `--limit`, `--cql` | Pass | |
175
+ | `plan create` | `--manifest`, `--output` | Pass | |
176
+ | `plan validate` | `--plan` (absolute path) | Pass | Relative path fails |
177
+ | `plan apply` | `--plan --dry-run` | Pass | |
178
+ | `attachment list` | `--page-id` | Pass | |
179
+ | `--quiet` | global flag | Pass | Suppresses stderr messages |
180
+ | `--verbose` | global flag | Pass | Adds diagnostics to metrics |
181
+ | Error: missing file | `page publish nonexistent.md` | Pass | Clear error |
182
+ | Error: missing page | `page inspect --title "..."` | Pass | |
183
+ | Error: missing args | `page publish` (no file) | Pass | |
184
+ | Error: missing space | `--space FAKESPACE` | Fail | Misleading ERR_AUTH_FORBIDDEN |
185
+
186
+ ---
187
+
188
+ ## Overall Assessment
189
+
190
+ **confpub v0.5.0 is production-ready for the core publish/pull/delete workflow.** The agent-first JSON design, error taxonomy, and guide command set a high bar for CLI ergonomics. The main areas for improvement are:
191
+
192
+ 1. Fix relative path resolution for plan commands (blocks scripted workflows)
193
+ 2. Fix the nested layout manifest ambiguity (blocks round-trip with nested trees)
194
+ 3. Suppress the "Can't find" stderr noise on expected new-page creation
195
+ 4. Normalize the `webui` field format
196
+
197
+ None of these are blockers for single-page publishing, which is the most common use case.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: confpub-cli
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: Agent-first CLI to publish Markdown to Confluence
5
5
  Project-URL: Homepage, https://github.com/ThomasRohde/confpub-cli
6
6
  Project-URL: Repository, https://github.com/ThomasRohde/confpub-cli.git
@@ -1,3 +1,3 @@
1
1
  """confpub — Agent-first CLI to publish Markdown to Confluence."""
2
2
 
3
- __version__ = "0.5.0"
3
+ __version__ = "0.6.0"
@@ -62,6 +62,8 @@ def apply_plan(
62
62
  raise ConfpubError(
63
63
  ERR_IO_FILE_NOT_FOUND,
64
64
  f"Source file missing: {page.source_file}",
65
+ retryable=False,
66
+ suggested_action="fix_input",
65
67
  )
66
68
 
67
69
  # Fingerprint check (unless skipped)
@@ -177,7 +177,7 @@ def page_list(
177
177
  from confpub.confluence import build_client, _slim_page
178
178
  client = build_client()
179
179
  pages = client.list_pages(space)
180
- ctx.result = {"pages": [_slim_page(p) for p in pages]}
180
+ ctx.result = {"pages": [_slim_page(p, base_url=client._config.base_url.rstrip("/")) for p in pages]}
181
181
 
182
182
 
183
183
  @page_app.command("inspect")
@@ -186,6 +186,7 @@ def page_inspect(
186
186
  title: str = typer.Option(None, "--title", help="Page title"),
187
187
  page_id: str = typer.Option(None, "--page-id", help="Confluence page ID"),
188
188
  raw: bool = typer.Option(False, "--raw", help="Return full raw API response"),
189
+ format: str = typer.Option("storage", "--format", help="Output format: storage (raw HTML) or markdown"),
189
190
  ) -> None:
190
191
  """Inspect a Confluence page."""
191
192
  with command_context("page.inspect", target={"space": space, "title": title, "page_id": page_id}) as ctx:
@@ -201,7 +202,20 @@ def page_inspect(
201
202
  if not page:
202
203
  from confpub.errors import ERR_VALIDATION_NOT_FOUND
203
204
  raise ConfpubError(ERR_VALIDATION_NOT_FOUND, f"Page not found")
204
- ctx.result = page if raw else _slim_page(page)
205
+ if raw:
206
+ ctx.result = page
207
+ else:
208
+ result = _slim_page(page, base_url=client._config.base_url.rstrip("/"))
209
+ if format == "markdown" and "body_storage" in result:
210
+ from confpub.reverse_converter import convert_storage_to_markdown
211
+ conversion = convert_storage_to_markdown(result["body_storage"])
212
+ result["body_markdown"] = conversion.markdown
213
+ del result["body_storage"]
214
+ if conversion.warnings:
215
+ result["conversion_warnings"] = conversion.warnings
216
+ if conversion.unknown_macros:
217
+ result["unknown_macros"] = conversion.unknown_macros
218
+ ctx.result = result
205
219
 
206
220
 
207
221
  @page_app.command("publish")
@@ -484,6 +498,7 @@ def guide(
484
498
  raise validation_error(
485
499
  ERR_VALIDATION_REQUIRED,
486
500
  f"Unknown guide section: {section}",
501
+ valid_sections=list(full_guide.keys()),
487
502
  )
488
503
  ctx.result = result
489
504
  else:
@@ -32,10 +32,10 @@ class ConfluenceClient:
32
32
  self._call_count = 0
33
33
 
34
34
  # Suppress noisy atlassian-python-api logging (e.g. "Can't find 'X' page")
35
- from confpub.output import is_quiet
35
+ from confpub.output import is_verbose
36
36
 
37
37
  atlassian_logger = logging.getLogger("atlassian")
38
- atlassian_logger.setLevel(logging.CRITICAL if is_quiet() else logging.WARNING)
38
+ atlassian_logger.setLevel(logging.WARNING if is_verbose() else logging.CRITICAL)
39
39
 
40
40
  @staticmethod
41
41
  def _build_api(config: ResolvedConfig) -> Any:
@@ -74,7 +74,7 @@ class ConfluenceClient:
74
74
  raise ConfpubError(
75
75
  ERR_AUTH_FORBIDDEN,
76
76
  f"Permission denied ({context}): {msg}",
77
- suggested_action="escalate",
77
+ details={"note": "This may indicate a nonexistent resource; Confluence returns 403 for both."},
78
78
  ) from exc
79
79
  # Not found (404 or explicit "not found")
80
80
  if "404" in msg or "not found" in msg.lower():
@@ -403,7 +403,7 @@ class ConfluenceClient:
403
403
  return None
404
404
 
405
405
 
406
- def _slim_page(page: dict[str, Any]) -> dict[str, Any]:
406
+ def _slim_page(page: dict[str, Any], *, base_url: str = "") -> dict[str, Any]:
407
407
  """Extract agent-relevant fields from a raw Confluence page object."""
408
408
  result: dict[str, Any] = {
409
409
  "id": page.get("id"),
@@ -421,7 +421,7 @@ def _slim_page(page: dict[str, Any]) -> dict[str, Any]:
421
421
  result["body_storage"] = body
422
422
  links = page.get("_links", {})
423
423
  if "webui" in links:
424
- base = links.get("base", "")
424
+ base = links.get("base", "") or base_url
425
425
  result["webui"] = base + links["webui"]
426
426
  return result
427
427
 
@@ -66,6 +66,7 @@ def build_guide() -> dict[str, Any]:
66
66
  "mutates": False,
67
67
  "description": "Search Confluence content using CQL",
68
68
  "flags": ["--cql", "--space", "--type", "--limit", "--start", "--include-archived", "--excerpt-length"],
69
+ "agent_hint": "Most agent workflows should include --type page to exclude attachments and space entities from results.",
69
70
  "result_schema": {
70
71
  "cql_query": "string — effective CQL sent to the API",
71
72
  "results": "list of {id, type, title, excerpt, url, space_key, entity_type, status, last_modified, container_title}",
@@ -90,7 +91,7 @@ def build_guide() -> dict[str, Any]:
90
91
  "group": "read",
91
92
  "mutates": False,
92
93
  "description": "Inspect a Confluence page",
93
- "flags": ["--space", "--title", "--page-id"],
94
+ "flags": ["--space", "--title", "--page-id", "--format"],
94
95
  },
95
96
  "page.publish": {
96
97
  "group": "write",
@@ -232,6 +233,21 @@ def build_guide() -> dict[str, Any]:
232
233
  "to the same workspace return ERR_CONFLICT_LOCK"
233
234
  ),
234
235
  },
236
+ "lockfile": {
237
+ "description": "Local state file tracking page IDs and versions from publish/pull operations.",
238
+ "file": "confpub.lock",
239
+ "schema": {
240
+ "schema_version": "Lockfile format version (currently '1.0')",
241
+ "last_updated": "ISO 8601 timestamp of last write",
242
+ "pages": "Map of page title to { page_id, version }",
243
+ },
244
+ "behavior": [
245
+ "Created/updated automatically by page.publish, page.pull, and plan.apply",
246
+ "Written atomically (temp file + rename) for crash safety",
247
+ "Used by plan.create to detect existing pages and versions",
248
+ "Does not prevent concurrent operations — purely local state tracking",
249
+ ],
250
+ },
235
251
  "auth": {
236
252
  "precedence": [
237
253
  "--token + --user",
@@ -139,7 +139,12 @@ def load_manifest(path: str) -> Manifest:
139
139
  p = Path(path)
140
140
  if not p.exists():
141
141
  from confpub.errors import ERR_IO_FILE_NOT_FOUND
142
- raise ConfpubError(ERR_IO_FILE_NOT_FOUND, f"Manifest not found: {path}")
142
+ raise ConfpubError(
143
+ ERR_IO_FILE_NOT_FOUND,
144
+ f"Manifest not found: {path}",
145
+ retryable=False,
146
+ suggested_action="fix_input",
147
+ )
143
148
  try:
144
149
  data = yaml.safe_load(p.read_text(encoding="utf-8"))
145
150
  if not isinstance(data, dict):
@@ -70,6 +70,8 @@ def create_plan(
70
70
  ERR_IO_FILE_NOT_FOUND,
71
71
  f"Source file not found: {fp.file}",
72
72
  details={"file": fp.file},
73
+ retryable=False,
74
+ suggested_action="fix_input",
73
75
  )
74
76
 
75
77
  # Read and convert markdown
@@ -49,6 +49,8 @@ def publish_page(
49
49
  ERR_IO_FILE_NOT_FOUND,
50
50
  f"Source file not found: {file}",
51
51
  details={"file": file},
52
+ retryable=False,
53
+ suggested_action="fix_input",
52
54
  )
53
55
 
54
56
  # Derive title from filename if not provided
@@ -107,8 +107,9 @@ def _compute_file_paths(
107
107
  while current and current != root_page_id:
108
108
  chain.append(id_to_slug.get(current, current))
109
109
  current = id_to_parent.get(current) # type: ignore[assignment]
110
- if pid == root_page_id:
111
- chain.append(slug)
110
+ # Always include root as the outermost directory
111
+ if root_slug:
112
+ chain.append(root_slug)
112
113
  chain.reverse()
113
114
  rel_path = os.path.join(*chain, "index.md") if len(chain) > 0 else f"{slug}/index.md"
114
115
  paths[pid] = os.path.join(output_dir, rel_path)
@@ -185,6 +186,7 @@ def _build_page_tree(
185
186
  pages: list[dict[str, Any]],
186
187
  file_paths: dict[str, str],
187
188
  root_page_id: str,
189
+ output_dir: str = ".",
188
190
  ) -> list[dict[str, Any]]:
189
191
  """Build a hierarchical page tree for manifest generation."""
190
192
  id_to_entry: dict[str, dict[str, Any]] = {}
@@ -198,7 +200,7 @@ def _build_page_tree(
198
200
 
199
201
  id_to_entry[pid] = {
200
202
  "title": page.get("title", ""),
201
- "file": os.path.basename(file_path) if file_path else "",
203
+ "file": os.path.relpath(file_path, output_dir) if file_path else "",
202
204
  "children": [],
203
205
  }
204
206
  children_map.setdefault(parent_id, []).append(pid)
@@ -308,9 +310,9 @@ def pull_pages(
308
310
 
309
311
  # Generate manifest if requested or recursive with multiple pages
310
312
  manifest_file: str | None = None
311
- if generate_manifest or (recursive and len(all_pages) > 1):
313
+ if generate_manifest:
312
314
  root_title = root_page.get("title", "")
313
- page_tree = _build_page_tree(all_pages, file_paths, root_id)
315
+ page_tree = _build_page_tree(all_pages, file_paths, root_id, output_dir)
314
316
  manifest_yaml = generate_manifest_yaml(root_space, root_title, page_tree)
315
317
  manifest_path = os.path.join(output_dir, "confpub.yaml")
316
318
  Path(manifest_path).write_text(manifest_yaml, encoding="utf-8")
@@ -19,9 +19,14 @@ from confpub.manifest import PlanArtifact, PlanPage
19
19
 
20
20
  def _load_plan(plan_path: str) -> PlanArtifact:
21
21
  """Load and parse a plan artifact JSON file."""
22
- p = Path(plan_path)
22
+ p = Path(plan_path).resolve()
23
23
  if not p.exists():
24
- raise ConfpubError(ERR_IO_FILE_NOT_FOUND, f"Plan file not found: {plan_path}")
24
+ raise ConfpubError(
25
+ ERR_IO_FILE_NOT_FOUND,
26
+ f"Plan file not found: {plan_path}",
27
+ retryable=False,
28
+ suggested_action="fix_input",
29
+ )
25
30
  try:
26
31
  data = json.loads(p.read_text(encoding="utf-8"))
27
32
  return PlanArtifact(**data)
@@ -20,7 +20,12 @@ def _load_assertions(assertions_path: str | None, plan_path: str | None) -> list
20
20
  if assertions_path:
21
21
  p = Path(assertions_path)
22
22
  if not p.exists():
23
- raise ConfpubError(ERR_IO_FILE_NOT_FOUND, f"Assertions file not found: {assertions_path}")
23
+ raise ConfpubError(
24
+ ERR_IO_FILE_NOT_FOUND,
25
+ f"Assertions file not found: {assertions_path}",
26
+ retryable=False,
27
+ suggested_action="fix_input",
28
+ )
24
29
  try:
25
30
  data = json.loads(p.read_text(encoding="utf-8"))
26
31
  if isinstance(data, list):
@@ -33,7 +38,12 @@ def _load_assertions(assertions_path: str | None, plan_path: str | None) -> list
33
38
  # Try to load assertions from the plan's source manifest
34
39
  p = Path(plan_path)
35
40
  if not p.exists():
36
- raise ConfpubError(ERR_IO_FILE_NOT_FOUND, f"Plan file not found: {plan_path}")
41
+ raise ConfpubError(
42
+ ERR_IO_FILE_NOT_FOUND,
43
+ f"Plan file not found: {plan_path}",
44
+ retryable=False,
45
+ suggested_action="fix_input",
46
+ )
37
47
  plan_data = json.loads(p.read_text(encoding="utf-8"))
38
48
  # Plan doesn't contain assertions directly — return empty
39
49
  return []
@@ -6,7 +6,7 @@ from confpub.guide import build_guide
6
6
  class TestBuildGuide:
7
7
  def test_has_required_top_level_keys(self):
8
8
  guide = build_guide()
9
- for key in ("schema_version", "commands", "error_codes", "auth", "concurrency"):
9
+ for key in ("schema_version", "commands", "error_codes", "auth", "concurrency", "lockfile"):
10
10
  assert key in guide, f"Missing top-level key: {key}"
11
11
 
12
12
  def test_all_commands_present(self):
@@ -176,6 +176,7 @@ class TestRecursivePull:
176
176
  page_id="1",
177
177
  output_dir=str(tmp_path),
178
178
  recursive=True,
179
+ generate_manifest=True,
179
180
  )
180
181
 
181
182
  assert result["summary"]["manifest_generated"] is True
@@ -265,7 +266,7 @@ class TestLayoutModes:
265
266
 
266
267
  assert result["layout"] == "nested"
267
268
  assert (tmp_path / "root" / "index.md").exists()
268
- assert (tmp_path / "child" / "index.md").exists()
269
+ assert (tmp_path / "root" / "child" / "index.md").exists()
269
270
 
270
271
 
271
272
  # ---------------------------------------------------------------------------
@@ -405,6 +406,7 @@ class TestDataCenterCompat:
405
406
  result = pull_pages(
406
407
  space="PROJ", title="Root",
407
408
  output_dir=str(tmp_path), recursive=True,
409
+ generate_manifest=True,
408
410
  )
409
411
 
410
412
  assert result["summary"]["pages_pulled"] == 2
@@ -1,226 +0,0 @@
1
- # confpub-cli v0.2.3 — Blind Test Feedback
2
-
3
- **Tester**: Claude Opus 4.6 (LLM agent)
4
- **Date**: 2026-03-01
5
- **Environment**: Windows 11, bash shell, `uvx confpub-cli`
6
- **Confluence**: Cloud instance (thomasklokrohde.atlassian.net)
7
-
8
- ---
9
-
10
- ## Overall Impression
11
-
12
- confpub-cli is a remarkably well-designed agent-first CLI. As an LLM agent driving it
13
- zero-shot, I was productive within seconds. The `guide` command gave me everything I
14
- needed to understand the full command surface, error taxonomy, and concurrency rules.
15
- The consistent JSON envelope made it trivial to parse every response. This is one of
16
- the best-designed CLIs I've encountered for agent consumption.
17
-
18
- **Rating: 4/5** — Excellent foundation with a handful of rough edges to polish.
19
-
20
- ---
21
-
22
- ## What Works Well
23
-
24
- ### Structured JSON Envelope
25
- Every command returns the same `{ ok, command, target, result, warnings, errors, metrics }`
26
- shape. Parsing is zero-effort. The `request_id` and `metrics.duration_ms` fields are a
27
- nice touch for tracing and diagnostics.
28
-
29
- ### `guide` Command
30
- Brilliant bootstrapping mechanism. One call gives an agent: every command with its flags,
31
- mutability annotations, error codes with exit codes and retry hints, auth precedence,
32
- and concurrency rules. The `--section` flag is useful for targeted queries. This alone
33
- puts confpub ahead of most CLI tools for agent integration.
34
-
35
- ### Transactional Plan Workflow
36
- The plan → validate → apply → verify pipeline is well thought out:
37
- - `plan create` generates a readable artifact with clear operation types
38
- - `plan validate` checks for drift before apply
39
- - `plan apply` supports `--dry-run` for preview
40
- - The lockfile (`confpub.lock`) enables idempotent re-publishing
41
-
42
- I tested the full cycle and it worked flawlessly: manifest → plan → validate → dry-run → apply → verify.
43
-
44
- ### Markdown Conversion
45
- Tested headings, bold/italic, inline code, fenced code blocks (with language), tables,
46
- admonitions (`[!NOTE]`, `[!WARNING]`, `[!TIP]`), strikethrough, and horizontal rules.
47
- All converted correctly to Confluence Storage Format. Particularly impressed that
48
- admonitions map to the proper Confluence Info/Warning/Tip macros.
49
-
50
- ### Error Taxonomy
51
- Stable error codes (`ERR_*`) with structured details, exit codes, and `suggested_action`
52
- hints (`fix_input`, `retry`, `reauth`, `escalate`). An agent can branch on these without
53
- parsing human-readable messages. The `retryable` flag and `retry_after_ms` for I/O errors
54
- are agent-friendly.
55
-
56
- ### Safety Design
57
- - Write commands are clearly annotated as `mutates: true` in the guide
58
- - `--dry-run` is available on both `page publish` and `plan apply`
59
- - `--cascade` is a separate opt-in for cascading deletes
60
- - `safety_flags` section in the guide calls out dangerous flags
61
-
62
- ### Auth Resolution
63
- Clean precedence chain (flags → env vars → config file → keychain) with auto-detection
64
- of Cloud vs Server from the URL. `auth inspect` gives a quick status check.
65
-
66
- ---
67
-
68
- ## Bugs Found
69
-
70
- ### BUG-1: `--space` flag ignored during page update (Severity: High)
71
-
72
- When I published a page to space `SD`, then re-published with `--space NONEXIST`, the
73
- command succeeded (exit 0) and updated the existing page in the `SD` space. The `--space`
74
- flag was silently ignored on the update path.
75
-
76
- **Repro:**
77
- ```bash
78
- confpub page publish test.md --space SD --parent "Software Development"
79
- # Creates page in SD, version 1
80
-
81
- confpub page publish test.md --space NONEXIST --parent "Software Development"
82
- # Expected: error (space not found or page not found in that space)
83
- # Actual: ok=true, updated page in SD to version 3
84
- ```
85
-
86
- **Impact**: An agent could accidentally update a page in the wrong space without any
87
- warning. The title-based lookup appears to match globally (or against the lockfile)
88
- rather than scoping to the specified space.
89
-
90
- ### BUG-2: Nonexistent page returns `ok: true` with `result: null` (Severity: Medium)
91
-
92
- ```bash
93
- confpub page inspect --space SD --title "nonexistent-page"
94
- # Returns: ok=true, result=null, errors=[]
95
- ```
96
-
97
- This should return `ok: false` with an appropriate error (e.g., a new `ERR_NOT_FOUND`
98
- code). An agent checking `ok` to determine success would incorrectly think the call
99
- succeeded. Currently the only way to detect "not found" is to check if `result` is null,
100
- which is an undocumented convention.
101
-
102
- A `"Can't find ... page"` message also leaks to stderr, suggesting the underlying library
103
- knows it's not found — the CLI just doesn't surface it as a structured error.
104
-
105
- ### BUG-3: Missing required options return exit code 2, not JSON envelope (Severity: Medium)
106
-
107
- ```bash
108
- confpub page list
109
- # Returns Typer's error format: "Missing option '--space'." with exit code 2
110
- ```
111
-
112
- This breaks the documented invariant: "stdout is exclusively JSON — one object, no
113
- preamble, no epilogue." An agent expecting to always parse JSON on stdout will crash.
114
- Exit code 2 is also undocumented (the error code table only covers 0/10/20/40/50/90).
115
-
116
- **Suggestion**: Catch Typer's `MissingParameter` and convert it to an
117
- `ERR_VALIDATION_REQUIRED` envelope with exit code 10.
118
-
119
- ### BUG-4: `--backup` flag produces no observable output (Severity: Low)
120
-
121
- ```bash
122
- confpub page publish test.md --space SD --parent "Software Development" --backup
123
- ```
124
-
125
- The command succeeded but the result JSON contains no mention of a backup being created —
126
- no backup file path, no `backup: true` field, nothing. Either the backup didn't happen,
127
- or it happened silently. An agent has no way to confirm.
128
-
129
- ### BUG-5: Nonexistent parent accepted in dry-run (Severity: Low)
130
-
131
- ```bash
132
- confpub page publish test.md --space SD --parent "Nonexistent Parent" --dry-run
133
- # Returns: ok=true, type=page.update
134
- ```
135
-
136
- Dry-run should ideally validate that the parent page exists, or at least emit a warning.
137
- Currently it plans an update to a nonexistent parent, which would fail on real apply.
138
-
139
- ---
140
-
141
- ## Improvement Suggestions
142
-
143
- ### 1. Normalize attachment command output
144
-
145
- `attachment.list` and `attachment.upload` return raw Confluence API responses with
146
- internal fields (`_expandable`, ARIs, `base64EncodedAri`, full user profiles). Every
147
- other command returns a curated result. These should be normalized to the same level
148
- of curation.
149
-
150
- **Suggested `attachment.list` shape:**
151
- ```json
152
- {
153
- "attachments": [
154
- {
155
- "id": "att262400",
156
- "title": "test-attachment.txt",
157
- "media_type": "application/binary",
158
- "file_size": 27,
159
- "download_url": "/download/attachments/360459/test-attachment.txt?..."
160
- }
161
- ]
162
- }
163
- ```
164
-
165
- ### 2. Add `--format` flag or page count to `page.list`
166
-
167
- For spaces with many pages, `page.list` returns everything. Consider:
168
- - A `--limit` / `--offset` for pagination
169
- - A `--format compact` option (just titles + IDs)
170
- - Include total count in the result
171
-
172
- ### 3. Suppress `"Can't find ... page"` stderr noise
173
-
174
- Multiple commands emit `"Can't find 'X' page on ..."` to stderr. This comes from the
175
- underlying `atlassian-python-api` library but leaks through even with `--quiet`. Since
176
- not finding a page is a normal flow (e.g., first publish), this shouldn't appear by
177
- default — maybe only with `--verbose`.
178
-
179
- ### 4. Add stdin support
180
-
181
- `echo "# Hello" | confpub page publish - --space SD --parent "Docs" --title "Hello"`
182
- would be useful for piped workflows and agent-generated content. Currently `-` is treated
183
- as a literal filename.
184
-
185
- ### 5. `plan verify` with no assertions is a no-op
186
-
187
- ```bash
188
- confpub plan verify --plan confpub-plan.json
189
- # Returns: all_passed=true, results=[]
190
- ```
191
-
192
- Without `--assertions`, this always passes vacuously. Consider:
193
- - Auto-generating basic assertions from the plan (pages exist, correct parent, version incremented)
194
- - Emitting a warning when no assertions are provided
195
-
196
- ### 6. Consider `--json` flag for Typer-level errors
197
-
198
- As a bridge until BUG-3 is fully fixed, a `--json` flag could force JSON output even
199
- for framework-level errors (missing options, unknown commands).
200
-
201
- ### 7. Lockfile includes pages from `page publish` and `plan apply`
202
-
203
- The lockfile accumulated entries from both `page publish` (single-file mode) and
204
- `plan apply`. This might be intentional for idempotency, but it could surprise users
205
- who expected the lockfile to only track manifest-managed pages. Consider documenting
206
- this behavior or separating the two.
207
-
208
- ---
209
-
210
- ## Summary
211
-
212
- | Area | Score |
213
- |------|-------|
214
- | Agent discoverability (`guide`) | 5/5 |
215
- | JSON envelope consistency | 4/5 (BUG-3 breaks it for Typer errors) |
216
- | Error handling | 4/5 (BUG-2 masks not-found) |
217
- | Markdown conversion | 5/5 |
218
- | Plan workflow | 5/5 |
219
- | Write correctness | 3/5 (BUG-1 is a real data integrity risk) |
220
- | Output curation | 3/5 (attachments leak raw API) |
221
- | Documentation (README) | 5/5 |
222
-
223
- confpub is production-ready for the core read + plan + publish workflows. The main
224
- blocker is BUG-1 (space flag ignored on updates), which could cause silent cross-space
225
- writes. Fixing that plus normalizing the attachment output would bring this to a
226
- strong 5/5.
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes