Skip to content

Implement the job state machine in memory - #13

Merged
monthop-gmail merged 2 commits into
mainfrom
feat/job-state-machine
Aug 18, 2026
Merged

Implement the job state machine in memory#13
monthop-gmail merged 2 commits into
mainfrom
feat/job-state-machine

Conversation

@monthop-gmail

Copy link
Copy Markdown
Owner

Closes the engine half of #2code ก้อนแรกของ repo นี้

235 passed · coverage 100% (gate 90%)

Problem statement

packages/* มีแต่ README บรรทัดเดียวมาตลอด · spec ที่ merge ไป 4 ฉบับ (RFC-0001 · 0006 · 0007 · 0008) ยังไม่เคยถูกทดสอบด้วย code สักบรรทัด และ conformance.status ของเราติด unknown เพราะไม่มี payload ให้ validate

⚠️ 2 จุดที่ issue #2 ไม่ตรงกับ repo แล้ว — ยึด repo

issue #2 (เขียน ก.พ.) สถานะจริง
"All 10 states implemented" 13 states — RFC-0007 เพิ่ม AWAITING_APPROVAL CANCELLED TIMED_OUT เมื่อ ส.ค.
ไม่มี tenant Job บังคับ tenant_id + workspace_id + principal ตาม RFC-0006

packages/core/state-machine.md เป็น spec ที่ยึด · acceptance criteria ข้ออื่นของ issue ครบหมด

🐍 stack decision — Python

repo ไม่มี stack decision อยู่เลยสักที่ (grep แล้วไม่เจอ) นี่คือครั้งแรก · เลือก Python เพราะ:

  • ecosystem เป็น Python ทั้งหมด — agent-platform/conformance/drift_check.py และ care-agent-platform (Python/FastAPI)
  • Phase 1: Implement Audit & Event Log #6 ต้อง validate payload กับ JSON Schema และ payload_check.py ของ care-agent-platform เป็น reference ที่รันผ่านจริงแล้วให้ลอกโครง ไม่ต้องออกแบบเอง
  • ไม่มี runtime dependency เลยCORE_BOUNDARY.md ห้าม framework ที่ผูก vendor ใน v0.x และ lifecycle engine ไม่ต้องใช้

packages/ เป็นชื่อแบบ Node monorepo แต่ไม่มีอะไรผูกไว้ · ถ้าอยากให้เป็นทางการเป็น RFC สั้น ๆ ทีหลังได้

โครง

packages/core/devfactory_core/
├── states.py     13 states · TRANSITIONS · TERMINAL/FAILABLE/TIMEOUTABLE/APPROVAL_PAUSABLE
├── identity.py   Principal · Id pattern ของ identity/v1 · DEFAULT_TENANT
├── events.py     Event → event/v1 wire shape (frozen)
├── errors.py     9 refusal ที่บอกเหตุผลเฉพาะเจาะจง
└── job.py        engine + guards

ตารางเดินทางอยู่ที่เดียว — ไม่มี edge ไหน hard-code นอก states.py · test_states.py ตรวจโครงตาราง (ทุก state reachable · terminal ไม่มีทางออก · APPROVED เป็นประตูเดียวเข้า execution)

Guards — สาระของ PR นี้

การปฏิเสธทุกครั้งไม่แตะ job และไม่เขียน log เลย — log ที่บันทึกความพยายามที่ถูกปฏิเสธไม่ใช่ audit trail

เรียกอะไร ปฏิเสธด้วย
edge ที่ไม่มีในตาราง InvalidTransitionบอกด้วยว่าตอนนั้นไปไหนได้บ้าง
อะไรก็ตามออกจาก terminal TerminalState — ชี้ไปที่ supersede()
TASK_PLANNING ก่อน APPROVED InvalidTransition + backstop ExecutionBeforeApproval
FAILED/CANCELLED/TIMED_OUT ไม่มี reason MissingReason (ช่องว่างล้วนไม่นับ)
CANCELLED ไม่มี principal MissingPrincipal
APPROVED/REJECTED ไม่มี authority + reason MissingAuthority
pause นอก IN_PROGRESS/VALIDATING/DEPLOYABLE MissingApprovalContext
resume ผิดที่ WrongResumeState
id ผิดรูป InvalidIdentifier (identity/v1 Id)

3 จุดที่ implement แล้วเห็นว่าสำคัญกว่าที่คิด

1. FAILED terminal · recovery = supersede() คืน job ใหม่ที่ DRAFT พร้อม supersedes_job_id — ปลุกตัวเดิมคือการเดินต่อบน APPROVED ที่ออกให้แผนซึ่งพังไปแล้ว

2. REJECTED ล้าง approval เดิม — approval ที่ออกให้ revision ก่อนต้องไม่อนุมัติ revision ใหม่ · ไม่ได้อยู่ใน spec ตรง ๆ แต่ตามมาจาก "APPROVED requires explicit decision"

3. awaiting_from บันทึกตอนเข้า — job ที่ค้างตอน DEPLOYABLE resume เป็น IN_PROGRESS ไม่ได้ · ถ้าไม่มี field นี้จะเสียตำแหน่งใน lifecycle เงียบ ๆ

Events — ของที่ #6 จะเอาไป validate

JOB_CREATED ตอนสร้าง · STATE_TRANSITION ทุก transition · JOB_COMPLETED ตอนจบ

ไม่มีทางเปลี่ยน state โดยไม่ผ่าน transition() ซึ่งเป็นสิ่งที่ทำให้ no silent state change เป็นจริง ไม่ใช่แค่เขียนไว้

event_payloads() render เป็น event/v1 wire 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_APPROVAL

PR นี้อนุญาตจาก 5 state ที่มีงานให้ล้ม (TASK_PLANNING IN_PROGRESS AWAITING_APPROVAL VALIDATING DEPLOYABLE) และปฏิเสธก่อน APPROVED เพราะยังไม่มีอะไรรัน — ผลลัพธ์ที่ซื่อสัตย์ตรงนั้นคือ REJECTED CANCELLED หรือ TIMED_OUT

ต้องมี RFC ยืนยัน · บันทึกไว้ 3 ที่: states.FAILABLE · docstring ของ job.py · README

นี่คือสิ่งที่เราคาดไว้ว่าการเขียน code จะทำให้เจอ

Tests

ไฟล์ ตรวจอะไร
test_states.py โครงตาราง · reachability · APPROVED เป็นประตูเดียว
test_transitions.py ทุก edge ที่ถูกต้องเดินได้ · ทุก non-edge ถูกปฏิเสธ (parametrize เต็ม matrix)
test_guards.py guard ทีละตัว + tenant scope + recovery
test_events.py append-only · payload shape · refuse ไม่เขียน log

clock เป็น fixture — ordering assertion ไม่ขึ้นกับเวลาจริง

Impacted planes

Control Plane เท่านั้น · orchestration/execution/observability ยังไม่แตะ (#5 #6 #7)

Risk analysis

  • FAILABLE เดาไว้เอง (medium) → บันทึกเป็น open question 3 ที่ · ถ้า RFC ตัดสินต่าง แก้ที่ states.py จุดเดียว
  • Python เป็นการตัดสินใจที่ทุกอย่างสืบทอด (medium) → CORE_BOUNDARY.md อนุญาต skeleton/test logic โดยไม่ต้องมี RFC และห้ามเฉพาะ vendor framework ซึ่งเราไม่มี dependency เลย
  • event_payloads() ยังไม่เคยถูก validate จริง (medium) → ตั้งใจ · เป็นงานของ Phase 1: Implement Audit & Event Log #6 และเป็นเหตุผลที่ payload ถูกทำให้ตรง shape ตั้งแต่ตอนนี้
  • in-memory ล้วน (low) → ตาม out-of-scope ของ issue

Related

#2 · #5 #6 #7 (Phase 1 ที่เหลือ) · ADR-0006 ข้อ 3 (release gate — workflow นี้คือเมล็ดของมัน)

🤖 Generated with Claude Code

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>
@monthop-gmail monthop-gmail added the enhancement New feature or request label Aug 18, 2026
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>
@monthop-gmail
monthop-gmail merged commit 07eb251 into main Aug 18, 2026
3 checks passed
@monthop-gmail
monthop-gmail deleted the feat/job-state-machine branch August 18, 2026 16:11
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant