Skip to content

[codex] Add decision and thread planning modules#1

Draft
Fly-Carrot wants to merge 1 commit into
mainfrom
codex/thread-plan-ledger-hardening
Draft

[codex] Add decision and thread planning modules#1
Fly-Carrot wants to merge 1 commit into
mainfrom
codex/thread-plan-ledger-hardening

Conversation

@Fly-Carrot
Copy link
Copy Markdown
Owner

Summary

  • add Decision Graph as an optional public decision-summary module
  • add Thread Plan Ledger as a chat-level, append-only natural-language planning module
  • harden the release by fixing duplicated Thread Plan HTML headings
  • replace stale branded legacy mount detection with a generic pre-OS legacy marker

Bug Fix Evidence

  • reproduced Thread Plan HTML duplicate headings before the fix: h1_count=3
  • verified the same reproduction after the fix: h1_count=2
  • removed stale branded legacy marker from final staged file content

Validation

  • python3 -B -m py_compile knowledgeos/cli.py
  • targeted Thread Plan and harness-audit tests
  • make smoke
  • git diff --check
  • staged added-lines and final file-content scans for local paths, token patterns, and stale markers

Notes

HTML remains presentation-only. Markdown, YAML, and NDJSON remain the canonical KnowledgeOS evidence formats.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces several optional modules to KnowledgeOS, including a Decision Graph module for auditable decision summaries, a chat-level Thread Plan Ledger, and a Mission Flow reporting layer with Mermaid diagrams. It also adds support for local external-write policy overlays to handle runtime migrations safely. The review feedback focuses on hardening the implementation in knowledgeos/cli.py against potential crashes and validation bypasses. Specifically, the reviewer identified several places where null values in YAML or NDJSON files could propagate as None or evaluate to the string 'None', bypassing empty checks or causing TypeError and AttributeError exceptions. Additionally, defensive checks were suggested to prevent crashes when local policy sections are omitted or when task lookups return no results.

Comment thread knowledgeos/cli.py
Comment on lines +1052 to +1080
external_forbidden_match = matches_any(
absolute_value,
local_external_policy["external_forbidden_without_human_gate"],
)
if external_forbidden_match:
return {
"decision": "human_gate_required",
"reason": f"matches external_forbidden_without_human_gate pattern {external_forbidden_match}",
"path": absolute_value,
"inside_project": False,
}

external_controlled_match = matches_any(
absolute_value,
local_external_policy["external_controlled"],
)
if external_controlled_match:
receipt_match = matches_any(
absolute_value,
local_external_policy["external_require_receipt_for"],
)
return {
"decision": "allow",
"reason": f"matches external_controlled pattern {external_controlled_match}",
"path": absolute_value,
"inside_project": False,
"receipt_required": bool(receipt_match),
"receipt_pattern": receipt_match,
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If the local external write policy file (.knowledgeos-local/write-policy.local.yaml) does not define all sections (e.g., if external_forbidden_without_human_gate or external_require_receipt_for is omitted), direct subscript access will raise a KeyError and crash the command. Using .get(..., []) provides a safe fallback to an empty list.

        external_forbidden_match = matches_any(
            absolute_value,
            local_external_policy.get("external_forbidden_without_human_gate", []),
        )
        if external_forbidden_match:
            return {
                "decision": "human_gate_required",
                "reason": f"matches external_forbidden_without_human_gate pattern {external_forbidden_match}",
                "path": absolute_value,
                "inside_project": False,
            }

        external_controlled_match = matches_any(
            absolute_value,
            local_external_policy.get("external_controlled", []),
        )
        if external_controlled_match:
            receipt_match = matches_any(
                absolute_value,
                local_external_policy.get("external_require_receipt_for", []),
            )
            return {
                "decision": "allow",
                "reason": f"matches external_controlled pattern {external_controlled_match}",
                "path": absolute_value,
                "inside_project": False,
                "receipt_required": bool(receipt_match),
                "receipt_pattern": receipt_match,
            }

Comment thread knowledgeos/cli.py
return {
"exists": True,
"strictness": (scalars.get("strictness") or "warn").lower(),
"downgrade_reason": scalars.get("downgrade_reason", ""),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If downgrade_reason is explicitly set to null or left empty in the YAML file, scalars.get("downgrade_reason", "") will return None. This propagates a None value to policy, which can cause issues or bypass validation checks. Using scalars.get("downgrade_reason") or "" ensures it always defaults to an empty string.

Suggested change
"downgrade_reason": scalars.get("downgrade_reason", ""),
"downgrade_reason": scalars.get("downgrade_reason") or "",

Comment thread knowledgeos/cli.py
Comment on lines +4339 to +4341
for field in ["title", "summary", "reason", "evidence"]:
if not str(event.get(field, "")).strip():
errors.append({"label": f"decision_missing_{field}", "detail": decision_id})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If any of the validated fields are explicitly set to null in the JSON lines file, event.get(field, "") returns None. str(None) evaluates to "None", which is truthy and bypasses the empty check. Using event.get(field) or "" ensures that both missing and null values are correctly treated as empty strings.

Suggested change
for field in ["title", "summary", "reason", "evidence"]:
if not str(event.get(field, "")).strip():
errors.append({"label": f"decision_missing_{field}", "detail": decision_id})
for field in ["title", "summary", "reason", "evidence"]:
if not str(event.get(field) or "").strip():
errors.append({"label": f"decision_missing_{field}", "detail": decision_id})

Comment thread knowledgeos/cli.py
errors.append({"label": "invalid_decision_kind", "detail": kind})
if status not in DECISION_EVENT_STATUSES:
errors.append({"label": "invalid_decision_status", "detail": status})
if (kind in DECISION_EXPLANATION_REQUIRED_KINDS or status in {"abandoned", "rolled_back", "superseded"}) and not str(event.get("reason", "")).strip():
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If reason is explicitly set to null in the JSON lines file, str(event.get("reason", "")) evaluates to "None", which bypasses the empty check. Using event.get("reason") or "" ensures that both missing and null values are correctly treated as empty strings.

Suggested change
if (kind in DECISION_EXPLANATION_REQUIRED_KINDS or status in {"abandoned", "rolled_back", "superseded"}) and not str(event.get("reason", "")).strip():
if (kind in DECISION_EXPLANATION_REQUIRED_KINDS or status in {"abandoned", "rolled_back", "superseded"}) and not str(event.get("reason") or "").strip():

Comment thread knowledgeos/cli.py
errors.append({"label": "decision_missing_explanation", "detail": decision_id})
if not decision_event_has_command_event(run_dir, event, task_id, run_id):
errors.append({"label": "decision_missing_command_event", "detail": decision_id})
parent = str(event.get("parent_id", "")).strip()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If parent_id is explicitly set to null in the JSON lines file, str(event.get("parent_id", "")) evaluates to "None". This causes "None" to be added to parent_ids, which is then flagged as an orphan parent. Using event.get("parent_id") or "" ensures that null or missing parent IDs are correctly treated as empty strings.

Suggested change
parent = str(event.get("parent_id", "")).strip()
parent = str(event.get("parent_id") or "").strip()

Comment thread knowledgeos/cli.py
lines.append(f"- Evidence: {event.get('evidence')}")
lines.extend(["", "## Full Decision Ledger", ""])
for event in events:
options = ", ".join(str(item) for item in event.get("options", []) if str(item).strip())
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If options is explicitly set to null in the JSON lines file, event.get("options", []) will return None. Iterating over None will raise a TypeError and crash the rendering command. Using event.get("options") or [] ensures a safe fallback to an empty list.

Suggested change
options = ", ".join(str(item) for item in event.get("options", []) if str(item).strip())
options = ", ".join(str(item) for item in (event.get("options") or []) if str(item).strip())

Comment thread knowledgeos/cli.py
eval_ok = eval_has_passed(run_dir / "eval.md")
postflight_text = read_text(run_dir / "postflight.md") if (run_dir / "postflight.md").exists() else ""
synced = sync_status == "SYNC_OK" or "[SYNC_OK]" in postflight_text
task_title = task.get("title") or run_meta.get("task_title") or task_id
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If task is None (e.g., if the task is not found in tasks.yaml), calling task.get("title") will raise an AttributeError and crash the command. Adding a defensive check ensures safe fallback behavior.

Suggested change
task_title = task.get("title") or run_meta.get("task_title") or task_id
task_title = (task.get("title") if task else None) or run_meta.get("task_title") or task_id

Comment thread knowledgeos/cli.py
"task": task,
"run_dir": run_dir,
"title": task_title,
"complexity": task.get("complexity", "medium"),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If task is None, calling task.get("complexity") will raise an AttributeError and crash the command. Adding a defensive check ensures safe fallback behavior.

Suggested change
"complexity": task.get("complexity", "medium"),
"complexity": task.get("complexity", "medium") if task else "medium",

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant