norn-cli 0.7.2__tar.gz → 0.7.3__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 (46) hide show
  1. {norn_cli-0.7.2 → norn_cli-0.7.3}/PKG-INFO +2 -2
  2. {norn_cli-0.7.2 → norn_cli-0.7.3}/README.md +1 -1
  3. {norn_cli-0.7.2 → norn_cli-0.7.3}/pyproject.toml +1 -1
  4. {norn_cli-0.7.2 → norn_cli-0.7.3}/src/norf_harness/__init__.py +1 -1
  5. {norn_cli-0.7.2 → norn_cli-0.7.3}/src/norf_harness/pipeline.py +62 -45
  6. {norn_cli-0.7.2 → norn_cli-0.7.3}/src/norf_harness/providers/grok.py +14 -1
  7. {norn_cli-0.7.2 → norn_cli-0.7.3}/src/norn_cli.egg-info/PKG-INFO +2 -2
  8. {norn_cli-0.7.2 → norn_cli-0.7.3}/tests/test_cli.py +1 -1
  9. {norn_cli-0.7.2 → norn_cli-0.7.3}/tests/test_pipeline.py +46 -0
  10. {norn_cli-0.7.2 → norn_cli-0.7.3}/tests/test_providers.py +6 -0
  11. {norn_cli-0.7.2 → norn_cli-0.7.3}/LICENSE +0 -0
  12. {norn_cli-0.7.2 → norn_cli-0.7.3}/setup.cfg +0 -0
  13. {norn_cli-0.7.2 → norn_cli-0.7.3}/src/norf_harness/cli.py +0 -0
  14. {norn_cli-0.7.2 → norn_cli-0.7.3}/src/norf_harness/config.py +0 -0
  15. {norn_cli-0.7.2 → norn_cli-0.7.3}/src/norf_harness/doctor.py +0 -0
  16. {norn_cli-0.7.2 → norn_cli-0.7.3}/src/norf_harness/e2e_evidence.py +0 -0
  17. {norn_cli-0.7.2 → norn_cli-0.7.3}/src/norf_harness/gate.py +0 -0
  18. {norn_cli-0.7.2 → norn_cli-0.7.3}/src/norf_harness/models.py +0 -0
  19. {norn_cli-0.7.2 → norn_cli-0.7.3}/src/norf_harness/project_gates.py +0 -0
  20. {norn_cli-0.7.2 → norn_cli-0.7.3}/src/norf_harness/providers/__init__.py +0 -0
  21. {norn_cli-0.7.2 → norn_cli-0.7.3}/src/norf_harness/providers/base.py +0 -0
  22. {norn_cli-0.7.2 → norn_cli-0.7.3}/src/norf_harness/providers/claude.py +0 -0
  23. {norn_cli-0.7.2 → norn_cli-0.7.3}/src/norf_harness/providers/codex.py +0 -0
  24. {norn_cli-0.7.2 → norn_cli-0.7.3}/src/norf_harness/providers/command.py +0 -0
  25. {norn_cli-0.7.2 → norn_cli-0.7.3}/src/norf_harness/providers/registry.py +0 -0
  26. {norn_cli-0.7.2 → norn_cli-0.7.3}/src/norf_harness/providers/router.py +0 -0
  27. {norn_cli-0.7.2 → norn_cli-0.7.3}/src/norf_harness/reporting.py +0 -0
  28. {norn_cli-0.7.2 → norn_cli-0.7.3}/src/norf_harness/resources/default.yaml +0 -0
  29. {norn_cli-0.7.2 → norn_cli-0.7.3}/src/norf_harness/resources/skills/norn/SKILL.md +0 -0
  30. {norn_cli-0.7.2 → norn_cli-0.7.3}/src/norf_harness/resources/skills/norn/agents/openai.yaml +0 -0
  31. {norn_cli-0.7.2 → norn_cli-0.7.3}/src/norf_harness/resources/skills/norn/references/cli.md +0 -0
  32. {norn_cli-0.7.2 → norn_cli-0.7.3}/src/norf_harness/run_store.py +0 -0
  33. {norn_cli-0.7.2 → norn_cli-0.7.3}/src/norf_harness/skill_install.py +0 -0
  34. {norn_cli-0.7.2 → norn_cli-0.7.3}/src/norf_harness/worktree.py +0 -0
  35. {norn_cli-0.7.2 → norn_cli-0.7.3}/src/norn_cli.egg-info/SOURCES.txt +0 -0
  36. {norn_cli-0.7.2 → norn_cli-0.7.3}/src/norn_cli.egg-info/dependency_links.txt +0 -0
  37. {norn_cli-0.7.2 → norn_cli-0.7.3}/src/norn_cli.egg-info/entry_points.txt +0 -0
  38. {norn_cli-0.7.2 → norn_cli-0.7.3}/src/norn_cli.egg-info/requires.txt +0 -0
  39. {norn_cli-0.7.2 → norn_cli-0.7.3}/src/norn_cli.egg-info/top_level.txt +0 -0
  40. {norn_cli-0.7.2 → norn_cli-0.7.3}/tests/test_config.py +0 -0
  41. {norn_cli-0.7.2 → norn_cli-0.7.3}/tests/test_e2e_evidence.py +0 -0
  42. {norn_cli-0.7.2 → norn_cli-0.7.3}/tests/test_gate.py +0 -0
  43. {norn_cli-0.7.2 → norn_cli-0.7.3}/tests/test_project_gates.py +0 -0
  44. {norn_cli-0.7.2 → norn_cli-0.7.3}/tests/test_run_store.py +0 -0
  45. {norn_cli-0.7.2 → norn_cli-0.7.3}/tests/test_skill_install.py +0 -0
  46. {norn_cli-0.7.2 → norn_cli-0.7.3}/tests/test_worktree.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: norn-cli
3
- Version: 0.7.2
3
+ Version: 0.7.3
4
4
  Summary: Norn: a provider-neutral multi-agent development harness
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: Repository, https://github.com/uruca-kk/norn
@@ -135,7 +135,7 @@ ruff format --check .
135
135
  ruff check .
136
136
  python3 -m unittest discover -v
137
137
  python3 -m build
138
- python3 scripts/package_smoke.py dist/norn_cli-0.7.2-py3-none-any.whl
138
+ python3 scripts/package_smoke.py dist/norn_cli-0.7.3-py3-none-any.whl
139
139
  ```
140
140
 
141
141
  CI runs the quality suite on Python 3.12, 3.13, and 3.14, then installs the
@@ -110,7 +110,7 @@ ruff format --check .
110
110
  ruff check .
111
111
  python3 -m unittest discover -v
112
112
  python3 -m build
113
- python3 scripts/package_smoke.py dist/norn_cli-0.7.2-py3-none-any.whl
113
+ python3 scripts/package_smoke.py dist/norn_cli-0.7.3-py3-none-any.whl
114
114
  ```
115
115
 
116
116
  CI runs the quality suite on Python 3.12, 3.13, and 3.14, then installs the
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "norn-cli"
7
- version = "0.7.2"
7
+ version = "0.7.3"
8
8
  description = "Norn: a provider-neutral multi-agent development harness"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -3,4 +3,4 @@
3
3
  from .config import HarnessConfig, load_config
4
4
 
5
5
  __all__ = ["HarnessConfig", "load_config"]
6
- __version__ = "0.7.2"
6
+ __version__ = "0.7.3"
@@ -669,53 +669,70 @@ class _PipelineRunner:
669
669
  ui_required: bool,
670
670
  ) -> list[tuple[str, dict[str, Any]]]:
671
671
  self._phase("testing")
672
- results: list[tuple[str, dict[str, Any]]] = []
673
- for role in _TESTER_ROLES:
674
- evidence_dir = None
675
- if ui_required:
676
- try:
677
- evidence_dir = prepare_evidence_directory(
678
- self.config.project_root,
679
- self.manifest.run_id,
680
- self.manifest.rounds.implementation,
681
- role,
682
- )
683
- except EvidenceError as exc:
684
- raise PipelineStop(
685
- StopReason.E2E_UNAVAILABLE,
686
- ExitCode.TEST_INFRA_FAILURE,
687
- f"E2E証跡ディレクトリを安全に準備できません: {exc}",
688
- ("worktree内の.norf pathとsymlinkを確認する",),
689
- ) from exc
690
- payload = await self._invoke(
691
- role,
692
- {
693
- "request": self.manifest.request,
694
- "plan": plan,
695
- "implementation": implementation,
696
- "local_gate_results": [item.to_dict() for item in local.results],
697
- "e2e_required": ui_required,
698
- "e2e_evidence_dir": str(evidence_dir) if evidence_dir else "",
699
- "e2e_instruction": (
700
- "UI変更があるため、このtester自身がE2Eを実行すること。実行したargv、"
701
- "exit code、具体的な結果を返し、ログ・スクリーンショット・trace等の"
702
- "少なくとも1つの非空ファイルを指定されたe2e_evidence_dir内に作成し、"
703
- "artifactsへそのpathを列挙すること。他testerの証跡を再利用しないこと。"
704
- if ui_required
705
- else "UI変更がない場合はnot_requiredを返すこと。"
706
- ),
707
- },
708
- _TESTER_SCHEMA,
709
- SandboxPolicy.TEST,
710
- timeout=(
711
- self.config.limits.e2e_timeout_seconds
672
+ results = await asyncio.gather(
673
+ *(
674
+ self._run_tester(role, plan, implementation, local, ui_required)
675
+ for role in _TESTER_ROLES
676
+ ),
677
+ return_exceptions=True,
678
+ )
679
+ for result in results:
680
+ if isinstance(result, BaseException):
681
+ raise result
682
+ return results
683
+
684
+ async def _run_tester(
685
+ self,
686
+ role: str,
687
+ plan: dict[str, Any],
688
+ implementation: dict[str, Any],
689
+ local: ProjectGateResult,
690
+ ui_required: bool,
691
+ ) -> tuple[str, dict[str, Any]]:
692
+ evidence_dir = None
693
+ if ui_required:
694
+ try:
695
+ evidence_dir = prepare_evidence_directory(
696
+ self.config.project_root,
697
+ self.manifest.run_id,
698
+ self.manifest.rounds.implementation,
699
+ role,
700
+ )
701
+ except EvidenceError as exc:
702
+ raise PipelineStop(
703
+ StopReason.E2E_UNAVAILABLE,
704
+ ExitCode.TEST_INFRA_FAILURE,
705
+ f"E2E証跡ディレクトリを安全に準備できません: {exc}",
706
+ ("worktree内の.norf pathとsymlinkを確認する",),
707
+ ) from exc
708
+ payload = await self._invoke(
709
+ role,
710
+ {
711
+ "request": self.manifest.request,
712
+ "plan": plan,
713
+ "implementation": implementation,
714
+ "local_gate_results": [item.to_dict() for item in local.results],
715
+ "e2e_required": ui_required,
716
+ "e2e_evidence_dir": str(evidence_dir) if evidence_dir else "",
717
+ "e2e_instruction": (
718
+ "UI変更があるため、このtester自身がE2Eを実行すること。実行したargv、"
719
+ "exit code、具体的な結果を返し、ログ・スクリーンショット・trace等の"
720
+ "少なくとも1つの非空ファイルを指定されたe2e_evidence_dir内に作成し、"
721
+ "artifactsへそのpathを列挙すること。他testerの証跡を再利用しないこと。"
712
722
  if ui_required
713
- else self.config.limits.role_timeout_seconds
723
+ else "UI変更がない場合はnot_requiredを返すこと。"
714
724
  ),
715
- )
716
- payload["_e2e_evidence_dir"] = str(evidence_dir) if evidence_dir else ""
717
- results.append((role, payload))
718
- return results
725
+ },
726
+ _TESTER_SCHEMA,
727
+ SandboxPolicy.TEST,
728
+ timeout=(
729
+ self.config.limits.e2e_timeout_seconds
730
+ if ui_required
731
+ else self.config.limits.role_timeout_seconds
732
+ ),
733
+ )
734
+ payload["_e2e_evidence_dir"] = str(evidence_dir) if evidence_dir else ""
735
+ return role, payload
719
736
 
720
737
  async def _triage(
721
738
  self,
@@ -23,6 +23,15 @@ from .base import (
23
23
  run_process,
24
24
  )
25
25
 
26
+ _TOOL_IDS = {
27
+ "Read": "read_file",
28
+ "Glob": "list_dir",
29
+ "Grep": "grep",
30
+ "Edit": "search_replace",
31
+ "Write": "write",
32
+ "Bash": "run_terminal_cmd",
33
+ }
34
+
26
35
 
27
36
  class GrokAdapter(ProviderAdapter):
28
37
  name = "grok"
@@ -61,7 +70,7 @@ class GrokAdapter(ProviderAdapter):
61
70
  if request.schema is not None:
62
71
  args.extend(["--json-schema", json.dumps(request.schema, ensure_ascii=False)])
63
72
  if request.tools:
64
- args.extend(["--tools", ",".join(request.tools)])
73
+ args.extend(["--tools", ",".join(_grok_tools(request.tools))])
65
74
  process = await run_process(
66
75
  args,
67
76
  stdin="",
@@ -113,6 +122,10 @@ def _sandbox(policy: SandboxPolicy) -> str:
113
122
  return "workspace" if policy in writable else "read-only"
114
123
 
115
124
 
125
+ def _grok_tools(tools: tuple[str, ...]) -> tuple[str, ...]:
126
+ return tuple(dict.fromkeys(_TOOL_IDS.get(tool, tool) for tool in tools))
127
+
128
+
116
129
  def _text(envelope: dict[str, Any]) -> str:
117
130
  for key in ("result", "text", "output_text", "content"):
118
131
  value = envelope.get(key)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: norn-cli
3
- Version: 0.7.2
3
+ Version: 0.7.3
4
4
  Summary: Norn: a provider-neutral multi-agent development harness
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: Repository, https://github.com/uruca-kk/norn
@@ -135,7 +135,7 @@ ruff format --check .
135
135
  ruff check .
136
136
  python3 -m unittest discover -v
137
137
  python3 -m build
138
- python3 scripts/package_smoke.py dist/norn_cli-0.7.2-py3-none-any.whl
138
+ python3 scripts/package_smoke.py dist/norn_cli-0.7.3-py3-none-any.whl
139
139
  ```
140
140
 
141
141
  CI runs the quality suite on Python 3.12, 3.13, and 3.14, then installs the
@@ -23,7 +23,7 @@ class CliTests(unittest.TestCase):
23
23
  main(["--version"])
24
24
 
25
25
  self.assertEqual(raised.exception.code, 0)
26
- self.assertEqual(output.getvalue().strip(), "norn 0.7.2")
26
+ self.assertEqual(output.getvalue().strip(), "norn 0.7.3")
27
27
 
28
28
  def test_init_and_validate(self) -> None:
29
29
  with tempfile.TemporaryDirectory() as tmp:
@@ -128,6 +128,24 @@ class ScriptedRouter:
128
128
  return RoutedResult(result, (attempt,), False)
129
129
 
130
130
 
131
+ class ConcurrentTesterRouter(ScriptedRouter):
132
+ def __init__(self, **responses: list[dict[str, object]]):
133
+ super().__init__(**responses)
134
+ self.active_testers = 0
135
+ self.max_active_testers = 0
136
+
137
+ async def invoke_profile(self, profile_name, request):
138
+ if not request.role.endswith("tester"):
139
+ return await super().invoke_profile(profile_name, request)
140
+ self.active_testers += 1
141
+ self.max_active_testers = max(self.max_active_testers, self.active_testers)
142
+ try:
143
+ await asyncio.sleep(0.01)
144
+ return await super().invoke_profile(profile_name, request)
145
+ finally:
146
+ self.active_testers -= 1
147
+
148
+
131
149
  class PipelineTests(unittest.TestCase):
132
150
  def config(self, root: Path, **overrides):
133
151
  values = {"gates.enabled": False, "git.worktree": False, **overrides}
@@ -177,6 +195,34 @@ class PipelineTests(unittest.TestCase):
177
195
  self.assertIn('argv=["node", "e2e.mjs"]', report)
178
196
  self.assertIn("artifacts=", report)
179
197
 
198
+ def test_run_executes_tester_roles_concurrently(self) -> None:
199
+ with tempfile.TemporaryDirectory() as tmp:
200
+ root = Path(tmp)
201
+ clean = [tester()]
202
+ router = ConcurrentTesterRouter(
203
+ orchestrator=[orchestration()],
204
+ planner=[plan()],
205
+ plan_reviewer=[review()],
206
+ implementer=[implementation()],
207
+ spec_tester=list(clean),
208
+ clean_tester=list(clean),
209
+ security_tester=list(clean),
210
+ chaos_tester=list(clean),
211
+ final_gate=[final()],
212
+ )
213
+
214
+ outcome = asyncio.run(
215
+ execute_pipeline(self.config(root), "parallel testers", RunMode.RUN, router=router)
216
+ )
217
+
218
+ self.assertEqual(outcome.exit_code, 0)
219
+ self.assertEqual(router.max_active_testers, 4)
220
+ tester_calls = [call.role for call in router.calls if call.role.endswith("tester")]
221
+ self.assertEqual(
222
+ tester_calls,
223
+ ["spec_tester", "clean_tester", "security_tester", "chaos_tester"],
224
+ )
225
+
180
226
  def test_quality_failure_is_triaged_then_reimplemented(self) -> None:
181
227
  with tempfile.TemporaryDirectory() as tmp:
182
228
  root = Path(tmp)
@@ -182,6 +182,7 @@ class AdapterIntegrationTests(unittest.IsolatedAsyncioTestCase):
182
182
  "implement",
183
183
  root,
184
184
  schema=_schema(),
185
+ tools=("Read", "Glob", "Grep", "Edit", "Write", "Bash"),
185
186
  sandbox=SandboxPolicy.WORKSPACE_WRITE,
186
187
  ),
187
188
  )
@@ -191,6 +192,11 @@ class AdapterIntegrationTests(unittest.IsolatedAsyncioTestCase):
191
192
  self.assertNotIn("implement", result.command_summary)
192
193
  sandbox_index = result.command_summary.index("--sandbox")
193
194
  self.assertEqual(result.command_summary[sandbox_index + 1], "workspace")
195
+ tools_index = result.command_summary.index("--tools")
196
+ self.assertEqual(
197
+ result.command_summary[tools_index + 1],
198
+ "read_file,list_dir,grep,search_replace,write,run_terminal_cmd",
199
+ )
194
200
 
195
201
  async def test_generic_adapter_passes_prompt_over_stdin_without_shell(self) -> None:
196
202
  with tempfile.TemporaryDirectory() as tmp:
File without changes
File without changes
File without changes
File without changes