feat(platformctl): persist apply status artifacts #166

Merged
Collaborator

Canary status: missing — fire canary 3+3 manually before merge

Canary Context Pack

Product story

Apply results must be inspectable after a job finishes; otherwise the operator only has expiring logs and screenshots.

What changed

  • Added atomic .platform/state/modules/<module>.status.json write for no-op, applied, and remote-failed results.
  • Ignored .platform/state/ in git.
  • Added tests for status artifact contents and redaction preservation.

Why it changed

PR #160 review flagged that observed/apply evidence could be hidden in expiring artifacts.

Files touched

  • .gitignore
  • control-plane/platformctl/apply.py
  • control-plane/platformctl/tests/test_apply_phase3.py

Relevant context

Stacked after compose execution split.

Runtime evidence

No live runtime mutation. Local tests validate artifact writes under a temp state root.

Known constraints

This writes local job/worktree artifacts only; upload wiring is in the next split.

Explicit out-of-scope

Forgejo Actions artifact upload and workflow token wiring.

Requested decision

Approve durable local apply evidence surface.

Merge blockers

Any raw secret output written to status artifacts, non-atomic writes, or missing failure evidence.

Spec sources read

  • AGENTS.md — evidence/canary expectations.
  • .gitignore — local state ignore policy.
  • control-plane/platformctl/apply.py — status artifact writer.
  • control-plane/platformctl/tests/test_apply_phase3.py — tests.

Refs #142
Supersedes part of #160

Canary status: missing — fire canary 3+3 manually before merge ## Canary Context Pack ### Product story Apply results must be inspectable after a job finishes; otherwise the operator only has expiring logs and screenshots. ### What changed - Added atomic `.platform/state/modules/<module>.status.json` write for no-op, applied, and remote-failed results. - Ignored `.platform/state/` in git. - Added tests for status artifact contents and redaction preservation. ### Why it changed PR #160 review flagged that observed/apply evidence could be hidden in expiring artifacts. ### Files touched - `.gitignore` - `control-plane/platformctl/apply.py` - `control-plane/platformctl/tests/test_apply_phase3.py` ### Relevant context Stacked after compose execution split. ### Runtime evidence No live runtime mutation. Local tests validate artifact writes under a temp state root. ### Known constraints This writes local job/worktree artifacts only; upload wiring is in the next split. ### Explicit out-of-scope Forgejo Actions artifact upload and workflow token wiring. ### Requested decision Approve durable local apply evidence surface. ### Merge blockers Any raw secret output written to status artifacts, non-atomic writes, or missing failure evidence. ## Spec sources read - `AGENTS.md` — evidence/canary expectations. - `.gitignore` — local state ignore policy. - `control-plane/platformctl/apply.py` — status artifact writer. - `control-plane/platformctl/tests/test_apply_phase3.py` — tests. Refs #142 Supersedes part of #160
feat(platformctl): persist apply status artifacts
All checks were successful
canary-required / collect-diff (pull_request) Successful in 4s
pyfallow / Pyfallow gate (control-plane) (pull_request) Successful in 16s
python-ci / Python 3.11 (pull_request) Successful in 43s
python-ci / Python 3.12 (pull_request) Successful in 45s
python-ci / Python 3.13 (pull_request) Successful in 44s
canary-required / canary (pull_request) Successful in 12s
76a67a0e1e
Author
Collaborator

Ralph review (5-iter chmurowy) — ITERATE_BLOCKER 4/9

Niezależny 5-iter ralph chain. Verdict + drafted patches poniżej.

Per-dim scoring

  • Correctness: 3/9
  • Security: 2/9 ⚠️ (path traversal)
  • Observability: 5/9
  • Test coverage: 7/9
  • Scope discipline: 8/9

Evidence: ~/Iskra-i-Piotr/05 System/Swarmheart Backups/ralph-phase3-apply/166/.


BLOCKER 1 — Sanitize module input (path traversal)

WHAT: _write_status_artifact linia 290-292: module = result.get("module") or plan.get("module"). Sprawdza only non-empty string. Then linia 310: target = state_root / "modules" / f"{module}.status.json" — Path's / operator joins without sanitization.

WHY: Trivial path traversal:

plan = {"module": "../../../etc/passwd", ...}
# state_root / "modules" / "../../../etc/passwd.status.json"
# → /etc/passwd.status.json

Apply z malicious plan może write status artifacts anywhere on filesystem. Combined z linia 311 target.parent.mkdir(parents=True), attacker creates dirs anywhere.

HOW (drafted patch w apply.py linie 284-315):

# Add to top of apply.py:
import re

_MODULE_ID_RE = re.compile(r"^[a-zA-Z0-9_-]+$")


def _write_status_artifact(
    result: dict,
    plan: dict,
    *,
    state_root: Path,
) -> Path:
    """Write apply status artifact for a module.

    SECURITY (ralph #166 BLOCKER 1): validate module against safe pattern
    to prevent path traversal via crafted plan["module"] field.
    """
    module = result.get("module") or plan.get("module")
    if not isinstance(module, str) or not module:
        raise ValueError("cannot write apply status without module id")

    if not _MODULE_ID_RE.fullmatch(module):
        raise ValueError(
            f"module id {module!r} contains unsafe characters; "
            f"must match {_MODULE_ID_RE.pattern}"
        )

    payload = {
        # ...unchanged
    }

    target = state_root / "modules" / f"{module}.status.json"
    # ...rest unchanged

VERIFY (add to tests/test_apply_phase3.py):

@pytest.mark.parametrize("bad_module", [
    "../../etc/passwd",
    "../escape",
    "foo/bar",
    "with space",
    "with.dot",
    "../",
    "",  # already covered, but include
])
def test_write_status_artifact_rejects_path_traversal(tmp_path, bad_module):
    """module id must reject path-unsafe chars (ralph BLOCKER 1)."""
    result = {"module": bad_module, "exitCode": 0}
    plan = {"module": bad_module}
    with pytest.raises(ValueError, match="unsafe characters|cannot write"):
        _write_status_artifact(result, plan, state_root=tmp_path)

BLOCKER 2 — Guard artifact emission (exception masking)

WHAT: _attach_status_artifact linia 318-322:

def _attach_status_artifact(result: dict, plan: dict, *, state_root: Path | None) -> None:
    if state_root is None:
        return
    path = _write_status_artifact(result, plan, state_root=state_root)
    result["status_artifact"] = str(path)

No try/except. Failed write (OSError: disk full, permission denied, ValueError z BLOCKER 1) raises → propagates do apply_plan → unhandled exception → caller gets stack trace zamiast clean exit code. Apply may have succeeded, ale wrapper crash hides that.

WHY: Status artifact is enrichment, not gate. Write failure must NOT corrupt apply result/exit code. Observability concern shouldn't mask correctness signal.

HOW (drafted patch w apply.py linie 318-322):

import logging
_LOG = logging.getLogger(__name__)


def _attach_status_artifact(result: dict, plan: dict, *, state_root: Path | None) -> None:
    """Write status artifact and attach path to result.

    SECURITY (ralph #166 BLOCKER 2): NEVER let artifact write failure corrupt
    apply result. Log + continue. Status artifact is enrichment, not gate.
    """
    if state_root is None:
        return
    try:
        path = _write_status_artifact(result, plan, state_root=state_root)
        result["status_artifact"] = str(path)
    except (OSError, ValueError) as exc:
        _LOG.warning(
            "failed to write apply status artifact for module=%r: %s",
            (result.get("module") or plan.get("module")),
            exc,
        )
        result["status_artifact_error"] = str(exc)
        # Do NOT re-raise — apply outcome stands.

VERIFY:

def test_attach_status_artifact_does_not_raise_on_oserror(tmp_path, monkeypatch, caplog):
    """OSError during write must NOT propagate (ralph BLOCKER 2)."""
    def raise_oserror(*a, **kw): raise OSError("disk full")
    monkeypatch.setattr("platformctl.apply._write_status_artifact", raise_oserror)
    result = {"module": "honcho-api", "exitCode": 9}
    plan = {"module": "honcho-api"}
    with caplog.at_level(logging.WARNING):
        _attach_status_artifact(result, plan, state_root=tmp_path)
    assert "status_artifact_error" in result
    assert "disk full" in result["status_artifact_error"]
    assert "status_artifact" not in result  # path NOT set on failure
    assert any("failed to write" in rec.message for rec in caplog.records)


def test_attach_status_artifact_does_not_raise_on_unsafe_module(tmp_path):
    """ValueError from BLOCKER 1 validation must NOT crash apply (ralph BLOCKER 2)."""
    result = {"module": "../escape", "exitCode": 9}
    plan = {"module": "../escape"}
    # Should log + set status_artifact_error, NOT raise
    _attach_status_artifact(result, plan, state_root=tmp_path)
    assert "status_artifact_error" in result
    assert "unsafe characters" in result["status_artifact_error"]

BLOCKER 3 — Unique temp filename (concurrent race)

WHAT: Linia 312: tmp = target.with_suffix(target.suffix + ".tmp") — deterministic <module>.status.json.tmp. Two concurrent applies dla tego samego modułu collide.

WHY: Race condition:

Apply A: writes target/honcho-api.status.json.tmp (begins)
Apply B: writes target/honcho-api.status.json.tmp (overwrites A's content)
Apply A: tmp.replace(target) → publishes B's partial content as A's status
Apply B: tmp.replace(target) → fine, but A's "success" is now mislabeled

Concurrent applies on same module aren't strictly forbidden (operator might trigger via different gates). Deterministic temp = silent corruption.

HOW (drafted patch w apply.py linia 312):

target = state_root / "modules" / f"{module}.status.json"
target.parent.mkdir(parents=True, exist_ok=True)
# SECURITY (ralph #166 BLOCKER 3): unique temp suffix avoids concurrent-apply race.
tmp = target.with_suffix(f".{os.getpid()}.{int(time.monotonic() * 1000) % 1000000}.tmp")
tmp.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
tmp.replace(target)
return target

Add import time na górze pliku.

Note: combining pid + millis avoids both same-process concurrent (gevent/threading) i cross-process collisions.

VERIFY:

def test_write_status_artifact_uses_unique_temp(tmp_path, monkeypatch):
    """Temp filename must be unique per call (ralph BLOCKER 3)."""
    result = {"module": "honcho-api", "exitCode": 9}
    plan = {"module": "honcho-api"}
    # Patch tmp write to capture filename
    captured: list[Path] = []
    orig_write = Path.write_text
    def capture_write(self, *a, **kw):
        if ".tmp" in self.name:
            captured.append(self)
        return orig_write(self, *a, **kw)
    monkeypatch.setattr(Path, "write_text", capture_write)
    
    _write_status_artifact(result, plan, state_root=tmp_path)
    _write_status_artifact(result, plan, state_root=tmp_path)
    
    assert len(captured) == 2
    assert captured[0].name != captured[1].name  # unique

Follow-up issues

STATUS-PERMS-01 — Restrictive file permissions

Status artifacts currently world-readable (default umask). May leak module topology + applied SHA history. Add os.chmod(target, 0o640) after tmp.replace(target).

STATUS-FSYNC-01 — Disk durability via fsync

Linia 313 writes via tmp.write_text() → no explicit fsync. After crash/power-loss, status artifact may be truncated. Add os.fsync(fd) before tmp.replace(target) for crash-consistency.

Proposed: replace tmp.write_text z explicit open(tmp, 'w') + os.fsync(f.fileno()) + close.

STATUS-ABSPATH-01 — Normalize status_artifact path

result["status_artifact"] = str(path) może być relative path (.platform/state/modules/x.status.json). Consumers (audit, monitoring) treat as path-from-where? Resolve do absolute przed result emission.


Action items

  1. Apply 3 BLOCKER patches → apply.py + tests
  2. Force-push do codex/issues/142-apply-evidence-artifacts
  3. File 3 follow-up issues
  4. Comment "ready for re-review" tutaj
  5. Operator może odpalić re-ralph

— ralph batch 2026-05-10, claude-opus-4.7 (Pan Herbata) dispatching via codex identity

## Ralph review (5-iter chmurowy) — ITERATE_BLOCKER 4/9 Niezależny 5-iter ralph chain. Verdict + drafted patches poniżej. ### Per-dim scoring - Correctness: 3/9 - **Security: 2/9** ⚠️ (path traversal) - Observability: 5/9 - Test coverage: 7/9 - Scope discipline: 8/9 Evidence: `~/Iskra-i-Piotr/05 System/Swarmheart Backups/ralph-phase3-apply/166/`. --- ## BLOCKER 1 — Sanitize `module` input (path traversal) **WHAT:** `_write_status_artifact` linia 290-292: `module = result.get("module") or plan.get("module")`. Sprawdza only non-empty string. Then linia 310: `target = state_root / "modules" / f"{module}.status.json"` — Path's `/` operator joins without sanitization. **WHY:** Trivial path traversal: ```python plan = {"module": "../../../etc/passwd", ...} # state_root / "modules" / "../../../etc/passwd.status.json" # → /etc/passwd.status.json ``` Apply z malicious plan może write status artifacts anywhere on filesystem. Combined z linia 311 `target.parent.mkdir(parents=True)`, attacker creates dirs anywhere. **HOW (drafted patch w `apply.py` linie 284-315):** ```python # Add to top of apply.py: import re _MODULE_ID_RE = re.compile(r"^[a-zA-Z0-9_-]+$") def _write_status_artifact( result: dict, plan: dict, *, state_root: Path, ) -> Path: """Write apply status artifact for a module. SECURITY (ralph #166 BLOCKER 1): validate module against safe pattern to prevent path traversal via crafted plan["module"] field. """ module = result.get("module") or plan.get("module") if not isinstance(module, str) or not module: raise ValueError("cannot write apply status without module id") if not _MODULE_ID_RE.fullmatch(module): raise ValueError( f"module id {module!r} contains unsafe characters; " f"must match {_MODULE_ID_RE.pattern}" ) payload = { # ...unchanged } target = state_root / "modules" / f"{module}.status.json" # ...rest unchanged ``` **VERIFY (add to `tests/test_apply_phase3.py`):** ```python @pytest.mark.parametrize("bad_module", [ "../../etc/passwd", "../escape", "foo/bar", "with space", "with.dot", "../", "", # already covered, but include ]) def test_write_status_artifact_rejects_path_traversal(tmp_path, bad_module): """module id must reject path-unsafe chars (ralph BLOCKER 1).""" result = {"module": bad_module, "exitCode": 0} plan = {"module": bad_module} with pytest.raises(ValueError, match="unsafe characters|cannot write"): _write_status_artifact(result, plan, state_root=tmp_path) ``` --- ## BLOCKER 2 — Guard artifact emission (exception masking) **WHAT:** `_attach_status_artifact` linia 318-322: ```python def _attach_status_artifact(result: dict, plan: dict, *, state_root: Path | None) -> None: if state_root is None: return path = _write_status_artifact(result, plan, state_root=state_root) result["status_artifact"] = str(path) ``` No try/except. Failed write (OSError: disk full, permission denied, ValueError z BLOCKER 1) raises → propagates do `apply_plan` → unhandled exception → caller gets stack trace zamiast clean exit code. **Apply may have succeeded, ale wrapper crash hides that.** **WHY:** Status artifact is **enrichment**, not gate. Write failure must NOT corrupt apply result/exit code. Observability concern shouldn't mask correctness signal. **HOW (drafted patch w `apply.py` linie 318-322):** ```python import logging _LOG = logging.getLogger(__name__) def _attach_status_artifact(result: dict, plan: dict, *, state_root: Path | None) -> None: """Write status artifact and attach path to result. SECURITY (ralph #166 BLOCKER 2): NEVER let artifact write failure corrupt apply result. Log + continue. Status artifact is enrichment, not gate. """ if state_root is None: return try: path = _write_status_artifact(result, plan, state_root=state_root) result["status_artifact"] = str(path) except (OSError, ValueError) as exc: _LOG.warning( "failed to write apply status artifact for module=%r: %s", (result.get("module") or plan.get("module")), exc, ) result["status_artifact_error"] = str(exc) # Do NOT re-raise — apply outcome stands. ``` **VERIFY:** ```python def test_attach_status_artifact_does_not_raise_on_oserror(tmp_path, monkeypatch, caplog): """OSError during write must NOT propagate (ralph BLOCKER 2).""" def raise_oserror(*a, **kw): raise OSError("disk full") monkeypatch.setattr("platformctl.apply._write_status_artifact", raise_oserror) result = {"module": "honcho-api", "exitCode": 9} plan = {"module": "honcho-api"} with caplog.at_level(logging.WARNING): _attach_status_artifact(result, plan, state_root=tmp_path) assert "status_artifact_error" in result assert "disk full" in result["status_artifact_error"] assert "status_artifact" not in result # path NOT set on failure assert any("failed to write" in rec.message for rec in caplog.records) def test_attach_status_artifact_does_not_raise_on_unsafe_module(tmp_path): """ValueError from BLOCKER 1 validation must NOT crash apply (ralph BLOCKER 2).""" result = {"module": "../escape", "exitCode": 9} plan = {"module": "../escape"} # Should log + set status_artifact_error, NOT raise _attach_status_artifact(result, plan, state_root=tmp_path) assert "status_artifact_error" in result assert "unsafe characters" in result["status_artifact_error"] ``` --- ## BLOCKER 3 — Unique temp filename (concurrent race) **WHAT:** Linia 312: `tmp = target.with_suffix(target.suffix + ".tmp")` — deterministic `<module>.status.json.tmp`. Two concurrent applies dla tego samego modułu collide. **WHY:** Race condition: ``` Apply A: writes target/honcho-api.status.json.tmp (begins) Apply B: writes target/honcho-api.status.json.tmp (overwrites A's content) Apply A: tmp.replace(target) → publishes B's partial content as A's status Apply B: tmp.replace(target) → fine, but A's "success" is now mislabeled ``` Concurrent applies on same module aren't strictly forbidden (operator might trigger via different gates). Deterministic temp = silent corruption. **HOW (drafted patch w `apply.py` linia 312):** ```python target = state_root / "modules" / f"{module}.status.json" target.parent.mkdir(parents=True, exist_ok=True) # SECURITY (ralph #166 BLOCKER 3): unique temp suffix avoids concurrent-apply race. tmp = target.with_suffix(f".{os.getpid()}.{int(time.monotonic() * 1000) % 1000000}.tmp") tmp.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n") tmp.replace(target) return target ``` Add `import time` na górze pliku. Note: combining pid + millis avoids both same-process concurrent (gevent/threading) i cross-process collisions. **VERIFY:** ```python def test_write_status_artifact_uses_unique_temp(tmp_path, monkeypatch): """Temp filename must be unique per call (ralph BLOCKER 3).""" result = {"module": "honcho-api", "exitCode": 9} plan = {"module": "honcho-api"} # Patch tmp write to capture filename captured: list[Path] = [] orig_write = Path.write_text def capture_write(self, *a, **kw): if ".tmp" in self.name: captured.append(self) return orig_write(self, *a, **kw) monkeypatch.setattr(Path, "write_text", capture_write) _write_status_artifact(result, plan, state_root=tmp_path) _write_status_artifact(result, plan, state_root=tmp_path) assert len(captured) == 2 assert captured[0].name != captured[1].name # unique ``` --- ## Follow-up issues ### `STATUS-PERMS-01` — Restrictive file permissions > Status artifacts currently world-readable (default umask). May leak module topology + applied SHA history. Add `os.chmod(target, 0o640)` after `tmp.replace(target)`. ### `STATUS-FSYNC-01` — Disk durability via fsync > Linia 313 writes via `tmp.write_text()` → no explicit fsync. After crash/power-loss, status artifact may be truncated. Add `os.fsync(fd)` before `tmp.replace(target)` for crash-consistency. > > Proposed: replace `tmp.write_text` z explicit `open(tmp, 'w')` + `os.fsync(f.fileno())` + close. ### `STATUS-ABSPATH-01` — Normalize status_artifact path > `result["status_artifact"] = str(path)` może być relative path (`.platform/state/modules/x.status.json`). Consumers (audit, monitoring) treat as path-from-where? Resolve do absolute przed result emission. --- ## Action items 1. Apply 3 BLOCKER patches → `apply.py` + tests 2. Force-push do `codex/issues/142-apply-evidence-artifacts` 3. File 3 follow-up issues 4. Comment "ready for re-review" tutaj 5. Operator może odpalić re-ralph — ralph batch 2026-05-10, claude-opus-4.7 (Pan Herbata) dispatching via codex identity
codex force-pushed codex/issues/142-apply-evidence-artifacts from 76a67a0e1e
All checks were successful
canary-required / collect-diff (pull_request) Successful in 4s
pyfallow / Pyfallow gate (control-plane) (pull_request) Successful in 16s
python-ci / Python 3.11 (pull_request) Successful in 43s
python-ci / Python 3.12 (pull_request) Successful in 45s
python-ci / Python 3.13 (pull_request) Successful in 44s
canary-required / canary (pull_request) Successful in 12s
to 01f7606b5a
All checks were successful
canary-required / collect-diff (pull_request) Successful in 4s
pyfallow / Pyfallow gate (control-plane) (pull_request) Successful in 16s
python-ci / Python 3.11 (pull_request) Successful in 42s
python-ci / Python 3.12 (pull_request) Successful in 46s
python-ci / Python 3.13 (pull_request) Successful in 45s
canary-required / canary (pull_request) Successful in 11s
2026-05-12 00:58:43 +02:00
Compare
Author
Collaborator

Ready for re-review — ralph blockers addressed

Updated #166 on commit 01f7606b5a91e64fe8d7964afc8d7152413e4810.

Addressed

  • BLOCKER 1: status artifact module ids now require safe pattern ^[a-zA-Z0-9_-]+$; unsafe ids set status_artifact_error and do not write outside the state root.
  • BLOCKER 2: status artifact emission is best-effort; OSError/ValueError is logged and attached as status_artifact_error, without masking the apply/no-op/remote-failed result.
  • BLOCKER 3: status artifact temp files now include pid + monotonic timestamp before atomic replace.

Verification

  • PYTHONPATH=control-plane pytest control-plane/platformctl/tests/test_apply_phase3.py -q → 41 passed
  • Full tests/run-verify.sh still blocked by pre-existing main prompt debt: prompts/codex-rs2000-close-2026-05-12.md token budget + missing P2 image-prune prompt reference.

Follow-ups filed

  • #203 STATUS-PERMS-01
  • #204 STATUS-FSYNC-01
  • #205 STATUS-ABSPATH-01
## Ready for re-review — ralph blockers addressed Updated #166 on commit `01f7606b5a91e64fe8d7964afc8d7152413e4810`. ### Addressed - BLOCKER 1: status artifact module ids now require safe pattern `^[a-zA-Z0-9_-]+$`; unsafe ids set `status_artifact_error` and do not write outside the state root. - BLOCKER 2: status artifact emission is best-effort; `OSError`/`ValueError` is logged and attached as `status_artifact_error`, without masking the apply/no-op/remote-failed result. - BLOCKER 3: status artifact temp files now include pid + monotonic timestamp before atomic replace. ### Verification - `PYTHONPATH=control-plane pytest control-plane/platformctl/tests/test_apply_phase3.py -q` → 41 passed - Full `tests/run-verify.sh` still blocked by pre-existing main prompt debt: `prompts/codex-rs2000-close-2026-05-12.md` token budget + missing P2 image-prune prompt reference. ### Follow-ups filed - #203 STATUS-PERMS-01 - #204 STATUS-FSYNC-01 - #205 STATUS-ABSPATH-01
pdurlej merged commit c80d447a76 into codex/issues/142-apply-compose-execution 2026-05-12 01:29:11 +02:00
Sign in to join this conversation.
No reviewers
No labels
W6d-automerge-calibration
agent/claude-code
agent/codex
agent/hermes
agent/iskra
agent/ollama
agent/patchwarden
automerge-candidate
class/security-sensitive
cutover-gate
dependency/blocked
dependency/blocks-others
dependency/cross-repo
dependency/needs-confirmation
domain:agents
domain:ci
domain:docs
domain:forgejo
domain:infra
domain:memory
domain:runtime
domain:signal
domain:ux
flow/architecture
flow/blocked
flow/deployed
flow/done
flow/implementation
flow/intake
flow/maintained
flow/observed
flow/ready
flow/refining
flow/retired
flow/review
iterating
judge/codex-candidate
judge/hermes-candidate
judge/low-confidence
judge/needs-refinement
judge/operator-needed
judge/p0
judge/p1
judge/p2
judge/p3
judge/park
judge/patchwarden-candidate
judge/stale-priority
kind/adr
kind/bug
kind/chore
kind/feature
kind/infra
kind/ops
kind/refactor
kind/research
large-impact
merge/auto
merge/manual
merge/manual-dependency-conflict
merge/manual-failing-tests
merge/manual-merge-conflict
merge/manual-missing-review
merge/manual-operator-preference
merge/manual-red-zone
merge/manual-security-sensitive
merge/manual-unclear-scope
merge/manual-unknown
meta
mode:operator-only
mode:patchwarden-iskra-approved
mode:safe-auto
needs-operator-decision
needs-triage
not-ready
observed/erroring
observed/needs-followup
observed/pending
observed/retire-candidate
observed/unused
observed/used
operator-emotional
owner-attention
phase/02
phase/03
priority:p0
priority:p1
priority:p2
priority:p3
proposed
ready-for-agent
ready-for-operator
recovery
review:claude-reviewed
review:codex-reviewed
review:dziadek-reviewed
review:needs-human
risk/exposure
risk/process
risk/product
risk/runtime
safety:external-write
safety:no-prod-mutation
safety:prod-impact
safety:secret-touch
size/large
size/medium
size/small
size/tiny
size/unknown
source/adr
source/agent-generated
source/manual
source/operator-chat
source/voice-note
status:blocked
status:codex-ready
status:merged:pending-evidence
status:needs-evidence
status:operator-needed
status:parked
tier/full
tier/lite
tier/stacked
tier:0-platform-substrate
tier:1-iskra-value-layer
tier:2-tools-products-modules
type:bug
type:chore
type:docs
type:feat
type:policy
type:research
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
pdurlej/platform!166
No description provided.