diff --git a/.agents/skills/tldrgraph-init/SKILL.md b/.agents/skills/tldrgraph-init/SKILL.md index 86dccf1..e7e58cf 100644 --- a/.agents/skills/tldrgraph-init/SKILL.md +++ b/.agents/skills/tldrgraph-init/SKILL.md @@ -14,21 +14,21 @@ One command handles layer design, extraction, enrichment, LLM route links, and e tldrgraph init ``` -By default TLDRGraph detects `claude`, `cursor-agent`, or `gemini`, asks once before enrichment token spend, processes every +By default TLDRGraph detects `claude`, `cursor-agent`, or `gemini`, processes every eligible node in batches of 200, and downloads/builds the local embedding model. -Use `--yes` for approval, `--batch N` for batch size, `--embeddings off|auto|on`, -`--no-llm-links` to skip LLM frontend/backend route inference, or `--no-agent-cli` -for manual file handoff. +In a detected coding-agent session, plain `tldrgraph init` auto-approves the full +enrichment campaign; normal terminal users and non-agent automation still get the +confirmation gate. -After the user approves the full run, use exactly `tldrgraph init --yes`. The -approval is saved for the current candidate set, so later `tldrgraph init` calls -must continue without asking again. `--batch 200` means all nodes in 200-node -batches; `--limit 200` means stop after only 200 nodes. Never add `--limit` or -`--embeddings off` unless the user explicitly requests a partial or no-embedding run. +Use exactly `tldrgraph init` for this workflow. Add `--yes` only if a non-agent +`needs_confirmation` response explicitly asks for approval. `--batch 200` means +all nodes in 200-node batches; `--limit 200` means stop after only 200 nodes. +Never add `--limit` or `--embeddings off` unless the user explicitly requests a +partial or no-embedding run. ## Completion contract for agents -Once the user approves a full enrichment campaign, you are not done until one of +Once a full enrichment campaign is approved or auto-approved, you are not done until one of these terminal states occurs: - `tldrgraph init` reports `status: done`. @@ -40,6 +40,7 @@ these terminal states occurs: These are **not** terminal states: - `status: needs_enrichment` +- `status: needs_llm_links` - `NEXT ACTION` - "nodes remaining" - "batches remaining" @@ -108,9 +109,9 @@ Design one from this repository. ## `status: needs_confirmation` -The output shows how many nodes need enrichment and how many agent round-trips -that implies. **Ask the user whether to proceed, and show them that estimate.** -Do not decide for them. +Detected coding-agent sessions should not reach this state for a full run. If a +non-agent run does, the output shows how many nodes need enrichment and how many +agent round-trips that implies; ask the user whether to proceed. - They agree: `tldrgraph init --yes` saves approval for the full campaign - Smaller first pass: `tldrgraph init --yes --limit 100` @@ -140,7 +141,8 @@ count as sentences. 4. Run `tldrgraph init` again. Approval is saved; process any next `needs_enrichment` batch immediately without asking the user again until - `status: done` or `needs_llm_links`. + init either reports `status: done` or advances to the required `needs_llm_links` + phase. Inside an existing Codex/Claude/Cursor session, nested-agent protection may stop the CLI from launching a second agent. In that case **you are the enrichment @@ -160,7 +162,8 @@ empty list is a correct answer, a wrong `calls` entry becomes a real wrong edge. 3. Write `.tldrgraph/llm_links_response.yaml` as a YAML list of `{source, target, confidence, frontend_evidence, backend_evidence, explanation}`. Only include source-backed links with file and line evidence. -4. Run `tldrgraph init` again, or use `--no-llm-links` to skip this optional stage. +4. Run `tldrgraph init` again. This is a required continuation state for a + complete init run unless the user explicitly requested `--no-llm-links`. ## Once it says DONE diff --git a/.claude/commands/tldrgraph-init.md b/.claude/commands/tldrgraph-init.md index 86dccf1..e7e58cf 100644 --- a/.claude/commands/tldrgraph-init.md +++ b/.claude/commands/tldrgraph-init.md @@ -14,21 +14,21 @@ One command handles layer design, extraction, enrichment, LLM route links, and e tldrgraph init ``` -By default TLDRGraph detects `claude`, `cursor-agent`, or `gemini`, asks once before enrichment token spend, processes every +By default TLDRGraph detects `claude`, `cursor-agent`, or `gemini`, processes every eligible node in batches of 200, and downloads/builds the local embedding model. -Use `--yes` for approval, `--batch N` for batch size, `--embeddings off|auto|on`, -`--no-llm-links` to skip LLM frontend/backend route inference, or `--no-agent-cli` -for manual file handoff. +In a detected coding-agent session, plain `tldrgraph init` auto-approves the full +enrichment campaign; normal terminal users and non-agent automation still get the +confirmation gate. -After the user approves the full run, use exactly `tldrgraph init --yes`. The -approval is saved for the current candidate set, so later `tldrgraph init` calls -must continue without asking again. `--batch 200` means all nodes in 200-node -batches; `--limit 200` means stop after only 200 nodes. Never add `--limit` or -`--embeddings off` unless the user explicitly requests a partial or no-embedding run. +Use exactly `tldrgraph init` for this workflow. Add `--yes` only if a non-agent +`needs_confirmation` response explicitly asks for approval. `--batch 200` means +all nodes in 200-node batches; `--limit 200` means stop after only 200 nodes. +Never add `--limit` or `--embeddings off` unless the user explicitly requests a +partial or no-embedding run. ## Completion contract for agents -Once the user approves a full enrichment campaign, you are not done until one of +Once a full enrichment campaign is approved or auto-approved, you are not done until one of these terminal states occurs: - `tldrgraph init` reports `status: done`. @@ -40,6 +40,7 @@ these terminal states occurs: These are **not** terminal states: - `status: needs_enrichment` +- `status: needs_llm_links` - `NEXT ACTION` - "nodes remaining" - "batches remaining" @@ -108,9 +109,9 @@ Design one from this repository. ## `status: needs_confirmation` -The output shows how many nodes need enrichment and how many agent round-trips -that implies. **Ask the user whether to proceed, and show them that estimate.** -Do not decide for them. +Detected coding-agent sessions should not reach this state for a full run. If a +non-agent run does, the output shows how many nodes need enrichment and how many +agent round-trips that implies; ask the user whether to proceed. - They agree: `tldrgraph init --yes` saves approval for the full campaign - Smaller first pass: `tldrgraph init --yes --limit 100` @@ -140,7 +141,8 @@ count as sentences. 4. Run `tldrgraph init` again. Approval is saved; process any next `needs_enrichment` batch immediately without asking the user again until - `status: done` or `needs_llm_links`. + init either reports `status: done` or advances to the required `needs_llm_links` + phase. Inside an existing Codex/Claude/Cursor session, nested-agent protection may stop the CLI from launching a second agent. In that case **you are the enrichment @@ -160,7 +162,8 @@ empty list is a correct answer, a wrong `calls` entry becomes a real wrong edge. 3. Write `.tldrgraph/llm_links_response.yaml` as a YAML list of `{source, target, confidence, frontend_evidence, backend_evidence, explanation}`. Only include source-backed links with file and line evidence. -4. Run `tldrgraph init` again, or use `--no-llm-links` to skip this optional stage. +4. Run `tldrgraph init` again. This is a required continuation state for a + complete init run unless the user explicitly requested `--no-llm-links`. ## Once it says DONE diff --git a/AGENTS.md b/AGENTS.md index 16a30e2..5fdcbf7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,16 +24,16 @@ tldrgraph dead-code # review candidates, never a dele Those are read-only and never trigger enrichment. -**To build or refresh the graph**, run `tldrgraph init`. It automatically handles -layer design, extraction, source-aware enrichment in 200-node batches, LLM -frontend/backend route-link inference, and dense embeddings when a supported -agent CLI is available. If it prints a `NEXT ACTION` fallback, follow that -handoff without guessing from symbol names. +**To build or refresh the graph**, run `tldrgraph init`. In a detected coding-agent +session, plain `tldrgraph init` auto-approves the full enrichment campaign; normal +terminal users and non-agent automation still get confirmation. It handles layer +design, extraction, source-aware enrichment in 200-node batches, LLM route links, +and dense embeddings. If it prints a `NEXT ACTION` fallback, follow that handoff. ### ✅ TLDRGraph Init Completion Contract -After the user approves a full TLDRGraph enrichment run, the agent MUST keep -working until `tldrgraph init` reports `status: done` or a genuine blocking error -requires user action. +After a full TLDRGraph enrichment run is approved or auto-approved, the agent MUST +keep working until `tldrgraph init` reports `status: done` or a genuine blocking +error requires user action. - `needs_enrichment`, "nodes remaining", "batches remaining", and "NEXT ACTION" are continuation states, not completion states. diff --git a/AGENT_CONTRACT.md b/AGENT_CONTRACT.md index b2655be..2c1aac6 100644 --- a/AGENT_CONTRACT.md +++ b/AGENT_CONTRACT.md @@ -24,14 +24,15 @@ One command handles layers, extraction, enrichment, and embeddings: tldrgraph init ``` -It asks once before enrichment token spend. Full approval is persisted for the current -candidate set until enrichment finishes, so continuation runs must not ask again. By -default it uses 200-node batches and builds dense embeddings. +In a detected coding-agent session, plain `tldrgraph init` auto-approves the full +enrichment campaign. Normal terminal users and non-agent automation still get the +confirmation gate. Full approval is persisted for the current candidate set until +enrichment finishes, so continuation runs must not ask again. | status | what it wants | | --- | --- | | `needs_layers` | Read the code and design this repository's architecture. **TLDRGraph ships no layer templates**; nothing will be applied for you. The request carries sketches of how other kinds of codebase divide — for shape only, never to copy. | -| `needs_confirmation` | Show the estimate and ask once. Approval via `tldrgraph init --yes` persists until the current campaign is done. | +| `needs_confirmation` | Non-agent runs only: show the estimate and ask once. Approval via `tldrgraph init --yes` persists until the current campaign is done. | | `needs_enrichment` | Open, read, and describe this batch, then continue immediately without asking again. | | `needs_embeddings` | Enrichment finished but the required dense model/index could not be built. Fix model access and rerun init. | | `done` | Nothing left. Use `query` / `trace` / `layers`. | @@ -48,7 +49,7 @@ dropped, and will be reported back to you — but the work is wasted. ## The loop ```bash -tldrgraph init --yes # 1. approve every current candidate; writes a 200-node request +tldrgraph init # 1. approve the full campaign in agent sessions # 2. read every requested source and write enrichment_response.yaml tldrgraph init # 3. applies it and emits the next batch; approval is remembered # 4. repeat steps 2-3 without asking until status: done diff --git a/README.md b/README.md index f0f3d84..3482061 100644 --- a/README.md +++ b/README.md @@ -81,10 +81,11 @@ repository, runs `init`, and handles every required follow-up. In Claude Code or Cursor, run `/tldrgraph-init`. In Codex, open `/skills` and select `tldrgraph-init`, or invoke `$tldrgraph-init`. -The agent designs the repository-specific layers, extracts the graph, asks once -before enrichment token spend, enriches every eligible node in 200-node batches, -and downloads/builds local dense embeddings. That approval is remembered for the -current candidate set until enrichment is complete. +The agent designs the repository-specific layers, extracts the graph, enriches +every eligible node in 200-node batches, and downloads/builds local dense +embeddings. In a detected coding-agent session, plain `tldrgraph init` +auto-approves the full enrichment campaign; normal terminal users and non-agent +automation still get the confirmation gate. `--batch 200` controls chunk size while still processing everything. `--limit 200` intentionally stops after 200 total nodes. Embeddings remain enabled unless @@ -99,7 +100,7 @@ It can report four resumable states: | status | what it needs | | --- | --- | | `needs_layers` | Read the code and design the architecture. No template will be applied for you. | -| `needs_confirmation` | Shows how many nodes need enrichment and how many agent rounds that is. **Your agent asks you before spending tokens.** | +| `needs_confirmation` | Non-agent runs only: shows how many nodes need enrichment and how many agent rounds that is before approval. | | `needs_enrichment` | A batch of nodes to open, read, and describe. | | `needs_embeddings` | Enrichment is complete, but the required dense model/index could not be built. | diff --git a/tests/test_auto_agent.py b/tests/test_auto_agent.py index 92289d4..1f01dec 100644 --- a/tests/test_auto_agent.py +++ b/tests/test_auto_agent.py @@ -371,6 +371,30 @@ def test_init_asks_before_spending_tokens_and_shows_the_estimate(cli_repo): assert "tldrgraph init --yes" in res.output +def test_coding_agent_init_auto_approves_full_campaign(monkeypatch, cli_repo): + _answer_layers(cli_repo) + monkeypatch.setenv("AI_AGENT", "1") + + res = CliRunner().invoke(cli, ["init", str(cli_repo)]) + + assert res.exit_code == 0, res.output + assert "Detected coding-agent session ($AI_AGENT)" in res.output + assert "status: needs_confirmation" not in res.output + assert "status: needs_enrichment" in res.output + assert (cli_repo / ".tldrgraph" / APPROVAL_FILENAME).is_file() + + +def test_coding_agent_init_with_limit_is_not_full_auto_approval(monkeypatch, cli_repo): + _answer_layers(cli_repo) + monkeypatch.setenv("AI_AGENT", "1") + + res = CliRunner().invoke(cli, ["init", str(cli_repo), "--limit", "1"]) + + assert res.exit_code == 0, res.output + assert "status: needs_confirmation" in res.output + assert not (cli_repo / ".tldrgraph" / APPROVAL_FILENAME).exists() + + def test_the_estimate_is_machine_readable(cli_repo): _answer_layers(cli_repo) res = CliRunner().invoke(cli, ["init", str(cli_repo), "--json"]) @@ -621,6 +645,38 @@ def _answer(prompt): assert edges[0]["frontend_file"].endswith("page.tsx") +def test_llm_route_failure_requires_manual_handoff(monkeypatch, cli_repo, agent_allowed): + (cli_repo / "frontend/src/orders").mkdir(parents=True) + (cli_repo / "backend/src").mkdir(parents=True) + (cli_repo / "frontend/src/orders/page.tsx").write_text( + "export function OrdersPage() { return getOrders() }\n", encoding="utf-8" + ) + (cli_repo / "backend/src/orders.controller.ts").write_text( + "@Controller('orders')\nexport class OrdersController {\n" + " @Get()\n findAll() { return [] }\n}\n", + encoding="utf-8", + ) + + def _answer(agent, prompt, cwd, timeout=None, model=None): + if _is_llm_links_prompt(prompt): + raise agent_runner.AgentError("route agent unavailable") + return _fake_answer(prompt) + + monkeypatch.setattr(agent_runner, "run_agent", _answer) + monkeypatch.setattr(agent_runner, "find_agent_cli", lambda **kw: fake_agent()) + monkeypatch.setattr( + agent_runner, "agent_status", + lambda: {"agent": fake_agent(), "reason": "ready", "detail": "Fake fake"}, + ) + res = CliRunner().invoke(cli, ["init", str(cli_repo), "--yes", "--agent-cli"]) + + assert res.exit_code == 0, res.output + assert "status: needs_llm_links" in res.output + assert "Required LLM frontend-backend link inference" in res.output + assert "optional" not in res.output.lower() + assert (cli_repo / ".tldrgraph" / "llm_links_request.yaml").is_file() + + def test_no_llm_links_flag_skips_route_inference(monkeypatch, cli_repo, agent_allowed): calls = [] diff --git a/tests/test_bpmn.py b/tests/test_bpmn.py index 661e81e..138702c 100644 --- a/tests/test_bpmn.py +++ b/tests/test_bpmn.py @@ -9,7 +9,9 @@ from tldrgraph.visualizer.bpmn_data import ( LANE_EXTERNAL, LANE_SYSTEM, + _build_label_index, _question_from, + _resolve_call, build_workflow_process, ) from tldrgraph.visualizer.bpmn_phrasing import ELEMENT_PHRASES, phrase_for_element @@ -87,6 +89,22 @@ def run(data, path): assert externals == {"File system"} +def test_browser_fetch_does_not_resolve_to_unrelated_project_method(): + nodes_by_id = { + "exchange": {"label": "exchangeImpersonationCode()", "file": "frontend/src/services/auth.ts"}, + "github_fetch": {"label": ".fetch()", "file": "backend/src/services/githubSync.ts"}, + } + + resolved = _resolve_call( + ["fetch"], + "frontend/src/services/auth.ts", + nodes_by_id, + _build_label_index(nodes_by_id), + ) + + assert resolved is None + + def _workflow(tmp_path): return { "id": "flow_test", @@ -285,6 +303,93 @@ def test_tests_and_vendored_code_are_never_offered_as_a_journey(): assert rank_entry_points(graph, nodes_by_id, 10) == [] +def test_workflow_process_prefers_intent_action_over_code_label(tmp_path): + graph = nx.DiGraph() + nodes_by_id = { + "endpoint": { + "label": "POST /containers/create", + "file": "backend/src/routes/containers.ts", + "layer_id": "api", + "layer": "API", + "intent": "Create a project container for the user. It validates the request and returns the new container.", + }, + "schema": { + "label": "Project", + "file": "schema.prisma", + "layer_id": "data", + "layer": "Data", + "intent": "Store the project record in the database. It keeps the workspace metadata available for later steps.", + }, + } + graph.add_nodes_from((node_id, data) for node_id, data in nodes_by_id.items()) + + workflow = { + "id": "flow_project_prompt", + "steps": [ + {"node_id": "endpoint", "symbol": "POST /containers/create", "file": "missing.ts", "code_start": 1, + "intent": nodes_by_id["endpoint"]["intent"]}, + {"node_id": "schema", "symbol": "Project", "file": "schema.prisma", "code_start": 1, + "intent": nodes_by_id["schema"]["intent"]}, + ], + } + + process = build_workflow_process(str(tmp_path), workflow, graph, nodes_by_id) + labels = [e["label"] for e in process["elements"] if e.get("node_id")] + titles = [e["step_title"] for e in process["elements"] if e.get("node_id")] + + assert labels == [ + "Create a project container for the user", + "Store the project record in the database", + ] + assert titles == labels + assert "POST /containers/create" not in labels + assert "Project" not in labels + + +def test_workflow_process_rejects_generic_symbol_intents(tmp_path): + graph = nx.DiGraph() + nodes_by_id = { + "endpoint": { + "label": "POST /containers/create", + "file": "backend/src/routes/containers.ts", + "layer_id": "api", + "layer": "API", + "intent": ( + "The symbol POST /containers/create is defined in backend/src/routes/" + "containers.ts as an HTTP-facing backend module." + ), + }, + "handler": { + "label": "POST /containers/create handler", + "file": "backend/src/routes/containers.ts", + "layer_id": "api", + "layer": "API", + "intent": "The symbol POST /containers/create handler is defined in backend/src/routes/containers.ts.", + }, + "schema": { + "label": "Project", + "file": "schema.prisma", + "layer_id": "persistence", + "layer": "Persistence & Domain Data", + "intent": "The symbol Project is defined in schema.prisma as a data model.", + }, + } + graph.add_nodes_from((node_id, data) for node_id, data in nodes_by_id.items()) + workflow = { + "id": "flow_project_prompt", + "steps": [ + {"node_id": node_id, "symbol": node["label"], "file": node["file"], "intent": node["intent"]} + for node_id, node in nodes_by_id.items() + ], + } + + process = build_workflow_process(str(tmp_path), workflow, graph, nodes_by_id) + labels = [e["label"] for e in process["elements"] if e.get("node_id")] + + assert labels == ["Create container", "Handle create container request", "Store project data"] + assert all(not label.startswith("The symbol") for label in labels) + + def test_enrichment_round_trip_stores_phrases_against_their_code(tmp_path): from tldrgraph.bpmn_enrichment import apply_response, collect_candidates, load_store, write_request diff --git a/tests/test_visualizer.py b/tests/test_visualizer.py index ab9cfd5..bd3260a 100644 --- a/tests/test_visualizer.py +++ b/tests/test_visualizer.py @@ -48,6 +48,8 @@ def test_generate_visualizer_html_file(mini_repo): assert "TLDRGraph" in content assert "findRouteRegistration" in content assert "route_path" in content + assert "normalizeFileHighlight" in content + assert "openFileViewer(hit.file, { start: start, end: end });" in content def test_payload_carries_source_pointers_not_source_text(mini_repo): diff --git a/tldrgraph/agent_commands.py b/tldrgraph/agent_commands.py index e6f39b5..c111b2b 100644 --- a/tldrgraph/agent_commands.py +++ b/tldrgraph/agent_commands.py @@ -149,16 +149,16 @@ class AgentTarget: tldrgraph init ``` -By default TLDRGraph detects `claude`, `cursor-agent`, or `gemini`, asks once -before enrichment token spend, processes 200-node batches, and builds embeddings. -Use `--yes` for approval, `--batch N` for batch size, `--embeddings off|auto|on`, -`--no-llm-links` to skip LLM frontend/backend route inference, or `--no-agent-cli` -for manual file handoff. +By default TLDRGraph detects `claude`, `cursor-agent`, or `gemini`, processes +200-node batches, and builds embeddings. In a detected coding-agent session, +plain `tldrgraph init` auto-approves the full enrichment campaign; normal +terminal users and non-agent automation still get the confirmation gate. -After approval, use exactly `tldrgraph init --yes`; later continuation runs must -continue without asking the user again. `--batch 200` means all nodes in chunks; -`--limit 200` means stop after only 200 nodes. Never add `--limit` or -`--embeddings off` unless the user explicitly requests it. +Use exactly `tldrgraph init` for this workflow. Add `--yes` only if a non-agent +`needs_confirmation` response explicitly asks for approval. `--batch 200` means +all nodes in chunks; `--limit 200` means stop after only 200 nodes. +Never add `--limit`, `--no-llm-links`, or `--embeddings off` unless the user +explicitly requests it. If no supported agent is available or dense embeddings cannot be built, `init` preserves the graph and prints a resumable status. It never guesses source intent, @@ -201,9 +201,9 @@ class AgentTarget: ## `status: needs_confirmation` -The output shows how many nodes need enrichment and how many agent round-trips -that implies. **Ask the user whether to proceed, and show them that estimate.** -Do not decide for them. +Detected coding-agent sessions should not reach this state for a full run. If a +non-agent run does, the output shows how many nodes need enrichment and how many +agent round-trips that implies; ask the user whether to proceed. - They agree: `tldrgraph init --yes` saves approval for the full campaign - Smaller first pass: `tldrgraph init --yes --limit 100` @@ -232,7 +232,7 @@ class AgentTarget: count as sentences. 4. Run `tldrgraph init` again. Approval is saved; process any next - `needs_enrichment` batch immediately until `status: done` or `needs_llm_links`. + `needs_enrichment` batch immediately without asking the user again. Inside an existing Codex/Claude/Cursor session, nested-agent protection may stop the CLI from launching a second agent. In that case **you are the enrichment @@ -251,7 +251,8 @@ class AgentTarget: files, then write `.tldrgraph/llm_links_response.yaml` as a YAML list of `{source, target, confidence, frontend_evidence, backend_evidence, explanation}`. Only include source-backed links with file and line evidence. Run `tldrgraph init` -again, or use `--no-llm-links` to skip this optional stage. +again. This is a required continuation state for a complete init run unless the +user explicitly requested `--no-llm-links`. ## Once it says DONE diff --git a/tldrgraph/cli_llm_links.py b/tldrgraph/cli_llm_links.py index 6c8d717..4b5481f 100644 --- a/tldrgraph/cli_llm_links.py +++ b/tldrgraph/cli_llm_links.py @@ -100,7 +100,7 @@ def run_llm_link_step( except agent_runner.AgentError as err: if not as_json: click.echo(f" ⚠️ LLM route link inference failed: {err}") - return None + click.echo(" Falling back to the required file handoff.") req_path = write_payload(state_path(path, REQUEST_FILENAME), { "schema": "codechakra/llm-route-links-request@1", @@ -114,13 +114,11 @@ def run_llm_link_step( "payload": payload, }) emit_status(STATUS_NEEDS_LLM_LINKS, "llm_links", [ - "The graph is built and enriched. LLM frontend-backend link inference needs the active agent:", + "The graph is built and enriched. Required LLM frontend-backend link inference needs the active agent:", "", f" 1. Read {os.path.relpath(req_path, root)}", " 2. Open the referenced frontend and backend source files.", f" 3. Write .tldrgraph/{RESPONSE_FILENAME} with strict evidence for every link.", " 4. Run: tldrgraph init", - "", - "Use --no-llm-links to skip this optional inference stage.", ], progress={"candidate_hash": candidate_hash}, as_json=as_json) return STATUS_NEEDS_LLM_LINKS diff --git a/tldrgraph/cli_pipeline.py b/tldrgraph/cli_pipeline.py index de8999b..a8c7194 100644 --- a/tldrgraph/cli_pipeline.py +++ b/tldrgraph/cli_pipeline.py @@ -151,7 +151,8 @@ def _run_agent_cli_enrichment( status = STATUS_NEEDS_ENRICHMENT if rem else ( STATUS_NEEDS_EMBEDDINGS if embedding_error else STATUS_DONE ) - retry = ["Run `tldrgraph init --yes` to continue."] if rem or embedding_error else [] + resume = "tldrgraph init" if progress.get("approval_persisted") else "tldrgraph init --yes" + retry = [f"Run `{resume}` to continue."] if rem or embedding_error else [] emit_status(status, "embeddings" if embedding_error else "enrichment", [ f"Enriched {totals['applied']} node(s) in {totals['batches']} batch(es); {totals['bridges']} bridge edge(s).", f"⚠️ {totals['intent_length_violations']} intent(s) were outside the recommended 2-3 sentences." if totals["intent_length_violations"] else "All applied intents met the recommended 2-3 sentence length.", @@ -196,7 +197,7 @@ def _emit_enrichment_done(loader: GraphLoader, total: int, enriched: int, exclud if embedding_error: lines.extend([ f"Dense embeddings could not be completed: {embedding_error}", - "Run `tldrgraph init --yes` again after fixing model access.", + "Run `tldrgraph init` again after fixing model access.", ]) else: lines.extend([ @@ -254,12 +255,16 @@ def _handle_enrichment_step( "approval_persisted": enrichment_approval_is_active(path, candidates), } - authorized = assume_yes or enrichment_approval_is_active(path, candidates) + agent_marker = agent_runner.nesting_marker() + auto_agent_approved = bool(agent_marker) and not max_nodes + authorized = assume_yes or enrichment_approval_is_active(path, candidates) or auto_agent_approved if not authorized: conf_status = _check_confirmation(candidates, total, enriched, excluded, rounds, batch_size, progress, as_json) if conf_status: return conf_status authorized = True + elif auto_agent_approved and not assume_yes and not as_json: + click.echo(f"🤖 Detected coding-agent session (${agent_marker}); proceeding with full enrichment.") if authorized and not max_nodes: remember_full_enrichment_approval(path, candidates) diff --git a/tldrgraph/installer_contract.py b/tldrgraph/installer_contract.py index cff1ec9..dcf52fb 100644 --- a/tldrgraph/installer_contract.py +++ b/tldrgraph/installer_contract.py @@ -35,15 +35,16 @@ def generate_layers_prose(registry: Optional[LayerRegistry] = None) -> str: _LOOP = """One command does everything -- layers, extraction, enrichment, LLM route links, and embeddings: ```bash -tldrgraph init # interactive: asks once before enrichment token spend -tldrgraph init --yes # approve every current candidate until the campaign is done +tldrgraph init # agents auto-approve full runs; terminals may ask once +tldrgraph init --yes # non-agent explicit approval when confirmation asks for it ``` `init` automatically detects a supported agent CLI, uses 200-node enrichment batches, infers evidence-backed frontend/backend route links, and downloads/builds dense embeddings. It never guesses: when no agent is usable it prints a manual layer, enrichment, or route-link handoff. -Full approval is persisted across continuation runs. In a nested coding-agent session, +Full approval is persisted across continuation runs. In a coding-agent session, +plain `tldrgraph init` approves the full campaign and nested-agent protection means the host agent must read, answer, and apply every 200-node batch without asking again. `--batch 200` means all nodes in chunks; `--limit 200` means only 200 total. Never add `--limit`, `--no-llm-links`, or `--embeddings off` unless the user explicitly requests it. @@ -73,6 +74,8 @@ def generate_layers_prose(registry: Optional[LayerRegistry] = None) -> str: - **Complete `needs_llm_links` when shown.** Read `.tldrgraph/llm_links_request.yaml`, open the referenced frontend/backend files, and write `.tldrgraph/llm_links_response.yaml` with `{source, target, confidence, frontend_evidence, backend_evidence, explanation}`. + This is required for a complete init run unless the user explicitly requested + `--no-llm-links`. - **Continue after approval until `status: done`.** A `needs_enrichment` batch is work to process, not a reason to ask again. Do not add `--limit`, `--no-llm-links`, or `--embeddings off`.""" diff --git a/tldrgraph/visualizer/action_labels.py b/tldrgraph/visualizer/action_labels.py new file mode 100644 index 0000000..8497edb --- /dev/null +++ b/tldrgraph/visualizer/action_labels.py @@ -0,0 +1,81 @@ +"""Plain-language labels for workflow nodes.""" + +from __future__ import annotations + +import re +from typing import Any, Dict + +HTTP_VERBS = {"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"} +ACTION_WORDS = {"create", "update", "delete", "remove", "list", "fetch", "get", "load", "save"} + + +def _words(text: str) -> str: + cleaned = re.sub(r"\(.*?\)", "", str(text or "")).strip().split(".")[-1].lstrip("_") + cleaned = re.sub(r"(?<=[a-z0-9])(?=[A-Z])", " ", cleaned).replace("_", " ").replace("-", " ") + return " ".join(w for w in cleaned.split() if w) + + +def humanize_symbol(text: str, default: str = "Do the work") -> str: + words = _words(text) + if not words: + return default + if words.lower().endswith(" page"): + words = "Show " + words[:-5] + elif not words.split()[0].lower() in ACTION_WORDS: + words = "Use " + words + return words[:1].upper() + words[1:] + + +def _usable_intent(text: str) -> str: + normalized = " ".join(str(text or "").split()) + if not normalized: + return "" + lower = normalized.lower() + bad = ( + lower.startswith(("the symbol ", "this symbol ", "core module symbol")), + " is defined in " in lower, + " located at " in lower, + "visible in that source module" in lower, + ) + if any(bad): + return "" + first = re.split(r"(?<=[.!?])\s+", normalized, 1)[0].strip(" .") + first = re.sub(r"^(This|The)\s+(function|method|class|component|route|endpoint|schema)\s+", "", first, flags=re.I) + first = re.sub(r"^(It|This)\s+", "", first, flags=re.I) + return first[:1].upper() + first[1:] if first else "" + + +def _route_action(label: str) -> str: + match = re.search(r"\b(" + "|".join(HTTP_VERBS) + r")\s+(/[^\s]+)", str(label or ""), re.I) + if not match: + return "" + method, path = match.group(1).upper(), match.group(2) + parts = [p for p in re.split(r"/+", path) if p and not p.startswith(":") and "{" not in p] + if not parts: + return "Handle web request" + last = parts[-1].lower() + resource = parts[-2] if last in ACTION_WORDS and len(parts) > 1 else parts[-1] + resource = _words(resource).lower().rstrip("s") or "item" + verb = last if last in ACTION_WORDS else { + "GET": "fetch", "POST": "create", "PUT": "update", "PATCH": "update", + "DELETE": "delete", + }.get(method, "handle") + action = f"{verb} {resource}".strip() + if "handler" in label.lower(): + action = "handle " + action + " request" + return action[:1].upper() + action[1:] + + +def action_label(node: Dict[str, Any], fallback: str = "") -> str: + intent = _usable_intent(str(node.get("intent") or node.get("summary") or "")) + if intent: + return intent + label = str(node.get("label") or node.get("symbol") or fallback or "") + route = _route_action(label) + if route: + return route + file_path = str(node.get("file") or "").lower() + layer = str(node.get("layer") or node.get("layer_id") or "").lower() + if ("schema.prisma" in file_path or "data" in layer or "persistence" in layer) and _words(label): + return f"Store {_words(label).lower()} data" + return humanize_symbol(label or fallback) diff --git a/tldrgraph/visualizer/assets/app.js b/tldrgraph/visualizer/assets/app.js index 8f9db0d..2abfd76 100644 --- a/tldrgraph/visualizer/assets/app.js +++ b/tldrgraph/visualizer/assets/app.js @@ -1630,9 +1630,12 @@ function paintSymbolCard(c, n, opts) { c.font = 'bold 12px -apple-system, sans-serif'; c.fillText(truncateText(n.display_label || n.label, w - titleRight, c), left + 34, top + 18); - c.fillStyle = '#94a3b8'; - c.font = '10px monospace'; - c.fillText(truncateText(baseName(n.file), w - 74, c), left + 9, top + 38); + const metaText = o.metaText === undefined ? baseName(n.file) : o.metaText; + if (metaText) { + c.fillStyle = '#94a3b8'; + c.font = '10px monospace'; + c.fillText(truncateText(metaText, w - 74, c), left + 9, top + 38); + } c.textAlign = 'right'; c.font = '9px monospace'; @@ -2740,7 +2743,6 @@ function renderWorkflowsList() {