duet-cli 0.2.20__tar.gz → 0.2.21__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.
- {duet_cli-0.2.20/duet_cli.egg-info → duet_cli-0.2.21}/PKG-INFO +3 -3
- {duet_cli-0.2.20 → duet_cli-0.2.21}/README.md +2 -2
- {duet_cli-0.2.20 → duet_cli-0.2.21}/duet.py +12 -5
- {duet_cli-0.2.20 → duet_cli-0.2.21/duet_cli.egg-info}/PKG-INFO +3 -3
- {duet_cli-0.2.20 → duet_cli-0.2.21}/tests/test_finding_reports.py +21 -2
- {duet_cli-0.2.20 → duet_cli-0.2.21}/tests/test_finding_workflow.py +29 -2
- {duet_cli-0.2.20 → duet_cli-0.2.21}/LICENSE +0 -0
- {duet_cli-0.2.20 → duet_cli-0.2.21}/MANIFEST.in +0 -0
- {duet_cli-0.2.20 → duet_cli-0.2.21}/duet_cli.egg-info/SOURCES.txt +0 -0
- {duet_cli-0.2.20 → duet_cli-0.2.21}/duet_cli.egg-info/dependency_links.txt +0 -0
- {duet_cli-0.2.20 → duet_cli-0.2.21}/duet_cli.egg-info/entry_points.txt +0 -0
- {duet_cli-0.2.20 → duet_cli-0.2.21}/duet_cli.egg-info/requires.txt +0 -0
- {duet_cli-0.2.20 → duet_cli-0.2.21}/duet_cli.egg-info/top_level.txt +0 -0
- {duet_cli-0.2.20 → duet_cli-0.2.21}/packaging/duet-chatgpt/assets/icon.png +0 -0
- {duet_cli-0.2.20 → duet_cli-0.2.21}/packaging/duet-chatgpt/openai.json +0 -0
- {duet_cli-0.2.20 → duet_cli-0.2.21}/plugins/duet-chatgpt/LICENSE +0 -0
- {duet_cli-0.2.20 → duet_cli-0.2.21}/plugins/duet-chatgpt/plugin.json +0 -0
- {duet_cli-0.2.20 → duet_cli-0.2.21}/plugins/duet-chatgpt/skills/duet-chatgpt/SKILL.md +0 -0
- {duet_cli-0.2.20 → duet_cli-0.2.21}/plugins/duet-chatgpt/skills/duet-chatgpt/references/peer-brief.md +0 -0
- {duet_cli-0.2.20 → duet_cli-0.2.21}/plugins/duet-chatgpt/skills/duet-chatgpt/scripts/control.py +0 -0
- {duet_cli-0.2.20 → duet_cli-0.2.21}/pyproject.toml +0 -0
- {duet_cli-0.2.20 → duet_cli-0.2.21}/scripts/bump_release_version.py +0 -0
- {duet_cli-0.2.20 → duet_cli-0.2.21}/scripts/check_distribution_metadata.py +0 -0
- {duet_cli-0.2.20 → duet_cli-0.2.21}/scripts/check_sdist_tests.py +0 -0
- {duet_cli-0.2.20 → duet_cli-0.2.21}/scripts/duet_loop_e2e.py +0 -0
- {duet_cli-0.2.20 → duet_cli-0.2.21}/scripts/package_chatgpt_plugin.py +0 -0
- {duet_cli-0.2.20 → duet_cli-0.2.21}/setup.cfg +0 -0
- {duet_cli-0.2.20 → duet_cli-0.2.21}/tests/test_backend_metrics.py +0 -0
- {duet_cli-0.2.20 → duet_cli-0.2.21}/tests/test_bump_release_version.py +0 -0
- {duet_cli-0.2.20 → duet_cli-0.2.21}/tests/test_chatgpt_plugin_control.py +0 -0
- {duet_cli-0.2.20 → duet_cli-0.2.21}/tests/test_chatgpt_plugin_package.py +0 -0
- {duet_cli-0.2.20 → duet_cli-0.2.21}/tests/test_control_plane.py +0 -0
- {duet_cli-0.2.20 → duet_cli-0.2.21}/tests/test_duet.py +0 -0
- {duet_cli-0.2.20 → duet_cli-0.2.21}/tests/test_feedback_concurrency.py +0 -0
- {duet_cli-0.2.20 → duet_cli-0.2.21}/tests/test_loop_run_info.py +0 -0
- {duet_cli-0.2.20 → duet_cli-0.2.21}/tests/test_metrics_cli.py +0 -0
- {duet_cli-0.2.20 → duet_cli-0.2.21}/tests/test_metrics_collection.py +0 -0
- {duet_cli-0.2.20 → duet_cli-0.2.21}/tests/test_metrics_lifecycle.py +0 -0
- {duet_cli-0.2.20 → duet_cli-0.2.21}/tests/test_metrics_reporting.py +0 -0
- {duet_cli-0.2.20 → duet_cli-0.2.21}/tests/test_no_subagents.py +0 -0
- {duet_cli-0.2.20 → duet_cli-0.2.21}/tests/test_sdist_tests.py +0 -0
- {duet_cli-0.2.20 → duet_cli-0.2.21}/tests/test_stop_control_plane.py +0 -0
- {duet_cli-0.2.20 → duet_cli-0.2.21}/tests/test_zcode_backend.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: duet-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.21
|
|
4
4
|
Summary: Two CLI agents in conversation. One Python file. Stdlib only.
|
|
5
5
|
Author: Volkan Altan
|
|
6
6
|
License-Expression: MIT
|
|
@@ -33,9 +33,9 @@ Codex. Gemini and Copilot are also supported. A native ZCode account can
|
|
|
33
33
|
provide a [read-only review peer](https://github.com/volkan/duet/blob/main/docs/ZCODE.md).
|
|
34
34
|
[See examples using one CLI](https://github.com/volkan/duet/blob/main/docs/USAGE.md#same-backend-peering).
|
|
35
35
|
|
|
36
|
-
[](https://volkan.github.io/duet/demos/)
|
|
37
37
|
|
|
38
|
-
An edited replay using a public toy fixture. [
|
|
38
|
+
An edited replay using a public toy fixture. [Demo details and evidence](https://github.com/volkan/duet/blob/main/docs/demos/README.md).
|
|
39
39
|
|
|
40
40
|
## A real example
|
|
41
41
|
|
|
@@ -12,9 +12,9 @@ Codex. Gemini and Copilot are also supported. A native ZCode account can
|
|
|
12
12
|
provide a [read-only review peer](https://github.com/volkan/duet/blob/main/docs/ZCODE.md).
|
|
13
13
|
[See examples using one CLI](https://github.com/volkan/duet/blob/main/docs/USAGE.md#same-backend-peering).
|
|
14
14
|
|
|
15
|
-
[](https://volkan.github.io/duet/demos/)
|
|
16
16
|
|
|
17
|
-
An edited replay using a public toy fixture. [
|
|
17
|
+
An edited replay using a public toy fixture. [Demo details and evidence](https://github.com/volkan/duet/blob/main/docs/demos/README.md).
|
|
18
18
|
|
|
19
19
|
## A real example
|
|
20
20
|
|
|
@@ -74,7 +74,7 @@ from typing import Callable, Iterable, Optional
|
|
|
74
74
|
|
|
75
75
|
# ---------- defaults ----------
|
|
76
76
|
|
|
77
|
-
__version__ = "0.2.
|
|
77
|
+
__version__ = "0.2.21"
|
|
78
78
|
|
|
79
79
|
DEFAULT_SENTINEL = "<<<LGTM>>>"
|
|
80
80
|
DEFAULT_SANDBOX = "workspace-write"
|
|
@@ -6230,7 +6230,7 @@ _RUN_ID_RE = re.compile(r"^\d{8}-\d{6}(?:-\d+)?$")
|
|
|
6230
6230
|
|
|
6231
6231
|
# ---------- structured finding reports ----------
|
|
6232
6232
|
|
|
6233
|
-
_FINDING_ID_RE = re.compile(r"\A[
|
|
6233
|
+
_FINDING_ID_RE = re.compile(r"\A[A-Za-z0-9][A-Za-z0-9._-]{0,31}\Z")
|
|
6234
6234
|
_FINDING_BLOCK_MAX_BYTES = 64 * 1024
|
|
6235
6235
|
_FINDING_EVENT_LIMIT = 1000
|
|
6236
6236
|
_FINDING_DISPOSITIONS = frozenset({"supported", "refuted", "unresolved"})
|
|
@@ -6497,7 +6497,8 @@ def _finding_code(value: object) -> str:
|
|
|
6497
6497
|
|
|
6498
6498
|
|
|
6499
6499
|
def _render_finding(finding: dict) -> str:
|
|
6500
|
-
|
|
6500
|
+
# Inline code keeps IDs such as www.example.com or a._b_ from rendering as a link or emphasis.
|
|
6501
|
+
parts = [f"## `{finding['id']}` — {finding['disposition']} (agent assessment)",
|
|
6501
6502
|
_finding_code(finding["claim"])]
|
|
6502
6503
|
for event in finding["assessments"]:
|
|
6503
6504
|
origin = "inherited" if event["inherited"] else "current run"
|
|
@@ -7721,8 +7722,14 @@ def _finding_prompt_addendum(agent: Agent, cfg: DuetConfig) -> str:
|
|
|
7721
7722
|
'"disposition":"supported|refuted|unresolved","evidence":["file:line or '
|
|
7722
7723
|
'concrete check and result"],"objection":"remaining concern or null"}. '
|
|
7723
7724
|
"Use actual enum values, not the pipe-separated alternatives. Preserve IDs "
|
|
7724
|
-
"from the prior conversation
|
|
7725
|
-
"
|
|
7725
|
+
"from the prior conversation. A valid ID has 1 to 32 ASCII characters: it "
|
|
7726
|
+
"starts with a letter or digit, then uses letters, digits, '.', '_', or '-'. "
|
|
7727
|
+
"When the task, the kickoff, or the other agent already labels a claim with a "
|
|
7728
|
+
"valid ID, reuse that exact ID, including its case. Otherwise allocate a new "
|
|
7729
|
+
"ID with your prefix " + prefix + " and an unused positive integer. Never "
|
|
7730
|
+
"copy a label that is not a valid ID, such as '#1' or 'Finding 3': allocate "
|
|
7731
|
+
"a new ID and name that label in the claim. Reassess disputed findings "
|
|
7732
|
+
"explicitly. "
|
|
7726
7733
|
"Use unresolved when evidence is missing. Cite only evidence you inspected; "
|
|
7727
7734
|
"never invent executed checks or human acceptance. An empty findings array "
|
|
7728
7735
|
"means no finding updates, not proof that the review is correct. Keep recap "
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: duet-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.21
|
|
4
4
|
Summary: Two CLI agents in conversation. One Python file. Stdlib only.
|
|
5
5
|
Author: Volkan Altan
|
|
6
6
|
License-Expression: MIT
|
|
@@ -33,9 +33,9 @@ Codex. Gemini and Copilot are also supported. A native ZCode account can
|
|
|
33
33
|
provide a [read-only review peer](https://github.com/volkan/duet/blob/main/docs/ZCODE.md).
|
|
34
34
|
[See examples using one CLI](https://github.com/volkan/duet/blob/main/docs/USAGE.md#same-backend-peering).
|
|
35
35
|
|
|
36
|
-
[](https://volkan.github.io/duet/demos/)
|
|
37
37
|
|
|
38
|
-
An edited replay using a public toy fixture. [
|
|
38
|
+
An edited replay using a public toy fixture. [Demo details and evidence](https://github.com/volkan/duet/blob/main/docs/demos/README.md).
|
|
39
39
|
|
|
40
40
|
## A real example
|
|
41
41
|
|
|
@@ -47,7 +47,7 @@ class TestFindingParser(unittest.TestCase):
|
|
|
47
47
|
def test_duplicate_blocks_unclosed_json_and_duplicate_ids_reject_atomically(self):
|
|
48
48
|
for reply in (block([]) + "\n" + block([]), "```duet-findings\n{}",
|
|
49
49
|
"```duet-findings\nnot JSON\n```", block([item(), item()]),
|
|
50
|
-
block([item(), item("bad")])):
|
|
50
|
+
block([item(), item("bad id")])):
|
|
51
51
|
with self.subTest(reply=reply):
|
|
52
52
|
self.assertEqual(duet.parse_finding_updates(reply), {"status": "malformed", "items": []})
|
|
53
53
|
|
|
@@ -56,8 +56,18 @@ class TestFindingParser(unittest.TestCase):
|
|
|
56
56
|
self.assertEqual(duet.parse_finding_updates(reply)["status"], "missing")
|
|
57
57
|
self.assertEqual(duet.parse_finding_updates(reply + "\n" + block([]))["status"], "ok")
|
|
58
58
|
|
|
59
|
+
def test_caller_defined_ids_are_accepted_and_bad_ids_rejected(self):
|
|
60
|
+
valid = ["F1", "C1", "P1-1", "a.b_c-d", "9", "L" + "x" * 31]
|
|
61
|
+
parsed = duet.parse_finding_updates(block([item(identity) for identity in valid]))
|
|
62
|
+
self.assertEqual([entry["id"] for entry in parsed["items"]], valid)
|
|
63
|
+
invalid = ["", "F 1", "L" + "x" * 32, "-F1", ".F1", "\u00c9\u00c91", "F1\n",
|
|
64
|
+
"_F1", "#1", "Finding 3"]
|
|
65
|
+
for identity in invalid:
|
|
66
|
+
with self.subTest(identity=identity):
|
|
67
|
+
self.assertEqual(duet.parse_finding_updates(block([item(identity)]))["status"], "malformed")
|
|
68
|
+
|
|
59
69
|
def test_field_limits_and_types(self):
|
|
60
|
-
invalid = [item(
|
|
70
|
+
invalid = [item(claim=""),
|
|
61
71
|
item(claim="x" * 2001), item(disposition=[]), item(evidence="test"),
|
|
62
72
|
item(evidence=["x"] * 9), item(evidence=["x" * 2001]),
|
|
63
73
|
item(objection="x" * 2001), item(claim="\ud800")]
|
|
@@ -165,6 +175,15 @@ class TestFindingRendering(unittest.TestCase):
|
|
|
165
175
|
self.assertIn("private", rendered)
|
|
166
176
|
self.assertIn("not proven facts", rendered)
|
|
167
177
|
|
|
178
|
+
def test_heading_shows_id_as_inline_code(self):
|
|
179
|
+
for identity in ["L1", "bug_12", "F1._note_", "www.example.com"]:
|
|
180
|
+
with self.subTest(identity=identity):
|
|
181
|
+
report = duet.build_finding_report(state([turn(1, "reviewer", [item(identity)])]))
|
|
182
|
+
rendered = duet.render_finding_report(report)
|
|
183
|
+
self.assertIn(f"## `{identity}` \u2014 unresolved (agent assessment)\n", rendered)
|
|
184
|
+
self.assertEqual(report["findings"][0]["id"], identity)
|
|
185
|
+
self.assertIn(f"--resolve {identity} --turns 2", rendered)
|
|
186
|
+
|
|
168
187
|
|
|
169
188
|
if __name__ == "__main__":
|
|
170
189
|
unittest.main()
|
|
@@ -137,18 +137,35 @@ class TestFindingWorkflow(unittest.TestCase):
|
|
|
137
137
|
self.assertNotIn("{SENTINEL}", prompt)
|
|
138
138
|
self.assertIn("duet-findings", prompt)
|
|
139
139
|
self.assertIn('"id":"L1"', prompt)
|
|
140
|
+
self.assertIn("1 to 32 ASCII characters", prompt)
|
|
141
|
+
self.assertIn("letters, digits, '.', '_', or '-'", prompt)
|
|
142
|
+
self.assertIn("starts with a letter or digit", prompt)
|
|
143
|
+
self.assertIn("reuse that exact ID, including its case", prompt)
|
|
144
|
+
self.assertIn("Never copy a label that is not a valid ID", prompt)
|
|
140
145
|
|
|
141
146
|
def test_successful_turns_persist_updates_and_rebuild_local_report(self) -> None:
|
|
142
147
|
state = self._run([_reply("supported"), _reply("supported")])
|
|
143
148
|
run = pathlib.Path(state["transcript_path"]).parent
|
|
144
149
|
saved = json.loads((run / "state.json").read_text(encoding="utf-8"))
|
|
145
150
|
self.assertEqual([entry["finding_updates"]["status"] for entry in saved["history"]], ["ok", "ok"])
|
|
146
|
-
self.assertIn("L1 — supported", (run / "review.md").read_text(encoding="utf-8"))
|
|
151
|
+
self.assertIn("`L1` — supported", (run / "review.md").read_text(encoding="utf-8"))
|
|
147
152
|
(run / "review.md").unlink()
|
|
148
153
|
duet._write_run_state(run / "state.json", saved)
|
|
149
154
|
self.assertEqual((run / "review.md").read_text(encoding="utf-8"),
|
|
150
155
|
duet.render_finding_report(duet.build_finding_report(saved)))
|
|
151
156
|
|
|
157
|
+
def test_caller_defined_ids_differing_by_case_stay_separate_through_a_run(self) -> None:
|
|
158
|
+
def reply(identity: str) -> str:
|
|
159
|
+
return "```duet-findings\n" + json.dumps(
|
|
160
|
+
{"findings": [_assessment(identity=identity)]}) + "\n```"
|
|
161
|
+
state = self._run([reply("F1"), reply("f1")])
|
|
162
|
+
stored = [entry["finding_updates"]["items"][0]["id"] for entry in state["history"]]
|
|
163
|
+
self.assertEqual(stored, ["F1", "f1"])
|
|
164
|
+
run = pathlib.Path(state["transcript_path"]).parent
|
|
165
|
+
report = (run / "review.md").read_text(encoding="utf-8")
|
|
166
|
+
self.assertIn("## `F1` — ", report)
|
|
167
|
+
self.assertIn("## `f1` — ", report)
|
|
168
|
+
|
|
152
169
|
def test_failed_reply_cannot_publish_structured_claims_from_error_text(self) -> None:
|
|
153
170
|
state = self._run([duet.AgentRunError("agent_error", _reply("supported"))])
|
|
154
171
|
self.assertNotIn("finding_updates", state["history"][0])
|
|
@@ -173,7 +190,7 @@ class TestFindingWorkflow(unittest.TestCase):
|
|
|
173
190
|
self.assertFalse((run / "review.md").exists())
|
|
174
191
|
code, out, err = self._cli("--report", str(run))
|
|
175
192
|
self.assertEqual(code, 0, err)
|
|
176
|
-
self.assertIn("L1 — supported", out)
|
|
193
|
+
self.assertIn("`L1` — supported", out)
|
|
177
194
|
self.assertFalse((run / "review.md").exists())
|
|
178
195
|
|
|
179
196
|
def test_forced_success_and_failure_follow_same_update_contract(self) -> None:
|
|
@@ -235,6 +252,16 @@ class TestFindingWorkflow(unittest.TestCase):
|
|
|
235
252
|
self.assertEqual([finding["id"] for finding in report["findings"]], ["L1"])
|
|
236
253
|
self.assertEqual(report["findings"][0]["disposition"], "refuted")
|
|
237
254
|
|
|
255
|
+
def test_resolve_selects_caller_defined_id(self) -> None:
|
|
256
|
+
history = [{"turn": turn, "agent": agent, "finding_updates": {
|
|
257
|
+
"status": "ok", "items": [_assessment(identity="P1-1", disposition=disposition)]}}
|
|
258
|
+
for turn, agent, disposition in ((1, "lead", "supported"), (2, "partner", "unresolved"))]
|
|
259
|
+
run = self._saved_run("caller-id", history=history, turns_used=2)
|
|
260
|
+
cfg = self._continue(run, "--resolve", "P1-1")
|
|
261
|
+
self.assertEqual(cfg.resolve_findings, ["P1-1"])
|
|
262
|
+
self.assertEqual([event["id"] for event in cfg.finding_baseline], ["P1-1", "P1-1"])
|
|
263
|
+
self.assertIn("P1-1", cfg.kickoff)
|
|
264
|
+
|
|
238
265
|
def test_resolve_rejects_unknown_resolved_ids_and_short_budget(self) -> None:
|
|
239
266
|
open_run = self._saved_run("open", history=self._history())
|
|
240
267
|
closed_run = self._saved_run("closed", history=self._history(resolved=True))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{duet_cli-0.2.20 → duet_cli-0.2.21}/plugins/duet-chatgpt/skills/duet-chatgpt/scripts/control.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|