Implement the job state machine in memory - #13
Merged
Conversation
Closes the engine half of issue #2: thirteen states, the transition table, the guards on it, an append-only audit trail, and 235 tests at 100% coverage against a 90% gate. Two things in the issue no longer match the repository, and the repository wins. The issue says ten states and was written in February; RFC-0007 added AWAITING_APPROVAL, CANCELLED, and TIMED_OUT in August, so packages/core/state-machine.md is the spec this follows. The issue also predates the tenant model, so Job requires tenant_id, workspace_id, and a creating principal per RFC-0006. No stack decision existed anywhere in the repo, so this is the first one. Python: the rest of the ecosystem is Python, and issue #6 needs to validate payloads against JSON Schema, where care-agent-platform's payload_check.py is a working reference to follow rather than a design to invent. No runtime dependencies - CORE_BOUNDARY.md forbids vendor-coupled frameworks in v0.x and a lifecycle engine does not need one. The guards are the point. Every refusal leaves the job untouched and writes nothing, because a log that records attempts nobody accepted is not an audit trail. FAILED stays terminal and recovery goes through supersede(), which returns a fresh job at DRAFT carrying supersedes_job_id - resuming the old one would continue under an APPROVED granted to a plan that has since failed. Rejection clears a prior approval for the same reason. AWAITING_APPROVAL records awaiting_from on entry so a job paused during DEPLOYABLE cannot resume as IN_PROGRESS. Events are shaped to event/v1 but not validated here; owning a copy of the schema would be the parallel schema RFC-0005 Rule 4 forbids. event_payloads() is what issue #6 will hand to a validator, and every payload carries job_id, a subject, and tenant scope - RFC-0008 says job_id is optional in the schema and not optional in our behaviour. Writing this surfaced one spec gap. state-machine.md never says which states may reach FAILED beyond AWAITING_APPROVAL. This permits it from the five states where work exists to fail and refuses it before APPROVED, where the honest outcomes are REJECTED, CANCELLED, or TIMED_OUT. That reading needs an RFC to confirm; it is recorded in states.FAILABLE, the job module docstring, and the package README. Also adds the test workflow, which is the seed of the release gate ADR-0006 requires of a consumer, and a .gitignore now that the repo has code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
setuptools refuses a license file= path outside the package directory, and LICENSE lives at the repository root, so the editable install failed on all three Python versions before a single test ran. Reproduced the CI step locally in a clean venv rather than guessing at it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 18, 2026
monthop-gmail
added a commit
that referenced
this pull request
Aug 19, 2026
…st (#14) (#16) * RFC-0010: ระบุว่า state ไหนไป FAILED ได้ + ประกาศ transition ลง manifest PR #13 ส่ง states.FAILABLE ขึ้น main พร้อมหมายเหตุว่ายังต้องยืนยันด้วย RFC ซึ่ง CONTRIBUTING.md บังคับสำหรับการเปลี่ยน lifecycle — RFC-0010 ยืนยัน สิ่งที่โค้ดเลือกไว้แล้วโดยไม่แก้พฤติกรรม พร้อมกันนี้ contract-semantics.yaml ประกาศ job_state_machine ให้ agent-platform อ้างอิงได้ แต่มีแค่ states/terminal/invariants/layering ไม่มี transition เลยสักอย่าง ทั้งที่ states.py ใช้สี่อย่างสร้างตาราง สามในสี่มี RFC-0001/0007 รองรับอยู่แล้ว ขาดแค่ยังไม่ได้บันทึก จึงลงให้ครบ รอบเดียว: progression, awaiting_from, failable, timeoutable, cancellable RFC บันทึกสองข้อที่การ implement ทำให้เห็นและยังไม่ตัดสินในนี้ - GOVERNANCE_ANALYSIS ที่ล้มแบบ error ลงเอยที่ TIMED_OUT — ยอมรับโดยตั้งใจ เพื่อให้ FAILED คงความหมายว่า "งานที่อนุมัติแล้วไม่สำเร็จ" - APPROVED ไม่อยู่ทั้งใน FAILABLE และ TIMEOUTABLE จึงค้างได้โดยไม่มี ทางออกอัตโนมัติ — เสนอให้เพิ่มเข้า TIMEOUTABLE ใน issue แยก ไม่ bump semantics_version เพราะ frozen scope ไม่ถูกแตะ และการ bump จะทำให้ drift check ของ agent-platform แดงทันทีจนกว่าจะ re-pin ยืนยันแล้วว่า manifest ตรงกับ states.py ทุกชุด · pytest 302 passed Closes #14 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * RFC-0010: บันทึกคำตอบของสอง Open Question + cancellable เป็น list Decision 2 — ยืนตามเดิมว่า GOVERNANCE_ANALYSIS ที่ error ลงเอยที่ TIMED_OUT แต่เพิ่มข้อกำหนดว่า reason ต้องแยก sla_exceeded / analysis_error ให้ได้ ไม่งั้น audit อ่านไม่ออกว่าช้าหรือพัง · และย้ำว่า retry เป็นของ orchestration ตาม RFC-0004 + RFC-0007 D1 — TIMED_OUT จึงเป็นคำตอบที่ถูกด้วยความหมาย ไม่ใช่เพราะเหลือตัวเลือกเดียว Open Question APPROVED — เคาะแล้วว่าควรเพิ่มเข้า TIMEOUTABLE เหตุผลหลักคือ การอนุมัติต้องมีวันหมดอายุ ซึ่งตรงกับที่ D1 ห้ามชุบชีวิต job ที่ FAILED (stale APPROVED) · เป็นการเปลี่ยนพฤติกรรมและเป็นกฎของ RFC-0007 จึงแยกไปทำที่ #17 ไม่ทำใน PR นี้ cancellable เปลี่ยนจาก prose เป็น list ให้ parse ได้เหมือน failable/timeoutable ตามที่ทีม agent-platform ติงไว้ · ตรวจแล้วว่าตรงกับ states ลบ terminal pytest 302 passed Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: monthop-gmail <phirapong@icbsolution.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the engine half of #2 — code ก้อนแรกของ repo นี้
Problem statement
packages/*มีแต่ README บรรทัดเดียวมาตลอด · spec ที่ merge ไป 4 ฉบับ (RFC-0001 · 0006 · 0007 · 0008) ยังไม่เคยถูกทดสอบด้วย code สักบรรทัด และconformance.statusของเราติดunknownเพราะไม่มี payload ให้ validateAWAITING_APPROVALCANCELLEDTIMED_OUTเมื่อ ส.ค.Jobบังคับtenant_id+workspace_id+principalตาม RFC-0006packages/core/state-machine.mdเป็น spec ที่ยึด · acceptance criteria ข้ออื่นของ issue ครบหมด🐍 stack decision — Python
repo ไม่มี stack decision อยู่เลยสักที่ (grep แล้วไม่เจอ) นี่คือครั้งแรก · เลือก Python เพราะ:
agent-platform/conformance/drift_check.pyและcare-agent-platform(Python/FastAPI)payload_check.pyของcare-agent-platformเป็น reference ที่รันผ่านจริงแล้วให้ลอกโครง ไม่ต้องออกแบบเองCORE_BOUNDARY.mdห้าม framework ที่ผูก vendor ใน v0.x และ lifecycle engine ไม่ต้องใช้packages/เป็นชื่อแบบ Node monorepo แต่ไม่มีอะไรผูกไว้ · ถ้าอยากให้เป็นทางการเป็น RFC สั้น ๆ ทีหลังได้โครง
ตารางเดินทางอยู่ที่เดียว — ไม่มี edge ไหน hard-code นอก
states.py·test_states.pyตรวจโครงตาราง (ทุก state reachable · terminal ไม่มีทางออก ·APPROVEDเป็นประตูเดียวเข้า execution)Guards — สาระของ PR นี้
การปฏิเสธทุกครั้งไม่แตะ job และไม่เขียน log เลย — log ที่บันทึกความพยายามที่ถูกปฏิเสธไม่ใช่ audit trail
InvalidTransition— บอกด้วยว่าตอนนั้นไปไหนได้บ้างTerminalState— ชี้ไปที่supersede()TASK_PLANNINGก่อนAPPROVEDInvalidTransition+ backstopExecutionBeforeApprovalFAILED/CANCELLED/TIMED_OUTไม่มี reasonMissingReason(ช่องว่างล้วนไม่นับ)CANCELLEDไม่มี principalMissingPrincipalAPPROVED/REJECTEDไม่มี authority + reasonMissingAuthorityIN_PROGRESS/VALIDATING/DEPLOYABLEMissingApprovalContextWrongResumeStateInvalidIdentifier(identity/v1Id)3 จุดที่ implement แล้วเห็นว่าสำคัญกว่าที่คิด
1.
FAILEDterminal · recovery =supersede()คืน job ใหม่ที่DRAFTพร้อมsupersedes_job_id— ปลุกตัวเดิมคือการเดินต่อบนAPPROVEDที่ออกให้แผนซึ่งพังไปแล้ว2.
REJECTEDล้าง approval เดิม — approval ที่ออกให้ revision ก่อนต้องไม่อนุมัติ revision ใหม่ · ไม่ได้อยู่ใน spec ตรง ๆ แต่ตามมาจาก "APPROVED requires explicit decision"3.
awaiting_fromบันทึกตอนเข้า — job ที่ค้างตอนDEPLOYABLEresume เป็นIN_PROGRESSไม่ได้ · ถ้าไม่มี field นี้จะเสียตำแหน่งใน lifecycle เงียบ ๆEvents — ของที่ #6 จะเอาไป validate
JOB_CREATEDตอนสร้าง ·STATE_TRANSITIONทุก transition ·JOB_COMPLETEDตอนจบไม่มีทางเปลี่ยน state โดยไม่ผ่าน
transition()ซึ่งเป็นสิ่งที่ทำให้ no silent state change เป็นจริง ไม่ใช่แค่เขียนไว้event_payloads()render เป็นevent/v1wire shape · ไม่ validate ที่นี่ — ถือ schema copy เองคือ parallel schema ที่ RFC-0005 Rule 4 ห้าม · payload พวกนี้คือสิ่งที่ #6 จะส่งให้ validatorทุก payload มี
job_idเสมอ ตาม RFC-0008 (optional ใน schema ไม่ optional ในพฤติกรรม) ·reasonที่ไม่มีคือ ไม่มี key ไม่ใช่ empty string — RFC-0008 ห้ามแต่งค่าให้ field ผ่าน🔍 เจอช่องว่างใน spec 1 จุด
state-machine.mdไม่เคยระบุว่า state ไหนไปFAILEDได้บ้าง นอกจากAWAITING_APPROVALPR นี้อนุญาตจาก 5 state ที่มีงานให้ล้ม (
TASK_PLANNINGIN_PROGRESSAWAITING_APPROVALVALIDATINGDEPLOYABLE) และปฏิเสธก่อนAPPROVEDเพราะยังไม่มีอะไรรัน — ผลลัพธ์ที่ซื่อสัตย์ตรงนั้นคือREJECTEDCANCELLEDหรือTIMED_OUTต้องมี RFC ยืนยัน · บันทึกไว้ 3 ที่:
states.FAILABLE· docstring ของjob.py· READMEนี่คือสิ่งที่เราคาดไว้ว่าการเขียน code จะทำให้เจอ
Tests
test_states.pyAPPROVEDเป็นประตูเดียวtest_transitions.pytest_guards.pytest_events.pyclock เป็น fixture — ordering assertion ไม่ขึ้นกับเวลาจริง
Impacted planes
Control Plane เท่านั้น · orchestration/execution/observability ยังไม่แตะ (#5 #6 #7)
Risk analysis
FAILABLEเดาไว้เอง (medium) → บันทึกเป็น open question 3 ที่ · ถ้า RFC ตัดสินต่าง แก้ที่states.pyจุดเดียวCORE_BOUNDARY.mdอนุญาต skeleton/test logic โดยไม่ต้องมี RFC และห้ามเฉพาะ vendor framework ซึ่งเราไม่มี dependency เลยevent_payloads()ยังไม่เคยถูก validate จริง (medium) → ตั้งใจ · เป็นงานของ Phase 1: Implement Audit & Event Log #6 และเป็นเหตุผลที่ payload ถูกทำให้ตรง shape ตั้งแต่ตอนนี้Related
#2 · #5 #6 #7 (Phase 1 ที่เหลือ) · ADR-0006 ข้อ 3 (release gate — workflow นี้คือเมล็ดของมัน)
🤖 Generated with Claude Code