methodology-framework 0.3.3__tar.gz → 0.4.1__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 (36) hide show
  1. {methodology_framework-0.3.3/src/methodology_framework.egg-info → methodology_framework-0.4.1}/PKG-INFO +1 -1
  2. {methodology_framework-0.3.3 → methodology_framework-0.4.1}/pyproject.toml +1 -1
  3. {methodology_framework-0.3.3 → methodology_framework-0.4.1}/src/methodology_framework/populate_acus.py +29 -12
  4. {methodology_framework-0.3.3 → methodology_framework-0.4.1/src/methodology_framework.egg-info}/PKG-INFO +1 -1
  5. {methodology_framework-0.3.3 → methodology_framework-0.4.1}/src/methodology_framework.egg-info/SOURCES.txt +1 -0
  6. {methodology_framework-0.3.3 → methodology_framework-0.4.1}/tests/test_populate_acus.py +99 -2
  7. methodology_framework-0.4.1/tests/test_populate_acus_integration.py +97 -0
  8. {methodology_framework-0.3.3 → methodology_framework-0.4.1}/LICENSE +0 -0
  9. {methodology_framework-0.3.3 → methodology_framework-0.4.1}/README.md +0 -0
  10. {methodology_framework-0.3.3 → methodology_framework-0.4.1}/setup.cfg +0 -0
  11. {methodology_framework-0.3.3 → methodology_framework-0.4.1}/src/methodology_framework/__init__.py +0 -0
  12. {methodology_framework-0.3.3 → methodology_framework-0.4.1}/src/methodology_framework/__main__.py +0 -0
  13. {methodology_framework-0.3.3 → methodology_framework-0.4.1}/src/methodology_framework/bootstrap_jira.py +0 -0
  14. {methodology_framework-0.3.3 → methodology_framework-0.4.1}/src/methodology_framework/build_playbook.py +0 -0
  15. {methodology_framework-0.3.3 → methodology_framework-0.4.1}/src/methodology_framework/jira_discovery.py +0 -0
  16. {methodology_framework-0.3.3 → methodology_framework-0.4.1}/src/methodology_framework/jira_shapes/__init__.py +0 -0
  17. {methodology_framework-0.3.3 → methodology_framework-0.4.1}/src/methodology_framework/jira_shapes/automation_rules.yaml +0 -0
  18. {methodology_framework-0.3.3 → methodology_framework-0.4.1}/src/methodology_framework/jira_shapes/custom_fields.yaml +0 -0
  19. {methodology_framework-0.3.3 → methodology_framework-0.4.1}/src/methodology_framework/jira_shapes/workflow.yaml +0 -0
  20. {methodology_framework-0.3.3 → methodology_framework-0.4.1}/src/methodology_framework/playbooks/bindings/meth.yaml +0 -0
  21. {methodology_framework-0.3.3 → methodology_framework-0.4.1}/src/methodology_framework/playbooks/bindings/scrum.yaml +0 -0
  22. {methodology_framework-0.3.3 → methodology_framework-0.4.1}/src/methodology_framework/playbooks/scrum-router.body.md +0 -0
  23. {methodology_framework-0.3.3 → methodology_framework-0.4.1}/src/methodology_framework/register_playbook_with_devin.py +0 -0
  24. {methodology_framework-0.3.3 → methodology_framework-0.4.1}/src/methodology_framework/specs/devin-story-format.md +0 -0
  25. {methodology_framework-0.3.3 → methodology_framework-0.4.1}/src/methodology_framework/sync_stories_to_jira.py +0 -0
  26. {methodology_framework-0.3.3 → methodology_framework-0.4.1}/src/methodology_framework/templates/github_workflows/populate-story-acus-caller.yml +0 -0
  27. {methodology_framework-0.3.3 → methodology_framework-0.4.1}/src/methodology_framework/templates/story.md +0 -0
  28. {methodology_framework-0.3.3 → methodology_framework-0.4.1}/src/methodology_framework.egg-info/dependency_links.txt +0 -0
  29. {methodology_framework-0.3.3 → methodology_framework-0.4.1}/src/methodology_framework.egg-info/requires.txt +0 -0
  30. {methodology_framework-0.3.3 → methodology_framework-0.4.1}/src/methodology_framework.egg-info/top_level.txt +0 -0
  31. {methodology_framework-0.3.3 → methodology_framework-0.4.1}/tests/test_bootstrap_jira.py +0 -0
  32. {methodology_framework-0.3.3 → methodology_framework-0.4.1}/tests/test_build_playbook.py +0 -0
  33. {methodology_framework-0.3.3 → methodology_framework-0.4.1}/tests/test_jira_discovery.py +0 -0
  34. {methodology_framework-0.3.3 → methodology_framework-0.4.1}/tests/test_register_playbook_with_devin.py +0 -0
  35. {methodology_framework-0.3.3 → methodology_framework-0.4.1}/tests/test_sync_stories_to_jira.py +0 -0
  36. {methodology_framework-0.3.3 → methodology_framework-0.4.1}/tests/test_workflows.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: methodology-framework
3
- Version: 0.3.3
3
+ Version: 0.4.1
4
4
  Summary: Portable process tooling for agent-driven delivery — scripts, playbooks, templates, and specs.
5
5
  Author: whiteout59
6
6
  License-Expression: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "methodology-framework"
7
- version = "0.3.3"
7
+ version = "0.4.1"
8
8
  description = "Portable process tooling for agent-driven delivery — scripts, playbooks, templates, and specs."
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -77,25 +77,32 @@ def rewrite_acus(content: str, acus_value: float) -> tuple[str, str]:
77
77
  return content, ""
78
78
 
79
79
 
80
- def fetch_acus_from_devin(session_id: str, api_token: str) -> float | None:
80
+ def fetch_acus_from_devin(session_id: str, api_token: str, org_id: str) -> float | None:
81
81
  """Fetch ACU consumption from Devin API for a given session ID.
82
82
 
83
83
  Returns the ACU value or None on error.
84
- # Devin API v2: GET /v2/session/{session_id} returns JSON with
85
- # "total_acus" at the top level of the response object.
84
+
85
+ Devin API v3 (org-scoped, service-user auth):
86
+ GET /v3/organizations/{org_id}/sessions/{devin_id}
87
+ Returns JSON with ``acus_consumed`` (float) at the top level.
88
+
89
+ The session ID extracted from story file URLs (e.g.
90
+ ``https://app.devin.ai/sessions/abc123``) is the raw ID without prefix.
91
+ The v3 path parameter expects the ``devin-`` prefix, so this function
92
+ prepends it when constructing the URL.
86
93
  """
87
- url = f"https://api.devin.ai/v2/session/{session_id}"
94
+ devin_id = f"devin-{session_id}" if not session_id.startswith("devin-") else session_id
95
+ url = f"https://api.devin.ai/v3/organizations/{org_id}/sessions/{devin_id}"
88
96
  headers = {"Authorization": f"Bearer {api_token}"}
89
97
  try:
90
98
  resp = requests.get(url, headers=headers, timeout=30)
91
99
  resp.raise_for_status()
92
100
  data = resp.json()
93
- acus = data.get("total_acus")
94
- if acus is None:
95
- # Fallback: try nested under "status_enum" or "acus"
96
- acus = data.get("acus")
97
- if acus is not None:
98
- return float(acus)
101
+ # v3 field name is "acus_consumed"; keep legacy fallbacks for robustness.
102
+ for field in ("acus_consumed", "total_acus", "acus"):
103
+ acus = data.get(field)
104
+ if acus is not None:
105
+ return float(acus)
99
106
  logger.warning(
100
107
  "Session %s: no ACU field found in response keys: %s",
101
108
  session_id,
@@ -113,6 +120,7 @@ def fetch_acus_from_devin(session_id: str, api_token: str) -> float | None:
113
120
  def scan_and_populate(
114
121
  story_path_pattern: str,
115
122
  api_token: str,
123
+ org_id: str,
116
124
  *,
117
125
  dry_run: bool = False,
118
126
  ) -> list[PopulateResult]:
@@ -136,7 +144,7 @@ def scan_and_populate(
136
144
  logger.info("Skipping %s: no agent_session_url found", filepath)
137
145
  continue
138
146
 
139
- acus = fetch_acus_from_devin(session_id, api_token)
147
+ acus = fetch_acus_from_devin(session_id, api_token, org_id)
140
148
  if acus is None:
141
149
  logger.warning(
142
150
  "Skipping %s: could not fetch ACUs for session %s",
@@ -193,7 +201,16 @@ def main() -> int:
193
201
  logger.error("DEVIN_API_TOKEN environment variable is required")
194
202
  return 1
195
203
 
196
- results = scan_and_populate(args.story_path_pattern, api_token, dry_run=args.dry_run)
204
+ org_id = os.environ.get("DEVIN_ORG_ID", "")
205
+ if not org_id:
206
+ logger.error(
207
+ "DEVIN_ORG_ID environment variable is required "
208
+ "(the Devin organization ID; obtain from your service user setup "
209
+ "or the Devin webapp settings)."
210
+ )
211
+ return 1
212
+
213
+ results = scan_and_populate(args.story_path_pattern, api_token, org_id, dry_run=args.dry_run)
197
214
 
198
215
  if not results:
199
216
  print("Nothing to do — all agent_acus fields already populated.")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: methodology-framework
3
- Version: 0.3.3
3
+ Version: 0.4.1
4
4
  Summary: Portable process tooling for agent-driven delivery — scripts, playbooks, templates, and specs.
5
5
  Author: whiteout59
6
6
  License-Expression: Apache-2.0
@@ -28,6 +28,7 @@ tests/test_bootstrap_jira.py
28
28
  tests/test_build_playbook.py
29
29
  tests/test_jira_discovery.py
30
30
  tests/test_populate_acus.py
31
+ tests/test_populate_acus_integration.py
31
32
  tests/test_register_playbook_with_devin.py
32
33
  tests/test_sync_stories_to_jira.py
33
34
  tests/test_workflows.py
@@ -135,7 +135,7 @@ def test_devin_api_error_skips_file() -> None:
135
135
  mock_resp.status_code = 404
136
136
  mock_resp.raise_for_status.side_effect = requests.HTTPError(response=mock_resp)
137
137
 
138
- result = fetch_acus_from_devin("nonexistent-session", "fake-token")
138
+ result = fetch_acus_from_devin("nonexistent-session", "fake-token", "org-test")
139
139
  assert result is None
140
140
 
141
141
  # Also test 401
@@ -144,5 +144,102 @@ def test_devin_api_error_skips_file() -> None:
144
144
  mock_resp.status_code = 401
145
145
  mock_resp.raise_for_status.side_effect = requests.HTTPError(response=mock_resp)
146
146
 
147
- result = fetch_acus_from_devin("bad-auth-session", "fake-token")
147
+ result = fetch_acus_from_devin("bad-auth-session", "fake-token", "org-test")
148
148
  assert result is None
149
+
150
+
151
+ # ---------------------------------------------------------------------------
152
+ # test_v3_url_pattern
153
+ # ---------------------------------------------------------------------------
154
+
155
+
156
+ def test_v3_url_pattern() -> None:
157
+ """fetch_acus_from_devin constructs the v3 org-scoped URL with devin- prefix."""
158
+ with patch("methodology_framework.populate_acus.requests.get") as mock_get:
159
+ mock_resp = mock_get.return_value
160
+ mock_resp.raise_for_status.return_value = None
161
+ mock_resp.json.return_value = {"acus_consumed": 3.5}
162
+
163
+ result = fetch_acus_from_devin("abc123", "fake-token", "org-myorg")
164
+ assert result == 3.5
165
+
166
+ # Verify the URL used
167
+ call_url = mock_get.call_args[0][0]
168
+ assert call_url == "https://api.devin.ai/v3/organizations/org-myorg/sessions/devin-abc123"
169
+
170
+
171
+ def test_v3_url_pattern_already_prefixed() -> None:
172
+ """Session IDs already prefixed with devin- are not double-prefixed."""
173
+ with patch("methodology_framework.populate_acus.requests.get") as mock_get:
174
+ mock_resp = mock_get.return_value
175
+ mock_resp.raise_for_status.return_value = None
176
+ mock_resp.json.return_value = {"acus_consumed": 7.0}
177
+
178
+ result = fetch_acus_from_devin("devin-abc123", "fake-token", "org-myorg")
179
+ assert result == 7.0
180
+
181
+ call_url = mock_get.call_args[0][0]
182
+ assert call_url == "https://api.devin.ai/v3/organizations/org-myorg/sessions/devin-abc123"
183
+
184
+
185
+ def test_acus_consumed_field_preferred() -> None:
186
+ """v3 field acus_consumed is preferred over legacy total_acus/acus."""
187
+ with patch("methodology_framework.populate_acus.requests.get") as mock_get:
188
+ mock_resp = mock_get.return_value
189
+ mock_resp.raise_for_status.return_value = None
190
+ mock_resp.json.return_value = {
191
+ "acus_consumed": 5.0,
192
+ "total_acus": 10.0,
193
+ "acus": 15.0,
194
+ }
195
+
196
+ result = fetch_acus_from_devin("sess1", "fake-token", "org-test")
197
+ assert result == 5.0
198
+
199
+
200
+ def test_legacy_total_acus_fallback() -> None:
201
+ """Falls back to total_acus when acus_consumed is absent."""
202
+ with patch("methodology_framework.populate_acus.requests.get") as mock_get:
203
+ mock_resp = mock_get.return_value
204
+ mock_resp.raise_for_status.return_value = None
205
+ mock_resp.json.return_value = {"total_acus": 10.0}
206
+
207
+ result = fetch_acus_from_devin("sess1", "fake-token", "org-test")
208
+ assert result == 10.0
209
+
210
+
211
+ def test_no_acu_field_returns_none() -> None:
212
+ """Returns None and logs warning when no ACU field is present."""
213
+ with patch("methodology_framework.populate_acus.requests.get") as mock_get:
214
+ mock_resp = mock_get.return_value
215
+ mock_resp.raise_for_status.return_value = None
216
+ mock_resp.json.return_value = {"session_id": "sess1", "status": "exit"}
217
+
218
+ result = fetch_acus_from_devin("sess1", "fake-token", "org-test")
219
+ assert result is None
220
+
221
+
222
+ def test_main_fails_without_devin_org_id() -> None:
223
+ """main() exits 1 with clear error when DEVIN_ORG_ID is missing."""
224
+ import os
225
+ import sys
226
+
227
+ from methodology_framework.populate_acus import main
228
+
229
+ env = {"DEVIN_API_TOKEN": "fake-token"}
230
+ with patch.dict(os.environ, env, clear=True), patch.object(sys, "argv", ["populate_acus"]):
231
+ result = main()
232
+ assert result == 1
233
+
234
+
235
+ def test_main_fails_without_devin_api_token() -> None:
236
+ """main() exits 1 when DEVIN_API_TOKEN is missing."""
237
+ import os
238
+ import sys
239
+
240
+ from methodology_framework.populate_acus import main
241
+
242
+ env = {"DEVIN_ORG_ID": "org-test"}
243
+ with patch.dict(os.environ, env, clear=True), patch.object(sys, "argv", ["populate_acus"]):
244
+ result = main()
245
+ assert result == 1
@@ -0,0 +1,97 @@
1
+ """Integration tests for methodology_framework.populate_acus — contract-level.
2
+
3
+ These tests hit the real Devin v3 API to verify the URL pattern, auth model,
4
+ and response shape. They are gated on CI secrets and skip locally when the
5
+ required environment variables are absent.
6
+
7
+ Required CI secrets / env vars:
8
+ DEVIN_API_TOKEN — service-user ``cog_*`` API key
9
+ DEVIN_ORG_ID — Devin organization ID (``org-…`` prefix)
10
+ DEVIN_TEST_SESSION_ID — a known sentinel session ID (raw, without ``devin-`` prefix)
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import os
16
+
17
+ import pytest
18
+ import requests
19
+
20
+ from methodology_framework.populate_acus import fetch_acus_from_devin
21
+
22
+ _API_TOKEN = os.environ.get("DEVIN_API_TOKEN", "")
23
+ _ORG_ID = os.environ.get("DEVIN_ORG_ID", "")
24
+ _SESSION_ID = os.environ.get("DEVIN_TEST_SESSION_ID", "")
25
+
26
+ _SKIP_REASON = (
27
+ "Integration test requires DEVIN_API_TOKEN, DEVIN_ORG_ID, "
28
+ "and DEVIN_TEST_SESSION_ID environment variables"
29
+ )
30
+
31
+ pytestmark = pytest.mark.integration
32
+
33
+
34
+ @pytest.mark.skipif(
35
+ not (_API_TOKEN and _ORG_ID and _SESSION_ID),
36
+ reason=_SKIP_REASON,
37
+ )
38
+ def test_v3_session_endpoint_returns_200() -> None:
39
+ """The v3 org-scoped session endpoint returns HTTP 200 for a known session."""
40
+ devin_id = f"devin-{_SESSION_ID}" if not _SESSION_ID.startswith("devin-") else _SESSION_ID
41
+ url = f"https://api.devin.ai/v3/organizations/{_ORG_ID}/sessions/{devin_id}"
42
+ headers = {"Authorization": f"Bearer {_API_TOKEN}"}
43
+
44
+ resp = requests.get(url, headers=headers, timeout=30)
45
+
46
+ assert resp.status_code == 200, (
47
+ f"Expected 200 from {url}, got {resp.status_code}: {resp.text[:200]}"
48
+ )
49
+
50
+
51
+ @pytest.mark.skipif(
52
+ not (_API_TOKEN and _ORG_ID and _SESSION_ID),
53
+ reason=_SKIP_REASON,
54
+ )
55
+ def test_v3_response_contains_acus_consumed() -> None:
56
+ """The v3 session response contains ``acus_consumed`` as a numeric value."""
57
+ devin_id = f"devin-{_SESSION_ID}" if not _SESSION_ID.startswith("devin-") else _SESSION_ID
58
+ url = f"https://api.devin.ai/v3/organizations/{_ORG_ID}/sessions/{devin_id}"
59
+ headers = {"Authorization": f"Bearer {_API_TOKEN}"}
60
+
61
+ resp = requests.get(url, headers=headers, timeout=30)
62
+ resp.raise_for_status()
63
+ data = resp.json()
64
+
65
+ assert "acus_consumed" in data, (
66
+ f"Expected 'acus_consumed' in response keys, got: {sorted(data.keys())}"
67
+ )
68
+ assert isinstance(data["acus_consumed"], (int, float)), (
69
+ f"Expected acus_consumed to be numeric, got {type(data['acus_consumed'])}: "
70
+ f"{data['acus_consumed']}"
71
+ )
72
+
73
+
74
+ @pytest.mark.skipif(
75
+ not (_API_TOKEN and _ORG_ID and _SESSION_ID),
76
+ reason=_SKIP_REASON,
77
+ )
78
+ def test_fetch_acus_from_devin_returns_float() -> None:
79
+ """The ``fetch_acus_from_devin()`` helper returns a float for a known session."""
80
+ result = fetch_acus_from_devin(_SESSION_ID, _API_TOKEN, _ORG_ID)
81
+
82
+ assert result is not None, f"fetch_acus_from_devin returned None for session {_SESSION_ID}"
83
+ assert isinstance(result, float), f"Expected float, got {type(result)}: {result}"
84
+
85
+
86
+ @pytest.mark.skipif(
87
+ not (_API_TOKEN and _ORG_ID and _SESSION_ID),
88
+ reason=_SKIP_REASON,
89
+ )
90
+ def test_url_pattern_matches_v3() -> None:
91
+ """Verify the URL constructed by fetch_acus_from_devin matches v3 pattern."""
92
+ devin_id = f"devin-{_SESSION_ID}" if not _SESSION_ID.startswith("devin-") else _SESSION_ID
93
+ expected_url = f"https://api.devin.ai/v3/organizations/{_ORG_ID}/sessions/{devin_id}"
94
+
95
+ assert "/v3/organizations/" in expected_url
96
+ assert _ORG_ID in expected_url
97
+ assert devin_id in expected_url