cuff-cli 0.1.0__tar.gz → 0.2.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cuff-cli
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Cuff subject-bound claim, evidence, and verification gate.
5
5
  Author: Fab7
6
6
  License-Expression: Apache-2.0
@@ -38,7 +38,7 @@ worktree, or stages, commits, fetches, pushes, releases, or deploys anything.
38
38
  Install a released version as a standard uv-managed tool:
39
39
 
40
40
  ```bash
41
- uv tool install cuff-cli==0.1.0
41
+ uv tool install cuff-cli==0.2.0
42
42
  cuff --version
43
43
  ```
44
44
 
@@ -57,12 +57,12 @@ Run initialization at the exact Git worktree root:
57
57
 
58
58
  ```bash
59
59
  cuff init --json
60
- git add .cuff/project.json
60
+ git add .fab7/cuff/project.json
61
61
  git commit -m "Initialize Cuff"
62
62
  ```
63
63
 
64
64
  The marker is exactly `{"schema":1}` and records live under
65
- `.cuff/records/`. An incompatible marker is never rewritten or migrated.
65
+ `.fab7/cuff/records/`. An incompatible marker is never rewritten or migrated.
66
66
 
67
67
  The preferred path atomically appends a claim and its observed evidence:
68
68
 
@@ -123,19 +123,36 @@ spend, or residual-risk authority.
123
123
 
124
124
  ## Static host integrations
125
125
 
126
- Small native assets live in [`plugins/claude/cuff`](plugins/claude/cuff) and
127
- [`plugins/codex/cuff`](plugins/codex/cuff). The corresponding host plugin
128
- manager owns their installation and removal. These assets require only the
129
- uv-managed `cuff` executable on `PATH`; Cuff itself does not install plugins.
126
+ One native payload lives in [`plugins/cuff`](plugins/cuff) and contains both
127
+ host manifests, Claude Code commands, and Codex skills. The shared
128
+ `fab7hq/fab7` marketplace owns registration; this repository owns the payload.
129
+ The assets require the uv-managed `cuff` executable on `PATH`.
130
130
 
131
131
  ```bash
132
132
  # Codex
133
- codex plugin marketplace add fab7hq/cuff --ref v0.1.0
134
- codex plugin add cuff@fab7hq
133
+ codex plugin marketplace add fab7hq/fab7
134
+ codex plugin add cuff@fab7
135
135
 
136
136
  # Claude Code
137
- claude plugin marketplace add fab7hq/cuff@v0.1.0
138
- claude plugin install cuff@fab7hq --scope user
137
+ claude plugin marketplace add fab7hq/fab7
138
+ claude plugin install cuff@fab7 --scope user
139
+ ```
140
+
141
+ Validate the built candidate and both host payloads without touching the
142
+ normal host configuration:
143
+
144
+ ```bash
145
+ uv build --out-dir ../sandbox/cuff-01/dist
146
+ uv run python tools/local_release_check.py --host all
147
+ uv run python tools/local_release_check.py --host all --prepare-auth \
148
+ --candidate-commit COMMIT \
149
+ --evidence-dir ../sandbox/cuff-01/e2e
150
+ # Log each CLI in using the isolated home paths printed by the prepare phase.
151
+ uv run python tools/local_release_check.py --host all --live --reuse-prepared \
152
+ --candidate-commit COMMIT \
153
+ --evidence-dir ../sandbox/cuff-01/e2e \
154
+ --codex-model MODEL \
155
+ --claude-model MODEL
139
156
  ```
140
157
 
141
158
  See [RUNBOOK.md](RUNBOOK.md) for operations, [the architecture overview](docs/architecture/overview.md)
@@ -27,7 +27,7 @@ worktree, or stages, commits, fetches, pushes, releases, or deploys anything.
27
27
  Install a released version as a standard uv-managed tool:
28
28
 
29
29
  ```bash
30
- uv tool install cuff-cli==0.1.0
30
+ uv tool install cuff-cli==0.2.0
31
31
  cuff --version
32
32
  ```
33
33
 
@@ -46,12 +46,12 @@ Run initialization at the exact Git worktree root:
46
46
 
47
47
  ```bash
48
48
  cuff init --json
49
- git add .cuff/project.json
49
+ git add .fab7/cuff/project.json
50
50
  git commit -m "Initialize Cuff"
51
51
  ```
52
52
 
53
53
  The marker is exactly `{"schema":1}` and records live under
54
- `.cuff/records/`. An incompatible marker is never rewritten or migrated.
54
+ `.fab7/cuff/records/`. An incompatible marker is never rewritten or migrated.
55
55
 
56
56
  The preferred path atomically appends a claim and its observed evidence:
57
57
 
@@ -112,19 +112,36 @@ spend, or residual-risk authority.
112
112
 
113
113
  ## Static host integrations
114
114
 
115
- Small native assets live in [`plugins/claude/cuff`](plugins/claude/cuff) and
116
- [`plugins/codex/cuff`](plugins/codex/cuff). The corresponding host plugin
117
- manager owns their installation and removal. These assets require only the
118
- uv-managed `cuff` executable on `PATH`; Cuff itself does not install plugins.
115
+ One native payload lives in [`plugins/cuff`](plugins/cuff) and contains both
116
+ host manifests, Claude Code commands, and Codex skills. The shared
117
+ `fab7hq/fab7` marketplace owns registration; this repository owns the payload.
118
+ The assets require the uv-managed `cuff` executable on `PATH`.
119
119
 
120
120
  ```bash
121
121
  # Codex
122
- codex plugin marketplace add fab7hq/cuff --ref v0.1.0
123
- codex plugin add cuff@fab7hq
122
+ codex plugin marketplace add fab7hq/fab7
123
+ codex plugin add cuff@fab7
124
124
 
125
125
  # Claude Code
126
- claude plugin marketplace add fab7hq/cuff@v0.1.0
127
- claude plugin install cuff@fab7hq --scope user
126
+ claude plugin marketplace add fab7hq/fab7
127
+ claude plugin install cuff@fab7 --scope user
128
+ ```
129
+
130
+ Validate the built candidate and both host payloads without touching the
131
+ normal host configuration:
132
+
133
+ ```bash
134
+ uv build --out-dir ../sandbox/cuff-01/dist
135
+ uv run python tools/local_release_check.py --host all
136
+ uv run python tools/local_release_check.py --host all --prepare-auth \
137
+ --candidate-commit COMMIT \
138
+ --evidence-dir ../sandbox/cuff-01/e2e
139
+ # Log each CLI in using the isolated home paths printed by the prepare phase.
140
+ uv run python tools/local_release_check.py --host all --live --reuse-prepared \
141
+ --candidate-commit COMMIT \
142
+ --evidence-dir ../sandbox/cuff-01/e2e \
143
+ --codex-model MODEL \
144
+ --claude-model MODEL
128
145
  ```
129
146
 
130
147
  See [RUNBOOK.md](RUNBOOK.md) for operations, [the architecture overview](docs/architecture/overview.md)
@@ -1,4 +1,4 @@
1
1
  """Cuff's host-neutral claim and evidence gate."""
2
2
 
3
3
  __all__ = ["__version__"]
4
- __version__ = "0.1.0"
4
+ __version__ = "0.2.0"
@@ -11,7 +11,7 @@ from typing import Any
11
11
  from . import __version__
12
12
  from .errors import CuffError
13
13
  from .gate import check
14
- from .ledger import append, create_claim, seal, verify
14
+ from .ledger import append, create_claim, normalize_work_item, seal, verify
15
15
  from .subject import declared_subject, filesystem_subject
16
16
  from .workspace import find_workspace, initialize_workspace
17
17
 
@@ -137,13 +137,15 @@ def main(argv: list[str] | None = None) -> int:
137
137
  except KeyboardInterrupt:
138
138
  return 130
139
139
  except Exception as exc:
140
+ error = {"code": "CUFF_UNEXPECTED", "message": f"{type(exc).__name__}: {exc}"}
141
+ if getattr(args, "command", None) == "check":
142
+ data = _check_error(args, error)
143
+ else:
144
+ data = {"ok": False, "errors": [error]}
140
145
  if getattr(args, "json", False):
141
- print(json.dumps({
142
- "ok": False,
143
- "errors": [{"code": "CUFF_UNEXPECTED", "message": f"{type(exc).__name__}: {exc}"}],
144
- }, sort_keys=True, indent=2))
146
+ print(json.dumps(data, sort_keys=True, indent=2))
145
147
  else:
146
- print(f"CUFF_UNEXPECTED: {type(exc).__name__}: {exc}", file=sys.stderr)
148
+ print(f"{error['code']}: {error['message']}", file=sys.stderr)
147
149
  return 3
148
150
  return 2
149
151
 
@@ -211,7 +213,11 @@ def _finish_result(args: argparse.Namespace, data: dict[str, Any], label: str) -
211
213
 
212
214
 
213
215
  def _finish_error(args: argparse.Namespace, error: CuffError) -> int:
214
- data = {"ok": False, "errors": [error.to_dict()]}
216
+ data = (
217
+ _check_error(args, error.to_dict())
218
+ if getattr(args, "command", None) == "check"
219
+ else {"ok": False, "errors": [error.to_dict()]}
220
+ )
215
221
  if getattr(args, "json", False):
216
222
  print(json.dumps(data, sort_keys=True, indent=2))
217
223
  else:
@@ -219,6 +225,22 @@ def _finish_error(args: argparse.Namespace, error: CuffError) -> int:
219
225
  return 1
220
226
 
221
227
 
228
+ def _check_error(args: argparse.Namespace, error: dict[str, Any]) -> dict[str, Any]:
229
+ work_item = getattr(args, "work_item", None)
230
+ try:
231
+ work_item = normalize_work_item(work_item)
232
+ except CuffError:
233
+ pass
234
+ return {
235
+ "ok": False,
236
+ "errors": [error],
237
+ "work_item": work_item,
238
+ "latest_claim": None,
239
+ "selected_evidence": None,
240
+ "record_count": 0,
241
+ }
242
+
243
+
222
244
  def _replay(content: bytes, stream: Any) -> None:
223
245
  if content:
224
246
  stream.write(content.decode(errors="replace"))
@@ -43,26 +43,54 @@ def check(
43
43
  _fail(result["errors"], "CUFF_CLAIM_MISSING", "No completion claim exists", work_item=normalized)
44
44
  return result
45
45
 
46
- passing = [
46
+ linked = [
47
47
  record
48
48
  for record in records
49
49
  if record["type"] == "evidence"
50
50
  and record["claim"] == claim["id"]
51
- and record["exit_code"] == 0
52
51
  ]
53
- for evidence in reversed(passing):
54
- if not _subject_fresh(root, claim):
55
- break
56
- if git_context is None or not _evidence_fresh(root, evidence, head, git_context):
57
- continue
58
- result["selected_evidence"] = evidence
59
- result["ok"] = not result["errors"]
52
+ if not linked:
53
+ _fail(
54
+ result["errors"],
55
+ "CUFF_EVIDENCE_MISSING",
56
+ "Latest claim has no linked evidence",
57
+ work_item=normalized,
58
+ record_id=claim["id"],
59
+ )
60
+ return result
61
+
62
+ passing = [record for record in linked if record["exit_code"] == 0]
63
+ if not passing:
64
+ _fail(
65
+ result["errors"],
66
+ "CUFF_EVIDENCE_FAILED",
67
+ "Latest claim has linked evidence, but none passed",
68
+ work_item=normalized,
69
+ record_id=claim["id"],
70
+ )
71
+ return result
72
+
73
+ if not _subject_fresh(root, claim):
74
+ _fail(
75
+ result["errors"],
76
+ "CUFF_SUBJECT_STALE",
77
+ "Latest claim subject changed or cannot be recomputed",
78
+ work_item=normalized,
79
+ record_id=claim["id"],
80
+ )
60
81
  return result
61
82
 
83
+ if git_context is not None and not result["errors"]:
84
+ for evidence in reversed(passing):
85
+ if _evidence_fresh(root, evidence, head, git_context):
86
+ result["selected_evidence"] = evidence
87
+ result["ok"] = True
88
+ return result
89
+
62
90
  _fail(
63
91
  result["errors"],
64
- "CUFF_EVIDENCE_MISSING",
65
- "Latest claim has no fresh passing evidence",
92
+ "CUFF_EVIDENCE_STALE",
93
+ "Passing evidence no longer applies to the selected Git state",
66
94
  work_item=normalized,
67
95
  record_id=claim["id"],
68
96
  )
@@ -21,7 +21,8 @@ from .errors import CuffError
21
21
  from .subject import SHA256_RE, current_subject, validate_subject
22
22
 
23
23
 
24
- RECORDS_DIR = ".cuff/records"
24
+ STATE_DIR = ".fab7/cuff"
25
+ RECORDS_DIR = ".fab7/cuff/records"
25
26
  WORK_ITEM_RE = re.compile(r"^[a-z0-9_.-]{1,120}$")
26
27
  RECORD_ID_RE = re.compile(r"^rec_[0-9a-f]{32}$")
27
28
  GIT_OID_RE = re.compile(r"^[0-9a-f]{40}(?:[0-9a-f]{24})?$")
@@ -91,7 +92,8 @@ def resolve_actor(explicit: str | None) -> str:
91
92
 
92
93
  def init(root: Path) -> Path:
93
94
  path = root / RECORDS_DIR
94
- if (root / ".cuff").is_symlink() or path.is_symlink():
95
+ state = root / STATE_DIR
96
+ if state.parent.is_symlink() or state.is_symlink() or path.is_symlink():
95
97
  raise CuffError("CUFF_PATH_INVALID", "Cuff workspace directories must not be symlinks")
96
98
  path.mkdir(parents=True, exist_ok=True)
97
99
  read_all(root)
@@ -315,7 +317,7 @@ def validate_record(record: Any) -> None:
315
317
  if "git_ref" in record:
316
318
  raise CuffError(
317
319
  "CUFF_LEDGER_INVALID",
318
- "Ledger schema is incompatible; archive or remove .cuff and run cuff init",
320
+ "Ledger schema is incompatible; archive or remove .fab7/cuff and run cuff init",
319
321
  )
320
322
  record_type = record.get("type")
321
323
  allowed = (
@@ -22,6 +22,7 @@ MAX_FILES = 10_000
22
22
  MAX_BYTES = 256 * 1024 * 1024
23
23
  MAX_DEPTH = 64
24
24
  MAX_NAME_BYTES = 255
25
+ STATE_PARTS = (".fab7", "cuff")
25
26
 
26
27
 
27
28
  def declared_subject(kind: str, ref: str, digest: str) -> dict[str, str]:
@@ -107,7 +108,7 @@ def _bounded_target(workspace: Path, path: Path) -> tuple[Path, str]:
107
108
  relative = lexical.relative_to(workspace)
108
109
  except ValueError as exc:
109
110
  raise CuffError("CUFF_SUBJECT_PATH_INVALID", "Subject path escapes the workspace") from exc
110
- if relative.parts[:1] == (".cuff",):
111
+ if relative.parts[:len(STATE_PARTS)] == STATE_PARTS:
111
112
  raise CuffError("CUFF_SUBJECT_PATH_INVALID", "Cuff controller state cannot be a subject")
112
113
  current = workspace
113
114
  for part in relative.parts:
@@ -143,7 +144,7 @@ def _tree_entries(workspace: Path, target: Path) -> list[dict[str, Any]]:
143
144
  for name in names:
144
145
  candidate = directory_path / name
145
146
  workspace_relative = candidate.relative_to(workspace)
146
- if workspace_relative.parts[:1] == (".cuff",):
147
+ if workspace_relative.parts[:len(STATE_PARTS)] == STATE_PARTS:
147
148
  continue
148
149
  directory_mode = candidate.stat(follow_symlinks=False).st_mode
149
150
  if stat.S_ISLNK(directory_mode):
@@ -10,11 +10,11 @@ from typing import Any
10
10
 
11
11
  from . import git
12
12
  from .errors import CuffError
13
- from .ledger import RECORDS_DIR, init as init_records
13
+ from .ledger import RECORDS_DIR, STATE_DIR, init as init_records
14
14
 
15
15
 
16
16
  MAX_PARENT_WALK = 64
17
- PROJECT_PATH = Path(".cuff/project.json")
17
+ PROJECT_PATH = Path(".fab7/cuff/project.json")
18
18
  PROJECT_MARKER = {"schema": 1}
19
19
  PROJECT_MARKER_BYTES = b'{"schema":1}\n'
20
20
 
@@ -28,7 +28,7 @@ def initialize_workspace(
28
28
  start = (cwd or Path.cwd()).resolve()
29
29
  root = _existing_directory(explicit or start, start)
30
30
  _require_git_root(root)
31
- state = root / ".cuff"
31
+ state = root / STATE_DIR
32
32
  marker = root / PROJECT_PATH
33
33
  _require_safe_state_paths(state, marker)
34
34
  _require_safe_records_path(root / RECORDS_DIR)
@@ -37,7 +37,7 @@ def initialize_workspace(
37
37
  if existed:
38
38
  _read_marker(marker)
39
39
  else:
40
- state.mkdir(mode=0o755, exist_ok=True)
40
+ state.mkdir(mode=0o755, parents=True, exist_ok=True)
41
41
  init_records(root)
42
42
  if not existed:
43
43
  _create_marker(marker)
@@ -63,9 +63,9 @@ def find_workspace(
63
63
 
64
64
  current = start
65
65
  for _ in range(MAX_PARENT_WALK):
66
- state = current / ".cuff"
66
+ state = current / STATE_DIR
67
67
  marker = current / PROJECT_PATH
68
- if state.is_symlink() or (state.exists() and not state.is_dir()):
68
+ if state.parent.is_symlink() or state.is_symlink() or (state.exists() and not state.is_dir()):
69
69
  raise CuffError(
70
70
  "CUFF_PATH_INVALID",
71
71
  "Cuff state path in workspace discovery is unsafe",
@@ -82,7 +82,7 @@ def find_workspace(
82
82
 
83
83
 
84
84
  def _validate_project(root: Path) -> None:
85
- state = root / ".cuff"
85
+ state = root / STATE_DIR
86
86
  marker = root / PROJECT_PATH
87
87
  if not marker.exists() and not marker.is_symlink():
88
88
  raise _not_initialized(root)
@@ -123,6 +123,12 @@ def _existing_directory(path: Path, start: Path) -> Path:
123
123
 
124
124
 
125
125
  def _require_safe_state_paths(state: Path, marker: Path) -> None:
126
+ if state.parent.is_symlink() or (state.parent.exists() and not state.parent.is_dir()):
127
+ raise CuffError(
128
+ "CUFF_PATH_INVALID",
129
+ "Cuff state directory must be a non-symlink directory",
130
+ {"path": str(state.parent)},
131
+ )
126
132
  if state.is_symlink() or (state.exists() and not state.is_dir()):
127
133
  raise CuffError(
128
134
  "CUFF_PATH_INVALID",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cuff-cli
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Cuff subject-bound claim, evidence, and verification gate.
5
5
  Author: Fab7
6
6
  License-Expression: Apache-2.0
@@ -38,7 +38,7 @@ worktree, or stages, commits, fetches, pushes, releases, or deploys anything.
38
38
  Install a released version as a standard uv-managed tool:
39
39
 
40
40
  ```bash
41
- uv tool install cuff-cli==0.1.0
41
+ uv tool install cuff-cli==0.2.0
42
42
  cuff --version
43
43
  ```
44
44
 
@@ -57,12 +57,12 @@ Run initialization at the exact Git worktree root:
57
57
 
58
58
  ```bash
59
59
  cuff init --json
60
- git add .cuff/project.json
60
+ git add .fab7/cuff/project.json
61
61
  git commit -m "Initialize Cuff"
62
62
  ```
63
63
 
64
64
  The marker is exactly `{"schema":1}` and records live under
65
- `.cuff/records/`. An incompatible marker is never rewritten or migrated.
65
+ `.fab7/cuff/records/`. An incompatible marker is never rewritten or migrated.
66
66
 
67
67
  The preferred path atomically appends a claim and its observed evidence:
68
68
 
@@ -123,19 +123,36 @@ spend, or residual-risk authority.
123
123
 
124
124
  ## Static host integrations
125
125
 
126
- Small native assets live in [`plugins/claude/cuff`](plugins/claude/cuff) and
127
- [`plugins/codex/cuff`](plugins/codex/cuff). The corresponding host plugin
128
- manager owns their installation and removal. These assets require only the
129
- uv-managed `cuff` executable on `PATH`; Cuff itself does not install plugins.
126
+ One native payload lives in [`plugins/cuff`](plugins/cuff) and contains both
127
+ host manifests, Claude Code commands, and Codex skills. The shared
128
+ `fab7hq/fab7` marketplace owns registration; this repository owns the payload.
129
+ The assets require the uv-managed `cuff` executable on `PATH`.
130
130
 
131
131
  ```bash
132
132
  # Codex
133
- codex plugin marketplace add fab7hq/cuff --ref v0.1.0
134
- codex plugin add cuff@fab7hq
133
+ codex plugin marketplace add fab7hq/fab7
134
+ codex plugin add cuff@fab7
135
135
 
136
136
  # Claude Code
137
- claude plugin marketplace add fab7hq/cuff@v0.1.0
138
- claude plugin install cuff@fab7hq --scope user
137
+ claude plugin marketplace add fab7hq/fab7
138
+ claude plugin install cuff@fab7 --scope user
139
+ ```
140
+
141
+ Validate the built candidate and both host payloads without touching the
142
+ normal host configuration:
143
+
144
+ ```bash
145
+ uv build --out-dir ../sandbox/cuff-01/dist
146
+ uv run python tools/local_release_check.py --host all
147
+ uv run python tools/local_release_check.py --host all --prepare-auth \
148
+ --candidate-commit COMMIT \
149
+ --evidence-dir ../sandbox/cuff-01/e2e
150
+ # Log each CLI in using the isolated home paths printed by the prepare phase.
151
+ uv run python tools/local_release_check.py --host all --live --reuse-prepared \
152
+ --candidate-commit COMMIT \
153
+ --evidence-dir ../sandbox/cuff-01/e2e \
154
+ --codex-model MODEL \
155
+ --claude-model MODEL
139
156
  ```
140
157
 
141
158
  See [RUNBOOK.md](RUNBOOK.md) for operations, [the architecture overview](docs/architecture/overview.md)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cuff-cli"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  description = "Cuff subject-bound claim, evidence, and verification gate."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
File without changes
File without changes
File without changes
File without changes
File without changes