giton 0.1.6__tar.gz → 0.1.8__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 (96) hide show
  1. {giton-0.1.6 → giton-0.1.8}/.idea/workspace.xml +56 -80
  2. {giton-0.1.6 → giton-0.1.8}/CHANGELOG.md +17 -0
  3. {giton-0.1.6 → giton-0.1.8}/PKG-INFO +5 -5
  4. {giton-0.1.6 → giton-0.1.8}/README.md +4 -4
  5. giton-0.1.8/VERSION +1 -0
  6. giton-0.1.8/examples/plugins/README.md +46 -0
  7. giton-0.1.8/examples/plugins/test_plugin_lifecycle.py +116 -0
  8. {giton-0.1.6 → giton-0.1.8}/pyproject.toml +1 -1
  9. {giton-0.1.6 → giton-0.1.8}/src/giton/__init__.py +1 -1
  10. {giton-0.1.6 → giton-0.1.8}/src/giton/catalog.py +3 -3
  11. giton-0.1.8/tests/test_plugin_runner.py +208 -0
  12. {giton-0.1.6 → giton-0.1.8}/uv.lock +1 -1
  13. giton-0.1.6/VERSION +0 -1
  14. {giton-0.1.6 → giton-0.1.8}/.gitignore +0 -0
  15. {giton-0.1.6 → giton-0.1.8}/.idea/.gitignore +0 -0
  16. {giton-0.1.6 → giton-0.1.8}/.koru/event-store.jsonl +0 -0
  17. {giton-0.1.6 → giton-0.1.8}/.koru/events/observability.jsonl +0 -0
  18. {giton-0.1.6 → giton-0.1.8}/.koru/history.jsonl +0 -0
  19. {giton-0.1.6 → giton-0.1.8}/.koru/onboarding.json +0 -0
  20. {giton-0.1.6 → giton-0.1.8}/.koru/project.json +0 -0
  21. {giton-0.1.6 → giton-0.1.8}/.planfile/.koru/autonomous-state.json +0 -0
  22. {giton-0.1.6 → giton-0.1.8}/.planfile/.koru/autonomy-telemetry.json +0 -0
  23. {giton-0.1.6 → giton-0.1.8}/.planfile/.koru/event-store.jsonl +0 -0
  24. {giton-0.1.6 → giton-0.1.8}/.planfile/.koru/nfo-events.jsonl +0 -0
  25. {giton-0.1.6 → giton-0.1.8}/.planfile/.koru/operator-steps/mcp_koru.ticket +0 -0
  26. {giton-0.1.6 → giton-0.1.8}/.planfile/.koru/operator-steps/self_control.ticket +0 -0
  27. {giton-0.1.6 → giton-0.1.8}/.planfile/.koru/queue-runner.lock +0 -0
  28. {giton-0.1.6 → giton-0.1.8}/.planfile/.koru/serve-endpoint.json +0 -0
  29. {giton-0.1.6 → giton-0.1.8}/.planfile/config.yaml +0 -0
  30. {giton-0.1.6 → giton-0.1.8}/.planfile/sprints/current.yaml +0 -0
  31. {giton-0.1.6 → giton-0.1.8}/.planfile/sprints/current.yaml.lock +0 -0
  32. {giton-0.1.6 → giton-0.1.8}/LICENSE +0 -0
  33. {giton-0.1.6 → giton-0.1.8}/SUMD.md +0 -0
  34. {giton-0.1.6 → giton-0.1.8}/SUMR.md +0 -0
  35. {giton-0.1.6 → giton-0.1.8}/TODO.md +0 -0
  36. {giton-0.1.6 → giton-0.1.8}/app.doql.less +0 -0
  37. {giton-0.1.6 → giton-0.1.8}/examples/advanced/Dockerfile +0 -0
  38. {giton-0.1.6 → giton-0.1.8}/examples/advanced/README.md +0 -0
  39. {giton-0.1.6 → giton-0.1.8}/examples/advanced/logs/log.txt +0 -0
  40. {giton-0.1.6 → giton-0.1.8}/examples/advanced/main.py +0 -0
  41. {giton-0.1.6 → giton-0.1.8}/examples/basic/Dockerfile +0 -0
  42. {giton-0.1.6 → giton-0.1.8}/examples/basic/README.md +0 -0
  43. {giton-0.1.6 → giton-0.1.8}/examples/basic/logs/log.txt +0 -0
  44. {giton-0.1.6 → giton-0.1.8}/examples/basic/main.py +0 -0
  45. {giton-0.1.6 → giton-0.1.8}/examples/testing/Dockerfile +0 -0
  46. {giton-0.1.6 → giton-0.1.8}/examples/testing/README.md +0 -0
  47. {giton-0.1.6 → giton-0.1.8}/examples/testing/docker-compose.yml +0 -0
  48. {giton-0.1.6 → giton-0.1.8}/examples/testing/logs/log.txt +0 -0
  49. {giton-0.1.6 → giton-0.1.8}/examples/testing/sample_project/pyproject.toml +0 -0
  50. {giton-0.1.6 → giton-0.1.8}/examples/testing/sample_project/src/example.py +0 -0
  51. {giton-0.1.6 → giton-0.1.8}/examples/testing/sample_project/tests/test_example.py +0 -0
  52. {giton-0.1.6 → giton-0.1.8}/examples/testing/test_giton_integration.py +0 -0
  53. {giton-0.1.6 → giton-0.1.8}/goal.yaml +0 -0
  54. {giton-0.1.6 → giton-0.1.8}/koru.yaml +0 -0
  55. {giton-0.1.6 → giton-0.1.8}/planfile.yaml +0 -0
  56. {giton-0.1.6 → giton-0.1.8}/prefact.yaml +0 -0
  57. {giton-0.1.6 → giton-0.1.8}/project/README.md +0 -0
  58. {giton-0.1.6 → giton-0.1.8}/project/analysis.toon.yaml +0 -0
  59. {giton-0.1.6 → giton-0.1.8}/project/calls.mmd +0 -0
  60. {giton-0.1.6 → giton-0.1.8}/project/calls.png +0 -0
  61. {giton-0.1.6 → giton-0.1.8}/project/calls.toon.yaml +0 -0
  62. {giton-0.1.6 → giton-0.1.8}/project/calls.yaml +0 -0
  63. {giton-0.1.6 → giton-0.1.8}/project/compact_flow.mmd +0 -0
  64. {giton-0.1.6 → giton-0.1.8}/project/compact_flow.png +0 -0
  65. {giton-0.1.6 → giton-0.1.8}/project/context.md +0 -0
  66. {giton-0.1.6 → giton-0.1.8}/project/duplication.toon.yaml +0 -0
  67. {giton-0.1.6 → giton-0.1.8}/project/evolution.toon.yaml +0 -0
  68. {giton-0.1.6 → giton-0.1.8}/project/flow.mmd +0 -0
  69. {giton-0.1.6 → giton-0.1.8}/project/flow.png +0 -0
  70. {giton-0.1.6 → giton-0.1.8}/project/index.html +0 -0
  71. {giton-0.1.6 → giton-0.1.8}/project/logic.pl +0 -0
  72. {giton-0.1.6 → giton-0.1.8}/project/map.toon.yaml +0 -0
  73. {giton-0.1.6 → giton-0.1.8}/project/mermaid.export +0 -0
  74. {giton-0.1.6 → giton-0.1.8}/project/planfile-tickets.yaml +0 -0
  75. {giton-0.1.6 → giton-0.1.8}/project/project.toon.yaml +0 -0
  76. {giton-0.1.6 → giton-0.1.8}/project/prompt.txt +0 -0
  77. {giton-0.1.6 → giton-0.1.8}/project.sh +0 -0
  78. {giton-0.1.6 → giton-0.1.8}/src/giton/__main__.py +0 -0
  79. {giton-0.1.6 → giton-0.1.8}/src/giton/cli.py +0 -0
  80. {giton-0.1.6 → giton-0.1.8}/src/giton/config.py +0 -0
  81. {giton-0.1.6 → giton-0.1.8}/src/giton/context.py +0 -0
  82. {giton-0.1.6 → giton-0.1.8}/src/giton/history.py +0 -0
  83. {giton-0.1.6 → giton-0.1.8}/src/giton/hooks.py +0 -0
  84. {giton-0.1.6 → giton-0.1.8}/src/giton/interactive.py +0 -0
  85. {giton-0.1.6 → giton-0.1.8}/src/giton/plugins.py +0 -0
  86. {giton-0.1.6 → giton-0.1.8}/src/giton/policies.py +0 -0
  87. {giton-0.1.6 → giton-0.1.8}/src/giton/repo_config.py +0 -0
  88. {giton-0.1.6 → giton-0.1.8}/src/giton/runner.py +0 -0
  89. {giton-0.1.6 → giton-0.1.8}/src/giton/shell.py +0 -0
  90. {giton-0.1.6 → giton-0.1.8}/testql-scenarios/generated-cli-tests.testql.toon.yaml +0 -0
  91. {giton-0.1.6 → giton-0.1.8}/testql-scenarios/generated-from-pytests.testql.toon.yaml +0 -0
  92. {giton-0.1.6 → giton-0.1.8}/tests/__init__.py +0 -0
  93. {giton-0.1.6 → giton-0.1.8}/tests/test_basic.py +0 -0
  94. {giton-0.1.6 → giton-0.1.8}/tests/test_history.py +0 -0
  95. {giton-0.1.6 → giton-0.1.8}/tests/test_policies.py +0 -0
  96. {giton-0.1.6 → giton-0.1.8}/tree.sh +0 -0
@@ -5,37 +5,7 @@
5
5
  </component>
6
6
  <component name="ChangeListManager">
7
7
  <list default="true" id="9532a670-1f01-4d5e-9501-f3491f105c64" name="Changes" comment="">
8
- <change afterPath="$PROJECT_DIR$/examples/advanced/Dockerfile" afterDir="false" />
9
- <change afterPath="$PROJECT_DIR$/examples/advanced/README.md" afterDir="false" />
10
- <change afterPath="$PROJECT_DIR$/examples/advanced/logs/log.txt" afterDir="false" />
11
- <change afterPath="$PROJECT_DIR$/examples/advanced/main.py" afterDir="false" />
12
- <change afterPath="$PROJECT_DIR$/examples/basic/Dockerfile" afterDir="false" />
13
- <change afterPath="$PROJECT_DIR$/examples/basic/README.md" afterDir="false" />
14
- <change afterPath="$PROJECT_DIR$/examples/basic/logs/log.txt" afterDir="false" />
15
- <change afterPath="$PROJECT_DIR$/examples/basic/main.py" afterDir="false" />
16
- <change afterPath="$PROJECT_DIR$/examples/testing/Dockerfile" afterDir="false" />
17
- <change afterPath="$PROJECT_DIR$/examples/testing/README.md" afterDir="false" />
18
- <change afterPath="$PROJECT_DIR$/examples/testing/docker-compose.yml" afterDir="false" />
19
- <change afterPath="$PROJECT_DIR$/examples/testing/logs/log.txt" afterDir="false" />
20
- <change afterPath="$PROJECT_DIR$/examples/testing/sample_project/pyproject.toml" afterDir="false" />
21
- <change afterPath="$PROJECT_DIR$/examples/testing/sample_project/src/example.py" afterDir="false" />
22
- <change afterPath="$PROJECT_DIR$/examples/testing/sample_project/tests/test_example.py" afterDir="false" />
23
- <change afterPath="$PROJECT_DIR$/examples/testing/test_giton_integration.py" afterDir="false" />
24
- <change afterPath="$PROJECT_DIR$/src/giton/history.py" afterDir="false" />
25
- <change afterPath="$PROJECT_DIR$/src/giton/interactive.py" afterDir="false" />
26
- <change afterPath="$PROJECT_DIR$/src/giton/policies.py" afterDir="false" />
27
- <change afterPath="$PROJECT_DIR$/src/giton/repo_config.py" afterDir="false" />
28
- <change afterPath="$PROJECT_DIR$/tests/test_history.py" afterDir="false" />
29
- <change afterPath="$PROJECT_DIR$/tests/test_policies.py" afterDir="false" />
30
- <change beforePath="$PROJECT_DIR$/VERSION" beforeDir="false" afterPath="$PROJECT_DIR$/VERSION" afterDir="false" />
31
- <change beforePath="$PROJECT_DIR$/goal.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/goal.yaml" afterDir="false" />
32
- <change beforePath="$PROJECT_DIR$/pyproject.toml" beforeDir="false" afterPath="$PROJECT_DIR$/pyproject.toml" afterDir="false" />
33
- <change beforePath="$PROJECT_DIR$/src/giton/__init__.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/giton/__init__.py" afterDir="false" />
34
- <change beforePath="$PROJECT_DIR$/src/giton/cli.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/giton/cli.py" afterDir="false" />
35
- <change beforePath="$PROJECT_DIR$/src/giton/hooks.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/giton/hooks.py" afterDir="false" />
36
- <change beforePath="$PROJECT_DIR$/src/giton/runner.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/giton/runner.py" afterDir="false" />
37
- <change beforePath="$PROJECT_DIR$/src/giton/shell.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/giton/shell.py" afterDir="false" />
38
- <change beforePath="$PROJECT_DIR$/uv.lock" beforeDir="false" afterPath="$PROJECT_DIR$/uv.lock" afterDir="false" />
8
+ <change beforePath="$PROJECT_DIR$/src/giton/catalog.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/giton/catalog.py" afterDir="false" />
39
9
  </list>
40
10
  <option name="SHOW_DIALOG" value="false" />
41
11
  <option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -55,61 +25,61 @@
55
25
  </component>
56
26
  <component name="NextEditCompletionFeaturesState">
57
27
  <decayedCancelled>
58
- <entry key="MS100" value="1.1840429966024764" />
59
- <entry key="MS500" value="3.008906012331558" />
60
- <entry key="S2" value="4.354063637612695" />
61
- <entry key="S5" value="4.726721936322459" />
62
- <entry key="S10" value="4.867729736902627" />
63
- <entry key="S30" value="6.007189586843474" />
64
- <entry key="S60" value="8.687315057904945" />
65
- <entry key="M2" value="12.390366740242463" />
66
- <entry key="M5" value="20.028499727277765" />
67
- <entry key="M10" value="28.647532831651066" />
68
- <entry key="M15" value="34.56395301166038" />
69
- <entry key="M30" value="44.528965358361326" />
70
- <entry key="H1" value="52.2573782114977" />
71
- <entry key="H2" value="57.174224782901376" />
72
- <entry key="H4" value="59.96191489846307" />
73
- <entry key="D1" value="62.475178727914766" />
74
- <entry key="W1" value="62.92455821571766" />
28
+ <entry key="MS100" value="1.0" />
29
+ <entry key="MS500" value="1.0" />
30
+ <entry key="S2" value="1.000000515659134" />
31
+ <entry key="S5" value="1.0113995476191333" />
32
+ <entry key="S10" value="1.3202335050280558" />
33
+ <entry key="S30" value="3.9991370479293304" />
34
+ <entry key="S60" value="6.862414454151789" />
35
+ <entry key="M2" value="11.33533696988415" />
36
+ <entry key="M5" value="21.159317272295162" />
37
+ <entry key="M10" value="31.78592550592875" />
38
+ <entry key="M15" value="38.89698914691506" />
39
+ <entry key="M30" value="50.78006318753607" />
40
+ <entry key="H1" value="60.02745731325951" />
41
+ <entry key="H2" value="65.94585128579381" />
42
+ <entry key="H4" value="69.31550862969083" />
43
+ <entry key="D1" value="72.36259560493188" />
44
+ <entry key="W1" value="72.908350951493" />
75
45
  </decayedCancelled>
76
46
  <decayedSelected>
77
47
  <entry key="MS100" value="0.0" />
78
48
  <entry key="MS500" value="0.0" />
79
- <entry key="S2" value="1.750725986257552E-98" />
80
- <entry key="S5" value="7.893820947435761E-40" />
81
- <entry key="S10" value="2.8095944453667604E-20" />
82
- <entry key="S30" value="3.0400533972351147E-7" />
83
- <entry key="S60" value="5.513667923655825E-4" />
84
- <entry key="M2" value="0.023481200828866962" />
85
- <entry key="M5" value="0.22299140361394168" />
86
- <entry key="M10" value="0.4722196561071362" />
87
- <entry key="M15" value="0.6064049071866291" />
88
- <entry key="M30" value="0.7787200441664701" />
89
- <entry key="H1" value="0.8824511568163249" />
90
- <entry key="H2" value="0.9393887144395157" />
91
- <entry key="H4" value="0.9692206737577957" />
92
- <entry key="D1" value="0.9948030578200769" />
93
- <entry key="W1" value="0.9992559207852288" />
49
+ <entry key="S2" value="2.8109941089587915E-131" />
50
+ <entry key="S5" value="6.019272075275395E-53" />
51
+ <entry key="S10" value="7.758396790107728E-27" />
52
+ <entry key="S30" value="1.9796602476348415E-9" />
53
+ <entry key="S60" value="4.449337307549115E-5" />
54
+ <entry key="M2" value="0.006670335304577357" />
55
+ <entry key="M5" value="0.1347904323160623" />
56
+ <entry key="M10" value="0.36713816515865333" />
57
+ <entry key="M15" value="0.5127271978620018" />
58
+ <entry key="M30" value="0.7160497174512412" />
59
+ <entry key="H1" value="0.8461972095506114" />
60
+ <entry key="H2" value="0.9198897811969714" />
61
+ <entry key="H4" value="0.9591088474187752" />
62
+ <entry key="D1" value="0.9930657023702063" />
63
+ <entry key="W1" value="0.999006429404611" />
94
64
  </decayedSelected>
95
65
  <decayedShown>
96
- <entry key="MS100" value="1.1676890724774422" />
97
- <entry key="MS500" value="3.0000523431376207" />
98
- <entry key="S2" value="4.350775089905487" />
99
- <entry key="S5" value="4.725285817140984" />
100
- <entry key="S10" value="4.866969299487099" />
101
- <entry key="S30" value="6.006050466143283" />
102
- <entry key="S60" value="8.686237365128678" />
103
- <entry key="M2" value="12.412310145277253" />
104
- <entry key="M5" value="20.250271675744575" />
105
- <entry key="M10" value="29.118834037658004" />
106
- <entry key="M15" value="35.16962424632721" />
107
- <entry key="M30" value="45.307228651231846" />
108
- <entry key="H1" value="53.13956938106742" />
109
- <entry key="H2" value="58.113473925261886" />
110
- <entry key="H4" value="60.93106313433165" />
111
- <entry key="D1" value="63.46996931990426" />
112
- <entry key="W1" value="63.92381234570905" />
66
+ <entry key="MS100" value="0.9330329915368074" />
67
+ <entry key="MS500" value="0.9862327044933592" />
68
+ <entry key="S2" value="0.9965407773974431" />
69
+ <entry key="S5" value="1.0100045652680452" />
70
+ <entry key="S10" value="1.3194050137041797" />
71
+ <entry key="S30" value="3.9984807274880163" />
72
+ <entry key="S60" value="6.861847613682888" />
73
+ <entry key="M2" value="11.34127212561773" />
74
+ <entry key="M5" value="21.293233781350274" />
75
+ <entry key="M10" value="32.15227794674544" />
76
+ <entry key="M15" value="39.40904749137232" />
77
+ <entry key="M30" value="51.49566827108885" />
78
+ <entry key="H1" value="60.87339279922141" />
79
+ <entry key="H2" value="66.86559815891809" />
80
+ <entry key="H4" value="70.27454267229896" />
81
+ <entry key="D1" value="73.35564834194766" />
82
+ <entry key="W1" value="73.9073555160594" />
113
83
  </decayedShown>
114
84
  </component>
115
85
  <component name="ProjectColorInfo"><![CDATA[{
@@ -134,11 +104,17 @@
134
104
  "RunOnceActivity.typescript.service.memoryLimit.init": "true",
135
105
  "codeWithMe.voiceChat.enabledByDefault": "false",
136
106
  "git-widget-placeholder": "main",
107
+ "last_opened_file_path": "/home/tom/github/semcod/gix",
137
108
  "node.js.detected.package.eslint": "true",
138
109
  "node.js.selected.package.eslint": "(autodetect)",
139
110
  "nodejs_package_manager_path": "npm"
140
111
  }
141
112
  }]]></component>
113
+ <component name="RecentsManager">
114
+ <key name="CopyFile.RECENT_KEYS">
115
+ <recent name="$PROJECT_DIR$" />
116
+ </key>
117
+ </component>
142
118
  <component name="RunManager">
143
119
  <configuration default="true" type="PythonConfigurationType" factoryName="Python">
144
120
  <module name="giton" />
@@ -54,6 +54,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
54
54
 
55
55
  ## [Unreleased]
56
56
 
57
+ ## [0.1.8] - 2026-05-27
58
+
59
+ ### Docs
60
+ - Update README.md
61
+ - Update examples/plugins/README.md
62
+
63
+ ### Test
64
+ - Update tests/test_plugin_runner.py
65
+
66
+ ### Other
67
+ - Update examples/plugins/test_plugin_lifecycle.py
68
+
69
+ ## [0.1.7] - 2026-05-27
70
+
71
+ ### Docs
72
+ - Update README.md
73
+
57
74
  ## [0.1.6] - 2026-05-27
58
75
 
59
76
  ### Docs
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: giton
3
- Version: 0.1.6
3
+ Version: 0.1.8
4
4
  Summary: Local AI layer for git: orchestrates policies & plugins between commit and push.
5
5
  Author: semcod
6
6
  Author-email: Tom Sapletta <tom@sapletta.com>
@@ -22,11 +22,11 @@ Description-Content-Type: text/markdown
22
22
 
23
23
  ## AI Cost Tracking
24
24
 
25
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.1.6-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
26
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$0.61-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-3.2h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
25
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.1.8-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
26
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$0.96-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-3.4h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
27
27
 
28
- - 🤖 **LLM usage:** $0.6135 (9 commits)
29
- - 👤 **Human dev:** ~$319 (3.2h @ $100/h, 30min dedup)
28
+ - 🤖 **LLM usage:** $0.9599 (11 commits)
29
+ - 👤 **Human dev:** ~$339 (3.4h @ $100/h, 30min dedup)
30
30
 
31
31
  Generated on 2026-05-27 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
32
32
 
@@ -3,11 +3,11 @@
3
3
 
4
4
  ## AI Cost Tracking
5
5
 
6
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.1.6-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$0.61-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-3.2h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
6
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.1.8-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$0.96-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-3.4h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $0.6135 (9 commits)
10
- - 👤 **Human dev:** ~$319 (3.2h @ $100/h, 30min dedup)
9
+ - 🤖 **LLM usage:** $0.9599 (11 commits)
10
+ - 👤 **Human dev:** ~$339 (3.4h @ $100/h, 30min dedup)
11
11
 
12
12
  Generated on 2026-05-27 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
13
13
 
giton-0.1.8/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.8
@@ -0,0 +1,46 @@
1
+ # Plugin example
2
+
3
+ End-to-end demonstration of a custom giton plugin.
4
+
5
+ This example registers a tiny shell-script plugin (`my_lint`) that simply
6
+ echoes the staged paths it received. It then stages a file, runs the
7
+ `pre-commit` trigger, and asserts the plugin actually executed and the
8
+ `{paths}` placeholder was correctly expanded.
9
+
10
+ ## Running
11
+
12
+ ```bash
13
+ # from the repo root
14
+ python -m pytest examples/plugins/test_plugin_lifecycle.py -v
15
+ ```
16
+
17
+ You should see:
18
+
19
+ ```
20
+ PASSED test_register_run_and_unregister
21
+ PASSED test_default_catalog_pyqual_command_is_valid
22
+ ```
23
+
24
+ ## What it covers
25
+
26
+ | Step | API used |
27
+ | ------------------------------ | ----------------------------------------------- |
28
+ | Build a fake plugin executable | plain `chmod +x` |
29
+ | Register a plugin record | `giton.config.upsert_plugin` |
30
+ | Run the hook trigger | `giton.runner.run_trigger("pre-commit")` |
31
+ | Inspect placeholder expansion | reading the file the fake plugin wrote |
32
+ | Unregister | `giton.config.remove_plugin` |
33
+
34
+ ## Verifying the real default catalog
35
+
36
+ The catalog ships three default plugins (`pyqual`, `vallm`, `pretest`)
37
+ that point at sibling repos under `semcod/`. They are real Python
38
+ packages with real CLIs, so you can install one and run it for real:
39
+
40
+ ```bash
41
+ giton plugin install pyqual
42
+ giton hook pre-commit # runs `pyqual gates --workdir <repo>`
43
+ ```
44
+
45
+ If `pyqual` reports a missing `pyqual.yaml`, run `pyqual init` first —
46
+ that's a `pyqual` configuration step, unrelated to giton.
@@ -0,0 +1,116 @@
1
+ """End-to-end example: register a custom plugin, run it, observe the result.
2
+
3
+ Run with:
4
+ pytest examples/plugins/test_plugin_lifecycle.py -v
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ import stat
10
+ import subprocess
11
+ from pathlib import Path
12
+
13
+ import pytest
14
+
15
+
16
+ def _git(args, cwd):
17
+ return subprocess.run(
18
+ ["git", *args], cwd=cwd, check=True, capture_output=True, text=True
19
+ )
20
+
21
+
22
+ @pytest.fixture
23
+ def isolated_repo(tmp_path, monkeypatch):
24
+ """A fresh git repo + isolated XDG_CONFIG_HOME so we don't touch
25
+ the real user's giton plugins."""
26
+ monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "xdg"))
27
+ import importlib
28
+ from giton import config as _cfg
29
+ importlib.reload(_cfg)
30
+ from giton import plugins as _pl, runner as _rn
31
+ importlib.reload(_pl)
32
+ importlib.reload(_rn)
33
+
34
+ repo = tmp_path / "repo"
35
+ repo.mkdir()
36
+ _git(["init", "-q"], repo)
37
+ _git(["config", "user.email", "demo@example.com"], repo)
38
+ _git(["config", "user.name", "demo"], repo)
39
+ (repo / "README.md").write_text("# demo\n")
40
+ _git(["add", "README.md"], repo)
41
+ _git(["commit", "-q", "-m", "chore: seed"], repo)
42
+ return repo
43
+
44
+
45
+ def test_register_run_and_unregister(isolated_repo, tmp_path, monkeypatch):
46
+ # 1) build a fake plugin executable that records its argv to a log
47
+ bin_dir = tmp_path / "bin"
48
+ bin_dir.mkdir()
49
+ plugin = bin_dir / "my_lint"
50
+ log = tmp_path / "my_lint.log"
51
+ plugin.write_text(
52
+ "#!/bin/sh\n"
53
+ f"echo \"argv:$@\" > '{log}'\n"
54
+ "echo 'my_lint: ok'\n"
55
+ "exit 0\n"
56
+ )
57
+ plugin.chmod(plugin.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
58
+ monkeypatch.setenv("PATH", f"{bin_dir}:{os.environ['PATH']}")
59
+
60
+ # 2) register it as a giton plugin firing on pre-commit
61
+ from giton.config import PluginRecord, upsert_plugin, load_plugins, remove_plugin
62
+ upsert_plugin(PluginRecord(
63
+ name="my_lint",
64
+ description="example plugin",
65
+ category="lang:python",
66
+ exec_type="cli",
67
+ command="my_lint {paths}",
68
+ triggers=["pre-commit"],
69
+ install=None,
70
+ source="user",
71
+ enabled=True,
72
+ ))
73
+ assert any(p.name == "my_lint" for p in load_plugins())
74
+
75
+ # 3) stage a python file and fire the pre-commit trigger
76
+ (isolated_repo / "module.py").write_text("def f():\n return 42\n")
77
+ _git(["add", "module.py"], isolated_repo)
78
+
79
+ from giton.runner import run_trigger
80
+ outcome = run_trigger("pre-commit", cwd=isolated_repo)
81
+
82
+ # 4) the plugin must have actually run
83
+ assert len(outcome.plugin_results) == 1
84
+ pr = outcome.plugin_results[0]
85
+ assert pr.plugin == "my_lint"
86
+ assert pr.ok, pr.stderr
87
+ assert "my_lint: ok" in pr.stdout
88
+
89
+ # 5) {paths} placeholder must have been expanded to the staged file
90
+ argv_line = log.read_text().strip()
91
+ assert "module.py" in argv_line, argv_line
92
+
93
+ # 6) unregister cleanly
94
+ assert remove_plugin("my_lint")
95
+ assert not any(p.name == "my_lint" for p in load_plugins())
96
+
97
+
98
+ def test_default_catalog_pyqual_command_is_valid():
99
+ """Smoke-test the catalog metadata for the three default plugins."""
100
+ from giton import catalog
101
+
102
+ expected = {"pyqual", "vallm", "pretest"}
103
+ actual = {e.record.name for e in catalog.defaults()}
104
+ assert expected == actual
105
+
106
+ for entry in catalog.defaults():
107
+ cmd = entry.record.command
108
+ # every default command must declare which binary to invoke and
109
+ # the binary name must equal the plugin name
110
+ first_token = cmd.split()[0]
111
+ assert first_token == entry.record.name, (
112
+ f"plugin {entry.record.name!r} command starts with "
113
+ f"{first_token!r}; commands should always invoke their own binary"
114
+ )
115
+ # placeholders must format cleanly with reasonable values
116
+ cmd.format(paths="a.py", diff_file="/tmp/d.patch", root="/tmp/r")
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "giton"
7
- version = "0.1.6"
7
+ version = "0.1.8"
8
8
  description = "Local AI layer for git: orchestrates policies & plugins between commit and push."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,3 +1,3 @@
1
1
  """giton — local AI layer for git, between commit and push."""
2
2
 
3
- __version__ = "0.1.6"
3
+ __version__ = "0.1.8"
@@ -54,7 +54,7 @@ CATALOG: dict[str, CatalogEntry] = {
54
54
  "Python code quality checks (lint, types, complexity).",
55
55
  category="lang:python",
56
56
  triggers=["pre-commit"],
57
- command="pyqual check {paths}",
57
+ command="pyqual gates --workdir {root}",
58
58
  pypi="pyqual",
59
59
  local="../pyqual",
60
60
  ),
@@ -63,7 +63,7 @@ CATALOG: dict[str, CatalogEntry] = {
63
63
  "Validate LLM-generated code/patches before they land in history.",
64
64
  category="task:validate",
65
65
  triggers=["post-commit"],
66
- command="vallm check --diff {diff_file}",
66
+ command="vallm batch {paths}",
67
67
  pypi="vallm",
68
68
  local="../vallm",
69
69
  ),
@@ -72,7 +72,7 @@ CATALOG: dict[str, CatalogEntry] = {
72
72
  "Run / generate tests before push.",
73
73
  category="task:test",
74
74
  triggers=["pre-push"],
75
- command="pretest run",
75
+ command="pretest scan",
76
76
  pypi="pretest",
77
77
  local="../pretest",
78
78
  ),
@@ -0,0 +1,208 @@
1
+ """End-to-end tests for the plugin runner using a real fake-plugin script.
2
+
3
+ These tests verify that:
4
+ - `{paths}`, `{diff_file}`, `{root}` placeholders are correctly expanded
5
+ - Plugin stdout/stderr/exit-code are propagated into `TriggerOutcome`
6
+ - `enabled=False` plugins are skipped
7
+ - `unknown command` plugins are reported, not crashed
8
+ - `commit-msg` plugin trigger is honoured
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ import stat
14
+ import subprocess
15
+ from pathlib import Path
16
+
17
+ import pytest
18
+
19
+ from giton.config import PluginRecord, USER_PLUGINS_FILE, load_plugins, save_plugins
20
+ from giton.runner import run_trigger
21
+
22
+
23
+ def _git(args, cwd):
24
+ return subprocess.run(
25
+ ["git", *args], cwd=cwd, check=True, capture_output=True, text=True
26
+ )
27
+
28
+
29
+ @pytest.fixture
30
+ def repo(tmp_path, monkeypatch):
31
+ """Initialise a clean repo + isolated giton config root."""
32
+ monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "xdg"))
33
+ # reload config so USER_PLUGINS_FILE picks up the new XDG path
34
+ import importlib
35
+ from giton import config as _cfg
36
+ importlib.reload(_cfg)
37
+ from giton import plugins as _pl, runner as _rn
38
+ importlib.reload(_pl)
39
+ importlib.reload(_rn)
40
+
41
+ r = tmp_path / "r"
42
+ r.mkdir()
43
+ _git(["init", "-q"], r)
44
+ _git(["config", "user.email", "t@t"], r)
45
+ _git(["config", "user.name", "t"], r)
46
+ (r / "seed.txt").write_text("seed\n")
47
+ _git(["add", "seed.txt"], r)
48
+ _git(["commit", "-q", "-m", "feat: seed"], r)
49
+ return r
50
+
51
+
52
+ def _write_fake_plugin(tmp_path: Path, name: str = "fakeplugin") -> Path:
53
+ """A real executable that records its argv and stdin to a file."""
54
+ script = tmp_path / name
55
+ log = tmp_path / f"{name}.log"
56
+ script.write_text(
57
+ "#!/bin/sh\n"
58
+ f"echo \"args:$@\" > '{log}'\n"
59
+ f"echo \"cwd:$PWD\" >> '{log}'\n"
60
+ "echo 'fake plugin ran'\n"
61
+ "exit ${FAKE_EXIT:-0}\n"
62
+ )
63
+ script.chmod(script.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
64
+ return script
65
+
66
+
67
+ def _register(name: str, command: str, trigger: str, *, enabled: bool = True):
68
+ from giton import config as _cfg
69
+ plugins = _cfg.load_plugins()
70
+ plugins = [p for p in plugins if p.name != name]
71
+ plugins.append(PluginRecord(
72
+ name=name,
73
+ description="fake",
74
+ category="task:test",
75
+ exec_type="cli",
76
+ command=command,
77
+ triggers=[trigger],
78
+ install=None,
79
+ source="user",
80
+ enabled=enabled,
81
+ ))
82
+ _cfg.save_plugins(plugins)
83
+
84
+
85
+ def test_plugin_executes_and_expands_paths(repo, tmp_path, monkeypatch):
86
+ fake = _write_fake_plugin(tmp_path)
87
+ log = tmp_path / "fakeplugin.log"
88
+ monkeypatch.setenv("PATH", f"{tmp_path}:{os.environ['PATH']}")
89
+ _register("fakeplugin", "fakeplugin {paths}", "pre-commit")
90
+
91
+ # stage a couple of files
92
+ (repo / "a.py").write_text("a=1\n")
93
+ (repo / "b.py").write_text("b=2\n")
94
+ _git(["add", "a.py", "b.py"], repo)
95
+
96
+ out = run_trigger("pre-commit", cwd=repo)
97
+ assert len(out.plugin_results) == 1
98
+ pr = out.plugin_results[0]
99
+ assert pr.ok, pr.stderr
100
+ assert "fake plugin ran" in pr.stdout
101
+ # placeholder expansion: {paths} → "a.py b.py"
102
+ args_line = log.read_text().splitlines()[0]
103
+ assert "a.py" in args_line and "b.py" in args_line
104
+
105
+
106
+ def test_plugin_diff_file_placeholder(repo, tmp_path, monkeypatch):
107
+ fake = _write_fake_plugin(tmp_path, "diffplug")
108
+ log = tmp_path / "diffplug.log"
109
+ monkeypatch.setenv("PATH", f"{tmp_path}:{os.environ['PATH']}")
110
+ _register("diffplug", "diffplug --diff {diff_file}", "post-commit")
111
+
112
+ (repo / "x.py").write_text("x=1\n")
113
+ _git(["add", "x.py"], repo)
114
+ _git(["commit", "-q", "-m", "feat: x"], repo)
115
+
116
+ out = run_trigger("post-commit", cwd=repo)
117
+ assert out.plugin_results and out.plugin_results[0].ok
118
+ args_line = log.read_text().splitlines()[0]
119
+ assert "--diff" in args_line
120
+ # diff_file path always resolves under .giton/
121
+ assert ".giton" in args_line
122
+
123
+
124
+ def test_plugin_root_placeholder(repo, tmp_path, monkeypatch):
125
+ fake = _write_fake_plugin(tmp_path, "rootplug")
126
+ log = tmp_path / "rootplug.log"
127
+ monkeypatch.setenv("PATH", f"{tmp_path}:{os.environ['PATH']}")
128
+ _register("rootplug", "rootplug --workdir {root}", "pre-commit")
129
+
130
+ (repo / "z.txt").write_text("z\n")
131
+ _git(["add", "z.txt"], repo)
132
+
133
+ out = run_trigger("pre-commit", cwd=repo)
134
+ assert out.plugin_results[0].ok
135
+ args_line = log.read_text().splitlines()[0]
136
+ assert str(repo) in args_line
137
+
138
+
139
+ def test_plugin_failure_propagates_to_outcome(repo, tmp_path, monkeypatch):
140
+ _write_fake_plugin(tmp_path, "failplug")
141
+ monkeypatch.setenv("PATH", f"{tmp_path}:{os.environ['PATH']}")
142
+ monkeypatch.setenv("FAKE_EXIT", "3")
143
+ _register("failplug", "failplug", "pre-commit")
144
+
145
+ (repo / "f.txt").write_text("f\n")
146
+ _git(["add", "f.txt"], repo)
147
+
148
+ out = run_trigger("pre-commit", cwd=repo)
149
+ assert out.plugin_results[0].returncode == 3
150
+ assert not out.plugin_results[0].ok
151
+ assert not out.ok
152
+
153
+
154
+ def test_plugin_disabled_is_skipped(repo, tmp_path, monkeypatch):
155
+ _write_fake_plugin(tmp_path, "skipplug")
156
+ monkeypatch.setenv("PATH", f"{tmp_path}:{os.environ['PATH']}")
157
+ _register("skipplug", "skipplug", "pre-commit", enabled=False)
158
+
159
+ (repo / "g.txt").write_text("g\n")
160
+ _git(["add", "g.txt"], repo)
161
+
162
+ out = run_trigger("pre-commit", cwd=repo)
163
+ assert out.plugin_results == []
164
+
165
+
166
+ def test_plugin_command_not_found(repo):
167
+ # do NOT alter PATH (git must remain available); use a binary name that
168
+ # is overwhelmingly unlikely to exist anywhere on the system.
169
+ _register(
170
+ "ghostplug", "giton-nonexistent-fake-binary-xyz123", "pre-commit"
171
+ )
172
+
173
+ (repo / "h.txt").write_text("h\n")
174
+ _git(["add", "h.txt"], repo)
175
+
176
+ out = run_trigger("pre-commit", cwd=repo)
177
+ assert out.plugin_results[0].returncode == 127
178
+ assert not out.ok
179
+
180
+
181
+ def test_multiple_plugins_run_in_order(repo, tmp_path, monkeypatch):
182
+ _write_fake_plugin(tmp_path, "p1")
183
+ _write_fake_plugin(tmp_path, "p2")
184
+ monkeypatch.setenv("PATH", f"{tmp_path}:{os.environ['PATH']}")
185
+ _register("p1", "p1", "pre-commit")
186
+ _register("p2", "p2", "pre-commit")
187
+
188
+ (repo / "i.txt").write_text("i\n")
189
+ _git(["add", "i.txt"], repo)
190
+
191
+ out = run_trigger("pre-commit", cwd=repo)
192
+ names = [r.plugin for r in out.plugin_results]
193
+ assert names == ["p1", "p2"]
194
+ assert all(r.ok for r in out.plugin_results)
195
+
196
+
197
+ def test_default_catalog_commands_are_well_formed():
198
+ """Catch regressions where a default plugin's command refers to a
199
+ non-existent subcommand. We can't run them in CI without the deps
200
+ installed, but we can verify the placeholder syntax parses."""
201
+ from giton import catalog
202
+ for entry in catalog.defaults():
203
+ cmd = entry.record.command
204
+ # placeholder substitution must succeed with reasonable inputs
205
+ formatted = cmd.format(paths="a.py", diff_file="/tmp/d.patch", root="/tmp/r")
206
+ assert formatted # non-empty
207
+ # and the binary name (first token) must equal the entry name
208
+ assert formatted.split()[0] == entry.record.name
@@ -819,7 +819,7 @@ wheels = [
819
819
 
820
820
  [[package]]
821
821
  name = "giton"
822
- version = "0.1.6"
822
+ version = "0.1.8"
823
823
  source = { editable = "." }
824
824
  dependencies = [
825
825
  { name = "pyyaml" },
giton-0.1.6/VERSION DELETED
@@ -1 +0,0 @@
1
- 0.1.6
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
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
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