github-task-protocol 1.0.1__py3-none-any.whl → 1.0.2__py3-none-any.whl

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.
@@ -0,0 +1,117 @@
1
+ Metadata-Version: 2.4
2
+ Name: github-task-protocol
3
+ Version: 1.0.2
4
+ Summary: Reconstruct AI coding task state from GitHub Issue records
5
+ Requires-Python: >=3.11
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Project-URL: Repository, https://github.com/shinya0x00/github-task-protocol
9
+ Description-Content-Type: text/markdown
10
+
11
+ # GitHub Task Protocol
12
+
13
+ GTPは、AIへ実装を任せても、人間が目的・変更範囲・現在地・根拠を理解し、停止・再開・やり直し・mergeの判断を手放さないための小さなprotocolです。
14
+
15
+ > 作業はAIに任せる。判断は手放さない。
16
+
17
+ AIの説明だけを信じるのではなく、GitHub Issue上のRecordと、実際のbranch・PR・commit・Check Runからtask stateを再構成します。GTP自身は変更、完了、mergeの権限を与えません。
18
+
19
+ task固有の未確認事項は、通常のIssue本文やcommentへ人が読める形で残します。開始前から完了判断に必要な不明点はDone Conditionにし、開始後にContract変更が必要になった場合はStopと後継Issueへ移ります。GTP Recordは自由文の意味を自動評価しないため、`status`はDone提示前にIssueを確認するURLを表示します。
20
+
21
+ ## 推奨: 明示的にsetupを依頼
22
+
23
+ bare GTP repository URLだけではsetup依頼にもrepository変更のauthorizationにもなりません。URLだけを受け取ったagentは、説明または目的確認に留まり、現在のrepositoryを変更しません。
24
+
25
+ 導入先repositoryを操作中のclean agentへ、変更対象とDraft setup PR作成を明示して依頼します。
26
+
27
+ ```text
28
+ このrepositoryへGTPを導入するDraft setup PRを作ってください。
29
+ GTP repository: https://github.com/shinya0x00/github-task-protocol
30
+ ```
31
+
32
+ この明示依頼を受けたagentは、次の順序でsetupします。
33
+
34
+ agentが手順を理解できることと、実行中ずっと意図の境界内に留まり続けることは別の能力です。この手順はbranch-first順序で境界逸脱のriskを下げます。GTP単独の強制力はこの手順の受入対象にしません。repository ownerは補完策としてGitHub branch protectionまたはrulesetでdefault branchへの直接pushを拒否し、pull request経由の変更を必須にできます。setup agentは保護設定を変更せず、未設定なら人間へ報告します。
35
+
36
+ 1. GitHubのlatest stable Releaseを取得し、`draft: false`かつ`prerelease: false`を確認する。未公開candidateやmoving `main`は選ばない。
37
+ 2. Releaseのtagをcommit SHAまでdereferenceし、選択したtagとexact commit SHAを記録する。
38
+ 3. target fileを変更する前にrepositoryのdefault branch名とhead SHAを記録し、そのheadから`gtp/setup-<tag>-<short-sha>` branchを作ってswitchする。現在branchがdefault branchではなくsetup branchであることを確認できなければ停止する。
39
+ 4. そのcommitの`GTP.md`だけを`https://raw.githubusercontent.com/shinya0x00/github-task-protocol/<commit-sha>/GTP.md`から取得し、導入先rootへvendorする。既存`GTP.md`とSHA-256が同一なら保持する。内容が異なるfile、別のGTP authority、または既存instructionとの衝突があれば、上書きせず停止して人間へ報告する。
40
+ 5. root `AGENTS.md`がなければ作成する。存在する場合は本文を変更・削除せず、下のexact adapterがなければ`## GitHub Task Protocol adapter` heading付きで追記する。既に同じadapterがあれば重複追加しない。
41
+ 6. commitとpushはsetup branchだけに行い、default branchへ直接pushしない。push後にdefault branch headを再取得し、setup開始前に記録したSHAから予期せず変化していたら停止して報告する。Draft setup PRのbodyにはrelease tag、exact commit SHA、immutable `GTP.md` URL、変更file、保持した既存instruction、次に必要な人間判断を書く。
42
+ 7. 人間がsetup PRをmergeするまで導入完了としません。merge後、taskごとにGitHub Issueを1件作り、agentへそのIssue URLだけを渡します。
43
+
44
+ 共通adapter文:
45
+
46
+ > このrepositoryはrootの`GTP.md`をtask protocolの唯一の正本とする。GitHub Issue URLを受け取ったら、Issue commentをServer Orderで読み、4 Record、6 state、7 halt reasonに従って既存branch・PR・次のprotocol actionを再構成する。Recordを推測、編集、独自拡張せず、矛盾時は原因URLを示して止まり、取得不能はhaltと混同しない。GTPの表示やRecordは変更・完了・mergeの権限を与えない。
47
+
48
+ 配置例は次のとおりです。runtimeごとに異なる指示を作る必要はありません。
49
+
50
+ - Codex: `AGENTS.md`
51
+ - Claude Code: `CLAUDE.md`または`AGENTS.md`
52
+ - Cursor: `AGENTS.md`または`.cursor/rules/gtp.md`
53
+
54
+ ## 手動導入
55
+
56
+ 自動setupを使わない場合は、次の3手順で導入できます。
57
+
58
+ 1. latest stable Releaseをexact commitへ固定し、file変更前にdefault branchからsetup用branchを作ってswitchする。
59
+ 2. setup branch上で`GTP.md`をrootへコピーし、共通adapter文を既存instructionへ非破壊で追加する。
60
+ 3. commitとpushをsetup branchだけに行ってDraft PRを作り、人間が内容を確認してmergeする。
61
+
62
+ ## 4つのRecord
63
+
64
+ | Record | 平易な意味 |
65
+ |---|---|
66
+ | `contract` | 目的、変更してよい範囲、完了条件を固定する |
67
+ | `start` | Contractと唯一の作業branchを結び付ける |
68
+ | `done` | PRのsource headと、条件ごとのEvidenceを提示する |
69
+ | `stop` | 完了を主張せず中止し、必要なら後継Issueを示す |
70
+
71
+ RecordはIssue commentへ人向け要約を先に、機械用JSONを折りたたんで記録します。1 Issue = 1 branch = 1 PRです。
72
+ Start branchへrepositoryのdefault branchは指定できません。
73
+ Startより前から存在するPRは、そのtaskのcandidateやDoneとして引き継げません。誤ったStartをStopする場合は、古いPRを対象外として安全に閉じます。
74
+
75
+ ## 6つのstate
76
+
77
+ | state | 平易な意味 |
78
+ |---|---|
79
+ | `unmanaged` | 有効なContractがない |
80
+ | `ready` | ContractはあるがStart前 |
81
+ | `in_progress` | 作業中、またはDone提示後のmerge待ち |
82
+ | `halt` | 特定transitionを矛盾や不適合のため進められない |
83
+ | `done` | Doneのsource headへEvidence resourceが結び付き、そのPRがnative mergeされた。条件内容の十分性は人がEvidenceを読んで判断する |
84
+ | `stopped` | Stopにより、このIssueでの作業を終了した |
85
+
86
+ GitHub情報を完全に取得できない場合はstateを推測しません。404だけではresource不在と権限不足を区別できないため、これは`halt`ではなくAcquisition Errorです。
87
+
88
+ ## CLIは任意の検証器
89
+
90
+ 人間がGTPを使うためにCLIをinstallする必要はありません。`gtp`はagentや自動検査がRecordと現在stateを確認するための、runtime dependency 0の任意toolです。
91
+
92
+ CLIはPyPI公開後、固定versionを指定して実行できます。現在のsource candidateは`1.0.2`であり、公開確認前です。公開済み`1.0.1`はこのcandidateと挙動が異なるため、下記commandは`1.0.2`の公開Evidence取得後に使用します。GTPを使うだけならCLIのinstallは不要です。
93
+
94
+ ```console
95
+ uvx --from github-task-protocol==1.0.2 gtp status <issue-url>
96
+ uvx --from github-task-protocol==1.0.2 gtp check <comment.md>
97
+ ```
98
+
99
+ - `status`はGitHubへGETだけを行い、日本語6項目の後にmachine JSONを出します。Evidenceの存在・種類・状態・source headとの結び付きを検査しますが、完了条件の自然言語上の充足までは自動判定しません。
100
+ - `check`は投稿前のMarkdown comment全文をoffline検査します。Issue上でもvalidだとは主張しません。
101
+ - exit code、緑色のCheck Run、Evidence URLは、変更やmergeの許可ではありません。
102
+
103
+ ## 仕様と判断記録
104
+
105
+ protocolの唯一の正本は400行以内の[`GTP.md`](GTP.md)です。Record作成やstate判断に、他の文書は必要ありません。
106
+
107
+ [`DECISIONS.md`](DECISIONS.md)は、設計変更の理由と履歴です。`GTP.md`と意味が衝突する場合は`GTP.md`を優先します。
108
+
109
+ 実GitHubで観測した引き継ぎ結果は[`acceptance/level0/`](acceptance/level0/)にあります。これは仕様の代わりではありません。
110
+
111
+ ## GTPが証明しないこと
112
+
113
+ GTPは、actor本人性、credential安全性、コード品質そのもの、Evidence内容の真実性を証明しません。filesystem削除や本番database操作を物理的に防ぐものでもありません。
114
+
115
+ サンドボックス、最小権限、不可逆操作前の確認、reviewと組み合わせてください。最終的な受理は、人間がPRとEvidenceを読み、GitHubのnative mergeで判断します。
116
+
117
+ License: [MIT](LICENSE)
@@ -0,0 +1,16 @@
1
+ gtp/__init__.py,sha256=Mgzlh0-l_XUHLRPtouhrFlM1lX4feMnDUULN0WuIsb0,58
2
+ gtp/__main__.py,sha256=D31U8_ux95qF64EQ8ReT25nabzxh7ped5avpobXz_bM,49
3
+ gtp/carrier.py,sha256=K4Wi1BGlFZ87VuRYa6CYC-oOuGnEPsFw5-2EkDWAbno,4039
4
+ gtp/cli.py,sha256=OSmVwjr9GiBqBUY64i2VpFV9putlCKGCrfeL_40laUU,1721
5
+ gtp/github.py,sha256=__WJN9-EvzYjVFB56bLAvWsH_alcY6WOQRmYwjwZ6qM,8465
6
+ gtp/model.py,sha256=0CnS7k0bMPn3Bw4voZGT-Jgvez65JX6X_hpCeeyyl4Y,2443
7
+ gtp/presentation.py,sha256=9lfmwcuIo9Ip-CEyrqNtr4Mwh0b6S-HwGjZGbsffRB4,18627
8
+ gtp/reducer.py,sha256=YuEq9ylDfkuYGLEa2YEM9PaUOrhxWY3HnU_p5rMK8G0,6997
9
+ gtp/schema.py,sha256=Jjltq80E8HjaUBPWoQJ_YwRM-Lkm85rCaHuv10sB0Sg,7768
10
+ gtp/status.py,sha256=vJ6J7skWqz1CzBgkYnS1M2ECa0OgSj7GQPLhKiI21c8,32702
11
+ gtp/urls.py,sha256=CxheRZ4ND__RMZx-cejSMqk5tCu6aaoin6g3Q_RK2i8,3332
12
+ github_task_protocol-1.0.2.dist-info/METADATA,sha256=ZLYZol7HPEg54qf7ez1xAoikqxZlqLE8KPVNm7ZTpVU,9005
13
+ github_task_protocol-1.0.2.dist-info/WHEEL,sha256=5CJ4bbElW8cq_GDpNSLjMgfb15S72ImTOLq9c70uj1Q,106
14
+ github_task_protocol-1.0.2.dist-info/entry_points.txt,sha256=ULDQmE66x4_NxwQH1Ru9cEVOJMEUBvT27g2a5FcDrAo,37
15
+ github_task_protocol-1.0.2.dist-info/licenses/LICENSE,sha256=yvK8Xx5r58kF13JNYhXfeRf9IAY9rm-jS3THRU0ttJY,1067
16
+ github_task_protocol-1.0.2.dist-info/RECORD,,
gtp/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """GitHub Task Protocol reader."""
2
2
 
3
- __version__ = "1.0.1"
3
+ __version__ = "1.0.2"
gtp/github.py CHANGED
@@ -164,13 +164,10 @@ class GitHubClient:
164
164
  )
165
165
  return comments
166
166
 
167
- def branch(self, owner: str, repo: str, branch: str) -> dict[str, Any] | None:
168
- try:
169
- return self._get(f"/repos/{quote(owner)}/{quote(repo)}/branches/{quote(branch, safe='')}")
170
- except AcquisitionError as error:
171
- if error.status == 404:
172
- return None
173
- raise
167
+ def branch(self, owner: str, repo: str, branch: str) -> dict[str, Any]:
168
+ return self._get(
169
+ f"/repos/{quote(owner)}/{quote(repo)}/branches/{quote(branch, safe='')}"
170
+ )
174
171
 
175
172
  def pull_requests(self, owner: str, repo: str, branch: str) -> list[dict[str, Any]]:
176
173
  resources = self._pages(
gtp/presentation.py CHANGED
@@ -19,6 +19,9 @@ HALT_MESSAGES = {
19
19
  "terminal_violation": "terminal stateの前後関係に違反するRecordまたはmergeがあります",
20
20
  }
21
21
  EVIDENCE_LIMITS = [
22
+ "Check RunがDone Conditionの内容を十分に検査したこと",
23
+ "Artifactの内容がDone Conditionを満たすこと",
24
+ "Issue本文・通常commentに未解決事項がないこと",
22
25
  "actor本人性",
23
26
  "credential安全性",
24
27
  "GitHub外情報を参照しなかったこと",
@@ -50,6 +53,7 @@ def _content(record: dict[str, Any] | None) -> dict[str, Any]:
50
53
 
51
54
  def _task_context(
52
55
  *,
56
+ issue_url: str,
53
57
  state: str | None,
54
58
  acquisition_complete: bool,
55
59
  contract: dict[str, Any] | None,
@@ -94,12 +98,17 @@ def _task_context(
94
98
  not_proven.extend(missing)
95
99
  if done is None:
96
100
  not_proven.append("Done Claim未提示")
97
- elif state == "in_progress":
98
- not_proven.append("native merge未確認")
99
- elif state == "halt" and not missing:
100
- not_proven.append("protocol不適合が未解決")
101
- elif state == "stopped":
102
- not_proven.append("このIssueは完了を証明せず停止済み")
101
+ else:
102
+ if state in {"in_progress", "done"} and not missing:
103
+ not_proven.append(
104
+ "Done Conditionの自然言語上の充足は自動判定していない"
105
+ )
106
+ if state == "in_progress":
107
+ not_proven.append("native merge未確認")
108
+ elif state == "halt" and not missing:
109
+ not_proven.append("protocol不適合が未解決")
110
+ elif state == "stopped":
111
+ not_proven.append("このIssueは完了を証明せず停止済み")
103
112
 
104
113
  branch_name = branch.get("name") if isinstance(branch, dict) else None
105
114
  if not isinstance(branch_name, str):
@@ -109,6 +118,8 @@ def _task_context(
109
118
  return {
110
119
  "goal": contract_content.get("goal"),
111
120
  "scope": scope if isinstance(scope, list) else [],
121
+ "handoff_url": issue_url,
122
+ "handoff_semantics": "Issue本文・通常commentの意味は自動判定しない",
112
123
  "branch": branch_name,
113
124
  "pr": pr,
114
125
  "conditions": conditions,
@@ -205,6 +216,7 @@ def status_projection(result: StatusResult) -> dict[str, Any]:
205
216
  "diagnostics": diagnostics,
206
217
  "acquisition_errors": result.acquisition_errors,
207
218
  "task_context": _task_context(
219
+ issue_url=result.issue_url,
208
220
  state=result.state,
209
221
  acquisition_complete=not result.acquisition_errors,
210
222
  contract=contract,
@@ -223,11 +235,17 @@ def _plain_summary(machine: dict[str, Any], context: dict[str, Any]) -> list[str
223
235
  if state is None
224
236
  else "このIssueの完了は確認できません。作業を止めて人が確認してください。"
225
237
  if state == "halt"
226
- else "このIssueの完了を確認しました。"
238
+ else (
239
+ "Done ClaimのEvidence bindingとnative mergeを確認しました。"
240
+ "条件内容の充足はEvidenceを読んで判断してください。"
241
+ )
227
242
  if state == "done"
228
243
  else "このIssueは完了を主張せず終了しています。"
229
244
  if state == "stopped"
230
- else "完了条件の確認資料はそろっていますが、マージはまだです。"
245
+ else (
246
+ "Done ClaimのEvidence bindingを確認しましたが、マージはまだです。"
247
+ "条件内容の充足はEvidenceを読んで判断してください。"
248
+ )
231
249
  if machine["next_action"] == "await_merge"
232
250
  else "このIssueはまだ作業途中です。"
233
251
  )
@@ -254,6 +272,9 @@ def _plain_summary(machine: dict[str, Any], context: dict[str, Any]) -> list[str
254
272
  f" 変更してよい場所: {scope_text}",
255
273
  f" 作業場所: branch {context.get('branch') or '未確認'}"
256
274
  f" / PR {context.get('pr') or '未確認'}",
275
+ " 未確認事項の確認先: "
276
+ f"{context.get('handoff_url') or machine['issue_url']}"
277
+ "(Issue本文・通常commentの意味は自動判定していません)",
257
278
  ]
258
279
  )
259
280
 
@@ -274,7 +295,7 @@ def _plain_summary(machine: dict[str, Any], context: dict[str, Any]) -> list[str
274
295
  evidence_confirmed = state == "done" or machine["next_action"] == "await_merge"
275
296
  if presented:
276
297
  lines.append(
277
- " 確認できた完了条件:"
298
+ " Evidence bindingを確認した条件(条件内容の充足は自動判定していません):"
278
299
  if evidence_confirmed
279
300
  else " 記録に確認資料へのリンクがある条件(達成済みとはまだ断定しません):"
280
301
  )
@@ -321,7 +342,10 @@ def _status_text(machine: dict[str, Any]) -> list[str]:
321
342
  "post_contract": "目的・変更範囲・完了条件をContractとして記録してください",
322
343
  "post_start": "Contractと作業branchをStartで束縛してください",
323
344
  "continue_work": "束縛されたbranchで作業を続けてください",
324
- "post_done": "完了条件を確認し、source headとEvidenceをDoneで提示してください",
345
+ "post_done": (
346
+ "Issue本文・通常commentの未確認事項と完了条件を確認し、"
347
+ "source headとEvidenceをDoneで提示してください"
348
+ ),
325
349
  "await_merge": "Done ClaimとEvidenceを確認し、人間のnative merge判断を待ってください",
326
350
  "inspect_halt": "最初のURLを確認し、記録を推測で補わず人間へ戻してください",
327
351
  "none_done": "追加のprotocol actionはありません",
@@ -336,7 +360,7 @@ def _status_text(machine: dict[str, Any]) -> list[str]:
336
360
  token = machine["halt_reason"]
337
361
  reason = f"{token} — {HALT_MESSAGES.get(token, 'protocol上の矛盾または不適合を確認しました')}"
338
362
  elif action == "await_merge":
339
- reason = "Done Claimは確認済みですが、native mergeはまだ確認できません"
363
+ reason = "Done ClaimのEvidence bindingは確認済みですが、native mergeはまだ確認できません"
340
364
  elif action == "post_done":
341
365
  reason = f"Start、作業branch {branch_name or '(名前不明)'}、PR candidateを確認しました"
342
366
  elif action == "continue_work":
@@ -345,7 +369,7 @@ def _status_text(machine: dict[str, Any]) -> list[str]:
345
369
  reason = {
346
370
  "unmanaged": "有効なContractがありません",
347
371
  "ready": "有効なContractがあり、Startはまだありません",
348
- "done": "Done Claimのsource head、Evidence、native mergeを確認しました",
372
+ "done": "Done Claimのsource head、Evidence binding、native mergeを確認しました",
349
373
  "stopped": "有効なStop Recordを確認しました",
350
374
  }[machine["state"]]
351
375
  lines = [
gtp/status.py CHANGED
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from dataclasses import dataclass, field
6
+ from datetime import datetime, timezone
6
7
  from typing import Any
7
8
  from urllib.parse import unquote
8
9
 
@@ -14,7 +15,7 @@ from .model import (
14
15
  RecordObservation,
15
16
  SuccessorFact,
16
17
  )
17
- from .reducer import fold_comments, historical_state, successor_refs
18
+ from .reducer import fold_comments, historical_state
18
19
  from .urls import parse_github_url
19
20
 
20
21
 
@@ -96,6 +97,72 @@ def _pr_matches(pr: dict[str, Any], repo_id: int, branch: str) -> bool:
96
97
  )
97
98
 
98
99
 
100
+ def _branch_snapshot_key(
101
+ branch: dict[str, Any] | None,
102
+ resource: str,
103
+ ) -> tuple[str, str] | None:
104
+ if branch is None:
105
+ return None
106
+ name = branch.get("name")
107
+ sha = branch.get("commit", {}).get("sha")
108
+ if not isinstance(name, str) or not isinstance(sha, str):
109
+ raise AcquisitionError(resource, "branch snapshot fields missing")
110
+ return name, sha
111
+
112
+
113
+ def _pr_snapshot_key(pr: dict[str, Any], resource: str) -> tuple[Any, ...]:
114
+ number = pr.get("number")
115
+ url = pr.get("html_url")
116
+ base = pr.get("base", {})
117
+ base_repo_id = base.get("repo", {}).get("id")
118
+ base_ref = base.get("ref")
119
+ base_sha = base.get("sha")
120
+ head = pr.get("head", {})
121
+ head_repo_id = head.get("repo", {}).get("id")
122
+ head_ref = head.get("ref")
123
+ head_sha = head.get("sha")
124
+ merged_at = pr.get("merged_at")
125
+ state = pr.get("state")
126
+ changed_files = pr.get("changed_files")
127
+ if (
128
+ not isinstance(number, int)
129
+ or not isinstance(url, str)
130
+ or not isinstance(base_repo_id, int)
131
+ or not isinstance(base_ref, str)
132
+ or not isinstance(base_sha, str)
133
+ or not isinstance(head_repo_id, int)
134
+ or not isinstance(head_ref, str)
135
+ or not isinstance(head_sha, str)
136
+ or not isinstance(state, str)
137
+ or (merged_at is not None and not isinstance(merged_at, str))
138
+ or (
139
+ changed_files is not None
140
+ and (not isinstance(changed_files, int) or changed_files < 0)
141
+ )
142
+ ):
143
+ raise AcquisitionError(resource, "pull request snapshot fields missing")
144
+ return (
145
+ number,
146
+ url,
147
+ base_repo_id,
148
+ base_ref,
149
+ base_sha,
150
+ head_repo_id,
151
+ head_ref,
152
+ head_sha,
153
+ state,
154
+ merged_at,
155
+ changed_files,
156
+ )
157
+
158
+
159
+ def _pr_collection_snapshot(
160
+ prs: list[dict[str, Any]],
161
+ resource: str,
162
+ ) -> tuple[tuple[Any, ...], ...]:
163
+ return tuple(sorted(_pr_snapshot_key(pr, resource) for pr in prs))
164
+
165
+
99
166
  def _path_in_scope(path: str, scope: list[str]) -> bool:
100
167
  for entry in scope:
101
168
  if entry == "." or path == entry or (entry.endswith("/") and path.startswith(entry)):
@@ -115,7 +182,22 @@ def _scope_diagnostics(
115
182
  pr_url = pr.get("html_url")
116
183
  if not isinstance(number, int) or not isinstance(pr_url, str):
117
184
  raise AcquisitionError(anchor_url, "pull request identity missing")
185
+ listed_key = _pr_snapshot_key(pr, pr_url)
186
+ detail = client.pull_request(owner, repo, number)
187
+ detail_key = _pr_snapshot_key(detail, pr_url)
188
+ if listed_key[:-1] != detail_key[:-1] or (
189
+ listed_key[-1] is not None and listed_key[-1] != detail_key[-1]
190
+ ):
191
+ raise AcquisitionError(pr_url, "pull request changed during acquisition")
192
+ changed_files = detail_key[-1]
193
+ if not isinstance(changed_files, int) or changed_files < 0:
194
+ raise AcquisitionError(pr_url, "pull request changed_files missing")
118
195
  files = client.pull_request_files(owner, repo, number)
196
+ detail_after = client.pull_request(owner, repo, number)
197
+ if _pr_snapshot_key(detail_after, pr_url) != detail_key:
198
+ raise AcquisitionError(pr_url, "pull request changed during file acquisition")
199
+ if len(files) != changed_files:
200
+ raise AcquisitionError(pr_url, "pull request file collection is incomplete")
119
201
  observed_paths: list[str] = []
120
202
  for item in files:
121
203
  if not isinstance(item, dict) or not isinstance(item.get("filename"), str):
@@ -134,26 +216,42 @@ def _scope_diagnostics(
134
216
  return []
135
217
 
136
218
 
137
- def _missing_is_mismatch(error: AcquisitionError) -> bool:
138
- return error.status == 404
219
+ def _github_time(value: object, resource: str, field: str) -> datetime:
220
+ if not isinstance(value, str):
221
+ raise AcquisitionError(resource, f"{field} missing")
222
+ try:
223
+ observed = datetime.fromisoformat(value.replace("Z", "+00:00"))
224
+ except ValueError as error:
225
+ raise AcquisitionError(resource, f"{field} invalid") from error
226
+ if observed.tzinfo is None:
227
+ raise AcquisitionError(resource, f"{field} timezone missing")
228
+ return observed.astimezone(timezone.utc)
229
+
230
+
231
+ def _pr_not_after_start(
232
+ pr: dict[str, Any],
233
+ start: RecordObservation,
234
+ resource: str,
235
+ ) -> bool:
236
+ created_time = _github_time(
237
+ pr.get("created_at"), resource, "pull request created_at"
238
+ )
239
+ start_time = _github_time(
240
+ start.comment.created_at, start.comment.url, "Start created_at"
241
+ )
242
+ return created_time <= start_time
139
243
 
140
244
 
141
245
  def _successor_context(
142
246
  client: GitHubClient,
143
- comments: list[Any],
247
+ refs: list[str],
144
248
  ) -> dict[str, SuccessorFact]:
145
249
  facts: dict[str, SuccessorFact] = {}
146
- for url in successor_refs(comments):
250
+ for url in refs:
147
251
  parsed = parse_github_url(url, "issue")
148
252
  assert parsed is not None
149
253
  repository = client.repository(parsed.owner, parsed.repo)
150
- try:
151
- issue = client.issue(parsed.owner, parsed.repo, parsed.number or 0)
152
- except AcquisitionError as error:
153
- if _missing_is_mismatch(error):
154
- facts[url] = SuccessorFact(url, False, repository_id=repository.get("id"))
155
- continue
156
- raise
254
+ issue = client.issue(parsed.owner, parsed.repo, parsed.number or 0)
157
255
  facts[url] = SuccessorFact(
158
256
  url,
159
257
  "pull_request" not in issue,
@@ -189,28 +287,16 @@ def _live_evidence(
189
287
  if parsed.sha != head_sha:
190
288
  diagnostics.append(_diagnostic("stale_evidence", done.comment.url, url))
191
289
  continue
192
- try:
193
- resource = client.artifact(
194
- parsed.owner,
195
- parsed.repo,
196
- unquote(parsed.path or "", encoding="utf-8", errors="strict"),
197
- head_sha,
198
- )
199
- except AcquisitionError as error:
200
- if _missing_is_mismatch(error):
201
- diagnostics.append(_diagnostic("invalid_evidence", done.comment.url, url))
202
- continue
203
- raise
290
+ resource = client.artifact(
291
+ parsed.owner,
292
+ parsed.repo,
293
+ unquote(parsed.path or "", encoding="utf-8", errors="strict"),
294
+ head_sha,
295
+ )
204
296
  if not isinstance(resource, dict) or resource.get("type") != "file":
205
297
  diagnostics.append(_diagnostic("invalid_evidence", done.comment.url, url))
206
298
  else:
207
- try:
208
- resource = client.check_run(parsed.owner, parsed.repo, parsed.number or 0)
209
- except AcquisitionError as error:
210
- if _missing_is_mismatch(error):
211
- diagnostics.append(_diagnostic("invalid_evidence", done.comment.url, url))
212
- continue
213
- raise
299
+ resource = client.check_run(parsed.owner, parsed.repo, parsed.number or 0)
214
300
  if resource.get("head_sha") != head_sha:
215
301
  diagnostics.append(_diagnostic("stale_evidence", done.comment.url, url))
216
302
  elif resource.get("status") != "completed":
@@ -228,6 +314,7 @@ def _evaluate_done(
228
314
  client: GitHubClient,
229
315
  repo_id: int,
230
316
  branch_name: str,
317
+ start: RecordObservation,
231
318
  contract: RecordObservation,
232
319
  done: RecordObservation,
233
320
  ) -> _DoneLive:
@@ -235,17 +322,13 @@ def _evaluate_done(
235
322
  pr_url = parse_github_url(done.record["pr_ref"], "pr")
236
323
  assert pr_url is not None
237
324
  pr_repo = client.repository(pr_url.owner, pr_url.repo)
238
- try:
239
- pr = client.pull_request(pr_url.owner, pr_url.repo, pr_url.number or 0)
240
- except AcquisitionError as error:
241
- if _missing_is_mismatch(error):
242
- result.diagnostics.append(
243
- _diagnostic("invalid_binding", done.comment.url, done.record["pr_ref"])
244
- )
245
- return result
246
- raise
325
+ pr = client.pull_request(pr_url.owner, pr_url.repo, pr_url.number or 0)
247
326
  result.pr = pr
248
- if not _repo_matches(pr_repo, repo_id) or not _pr_matches(pr, repo_id, branch_name):
327
+ if (
328
+ not _repo_matches(pr_repo, repo_id)
329
+ or not _pr_matches(pr, repo_id, branch_name)
330
+ or _pr_not_after_start(pr, start, done.record["pr_ref"])
331
+ ):
249
332
  result.diagnostics.append(
250
333
  _diagnostic("invalid_binding", done.comment.url, done.record["pr_ref"])
251
334
  )
@@ -276,7 +359,9 @@ def _evaluate_done(
276
359
  client, repo_id, contract, done
277
360
  )
278
361
  pr_after = client.pull_request(pr_url.owner, pr_url.repo, pr_url.number or 0)
279
- if pr_after.get("head", {}).get("sha") != pr.get("head", {}).get("sha"):
362
+ if _pr_snapshot_key(pr_after, done.record["pr_ref"]) != _pr_snapshot_key(
363
+ pr, done.record["pr_ref"]
364
+ ):
280
365
  raise AcquisitionError(done.record["pr_ref"], "bound pull request head changed during acquisition")
281
366
  result.pr = pr_after
282
367
  if scope_diagnostics:
@@ -299,12 +384,20 @@ def _terminal_violations(
299
384
  diagnostics: list[Diagnostic],
300
385
  recognized_comments: list[Any],
301
386
  terminal_at: str,
302
- terminal_record: RecordObservation,
387
+ safe_retry_urls: set[str],
303
388
  ) -> list[Diagnostic]:
304
- result = list(diagnostics)
305
- terminal_aliases = set(terminal_record.alias_urls)
389
+ violating_urls = {
390
+ comment.url
391
+ for comment in recognized_comments
392
+ if comment.created_at > terminal_at and comment.url not in safe_retry_urls
393
+ }
394
+ result = [
395
+ diagnostic
396
+ for diagnostic in diagnostics
397
+ if not any(url in violating_urls for url in diagnostic.urls)
398
+ ]
306
399
  for comment in recognized_comments:
307
- if comment.created_at > terminal_at and comment.url not in terminal_aliases:
400
+ if comment.url in violating_urls:
308
401
  item = _diagnostic("terminal_violation", comment.url)
309
402
  if item not in result:
310
403
  result.append(item)
@@ -338,16 +431,65 @@ def _stop_diagnostics(
338
431
  if start is None:
339
432
  return diagnostics
340
433
  branch_name = start.record["branch"]
341
- candidates = [
342
- pr
343
- for pr in client.pull_requests(owner, repo, branch_name)
344
- if _pr_matches(pr, repo_id, branch_name) and isinstance(pr.get("merged_at"), str)
345
- ]
346
- for pr in candidates:
347
- diagnostics.append(_diagnostic("terminal_violation", stop.comment.url, pr["html_url"]))
434
+ resource = f"https://api.github.com/repos/{owner}/{repo}/pulls"
435
+ candidates = client.pull_requests(owner, repo, branch_name)
436
+ candidates_after = client.pull_requests(owner, repo, branch_name)
437
+ if _pr_collection_snapshot(candidates, resource) != _pr_collection_snapshot(
438
+ candidates_after, resource
439
+ ):
440
+ raise AcquisitionError(resource, "pull request collection changed during acquisition")
441
+ for pr in candidates_after:
442
+ if not _pr_matches(pr, repo_id, branch_name):
443
+ continue
444
+ pr_url = pr.get("html_url")
445
+ if not isinstance(pr_url, str):
446
+ raise AcquisitionError(stop.comment.url, "pull request ordering fields missing")
447
+ created_time = _github_time(
448
+ pr.get("created_at"), pr_url, "pull request created_at"
449
+ )
450
+ stop_time = _github_time(
451
+ stop.comment.created_at, stop.comment.url, "Stop created_at"
452
+ )
453
+ if created_time == stop_time:
454
+ raise AcquisitionError(
455
+ pr_url, "pull request creation and Stop ordering is ambiguous"
456
+ )
457
+ if _pr_not_after_start(pr, start, pr_url):
458
+ continue
459
+ if created_time > stop_time:
460
+ continue
461
+ merged_at = pr.get("merged_at")
462
+ if not isinstance(merged_at, str):
463
+ continue
464
+ merged_time = _github_time(merged_at, pr_url, "pull request merged_at")
465
+ if merged_time == stop_time:
466
+ raise AcquisitionError(pr_url, "merge and Stop ordering is ambiguous")
467
+ if merged_time > stop_time:
468
+ diagnostics.append(
469
+ _diagnostic("terminal_violation", stop.comment.url, pr_url)
470
+ )
348
471
  return diagnostics
349
472
 
350
473
 
474
+ def _has_terminal_violation(diagnostics: list[Diagnostic]) -> bool:
475
+ return any(item.token == "terminal_violation" for item in diagnostics)
476
+
477
+
478
+ def _done_can_be_evaluated(
479
+ contract: RecordObservation,
480
+ done: RecordObservation,
481
+ ) -> bool:
482
+ expected = contract.record["done_conditions"]
483
+ actual = done.record["evidence"]
484
+ if set(expected) != set(actual):
485
+ return False
486
+ return all(
487
+ parse_github_url(actual[condition_id], condition["evidence_kind"])
488
+ is not None
489
+ for condition_id, condition in expected.items()
490
+ )
491
+
492
+
351
493
  def _evaluate_acquired(
352
494
  client: GitHubClient,
353
495
  issue_url: str,
@@ -380,9 +522,16 @@ def _evaluate_acquired(
380
522
  issue_id=issue.get("id"),
381
523
  issue_created_at=issue.get("created_at"),
382
524
  repository_id=repo_id,
383
- successors=_successor_context(client, comments),
384
525
  )
385
526
  fold = fold_comments(comments, context)
527
+ if fold.terminal_stop is not None and not _has_terminal_violation(
528
+ fold.diagnostics
529
+ ):
530
+ stop_record = fold.terminal_stop.record
531
+ if stop_record["reason"] == "superseded":
532
+ context.successors = _successor_context(
533
+ client, [stop_record["successor_ref"]]
534
+ )
386
535
  except IncompleteSnapshotError as error:
387
536
  return _acquisition(issue_url, AcquisitionError(issue_url, str(error)))
388
537
  except AcquisitionError as error:
@@ -392,6 +541,7 @@ def _evaluate_acquired(
392
541
  fold.active["contract"][0] if len(fold.active["contract"]) == 1 else None
393
542
  )
394
543
  active_done = fold.active["done"][0] if len(fold.active["done"]) == 1 else None
544
+ terminal_done = fold.active["done"][0] if fold.active["done"] else None
395
545
  current: dict[str, Any] = {
396
546
  "contract": _record_projection(active_contract),
397
547
  "start": _record_projection(fold.bound_start),
@@ -403,6 +553,8 @@ def _evaluate_acquired(
403
553
  return StatusResult(issue_url, base_state, list(fold.diagnostics), current)
404
554
  if fold.bound_start is None or fold.bound_contract is None:
405
555
  if fold.terminal_stop is not None:
556
+ if _has_terminal_violation(fold.diagnostics):
557
+ return StatusResult(issue_url, "halt", list(fold.diagnostics), current)
406
558
  try:
407
559
  stop_diagnostics = _stop_diagnostics(
408
560
  client,
@@ -425,7 +577,41 @@ def _evaluate_acquired(
425
577
  branch_name = fold.bound_start.record["branch"]
426
578
  current["branch"] = {"name": branch_name}
427
579
  if fold.terminal_stop is not None:
580
+ if _has_terminal_violation(fold.diagnostics):
581
+ return StatusResult(issue_url, "halt", list(fold.diagnostics), current)
428
582
  try:
583
+ if terminal_done is not None and _done_can_be_evaluated(
584
+ fold.bound_contract, terminal_done
585
+ ):
586
+ live_done = _evaluate_done(
587
+ client,
588
+ repo_id,
589
+ branch_name,
590
+ fold.bound_start,
591
+ fold.bound_contract,
592
+ terminal_done,
593
+ )
594
+ if live_done.terminal_at == fold.terminal_stop.comment.created_at:
595
+ raise AcquisitionError(
596
+ terminal_done.record["pr_ref"],
597
+ "Done terminal and Stop ordering is ambiguous",
598
+ )
599
+ if (
600
+ live_done.terminal_at is not None
601
+ and live_done.terminal_at < fold.terminal_stop.comment.created_at
602
+ ):
603
+ return StatusResult(
604
+ issue_url,
605
+ "halt",
606
+ [
607
+ _diagnostic(
608
+ "terminal_violation",
609
+ fold.terminal_stop.comment.url,
610
+ terminal_done.record["pr_ref"],
611
+ )
612
+ ],
613
+ current,
614
+ )
429
615
  stop_diagnostics = _stop_diagnostics(
430
616
  client, parsed.owner, parsed.repo, repo_id, fold.terminal_stop, fold.bound_start, context
431
617
  )
@@ -435,25 +621,88 @@ def _evaluate_acquired(
435
621
  return StatusResult(issue_url, "halt", [*fold.diagnostics, *stop_diagnostics], current)
436
622
  return StatusResult(issue_url, "stopped", list(fold.diagnostics), current)
437
623
 
438
- if fold.diagnostics:
439
- return StatusResult(issue_url, "halt", list(fold.diagnostics), current)
624
+ default_branch = repository.get("default_branch")
625
+ if not isinstance(default_branch, str):
626
+ return _acquisition(
627
+ issue_url,
628
+ AcquisitionError(issue_url, "repository default_branch missing"),
629
+ )
630
+ if branch_name == default_branch:
631
+ return StatusResult(
632
+ issue_url,
633
+ "halt",
634
+ [_diagnostic("invalid_binding", fold.bound_start.comment.url)],
635
+ current,
636
+ )
440
637
 
441
638
  try:
442
- if active_done is not None:
443
- live = _evaluate_done(client, repo_id, branch_name, fold.bound_contract, active_done)
639
+ if terminal_done is not None and _done_can_be_evaluated(
640
+ fold.bound_contract, terminal_done
641
+ ):
642
+ live = _evaluate_done(
643
+ client,
644
+ repo_id,
645
+ branch_name,
646
+ fold.bound_start,
647
+ fold.bound_contract,
648
+ terminal_done,
649
+ )
444
650
  if live.pr is not None:
445
651
  current["bound_pr"] = live.pr.get("html_url")
446
652
  current["bound_pr_head_sha"] = live.pr.get("head", {}).get("sha")
447
653
  if live.diagnostics:
448
- return StatusResult(issue_url, "halt", live.diagnostics, current)
654
+ return StatusResult(
655
+ issue_url,
656
+ "halt",
657
+ [*fold.diagnostics, *live.diagnostics],
658
+ current,
659
+ )
449
660
  if live.terminal_at is not None:
661
+ safe_retry_urls = {
662
+ url
663
+ for observations in fold.ids.values()
664
+ for observation in observations
665
+ if len(observation.alias_urls) > 1
666
+ for url in observation.alias_urls
667
+ }
450
668
  diagnostics = _terminal_violations(
451
- fold.diagnostics, fold.recognized_comments, live.terminal_at, active_done
669
+ fold.diagnostics,
670
+ fold.recognized_comments,
671
+ live.terminal_at,
672
+ safe_retry_urls,
452
673
  )
453
- return StatusResult(issue_url, "done", diagnostics, current)
674
+ return StatusResult(
675
+ issue_url,
676
+ "halt" if diagnostics else "done",
677
+ diagnostics,
678
+ current,
679
+ )
680
+ if fold.diagnostics:
681
+ return StatusResult(issue_url, "halt", list(fold.diagnostics), current)
454
682
  if live.pr and live.pr.get("merged_at"):
455
683
  return StatusResult(issue_url, "in_progress", [], current)
456
684
  branch = client.branch(parsed.owner, parsed.repo, branch_name)
685
+ branch_after = client.branch(parsed.owner, parsed.repo, branch_name)
686
+ branch_resource = (
687
+ f"https://api.github.com/repos/{parsed.owner}/{parsed.repo}/branches/"
688
+ f"{branch_name}"
689
+ )
690
+ if _branch_snapshot_key(branch, branch_resource) != _branch_snapshot_key(
691
+ branch_after, branch_resource
692
+ ):
693
+ raise AcquisitionError(
694
+ branch_resource, "bound branch changed during acquisition"
695
+ )
696
+ branch_key = _branch_snapshot_key(branch_after, branch_resource)
697
+ pr_head = live.pr.get("head", {}).get("sha") if live.pr else None
698
+ if (
699
+ branch_key is None
700
+ or branch_key[1] != pr_head
701
+ or pr_head != terminal_done.record["head_sha"]
702
+ ):
703
+ raise AcquisitionError(
704
+ branch_resource, "bound branch and pull request head disagree"
705
+ )
457
706
  current["branch"]["exists"] = branch is not None
458
707
  if branch is None:
459
708
  return StatusResult(
@@ -464,10 +713,16 @@ def _evaluate_acquired(
464
713
  )
465
714
  return StatusResult(issue_url, "in_progress", [], current)
466
715
 
716
+ if fold.diagnostics:
717
+ return StatusResult(issue_url, "halt", list(fold.diagnostics), current)
718
+
467
719
  branch = client.branch(parsed.owner, parsed.repo, branch_name)
468
720
  observed_candidates = client.pull_requests(parsed.owner, parsed.repo, branch_name)
469
721
  invalid_candidates = [
470
- pr for pr in observed_candidates if not _pr_matches(pr, repo_id, branch_name)
722
+ pr
723
+ for pr in observed_candidates
724
+ if not _pr_matches(pr, repo_id, branch_name)
725
+ or _pr_not_after_start(pr, fold.bound_start, pr.get("html_url", issue_url))
471
726
  ]
472
727
  if invalid_candidates:
473
728
  urls = [fold.bound_start.comment.url] + [
@@ -503,6 +758,23 @@ def _evaluate_acquired(
503
758
  )
504
759
  if scope_diagnostics:
505
760
  return StatusResult(issue_url, "halt", scope_diagnostics, current)
761
+ branch_after = client.branch(parsed.owner, parsed.repo, branch_name)
762
+ candidates_after = client.pull_requests(parsed.owner, parsed.repo, branch_name)
763
+ branch_resource = (
764
+ f"https://api.github.com/repos/{parsed.owner}/{parsed.repo}/branches/"
765
+ f"{branch_name}"
766
+ )
767
+ pulls_resource = (
768
+ f"https://api.github.com/repos/{parsed.owner}/{parsed.repo}/pulls"
769
+ )
770
+ if _branch_snapshot_key(branch, branch_resource) != _branch_snapshot_key(
771
+ branch_after, branch_resource
772
+ ) or _pr_collection_snapshot(
773
+ observed_candidates, pulls_resource
774
+ ) != _pr_collection_snapshot(candidates_after, pulls_resource):
775
+ raise AcquisitionError(
776
+ issue_url, "branch or pull request collection changed during acquisition"
777
+ )
506
778
  if branch is None:
507
779
  return StatusResult(
508
780
  issue_url,
@@ -524,6 +796,21 @@ def _issue_snapshot_key(issue: dict[str, Any]) -> tuple[int, str, str]:
524
796
  return issue_id, created_at, updated_at
525
797
 
526
798
 
799
+ def _repository_snapshot_key(
800
+ repository: dict[str, Any], resource: str
801
+ ) -> tuple[int, str, str]:
802
+ repo_id = repository.get("id")
803
+ full_name = repository.get("full_name")
804
+ default_branch = repository.get("default_branch")
805
+ if (
806
+ not isinstance(repo_id, int)
807
+ or not isinstance(full_name, str)
808
+ or not isinstance(default_branch, str)
809
+ ):
810
+ raise AcquisitionError(resource, "repository snapshot fields missing")
811
+ return repo_id, full_name, default_branch
812
+
813
+
527
814
  def evaluate_issue(client: GitHubClient, issue_url: str) -> StatusResult:
528
815
  parsed = parse_github_url(issue_url, "issue")
529
816
  if parsed is None:
@@ -534,6 +821,12 @@ def evaluate_issue(client: GitHubClient, issue_url: str) -> StatusResult:
534
821
  )
535
822
  try:
536
823
  repository = client.repository(parsed.owner, parsed.repo)
824
+ repository_resource = (
825
+ f"https://api.github.com/repos/{parsed.owner}/{parsed.repo}"
826
+ )
827
+ repository_key = _repository_snapshot_key(
828
+ repository, repository_resource
829
+ )
537
830
  issue = client.issue(parsed.owner, parsed.repo, parsed.number or 0)
538
831
  initial_key = _issue_snapshot_key(issue)
539
832
  comments = client.comments(parsed.owner, parsed.repo, parsed.number or 0)
@@ -544,6 +837,13 @@ def evaluate_issue(client: GitHubClient, issue_url: str) -> StatusResult:
544
837
  if result.state is None:
545
838
  return result
546
839
  try:
840
+ repository_after = client.repository(parsed.owner, parsed.repo)
841
+ if _repository_snapshot_key(
842
+ repository_after, repository_resource
843
+ ) != repository_key:
844
+ raise AcquisitionError(
845
+ repository_resource, "repository snapshot changed during acquisition"
846
+ )
547
847
  issue_after = client.issue(parsed.owner, parsed.repo, parsed.number or 0)
548
848
  if _issue_snapshot_key(issue_after) != initial_key:
549
849
  raise AcquisitionError(issue_url, "issue snapshot changed during acquisition")
@@ -1,88 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: github-task-protocol
3
- Version: 1.0.1
4
- Summary: Reconstruct AI coding task state from GitHub Issue records
5
- Requires-Python: >=3.11
6
- License-Expression: MIT
7
- License-File: LICENSE
8
- Project-URL: Repository, https://github.com/shinya0x00/github-task-protocol
9
- Description-Content-Type: text/markdown
10
-
11
- # GitHub Task Protocol
12
-
13
- GTPは、AIへ実装を任せても、人間が目的・変更範囲・現在地・根拠・未確認事項を理解し、停止・再開・やり直し・mergeの判断を手放さないための小さなprotocolです。
14
-
15
- > 作業はAIに任せる。判断は手放さない。
16
-
17
- AIの説明だけを信じるのではなく、GitHub Issue上のRecordと、実際のbranch・PR・commit・Check Runからtask stateを再構成します。GTP自身は変更、完了、mergeの権限を与えません。
18
-
19
- ## 導入は3手順
20
-
21
- 1. [`GTP.md`](GTP.md)を導入先repositoryのrootへコピーする。
22
- 2. 下の共通adapter文を、agentが必ず読む文書へ1段落追加する。
23
- 3. taskごとにGitHub Issueを1件作り、agentへそのIssue URLを渡す。
24
-
25
- 共通adapter文:
26
-
27
- > このrepositoryはrootの`GTP.md`をtask protocolの唯一の正本とする。GitHub Issue URLを受け取ったら、Issue commentをServer Orderで読み、4 Record、6 state、7 halt reasonに従って既存branch・PR・次のprotocol actionを再構成する。Recordを推測、編集、独自拡張せず、矛盾時は原因URLを示して止まり、取得不能はhaltと混同しない。GTPの表示やRecordは変更・完了・mergeの権限を与えない。
28
-
29
- 配置例は次のとおりです。runtimeごとに異なる指示を作る必要はありません。
30
-
31
- - Codex: `AGENTS.md`
32
- - Claude Code: `CLAUDE.md`または`AGENTS.md`
33
- - Cursor: `AGENTS.md`または`.cursor/rules/gtp.md`
34
-
35
- ## 4つのRecord
36
-
37
- | Record | 平易な意味 |
38
- |---|---|
39
- | `contract` | 目的、変更してよい範囲、完了条件を固定する |
40
- | `start` | Contractと唯一の作業branchを結び付ける |
41
- | `done` | PRのsource headと、条件ごとのEvidenceを提示する |
42
- | `stop` | 完了を主張せず中止し、必要なら後継Issueを示す |
43
-
44
- RecordはIssue commentへ人向け要約を先に、機械用JSONを折りたたんで記録します。1 Issue = 1 branch = 1 PRです。
45
-
46
- ## 6つのstate
47
-
48
- | state | 平易な意味 |
49
- |---|---|
50
- | `unmanaged` | 有効なContractがない |
51
- | `ready` | ContractはあるがStart前 |
52
- | `in_progress` | 作業中、またはDone提示後のmerge待ち |
53
- | `halt` | 特定transitionを矛盾や不適合のため進められない |
54
- | `done` | Doneのsource headとEvidenceを持つPRがnative mergeされた |
55
- | `stopped` | Stopにより、このIssueでの作業を終了した |
56
-
57
- GitHub情報を完全に取得できない場合はstateを推測しません。これは`halt`ではなくAcquisition Errorです。
58
-
59
- ## CLIは任意の検証器
60
-
61
- 人間がGTPを使うためにCLIをinstallする必要はありません。`gtp`はagentや自動検査がRecordと現在stateを確認するための、runtime dependency 0の任意toolです。
62
-
63
- CLIはPyPI公開後、固定versionを指定して実行できます。GTPを使うだけならCLIのinstallは不要です。
64
-
65
- ```console
66
- uvx --from github-task-protocol==1.0.1 gtp status <issue-url>
67
- uvx --from github-task-protocol==1.0.1 gtp check <comment.md>
68
- ```
69
-
70
- - `status`はGitHubへGETだけを行い、日本語6項目の後にmachine JSONを出します。
71
- - `check`は投稿前のMarkdown comment全文をoffline検査します。Issue上でもvalidだとは主張しません。
72
- - exit code、緑色のCheck Run、Evidence URLは、変更やmergeの許可ではありません。
73
-
74
- ## 仕様と判断記録
75
-
76
- protocolの唯一の正本は400行以内の[`GTP.md`](GTP.md)です。Record作成やstate判断に、他の文書は必要ありません。
77
-
78
- [`DECISIONS.md`](DECISIONS.md)は、設計変更の理由と履歴です。`GTP.md`と意味が衝突する場合は`GTP.md`を優先します。
79
-
80
- 実GitHubで観測した引き継ぎ結果は[`acceptance/level0/`](acceptance/level0/)にあります。これは仕様の代わりではありません。
81
-
82
- ## GTPが証明しないこと
83
-
84
- GTPは、actor本人性、credential安全性、コード品質そのもの、Evidence内容の真実性を証明しません。filesystem削除や本番database操作を物理的に防ぐものでもありません。
85
-
86
- サンドボックス、最小権限、不可逆操作前の確認、reviewと組み合わせてください。最終的な受理は、人間がPRとEvidenceを読み、GitHubのnative mergeで判断します。
87
-
88
- License: [MIT](LICENSE)
@@ -1,16 +0,0 @@
1
- gtp/__init__.py,sha256=p2yfQm17fjTB7nHssAmxA-IfIMg5gIUIfUsc6HA5gwc,58
2
- gtp/__main__.py,sha256=D31U8_ux95qF64EQ8ReT25nabzxh7ped5avpobXz_bM,49
3
- gtp/carrier.py,sha256=K4Wi1BGlFZ87VuRYa6CYC-oOuGnEPsFw5-2EkDWAbno,4039
4
- gtp/cli.py,sha256=OSmVwjr9GiBqBUY64i2VpFV9putlCKGCrfeL_40laUU,1721
5
- gtp/github.py,sha256=zK4HZ-yATvhoN03j91RYnVZLWaHJT4VyhUVCK3I-75w,8591
6
- gtp/model.py,sha256=0CnS7k0bMPn3Bw4voZGT-Jgvez65JX6X_hpCeeyyl4Y,2443
7
- gtp/presentation.py,sha256=F50zagAcedp4xyRIuAS-CKu2s6JXrarhAglnPiT_uzM,17347
8
- gtp/reducer.py,sha256=YuEq9ylDfkuYGLEa2YEM9PaUOrhxWY3HnU_p5rMK8G0,6997
9
- gtp/schema.py,sha256=Jjltq80E8HjaUBPWoQJ_YwRM-Lkm85rCaHuv10sB0Sg,7768
10
- gtp/status.py,sha256=PlubWkx6WB1uVuf5cgSklVjVXlrqoViSHoks7m6v8LA,21434
11
- gtp/urls.py,sha256=CxheRZ4ND__RMZx-cejSMqk5tCu6aaoin6g3Q_RK2i8,3332
12
- github_task_protocol-1.0.1.dist-info/METADATA,sha256=yxHr1cvH9lYHwvHr4F6RsxzpdCE7KvzgF27N4bUW0fQ,4687
13
- github_task_protocol-1.0.1.dist-info/WHEEL,sha256=5CJ4bbElW8cq_GDpNSLjMgfb15S72ImTOLq9c70uj1Q,106
14
- github_task_protocol-1.0.1.dist-info/entry_points.txt,sha256=ULDQmE66x4_NxwQH1Ru9cEVOJMEUBvT27g2a5FcDrAo,37
15
- github_task_protocol-1.0.1.dist-info/licenses/LICENSE,sha256=yvK8Xx5r58kF13JNYhXfeRf9IAY9rm-jS3THRU0ttJY,1067
16
- github_task_protocol-1.0.1.dist-info/RECORD,,