@agxnte/reidx 2.0.2

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.
Files changed (135) hide show
  1. package/README.md +724 -0
  2. package/bin/reidx.js +33 -0
  3. package/package.json +40 -0
  4. package/pyproject.toml +45 -0
  5. package/scripts/find-python.js +24 -0
  6. package/scripts/postinstall.js +35 -0
  7. package/src/reidx/__init__.py +3 -0
  8. package/src/reidx/__main__.py +9 -0
  9. package/src/reidx/__pycache__/__init__.cpython-312.pyc +0 -0
  10. package/src/reidx/app/__init__.py +3 -0
  11. package/src/reidx/app/__pycache__/__init__.cpython-312.pyc +0 -0
  12. package/src/reidx/app/__pycache__/commands.cpython-312.pyc +0 -0
  13. package/src/reidx/app/commands.py +221 -0
  14. package/src/reidx/automation/__init__.py +3 -0
  15. package/src/reidx/automation/__pycache__/__init__.cpython-312.pyc +0 -0
  16. package/src/reidx/automation/__pycache__/exec.cpython-312.pyc +0 -0
  17. package/src/reidx/automation/exec.py +35 -0
  18. package/src/reidx/config/__init__.py +10 -0
  19. package/src/reidx/config/__pycache__/__init__.cpython-312.pyc +0 -0
  20. package/src/reidx/config/__pycache__/loader.cpython-312.pyc +0 -0
  21. package/src/reidx/config/__pycache__/models.cpython-312.pyc +0 -0
  22. package/src/reidx/config/__pycache__/settings.cpython-312.pyc +0 -0
  23. package/src/reidx/config/loader.py +104 -0
  24. package/src/reidx/config/models.py +49 -0
  25. package/src/reidx/config/settings.py +127 -0
  26. package/src/reidx/deepreid/__init__.py +8 -0
  27. package/src/reidx/deepreid/__pycache__/__init__.cpython-312.pyc +0 -0
  28. package/src/reidx/deepreid/__pycache__/pipeline.cpython-312.pyc +0 -0
  29. package/src/reidx/deepreid/pipeline.py +221 -0
  30. package/src/reidx/diagnostics/__init__.py +0 -0
  31. package/src/reidx/diagnostics/__pycache__/__init__.cpython-312.pyc +0 -0
  32. package/src/reidx/diagnostics/__pycache__/logger.cpython-312.pyc +0 -0
  33. package/src/reidx/diagnostics/logger.py +56 -0
  34. package/src/reidx/goals/__init__.py +21 -0
  35. package/src/reidx/goals/__pycache__/__init__.cpython-312.pyc +0 -0
  36. package/src/reidx/goals/__pycache__/models.cpython-312.pyc +0 -0
  37. package/src/reidx/goals/__pycache__/store.cpython-312.pyc +0 -0
  38. package/src/reidx/goals/models.py +101 -0
  39. package/src/reidx/goals/store.py +233 -0
  40. package/src/reidx/integrations/__init__.py +3 -0
  41. package/src/reidx/integrations/mcp.py +70 -0
  42. package/src/reidx/nyx/__init__.py +3 -0
  43. package/src/reidx/nyx/__pycache__/__init__.cpython-312.pyc +0 -0
  44. package/src/reidx/nyx/__pycache__/persona.cpython-312.pyc +0 -0
  45. package/src/reidx/nyx/persona.py +35 -0
  46. package/src/reidx/policy/__init__.py +15 -0
  47. package/src/reidx/policy/__pycache__/__init__.cpython-312.pyc +0 -0
  48. package/src/reidx/policy/__pycache__/engine.cpython-312.pyc +0 -0
  49. package/src/reidx/policy/__pycache__/models.cpython-312.pyc +0 -0
  50. package/src/reidx/policy/engine.py +101 -0
  51. package/src/reidx/policy/models.py +35 -0
  52. package/src/reidx/provider/__init__.py +16 -0
  53. package/src/reidx/provider/__pycache__/__init__.cpython-312.pyc +0 -0
  54. package/src/reidx/provider/__pycache__/_http.cpython-312.pyc +0 -0
  55. package/src/reidx/provider/__pycache__/anthropic.cpython-312.pyc +0 -0
  56. package/src/reidx/provider/__pycache__/base.cpython-312.pyc +0 -0
  57. package/src/reidx/provider/__pycache__/ollama.cpython-312.pyc +0 -0
  58. package/src/reidx/provider/__pycache__/openai.cpython-312.pyc +0 -0
  59. package/src/reidx/provider/__pycache__/registry.cpython-312.pyc +0 -0
  60. package/src/reidx/provider/__pycache__/store.cpython-312.pyc +0 -0
  61. package/src/reidx/provider/__pycache__/stub.cpython-312.pyc +0 -0
  62. package/src/reidx/provider/_http.py +29 -0
  63. package/src/reidx/provider/anthropic.py +176 -0
  64. package/src/reidx/provider/base.py +50 -0
  65. package/src/reidx/provider/ollama.py +109 -0
  66. package/src/reidx/provider/openai.py +144 -0
  67. package/src/reidx/provider/registry.py +88 -0
  68. package/src/reidx/provider/store.py +141 -0
  69. package/src/reidx/provider/stub.py +60 -0
  70. package/src/reidx/provider_manager/__init__.py +40 -0
  71. package/src/reidx/provider_manager/__pycache__/__init__.cpython-312.pyc +0 -0
  72. package/src/reidx/provider_manager/__pycache__/catalog.cpython-312.pyc +0 -0
  73. package/src/reidx/provider_manager/__pycache__/database.cpython-312.pyc +0 -0
  74. package/src/reidx/provider_manager/__pycache__/keychain.cpython-312.pyc +0 -0
  75. package/src/reidx/provider_manager/__pycache__/palette.cpython-312.pyc +0 -0
  76. package/src/reidx/provider_manager/catalog.py +330 -0
  77. package/src/reidx/provider_manager/database.py +234 -0
  78. package/src/reidx/provider_manager/keychain.py +102 -0
  79. package/src/reidx/provider_manager/palette.py +831 -0
  80. package/src/reidx/runtime/__init__.py +5 -0
  81. package/src/reidx/runtime/__pycache__/__init__.cpython-312.pyc +0 -0
  82. package/src/reidx/runtime/__pycache__/agent.cpython-312.pyc +0 -0
  83. package/src/reidx/runtime/__pycache__/orchestrator.cpython-312.pyc +0 -0
  84. package/src/reidx/runtime/__pycache__/reasoning.cpython-312.pyc +0 -0
  85. package/src/reidx/runtime/__pycache__/state.cpython-312.pyc +0 -0
  86. package/src/reidx/runtime/__pycache__/subagent.cpython-312.pyc +0 -0
  87. package/src/reidx/runtime/agent.py +198 -0
  88. package/src/reidx/runtime/orchestrator.py +244 -0
  89. package/src/reidx/runtime/reasoning.py +77 -0
  90. package/src/reidx/runtime/state.py +32 -0
  91. package/src/reidx/runtime/subagent.py +174 -0
  92. package/src/reidx/session/__init__.py +4 -0
  93. package/src/reidx/session/__pycache__/__init__.cpython-312.pyc +0 -0
  94. package/src/reidx/session/__pycache__/models.cpython-312.pyc +0 -0
  95. package/src/reidx/session/__pycache__/store.cpython-312.pyc +0 -0
  96. package/src/reidx/session/models.py +43 -0
  97. package/src/reidx/session/store.py +101 -0
  98. package/src/reidx/tasks/__init__.py +4 -0
  99. package/src/reidx/tasks/__pycache__/__init__.cpython-312.pyc +0 -0
  100. package/src/reidx/tasks/__pycache__/models.cpython-312.pyc +0 -0
  101. package/src/reidx/tasks/__pycache__/store.cpython-312.pyc +0 -0
  102. package/src/reidx/tasks/models.py +44 -0
  103. package/src/reidx/tasks/store.py +86 -0
  104. package/src/reidx/tools/__init__.py +44 -0
  105. package/src/reidx/tools/__pycache__/__init__.cpython-312.pyc +0 -0
  106. package/src/reidx/tools/__pycache__/base.cpython-312.pyc +0 -0
  107. package/src/reidx/tools/__pycache__/file_tools.cpython-312.pyc +0 -0
  108. package/src/reidx/tools/__pycache__/registry.cpython-312.pyc +0 -0
  109. package/src/reidx/tools/__pycache__/shell_tool.cpython-312.pyc +0 -0
  110. package/src/reidx/tools/__pycache__/spawn_agent.cpython-312.pyc +0 -0
  111. package/src/reidx/tools/__pycache__/web_tools.cpython-312.pyc +0 -0
  112. package/src/reidx/tools/base.py +84 -0
  113. package/src/reidx/tools/file_tools.py +222 -0
  114. package/src/reidx/tools/registry.py +45 -0
  115. package/src/reidx/tools/shell_tool.py +75 -0
  116. package/src/reidx/tools/spawn_agent.py +185 -0
  117. package/src/reidx/tools/web_tools.py +216 -0
  118. package/src/reidx/ui/__init__.py +41 -0
  119. package/src/reidx/ui/__pycache__/__init__.cpython-312.pyc +0 -0
  120. package/src/reidx/ui/__pycache__/app.cpython-312.pyc +0 -0
  121. package/src/reidx/ui/__pycache__/commands.cpython-312.pyc +0 -0
  122. package/src/reidx/ui/__pycache__/render.cpython-312.pyc +0 -0
  123. package/src/reidx/ui/__pycache__/repl.cpython-312.pyc +0 -0
  124. package/src/reidx/ui/__pycache__/theme.cpython-312.pyc +0 -0
  125. package/src/reidx/ui/app.py +1343 -0
  126. package/src/reidx/ui/commands.py +663 -0
  127. package/src/reidx/ui/render.py +517 -0
  128. package/src/reidx/ui/repl.py +14 -0
  129. package/src/reidx/ui/theme.py +126 -0
  130. package/src/reidx/workflows/__init__.py +4 -0
  131. package/src/reidx/workflows/__pycache__/__init__.cpython-312.pyc +0 -0
  132. package/src/reidx/workflows/__pycache__/models.cpython-312.pyc +0 -0
  133. package/src/reidx/workflows/__pycache__/store.cpython-312.pyc +0 -0
  134. package/src/reidx/workflows/models.py +19 -0
  135. package/src/reidx/workflows/store.py +56 -0
@@ -0,0 +1,127 @@
1
+ """Claude-Code-style settings file support.
2
+
3
+ A settings.json holds an `env` block (applied to os.environ before any provider
4
+ reads credentials) and an optional `reidx` block (project-baked ReidX
5
+ configuration — default_provider, policy, providers, etc.). Same file shape as
6
+ Claude Code's settings.json so existing files work as-is; ReidX-specific
7
+ config lives under `reidx` so unknown keys (`theme`, `effortLevel`, ...) are
8
+ ignored harmlessly.
9
+
10
+ Path resolution (first hit wins):
11
+ 1. $REIDCHAT_SETTINGS (explicit override)
12
+ 2. ./settings.json (project-local, when it exists)
13
+ 3. E:/leech/Reidchat.json (legacy default — the shared Reidchat file)
14
+
15
+ Project-local wins over the shared Reidchat file so a project can bake in a
16
+ different backend, permission mode, or provider set without editing the
17
+ global file. The env block is authoritative for the process (overrides any
18
+ ambient env) — this is how ReidX picks up the Reidchat proxy credentials
19
+ even when the shell's ANTHROPIC_* vars point somewhere else.
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ import os
25
+ from pathlib import Path
26
+
27
+ from reidx.diagnostics.logger import get_logger
28
+
29
+ log = get_logger("reidx.config.settings")
30
+
31
+ LEGACY_SETTINGS_PATH = Path("E:/leech/Reidchat.json")
32
+ GLOBAL_SETTINGS_PATH = Path.home() / ".reidx" / "settings.json"
33
+ PROJECT_SETTINGS_FILENAME = "settings.json"
34
+
35
+
36
+ def _walk_upward_for_project_settings(start: Path) -> Path | None:
37
+ """Walk from `start` toward the filesystem root looking for settings.json.
38
+
39
+ Mirrors how git finds `.git` — so launching `reid` from any
40
+ subdirectory of a project still finds that project's baked-in
41
+ settings.json. Stops at the root (path.parent == path).
42
+ """
43
+ seen: set[Path] = set()
44
+ current = start.resolve()
45
+ while current not in seen:
46
+ seen.add(current)
47
+ candidate = current / PROJECT_SETTINGS_FILENAME
48
+ if candidate.exists():
49
+ return candidate
50
+ parent = current.parent
51
+ if parent == current:
52
+ break
53
+ current = parent
54
+ return None
55
+
56
+
57
+ def settings_path() -> Path:
58
+ """Resolve the settings file path (first hit wins).
59
+
60
+ Order:
61
+ 1. $REIDCHAT_SETTINGS (explicit override)
62
+ 2. project settings.json (walk upward from CWD)
63
+ 3. ~/.reidx/settings.json (global default)
64
+ 4. E:/leech/Reidchat.json (legacy shared file)
65
+
66
+ Returns the last fallback even if it doesn't exist, so `doctor` can
67
+ report it as "missing" rather than crashing on a None.
68
+ """
69
+ override = os.environ.get("REIDCHAT_SETTINGS", "").strip()
70
+ if override:
71
+ return Path(override)
72
+ project = _walk_upward_for_project_settings(Path.cwd())
73
+ if project is not None:
74
+ return project
75
+ if GLOBAL_SETTINGS_PATH.exists():
76
+ return GLOBAL_SETTINGS_PATH
77
+ return LEGACY_SETTINGS_PATH
78
+
79
+
80
+ def _read_settings(path: Path | None = None) -> dict:
81
+ path = path or settings_path()
82
+ if not path.exists():
83
+ log.debug("settings file not found: %s", path)
84
+ return {}
85
+ try:
86
+ return json.loads(path.read_text(encoding="utf-8"))
87
+ except (json.JSONDecodeError, OSError) as exc:
88
+ log.warning("failed to read settings %s: %s", path, exc)
89
+ return {}
90
+
91
+
92
+ def apply_settings_env(path: Path | None = None) -> dict[str, str]:
93
+ """Apply a settings file's `env` block to os.environ.
94
+
95
+ Returns the mapping of env vars that were actually applied. Missing or
96
+ invalid files are a no-op.
97
+ """
98
+ data = _read_settings(path)
99
+ env = data.get("env")
100
+ if not isinstance(env, dict):
101
+ return {}
102
+
103
+ applied: dict[str, str] = {}
104
+ for key, value in env.items():
105
+ if value is None:
106
+ continue
107
+ new = str(value)
108
+ if os.environ.get(key) not in (None, new):
109
+ log.debug("settings override %s (ambient env replaced)", key)
110
+ os.environ[key] = new # the settings file is authoritative
111
+ applied[key] = new
112
+
113
+ if applied:
114
+ log.debug("applied %d env var(s) from settings: %s", len(applied), ", ".join(applied))
115
+ return applied
116
+
117
+
118
+ def read_reidx_block(path: Path | None = None) -> dict:
119
+ """Return the settings file's `reidx` block, or {} if absent.
120
+
121
+ Loader merges this into Config below the env-var overrides but above the
122
+ on-disk .reidx/config.json, so `settings.json` is the project's baked-in
123
+ source of truth for anything that isn't an env credential.
124
+ """
125
+ data = _read_settings(path)
126
+ block = data.get("reidx")
127
+ return block if isinstance(block, dict) else {}
@@ -0,0 +1,8 @@
1
+ from reidx.deepreid.pipeline import (
2
+ DeepReidResult,
3
+ format_markdown,
4
+ run_deepreid,
5
+ save_deepreid_result,
6
+ )
7
+
8
+ __all__ = ["DeepReidResult", "format_markdown", "run_deepreid", "save_deepreid_result"]
@@ -0,0 +1,221 @@
1
+ """DeepReid: Researcher -> Planner -> Critic planning-and-review pipeline.
2
+
3
+ See deepreid-spec.md (repo root, one level above ReidX/) for the full
4
+ design. DeepReid never writes files or runs commands — it produces a
5
+ Markdown plan + critique. Building is a separate step.
6
+
7
+ Each role is a fresh, independent Agent + RuntimeState + Session +
8
+ PolicyEngine — not turns on one shared conversation, so each role's context
9
+ window stays clean and the "Planner/Critic have no tools" constraint is real
10
+ (a shared agent could otherwise still see prior tool results). All three
11
+ subagent PolicyEngines run in AUTONOMOUS mode regardless of the caller's
12
+ configured mode: Planner/Critic have zero tools registered (nothing to ever
13
+ approve), and the Researcher's tools are all read-only/search — the
14
+ restricted ToolRegistry is the actual safety boundary, so auto-approving
15
+ inside it is safe and avoids the pipeline hanging on an unattended prompt.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import re
20
+ import time
21
+ from collections.abc import Callable
22
+ from dataclasses import dataclass
23
+ from pathlib import Path
24
+
25
+ from reidx.config.models import Config
26
+ from reidx.policy.engine import PolicyEngine
27
+ from reidx.policy.models import PermissionMode
28
+ from reidx.provider.base import BaseProvider
29
+ from reidx.runtime.agent import Agent
30
+ from reidx.runtime.state import RuntimeState
31
+ from reidx.session.models import Session
32
+ from reidx.tools.file_tools import FindFilesTool, GrepFilesTool, ListDirTool, ReadFileTool
33
+ from reidx.tools.registry import ToolRegistry
34
+ from reidx.tools.web_tools import WebSearchTool
35
+
36
+ MAX_REVISION_ROUNDS = 2
37
+ # The Researcher is the only role that calls tools, so it's the only one that
38
+ # can run out of Agent.run_turn's step budget mid-exploration — observed in
39
+ # practice: a broad task ("read the dir") led it to keep calling list_dir/
40
+ # read_file for all of the default 8 steps and never stop to write up
41
+ # Findings, producing "[agent] step budget exhausted" instead of any usable
42
+ # output. Give it real headroom; Planner/Critic have zero tools so they
43
+ # always finish in exactly 1 step regardless of this number.
44
+ RESEARCHER_MAX_STEPS = 20
45
+
46
+ _RESEARCHER_PROMPT = (
47
+ "You are the Researcher in a DeepReid planning pipeline. Investigate the "
48
+ "codebase (read_file/grep_files/find_files/list_dir) and, if useful, the "
49
+ "web (web_search) to gather everything needed to plan the given task. You "
50
+ "cannot write files or run commands — you only look and report.\n\n"
51
+ "You have a limited number of tool calls. Investigate efficiently: prefer "
52
+ "list_dir/find_files/grep_files to survey before read_file-ing whole "
53
+ "files, and stop once you have enough to plan from — you do not need to "
54
+ "read every file. Always end with your Findings list as plain text (no "
55
+ "further tool calls) well before you run out of turns; a partial Findings "
56
+ "list from what you've seen so far is far more useful than none at all.\n\n"
57
+ "Produce a Findings list: one bullet per finding, each citing a "
58
+ "`file:line` reference or a web_search source. Be specific, not vague. Do "
59
+ "not propose a plan yourself; that is the Planner's job."
60
+ )
61
+
62
+ _PLANNER_PROMPT = (
63
+ "You are the Planner in a DeepReid planning pipeline. You have no tools — "
64
+ "reason only over the task and the Researcher's Findings given to you. "
65
+ "Produce: numbered implementation steps (each naming the file(s) it "
66
+ "touches and what changes), explicit risks or things you're unsure about, "
67
+ "and open questions for a human. Do not invent findings the Researcher "
68
+ "didn't report."
69
+ )
70
+
71
+ _PLANNER_REVISION_PROMPT = (
72
+ "You are the Planner, revising your plan after Critic feedback. You have "
73
+ "no tools. Given the original task, the Researcher's Findings, your prior "
74
+ "Plan, and the Critic's Critique below, produce a revised Plan in the "
75
+ "same format (numbered steps, risks, open questions) that addresses the "
76
+ "critique."
77
+ )
78
+
79
+ _CRITIC_PROMPT = (
80
+ "You are the Critic in a DeepReid planning pipeline. You have no tools — "
81
+ "reason only over the task, the Researcher's Findings, and the Planner's "
82
+ "Plan given to you. Find: claims in the Plan the Findings don't actually "
83
+ "support, missing edge cases or files the Researcher didn't look at, and "
84
+ "any internal contradictions. Write your Critique, then end your reply "
85
+ "with a literal final line in exactly one of these forms:\n"
86
+ "Verdict: ready to build\n"
87
+ "Verdict: needs revision\n"
88
+ "Verdict: blocked on: <what it's blocked on>"
89
+ )
90
+
91
+ _VERDICT_RE = re.compile(r"^\s*Verdict:\s*(.+?)\s*$", re.IGNORECASE | re.MULTILINE)
92
+
93
+
94
+ @dataclass
95
+ class DeepReidResult:
96
+ task: str
97
+ findings: str
98
+ plan: str
99
+ critique: str
100
+ verdict: str
101
+ rounds: int
102
+
103
+
104
+ def _researcher_registry() -> ToolRegistry:
105
+ reg = ToolRegistry()
106
+ for tool in (ReadFileTool(), GrepFilesTool(), FindFilesTool(), ListDirTool(), WebSearchTool()):
107
+ reg.register(tool)
108
+ return reg # write_file/patch_file/run_command are never registered at all
109
+
110
+
111
+ def _make_subagent(
112
+ config: Config, provider: BaseProvider, workspace: Path, registry: ToolRegistry, base_prompt: str
113
+ ) -> tuple[Agent, RuntimeState]:
114
+ policy = PolicyEngine(config)
115
+ policy.set_mode(PermissionMode.AUTONOMOUS)
116
+ session = Session(title="deepreid", workspace=workspace)
117
+ agent = Agent(provider, registry, policy, base_system_prompt=base_prompt)
118
+ return agent, RuntimeState(session=session)
119
+
120
+
121
+ def _run_role(
122
+ config: Config,
123
+ provider: BaseProvider,
124
+ workspace: Path,
125
+ registry: ToolRegistry,
126
+ base_prompt: str,
127
+ user_input: str,
128
+ *,
129
+ max_steps: int | None = None,
130
+ ) -> str:
131
+ agent, state = _make_subagent(config, provider, workspace, registry, base_prompt)
132
+ kwargs = {"max_steps": max_steps} if max_steps is not None else {}
133
+ text, _tools = agent.run_turn(state, user_input, approver=lambda _prompt: True, **kwargs)
134
+ return text
135
+
136
+
137
+ def _report(on_progress: Callable[[str], None] | None, stage: str) -> None:
138
+ if on_progress is not None:
139
+ on_progress(stage)
140
+
141
+
142
+ def _split_verdict(critic_text: str) -> tuple[str, str]:
143
+ """Split the Critic's reply into (critique_body, verdict_line).
144
+
145
+ Falls back to treating the whole reply as critique with an empty verdict
146
+ (never crashes on a Critic that ignores the requested format — same
147
+ "never hide the answer" philosophy as split_reasoning)."""
148
+ match = _VERDICT_RE.search(critic_text)
149
+ if match is None:
150
+ return critic_text.strip(), ""
151
+ verdict = match.group(1).strip()
152
+ critique = (critic_text[: match.start()] + critic_text[match.end() :]).strip()
153
+ return critique, verdict
154
+
155
+
156
+ def run_deepreid(
157
+ config: Config,
158
+ provider: BaseProvider,
159
+ workspace: Path,
160
+ task: str,
161
+ on_progress: Callable[[str], None] | None = None,
162
+ ) -> DeepReidResult:
163
+ researcher_registry = _researcher_registry()
164
+ no_tools_registry = ToolRegistry()
165
+
166
+ _report(on_progress, "researching")
167
+ findings = _run_role(
168
+ config, provider, workspace, researcher_registry, _RESEARCHER_PROMPT, task,
169
+ max_steps=RESEARCHER_MAX_STEPS,
170
+ )
171
+
172
+ _report(on_progress, "planning")
173
+ plan = _run_role(
174
+ config, provider, workspace, no_tools_registry, _PLANNER_PROMPT,
175
+ f"Task: {task}\n\nFindings:\n{findings}",
176
+ )
177
+
178
+ _report(on_progress, "reviewing")
179
+ critic_text = _run_role(
180
+ config, provider, workspace, no_tools_registry, _CRITIC_PROMPT,
181
+ f"Task: {task}\n\nFindings:\n{findings}\n\nPlan:\n{plan}",
182
+ )
183
+ critique, verdict = _split_verdict(critic_text)
184
+
185
+ rounds = 1
186
+ while "revision" in verdict.lower() and rounds < MAX_REVISION_ROUNDS:
187
+ _report(on_progress, "revising plan")
188
+ plan = _run_role(
189
+ config, provider, workspace, no_tools_registry, _PLANNER_REVISION_PROMPT,
190
+ f"Task: {task}\n\nFindings:\n{findings}\n\nPrior Plan:\n{plan}\n\nCritique:\n{critique}",
191
+ )
192
+ _report(on_progress, "reviewing revision")
193
+ critic_text = _run_role(
194
+ config, provider, workspace, no_tools_registry, _CRITIC_PROMPT,
195
+ f"Task: {task}\n\nFindings:\n{findings}\n\nPlan:\n{plan}",
196
+ )
197
+ critique, verdict = _split_verdict(critic_text)
198
+ rounds += 1
199
+
200
+ return DeepReidResult(task=task, findings=findings, plan=plan, critique=critique, verdict=verdict, rounds=rounds)
201
+
202
+
203
+ def format_markdown(result: DeepReidResult) -> str:
204
+ return (
205
+ f"# DeepReid: {result.task}\n\n"
206
+ f"## Findings\n{result.findings}\n\n"
207
+ f"## Plan\n{result.plan}\n\n"
208
+ f"## Critique\n{result.critique or '(none)'}\n\n"
209
+ f"## Verdict\n{result.verdict or '(not provided)'} "
210
+ f"({result.rounds} round{'s' if result.rounds != 1 else ''})\n"
211
+ )
212
+
213
+
214
+ def save_deepreid_result(config: Config, result: DeepReidResult) -> Path:
215
+ storage_root = config.storage_root or (Path.home() / ".reidx")
216
+ out_dir = storage_root / "deepreid"
217
+ out_dir.mkdir(parents=True, exist_ok=True)
218
+ run_id = time.strftime("%Y%m%d-%H%M%S")
219
+ path = out_dir / f"{run_id}.md"
220
+ path.write_text(format_markdown(result), encoding="utf-8")
221
+ return path
File without changes
@@ -0,0 +1,56 @@
1
+ """Diagnostics: logging + structured event log.
2
+
3
+ Foundation is intentionally simple — a tagged logger and a JSONL event writer for
4
+ runtime actions. Designed to grow into trace mode and token/usage reporting.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import logging
10
+ from datetime import UTC, datetime
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+
15
+ def get_logger(name: str = "reidx") -> logging.Logger:
16
+ logger = logging.getLogger(name)
17
+ if not logger.handlers:
18
+ handler = logging.StreamHandler()
19
+ handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s"))
20
+ logger.addHandler(handler)
21
+ # Default to WARNING so INFO logs don't pollute the interactive UI.
22
+ # Set REIDX_LOG_LEVEL=INFO (or DEBUG) to see verbose logs.
23
+ import os
24
+
25
+ level = os.environ.get("REIDX_LOG_LEVEL", "WARNING").upper()
26
+ logger.setLevel(getattr(logging, level, logging.WARNING))
27
+ return logger
28
+
29
+
30
+ class EventLog:
31
+ """Append-only JSONL event log for runtime actions (tool calls, decisions, runs)."""
32
+
33
+ def __init__(self, path: Path) -> None:
34
+ self.path = path
35
+ self.path.parent.mkdir(parents=True, exist_ok=True)
36
+
37
+ def write(self, kind: str, data: dict[str, Any] | None = None) -> None:
38
+ record = {
39
+ "ts": datetime.now(UTC).isoformat(),
40
+ "kind": kind,
41
+ "data": data or {},
42
+ }
43
+ with self.path.open("a", encoding="utf-8") as fh:
44
+ fh.write(json.dumps(record, default=str) + "\n")
45
+
46
+ def tail(self, n: int = 50) -> list[dict[str, Any]]:
47
+ if not self.path.exists():
48
+ return []
49
+ lines = self.path.read_text(encoding="utf-8").splitlines()
50
+ out: list[dict[str, Any]] = []
51
+ for line in lines[-n:]:
52
+ try:
53
+ out.append(json.loads(line))
54
+ except json.JSONDecodeError:
55
+ continue
56
+ return out
@@ -0,0 +1,21 @@
1
+ from reidx.goals.models import (
2
+ Goal,
3
+ GoalConstraint,
4
+ GoalEvidence,
5
+ GoalNode,
6
+ GoalNodeKind,
7
+ GoalRevision,
8
+ GoalStatus,
9
+ )
10
+ from reidx.goals.store import GoalStore
11
+
12
+ __all__ = [
13
+ "Goal",
14
+ "GoalConstraint",
15
+ "GoalEvidence",
16
+ "GoalNode",
17
+ "GoalNodeKind",
18
+ "GoalRevision",
19
+ "GoalStatus",
20
+ "GoalStore",
21
+ ]
@@ -0,0 +1,101 @@
1
+ """Goal models.
2
+
3
+ Goals are durable outcome structures for long-horizon work. They are separate
4
+ from Tasks: a task is one execution record, while a goal captures the desired
5
+ end state, evidence, decomposition, dependencies, and revision trail.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from datetime import UTC, datetime
10
+ from enum import StrEnum
11
+
12
+ from pydantic import BaseModel, Field
13
+
14
+
15
+ def _new_id() -> str:
16
+ import uuid
17
+
18
+ return uuid.uuid4().hex[:10]
19
+
20
+
21
+ class GoalStatus(StrEnum):
22
+ DRAFT = "draft"
23
+ ACTIVE = "active"
24
+ BLOCKED = "blocked"
25
+ COMPLETED = "completed"
26
+ ABANDONED = "abandoned"
27
+
28
+
29
+ class GoalNodeKind(StrEnum):
30
+ GOAL = "goal"
31
+ SUBGOAL = "subgoal"
32
+ MILESTONE = "milestone"
33
+ HABIT = "habit"
34
+ TASK_REF = "task_ref"
35
+
36
+
37
+ class GoalEvidence(BaseModel):
38
+ description: str
39
+ satisfied: bool = False
40
+ note: str = ""
41
+ created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
42
+ satisfied_at: datetime | None = None
43
+
44
+ def satisfy(self, note: str = "") -> None:
45
+ self.satisfied = True
46
+ if note:
47
+ self.note = note
48
+ self.satisfied_at = datetime.now(UTC)
49
+
50
+
51
+ class GoalConstraint(BaseModel):
52
+ description: str
53
+ kind: str = "general"
54
+
55
+
56
+ class GoalRevision(BaseModel):
57
+ at: datetime = Field(default_factory=lambda: datetime.now(UTC))
58
+ note: str
59
+
60
+
61
+ class GoalNode(BaseModel):
62
+ id: str = Field(default_factory=_new_id)
63
+ title: str
64
+ kind: GoalNodeKind = GoalNodeKind.SUBGOAL
65
+ status: GoalStatus = GoalStatus.DRAFT
66
+ parent_id: str | None = None
67
+ depends_on: list[str] = Field(default_factory=list)
68
+ evidence: list[GoalEvidence] = Field(default_factory=list)
69
+ constraints: list[GoalConstraint] = Field(default_factory=list)
70
+ owner: str = "user"
71
+ notes: list[str] = Field(default_factory=list)
72
+ task_ids: list[str] = Field(default_factory=list)
73
+ created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
74
+ updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
75
+
76
+ def touch(self) -> None:
77
+ self.updated_at = datetime.now(UTC)
78
+
79
+
80
+ class Goal(BaseModel):
81
+ id: str = Field(default_factory=_new_id)
82
+ session_id: str
83
+ title: str
84
+ status: GoalStatus = GoalStatus.DRAFT
85
+ outcome: str = ""
86
+ evidence: list[GoalEvidence] = Field(default_factory=list)
87
+ constraints: list[GoalConstraint] = Field(default_factory=list)
88
+ nodes: list[GoalNode] = Field(default_factory=list)
89
+ active_node_id: str | None = None
90
+ task_ids: list[str] = Field(default_factory=list)
91
+ revisions: list[GoalRevision] = Field(default_factory=list)
92
+ created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
93
+ updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
94
+
95
+ def touch(self) -> None:
96
+ self.updated_at = datetime.now(UTC)
97
+
98
+ def add_revision(self, note: str) -> None:
99
+ if note:
100
+ self.revisions.append(GoalRevision(note=note))
101
+ self.touch()