pythia-plsql 0.1.2__tar.gz → 0.2.0__tar.gz

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 (39) hide show
  1. {pythia_plsql-0.1.2/scripts/pythia_plsql.egg-info → pythia_plsql-0.2.0}/PKG-INFO +13 -8
  2. {pythia_plsql-0.1.2 → pythia_plsql-0.2.0}/README.md +12 -7
  3. {pythia_plsql-0.1.2 → pythia_plsql-0.2.0}/pyproject.toml +1 -1
  4. {pythia_plsql-0.1.2 → pythia_plsql-0.2.0}/scripts/pythia.py +152 -17
  5. {pythia_plsql-0.1.2 → pythia_plsql-0.2.0/scripts/pythia_plsql.egg-info}/PKG-INFO +13 -8
  6. {pythia_plsql-0.1.2 → pythia_plsql-0.2.0}/scripts/pythia_plsql.egg-info/SOURCES.txt +10 -10
  7. {pythia_plsql-0.1.2/skills/plsql-apply → pythia_plsql-0.2.0/skills/pythia-apply}/SKILL.md +5 -5
  8. {pythia_plsql-0.1.2/skills/plsql-explore → pythia_plsql-0.2.0/skills/pythia-explore}/SKILL.md +2 -2
  9. {pythia_plsql-0.1.2/skills/plsql-impact → pythia_plsql-0.2.0/skills/pythia-impact}/SKILL.md +4 -4
  10. {pythia_plsql-0.1.2/skills/plsql-review → pythia_plsql-0.2.0/skills/pythia-review}/SKILL.md +4 -4
  11. {pythia_plsql-0.1.2/skills/plsql-setup → pythia_plsql-0.2.0/skills/pythia-setup}/SKILL.md +2 -2
  12. {pythia_plsql-0.1.2/skills/plsql-skill-author → pythia_plsql-0.2.0/skills/pythia-skill-author}/SKILL.md +2 -2
  13. {pythia_plsql-0.1.2/skills/plsql-write → pythia_plsql-0.2.0/skills/pythia-write}/SKILL.md +5 -5
  14. {pythia_plsql-0.1.2 → pythia_plsql-0.2.0}/tests/test_install.py +22 -10
  15. {pythia_plsql-0.1.2 → pythia_plsql-0.2.0}/tests/test_phase1.py +39 -0
  16. {pythia_plsql-0.1.2 → pythia_plsql-0.2.0}/tests/test_phase3.py +69 -0
  17. {pythia_plsql-0.1.2 → pythia_plsql-0.2.0}/tests/test_phase5.py +8 -8
  18. {pythia_plsql-0.1.2 → pythia_plsql-0.2.0}/LICENSE +0 -0
  19. {pythia_plsql-0.1.2 → pythia_plsql-0.2.0}/queries/compile-errors.sql +0 -0
  20. {pythia_plsql-0.1.2 → pythia_plsql-0.2.0}/queries/dependencies.sql +0 -0
  21. {pythia_plsql-0.1.2 → pythia_plsql-0.2.0}/queries/impact.sql +0 -0
  22. {pythia_plsql-0.1.2 → pythia_plsql-0.2.0}/queries/invalid-objects.sql +0 -0
  23. {pythia_plsql-0.1.2 → pythia_plsql-0.2.0}/queries/name-occupants.sql +0 -0
  24. {pythia_plsql-0.1.2 → pythia_plsql-0.2.0}/queries/object-source.sql +0 -0
  25. {pythia_plsql-0.1.2 → pythia_plsql-0.2.0}/queries/plscope-enabled.sql +0 -0
  26. {pythia_plsql-0.1.2 → pythia_plsql-0.2.0}/queries/plscope-statements.sql +0 -0
  27. {pythia_plsql-0.1.2 → pythia_plsql-0.2.0}/queries/plscope-usages.sql +0 -0
  28. {pythia_plsql-0.1.2 → pythia_plsql-0.2.0}/queries/session-privileges.sql +0 -0
  29. {pythia_plsql-0.1.2 → pythia_plsql-0.2.0}/queries/similar-candidates.sql +0 -0
  30. {pythia_plsql-0.1.2 → pythia_plsql-0.2.0}/queries/source.sql +0 -0
  31. {pythia_plsql-0.1.2 → pythia_plsql-0.2.0}/scripts/pythia_plsql.egg-info/dependency_links.txt +0 -0
  32. {pythia_plsql-0.1.2 → pythia_plsql-0.2.0}/scripts/pythia_plsql.egg-info/entry_points.txt +0 -0
  33. {pythia_plsql-0.1.2 → pythia_plsql-0.2.0}/scripts/pythia_plsql.egg-info/requires.txt +0 -0
  34. {pythia_plsql-0.1.2 → pythia_plsql-0.2.0}/scripts/pythia_plsql.egg-info/top_level.txt +0 -0
  35. {pythia_plsql-0.1.2 → pythia_plsql-0.2.0}/setup.cfg +0 -0
  36. {pythia_plsql-0.1.2/skills/plsql-explore → pythia_plsql-0.2.0/skills/pythia-explore}/reference/data-dictionary.md +0 -0
  37. {pythia_plsql-0.1.2/skills/plsql-review → pythia_plsql-0.2.0/skills/pythia-review}/reference/antipatterns.md +0 -0
  38. {pythia_plsql-0.1.2/skills/plsql-write → pythia_plsql-0.2.0/skills/pythia-write}/reference/patterns.md +0 -0
  39. {pythia_plsql-0.1.2 → pythia_plsql-0.2.0}/tests/test_phase2.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythia-plsql
3
- Version: 0.1.2
3
+ Version: 0.2.0
4
4
  Summary: PL/SQL development for AI agents on Oracle Database - expert data-dictionary queries, impact analysis, and a snapshot-verified write path with honest rollback.
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/thaildhe172591/pythia
@@ -82,7 +82,8 @@ pythia check # fill in connections.json first, then verify
82
82
  The pip package is the whole kit: with Node.js present, `pythia install`
83
83
  runs `npx skills add` (77 agents, symlinked updates; `--source <git-url>`
84
84
  for internal mirrors) — without Node it copies the bundled skill pack into
85
- `.claude/skills/` and `.agents/skills/`. Skills alone: `npx skills add
85
+ `.agents/skills/`, the universal layout Claude Code, Codex and Cursor all
86
+ read (one copy, so no skill shows up twice). Skills alone: `npx skills add
86
87
  thaildhe172591/pythia`, or `/plugin marketplace add thaildhe172591/pythia`.
87
88
 
88
89
  `pip install` is **once per machine**; `pythia install` is **once per
@@ -103,7 +104,7 @@ and WSL are all CI-tested.
103
104
  | `ls` find objects | `impact` what depends on it | `journal` list · diff · export · restore |
104
105
  | `src` source, compiler line numbers | `errors` compile errors, line:col | `policy` show · set |
105
106
  | `args` signatures | `invalid` everything broken | `unistr` exact non-ASCII literals |
106
- | `ddl` via DBMS_METADATA | `plscope` exact identifier usages | |
107
+ | `ddl` via DBMS_METADATA | `plscope` exact identifier usages | `agent-user` least-privilege setup |
107
108
  | `cols` columns + types | `similar` programs named like this | |
108
109
  | `grep` search all source | | |
109
110
  | `sql` free query (SELECT/WITH only) | | |
@@ -120,8 +121,12 @@ every agent read them first (`pythia conventions` shows both).
120
121
 
121
122
  **The account is the real security layer** — the policy file is an application-side
122
123
  fence. Give the agent its own revocable credential with proxy authentication
123
- (`agent_user[schema_owner]`, no `ANY` privileges, no owner password shared):
124
- see [`examples/agent-user-setup.example.sql`](examples/agent-user-setup.example.sql).
124
+ (`agent_user[schema_owner]`, no `ANY` privileges, no owner password shared).
125
+ `pythia agent-user` generates that SQL for your current connection — password
126
+ included, `--save` drops the credential into `connections.json` — but it is
127
+ optional convenience: running
128
+ [`examples/agent-user-setup.example.sql`](examples/agent-user-setup.example.sql)
129
+ by hand works just as well.
125
130
  `pythia check` warns when the session runs with more power than the task needs.
126
131
 
127
132
  Per-group write policy, `.pythia/policy.json` (defaults shown):
@@ -146,10 +151,10 @@ it audits every interaction in `DBTOOLS$MCP_LOG`. **Writes never do** — only
146
151
 
147
152
  Seven skills teach the agent the workflow — superpowers-style gates, not suggestions:
148
153
 
149
- `plsql-setup` · `plsql-explore` · `plsql-impact` (impact **before** any change) ·
150
- `plsql-write` (copy the codebase's conventions) · `plsql-apply` (the gate: the
154
+ `pythia-setup` · `pythia-explore` · `pythia-impact` (impact **before** any change) ·
155
+ `pythia-write` (copy the codebase's conventions) · `pythia-apply` (the gate: the
151
156
  developer sees the preview and approves in chat before anything is written) ·
152
- `plsql-review` (seven antipatterns) · `plsql-skill-author` (capture *your team's*
157
+ `pythia-review` (seven antipatterns) · `pythia-skill-author` (capture *your team's*
153
158
  workflow as a new skill, mined from the live schema).
154
159
 
155
160
  ## Compatibility
@@ -68,7 +68,8 @@ pythia check # fill in connections.json first, then verify
68
68
  The pip package is the whole kit: with Node.js present, `pythia install`
69
69
  runs `npx skills add` (77 agents, symlinked updates; `--source <git-url>`
70
70
  for internal mirrors) — without Node it copies the bundled skill pack into
71
- `.claude/skills/` and `.agents/skills/`. Skills alone: `npx skills add
71
+ `.agents/skills/`, the universal layout Claude Code, Codex and Cursor all
72
+ read (one copy, so no skill shows up twice). Skills alone: `npx skills add
72
73
  thaildhe172591/pythia`, or `/plugin marketplace add thaildhe172591/pythia`.
73
74
 
74
75
  `pip install` is **once per machine**; `pythia install` is **once per
@@ -89,7 +90,7 @@ and WSL are all CI-tested.
89
90
  | `ls` find objects | `impact` what depends on it | `journal` list · diff · export · restore |
90
91
  | `src` source, compiler line numbers | `errors` compile errors, line:col | `policy` show · set |
91
92
  | `args` signatures | `invalid` everything broken | `unistr` exact non-ASCII literals |
92
- | `ddl` via DBMS_METADATA | `plscope` exact identifier usages | |
93
+ | `ddl` via DBMS_METADATA | `plscope` exact identifier usages | `agent-user` least-privilege setup |
93
94
  | `cols` columns + types | `similar` programs named like this | |
94
95
  | `grep` search all source | | |
95
96
  | `sql` free query (SELECT/WITH only) | | |
@@ -106,8 +107,12 @@ every agent read them first (`pythia conventions` shows both).
106
107
 
107
108
  **The account is the real security layer** — the policy file is an application-side
108
109
  fence. Give the agent its own revocable credential with proxy authentication
109
- (`agent_user[schema_owner]`, no `ANY` privileges, no owner password shared):
110
- see [`examples/agent-user-setup.example.sql`](examples/agent-user-setup.example.sql).
110
+ (`agent_user[schema_owner]`, no `ANY` privileges, no owner password shared).
111
+ `pythia agent-user` generates that SQL for your current connection — password
112
+ included, `--save` drops the credential into `connections.json` — but it is
113
+ optional convenience: running
114
+ [`examples/agent-user-setup.example.sql`](examples/agent-user-setup.example.sql)
115
+ by hand works just as well.
111
116
  `pythia check` warns when the session runs with more power than the task needs.
112
117
 
113
118
  Per-group write policy, `.pythia/policy.json` (defaults shown):
@@ -132,10 +137,10 @@ it audits every interaction in `DBTOOLS$MCP_LOG`. **Writes never do** — only
132
137
 
133
138
  Seven skills teach the agent the workflow — superpowers-style gates, not suggestions:
134
139
 
135
- `plsql-setup` · `plsql-explore` · `plsql-impact` (impact **before** any change) ·
136
- `plsql-write` (copy the codebase's conventions) · `plsql-apply` (the gate: the
140
+ `pythia-setup` · `pythia-explore` · `pythia-impact` (impact **before** any change) ·
141
+ `pythia-write` (copy the codebase's conventions) · `pythia-apply` (the gate: the
137
142
  developer sees the preview and approves in chat before anything is written) ·
138
- `plsql-review` (seven antipatterns) · `plsql-skill-author` (capture *your team's*
143
+ `pythia-review` (seven antipatterns) · `pythia-skill-author` (capture *your team's*
139
144
  workflow as a new skill, mined from the live schema).
140
145
 
141
146
  ## Compatibility
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pythia-plsql"
7
- version = "0.1.2"
7
+ version = "0.2.0"
8
8
  description = "PL/SQL development for AI agents on Oracle Database - expert data-dictionary queries, impact analysis, and a snapshot-verified write path with honest rollback."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -43,6 +43,7 @@ you always know whether you saw everything.
43
43
  pythia policy
44
44
  pythia install
45
45
  pythia unistr "Nhóm không được để trống"
46
+ pythia agent-user --save
46
47
  """
47
48
  import argparse
48
49
  import json
@@ -535,7 +536,7 @@ POLICY_DEFAULTS = {"plsql_source": "confirm", "data_dml": "deny",
535
536
  "structural": "deny", "grants": "deny", "session": "allow"}
536
537
 
537
538
  ROLLBACK_TABLE = """\
538
- Is rollback real? (this table also appears in README.md and plsql-apply)
539
+ Is rollback real? (this table also appears in README.md and pythia-apply)
539
540
  plsql_source Yes - completely. Source is recoverable from ALL_SOURCE.
540
541
  data_dml No. After commit only Flashback Query remains, within undo retention.
541
542
  structural Almost never. DROP COLUMN is permanent; a dropped table may be in the Recycle Bin.
@@ -585,6 +586,17 @@ def load_policy(root):
585
586
  sys.exit(f"Cannot parse {path}: {e}")
586
587
 
587
588
 
589
+ def load_settings(root):
590
+ """Optional .pythia/settings.json — an absent file means defaults."""
591
+ path = pathlib.Path(root) / CONFIG_DIR / "settings.json"
592
+ if not path.is_file():
593
+ return {}
594
+ try:
595
+ return json.loads(path.read_text(encoding="utf-8"))
596
+ except ValueError as e:
597
+ sys.exit(f"Cannot parse {path}: {e}")
598
+
599
+
588
600
  def journal_root(root):
589
601
  return pathlib.Path(root) / CONFIG_DIR / "journal"
590
602
 
@@ -1013,6 +1025,19 @@ def privilege_warning(conn, schema, conn_user):
1013
1025
  this session is running with more power than the task needs."""
1014
1026
  _, rows = run_query(conn, load_query("session-privileges.sql"))
1015
1027
  dangerous = [r[0] for r in rows]
1028
+ with conn.cursor() as cur:
1029
+ cur.execute("select sys_context('userenv','proxy_user') from dual")
1030
+ prow = cur.fetchall()
1031
+ proxy = prow[0][0] if prow and prow[0] and prow[0][0] else None
1032
+ if proxy:
1033
+ # sanctioned least-privilege entrance; any excess power is the
1034
+ # OWNER's, inherited by every proxied session — say that
1035
+ if not dangerous:
1036
+ return None
1037
+ return (f"! Proxy session ({proxy} → {schema}), but the owner holds "
1038
+ f"{', '.join(dangerous[:3])}" + ("…" if len(dangerous) > 3 else "")
1039
+ + ":\n every proxied session inherits this — trim the "
1040
+ "owner's grants; pythia policy explains what is at stake.")
1016
1041
  owner = bool(conn_user) and conn_user.upper() == schema
1017
1042
  if not owner and not dangerous:
1018
1043
  return None
@@ -1150,6 +1175,14 @@ def run_apply(conn, schema, ns, file_text, origin=None):
1150
1175
  return 0
1151
1176
 
1152
1177
  # 4. APPLY
1178
+ if group == "plsql_source" and load_settings(ns.project_root).get(
1179
+ "plscope_on_apply", True):
1180
+ # session-scoped: every object applied through pythia builds the
1181
+ # PL/Scope index as a side effect; opt out with
1182
+ # {"plscope_on_apply": false} in .pythia/settings.json
1183
+ with conn.cursor() as cur:
1184
+ cur.execute("ALTER SESSION SET plscope_settings = "
1185
+ "'IDENTIFIERS:ALL, STATEMENTS:ALL'")
1153
1186
  with conn.cursor() as cur:
1154
1187
  cur.execute(stmt)
1155
1188
 
@@ -1263,9 +1296,22 @@ def cmd_journal(conn, schema, ns):
1263
1296
  state = "applied" if meta.get("applied") else "preview"
1264
1297
  print(f"{eid} [{state}]")
1265
1298
  return
1299
+ if ns.action == "prune":
1300
+ import shutil
1301
+ removed = 0
1302
+ for eid in list_journal_entries(root):
1303
+ if not read_journal_entry(root, eid)["meta"].get("applied"):
1304
+ shutil.rmtree(journal_root(root) / eid)
1305
+ removed += 1
1306
+ if ns.json:
1307
+ print(json.dumps({"pruned": removed}))
1308
+ else:
1309
+ print(f"-- pruned {removed} preview-only entries; applied entries "
1310
+ "(the real snapshots) are all kept")
1311
+ return
1266
1312
  if not ns.id:
1267
1313
  sys.exit(f"Usage: {invocation()} journal "
1268
- "{list | show <id> | diff <id> | export <id> | restore <id>}")
1314
+ "{list | show <id> | diff <id> | export <id> | prune | restore <id>}")
1269
1315
  e = read_journal_entry(root, ns.id)
1270
1316
  if ns.action == "show":
1271
1317
  print(json.dumps(e["meta"], indent=2))
@@ -1301,6 +1347,76 @@ def cmd_conventions(conn, schema, ns):
1301
1347
  print(f"\nProse rules for agents: {md}")
1302
1348
 
1303
1349
 
1350
+ AGENT_USER_SQL = """\
1351
+ -- Least-privilege agent credential for schema {owner} — run as a DBA.
1352
+ -- Proxy authentication: the agent logs in with its OWN password but works
1353
+ -- inside {owner}; it never learns the owner password, owns nothing, and
1354
+ -- revocation is one statement. Deliberately absent: DBA, RESOURCE, ANY
1355
+ -- privileges, utility grants — the agent needs none of them to develop
1356
+ -- PL/SQL, and every extra grant widens the blast radius.
1357
+
1358
+ CREATE USER {agent} IDENTIFIED BY "{password}";
1359
+ GRANT CREATE SESSION TO {agent};
1360
+ ALTER USER {owner} GRANT CONNECT THROUGH {agent};
1361
+
1362
+ -- Cut the agent off later (owner untouched):
1363
+ -- ALTER USER {owner} REVOKE CONNECT THROUGH {agent};
1364
+ -- Fresh owner schema instead? See examples/agent-user-setup.example.sql.
1365
+ """
1366
+
1367
+
1368
+ def agent_password():
1369
+ """Random password satisfying common Oracle verify functions: letter
1370
+ first, upper+lower+digit+special, 16+ chars. Always quoted in SQL."""
1371
+ import secrets
1372
+ import string
1373
+ body = "".join(secrets.choice(string.ascii_letters + string.digits)
1374
+ for _ in range(12))
1375
+ return "Ag" + body + "#7"
1376
+
1377
+
1378
+ def save_agent_connection(root, base_name, base, agent, password):
1379
+ """Add <base>_agent alongside the owner entry (never overwrite it) and
1380
+ make it the default. Returns the new entry name."""
1381
+ path = pathlib.Path(root) / CONFIG_DIR / CONFIG_NAME
1382
+ cfg = json.loads(path.read_text(encoding="utf-8"))
1383
+ owner = (base.get("schema") or base["user"]).upper()
1384
+ entry = dict(base)
1385
+ entry["user"] = f"{agent.lower()}[{owner.lower()}]"
1386
+ entry["password"] = password
1387
+ entry["schema"] = owner
1388
+ name = f"{base_name}_agent"
1389
+ cfg[name] = entry
1390
+ cfg["default"] = name
1391
+ path.write_text(json.dumps(cfg, indent=2, ensure_ascii=False) + "\n",
1392
+ encoding="utf-8")
1393
+ return name
1394
+
1395
+
1396
+ def cmd_agent_user(conn, schema, ns):
1397
+ cwd = pathlib.Path.cwd()
1398
+ cfg, root = find_config(cwd, os.environ)
1399
+ base_name, base = resolve_connection(cfg, ns.conn, os.environ, cwd, root)
1400
+ owner = (base.get("schema") or base["user"]).upper()
1401
+ if "[" in base["user"]:
1402
+ sys.exit(f"Connection {base_name} already uses a proxy user "
1403
+ f"({base['user']}) — nothing to set up.")
1404
+ agent = (ns.name or f"{owner}_AGENT").upper()
1405
+ password = agent_password()
1406
+ print(AGENT_USER_SQL.format(owner=owner, agent=agent, password=password))
1407
+ if ns.save:
1408
+ name = save_agent_connection(root, base_name, base, agent, password)
1409
+ print(f"Saved connection '{name}' (now the default) in "
1410
+ f"{pathlib.Path(root) / CONFIG_DIR / CONFIG_NAME};\n"
1411
+ f"the owner entry '{base_name}' is untouched — switch back "
1412
+ f"any time with --conn {base_name}.")
1413
+ print(f"\nNext: run the SQL above as a DBA, then: {invocation()} check")
1414
+ else:
1415
+ print(f"-- Rerun with --save to add this credential to "
1416
+ f"connections.json as '{base_name}_agent' and make it the "
1417
+ f"default once the DBA has run it.")
1418
+
1419
+
1304
1420
  # unistr escapes. Single quote doubles to '' per SQL (a \' is ORA-01756 for
1305
1421
  # Oracle); backslash doubles for unistr itself.
1306
1422
  UNISTR_ESC = {0: r"\0", 8: r"\b", 9: r"\t", 10: r"\n", 13: r"\r",
@@ -1389,20 +1505,31 @@ def run_skills_add(source, interactive=False):
1389
1505
  return subprocess.run(cmd).returncode
1390
1506
 
1391
1507
 
1508
+ LEGACY_SKILLS = ("plsql-setup", "plsql-explore", "plsql-impact",
1509
+ "plsql-write", "plsql-apply", "plsql-review",
1510
+ "plsql-skill-author")
1511
+
1512
+
1392
1513
  def copy_bundled_skills(root):
1393
- """No-Node fallback: copy the wheel-bundled pack into the two
1394
- conventional project layouts Claude Code and the universal .agents one
1395
- (Codex, Cursor, Copilot, Gemini CLI and friends all read it)."""
1514
+ """No-Node fallback: copy the wheel-bundled pack into .agents/skills/
1515
+ ONLY the universal layout that Claude Code, Codex, Cursor, Copilot and
1516
+ friends all read. A second copy in .claude/skills/ would show every
1517
+ skill twice in Claude Code's menu. The copy merges: only the pack's own
1518
+ skills are refreshed, anything else in the directory is untouched.
1519
+ Stale copies under the pack's old plsql-* names are removed from both
1520
+ conventional roots (real directories only, never symlinks)."""
1396
1521
  import shutil
1397
- targets = []
1398
- for rel in (".claude/skills", ".agents/skills"):
1399
- dest_root = pathlib.Path(root) / rel
1400
- for pack in sorted(SKILLS_DIR.iterdir()):
1401
- if not (pack / "SKILL.md").is_file():
1402
- continue
1403
- shutil.copytree(pack, dest_root / pack.name, dirs_exist_ok=True)
1404
- targets.append(dest_root)
1405
- return targets
1522
+ dest_root = pathlib.Path(root) / ".agents" / "skills"
1523
+ for pack in sorted(SKILLS_DIR.iterdir()):
1524
+ if not (pack / "SKILL.md").is_file():
1525
+ continue
1526
+ shutil.copytree(pack, dest_root / pack.name, dirs_exist_ok=True)
1527
+ for rel in (".agents/skills", ".claude/skills"):
1528
+ for old_name in LEGACY_SKILLS:
1529
+ legacy = pathlib.Path(root) / rel / old_name
1530
+ if legacy.is_dir() and not legacy.is_symlink():
1531
+ shutil.rmtree(legacy)
1532
+ return [dest_root]
1406
1533
 
1407
1534
 
1408
1535
  def cmd_install(conn, schema, ns):
@@ -1430,9 +1557,10 @@ COMMANDS = {"check": cmd_check, "ls": cmd_ls, "src": cmd_src, "args": cmd_args,
1430
1557
  "impact": cmd_impact, "similar": cmd_similar, "plscope": cmd_plscope,
1431
1558
  "policy": cmd_policy, "journal": cmd_journal, "apply": cmd_apply,
1432
1559
  "conventions": cmd_conventions, "install": cmd_install,
1433
- "unistr": cmd_unistr}
1560
+ "unistr": cmd_unistr, "agent-user": cmd_agent_user}
1434
1561
 
1435
- NO_DB_COMMANDS = {"policy", "journal", "conventions", "install", "unistr"}
1562
+ NO_DB_COMMANDS = {"policy", "journal", "conventions", "install", "unistr",
1563
+ "agent-user"}
1436
1564
 
1437
1565
 
1438
1566
  # --- CLI ---------------------------------------------------------------------
@@ -1512,7 +1640,7 @@ def build_parser():
1512
1640
  s = sub.add_parser("journal", parents=[common()],
1513
1641
  help="list, inspect, export and restore write snapshots")
1514
1642
  s.add_argument("action", nargs="?",
1515
- choices=["list", "show", "diff", "export", "restore"],
1643
+ choices=["list", "show", "diff", "export", "prune", "restore"],
1516
1644
  default="list")
1517
1645
  s.add_argument("id", nargs="?")
1518
1646
  s.add_argument("--what", choices=["after", "before", "restore"],
@@ -1531,6 +1659,13 @@ def build_parser():
1531
1659
  help="impact depth for the preview (default 3)")
1532
1660
  sub.add_parser("conventions", parents=[common()],
1533
1661
  help="show the project's house-style naming patterns")
1662
+ s = sub.add_parser("agent-user", parents=[common()],
1663
+ help="SQL for a least-privilege proxy agent user; "
1664
+ "--save adds it to connections.json")
1665
+ s.add_argument("--name", help="agent user name (default <OWNER>_AGENT)")
1666
+ s.add_argument("--save", action="store_true",
1667
+ help="add the new credential as <conn>_agent and make it "
1668
+ "the default connection")
1534
1669
  s = sub.add_parser("unistr", parents=[common()],
1535
1670
  help="Oracle unistr('...') literal for non-ASCII text "
1536
1671
  "(Vietnamese messages stay exact)")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythia-plsql
3
- Version: 0.1.2
3
+ Version: 0.2.0
4
4
  Summary: PL/SQL development for AI agents on Oracle Database - expert data-dictionary queries, impact analysis, and a snapshot-verified write path with honest rollback.
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/thaildhe172591/pythia
@@ -82,7 +82,8 @@ pythia check # fill in connections.json first, then verify
82
82
  The pip package is the whole kit: with Node.js present, `pythia install`
83
83
  runs `npx skills add` (77 agents, symlinked updates; `--source <git-url>`
84
84
  for internal mirrors) — without Node it copies the bundled skill pack into
85
- `.claude/skills/` and `.agents/skills/`. Skills alone: `npx skills add
85
+ `.agents/skills/`, the universal layout Claude Code, Codex and Cursor all
86
+ read (one copy, so no skill shows up twice). Skills alone: `npx skills add
86
87
  thaildhe172591/pythia`, or `/plugin marketplace add thaildhe172591/pythia`.
87
88
 
88
89
  `pip install` is **once per machine**; `pythia install` is **once per
@@ -103,7 +104,7 @@ and WSL are all CI-tested.
103
104
  | `ls` find objects | `impact` what depends on it | `journal` list · diff · export · restore |
104
105
  | `src` source, compiler line numbers | `errors` compile errors, line:col | `policy` show · set |
105
106
  | `args` signatures | `invalid` everything broken | `unistr` exact non-ASCII literals |
106
- | `ddl` via DBMS_METADATA | `plscope` exact identifier usages | |
107
+ | `ddl` via DBMS_METADATA | `plscope` exact identifier usages | `agent-user` least-privilege setup |
107
108
  | `cols` columns + types | `similar` programs named like this | |
108
109
  | `grep` search all source | | |
109
110
  | `sql` free query (SELECT/WITH only) | | |
@@ -120,8 +121,12 @@ every agent read them first (`pythia conventions` shows both).
120
121
 
121
122
  **The account is the real security layer** — the policy file is an application-side
122
123
  fence. Give the agent its own revocable credential with proxy authentication
123
- (`agent_user[schema_owner]`, no `ANY` privileges, no owner password shared):
124
- see [`examples/agent-user-setup.example.sql`](examples/agent-user-setup.example.sql).
124
+ (`agent_user[schema_owner]`, no `ANY` privileges, no owner password shared).
125
+ `pythia agent-user` generates that SQL for your current connection — password
126
+ included, `--save` drops the credential into `connections.json` — but it is
127
+ optional convenience: running
128
+ [`examples/agent-user-setup.example.sql`](examples/agent-user-setup.example.sql)
129
+ by hand works just as well.
125
130
  `pythia check` warns when the session runs with more power than the task needs.
126
131
 
127
132
  Per-group write policy, `.pythia/policy.json` (defaults shown):
@@ -146,10 +151,10 @@ it audits every interaction in `DBTOOLS$MCP_LOG`. **Writes never do** — only
146
151
 
147
152
  Seven skills teach the agent the workflow — superpowers-style gates, not suggestions:
148
153
 
149
- `plsql-setup` · `plsql-explore` · `plsql-impact` (impact **before** any change) ·
150
- `plsql-write` (copy the codebase's conventions) · `plsql-apply` (the gate: the
154
+ `pythia-setup` · `pythia-explore` · `pythia-impact` (impact **before** any change) ·
155
+ `pythia-write` (copy the codebase's conventions) · `pythia-apply` (the gate: the
151
156
  developer sees the preview and approves in chat before anything is written) ·
152
- `plsql-review` (seven antipatterns) · `plsql-skill-author` (capture *your team's*
157
+ `pythia-review` (seven antipatterns) · `pythia-skill-author` (capture *your team's*
153
158
  workflow as a new skill, mined from the live schema).
154
159
 
155
160
  ## Compatibility
@@ -20,16 +20,16 @@ scripts/pythia_plsql.egg-info/dependency_links.txt
20
20
  scripts/pythia_plsql.egg-info/entry_points.txt
21
21
  scripts/pythia_plsql.egg-info/requires.txt
22
22
  scripts/pythia_plsql.egg-info/top_level.txt
23
- skills/plsql-apply/SKILL.md
24
- skills/plsql-explore/SKILL.md
25
- skills/plsql-explore/reference/data-dictionary.md
26
- skills/plsql-impact/SKILL.md
27
- skills/plsql-review/SKILL.md
28
- skills/plsql-review/reference/antipatterns.md
29
- skills/plsql-setup/SKILL.md
30
- skills/plsql-skill-author/SKILL.md
31
- skills/plsql-write/SKILL.md
32
- skills/plsql-write/reference/patterns.md
23
+ skills/pythia-apply/SKILL.md
24
+ skills/pythia-explore/SKILL.md
25
+ skills/pythia-explore/reference/data-dictionary.md
26
+ skills/pythia-impact/SKILL.md
27
+ skills/pythia-review/SKILL.md
28
+ skills/pythia-review/reference/antipatterns.md
29
+ skills/pythia-setup/SKILL.md
30
+ skills/pythia-skill-author/SKILL.md
31
+ skills/pythia-write/SKILL.md
32
+ skills/pythia-write/reference/patterns.md
33
33
  tests/test_install.py
34
34
  tests/test_phase1.py
35
35
  tests/test_phase2.py
@@ -1,11 +1,11 @@
1
1
  ---
2
- name: plsql-apply
2
+ name: pythia-apply
3
3
  description: Use when a PL/SQL change is ready to reach the database - applying a CREATE OR REPLACE, restoring from the journal, or any other write. Runs the six-step flow through pythia apply and enforces the gate - the developer sees the preview and approves in chat before anything is written, and a broken result is never reported as success.
4
4
  ---
5
5
 
6
6
  # Applying PL/SQL Changes
7
7
 
8
- **Announce at start:** "Using plsql-apply — I'll preview the change first."
8
+ **Announce at start:** "Using pythia-apply — I'll preview the change first."
9
9
 
10
10
  DDL in Oracle commits itself. There is no transaction to roll back — the
11
11
  snapshot pythia takes before writing is the only undo that exists. This skill
@@ -24,7 +24,7 @@ dependents.
24
24
  ## The Workflow
25
25
 
26
26
  **Before step 1:** if this conversation has not yet seen a standalone impact
27
- analysis for this object, run `plsql-impact` first. The `impact:` line inside
27
+ analysis for this object, run `pythia-impact` first. The `impact:` line inside
28
28
  apply's preview is confirmation of a number you already knew — if it is the
29
29
  first time anyone sees it, a step was skipped.
30
30
 
@@ -114,8 +114,8 @@ the stakes are highest.
114
114
 
115
115
  ## When NOT to use this skill
116
116
 
117
- - Reading or exploring — use `plsql-explore`.
118
- - Judging blast radius before editing — use `plsql-impact` (always run it
117
+ - Reading or exploring — use `pythia-explore`.
118
+ - Judging blast radius before editing — use `pythia-impact` (always run it
119
119
  before proposing a change; apply's preview is confirmation, not discovery).
120
120
  - Editing files the developer has not asked to land on the database yet.
121
121
 
@@ -1,11 +1,11 @@
1
1
  ---
2
- name: plsql-explore
2
+ name: pythia-explore
3
3
  description: Use when you need to understand anything in an Oracle schema - finding objects, reading PL/SQL source, signatures, table columns, DDL, searching code, or asking who uses what. The database is the only source of truth; repo dumps and exports drift and lie.
4
4
  ---
5
5
 
6
6
  # Exploring the Schema
7
7
 
8
- **Announce at start:** "Using plsql-explore — asking the database directly."
8
+ **Announce at start:** "Using pythia-explore — asking the database directly."
9
9
 
10
10
  ## The principle: ask the database, never the dump
11
11
 
@@ -1,11 +1,11 @@
1
1
  ---
2
- name: plsql-impact
2
+ name: pythia-impact
3
3
  description: Use before proposing or writing any change to an Oracle object - a procedure, package, function, trigger, view, or table. Impact analysis comes first, because what depends on an object decides how careful the change must be, and Oracle invalidates dependents in cascade.
4
4
  ---
5
5
 
6
6
  # Impact Before Change
7
7
 
8
- **Announce at start:** "Using plsql-impact to measure the blast radius first."
8
+ **Announce at start:** "Using pythia-impact to measure the blast radius first."
9
9
 
10
10
  Changing an Oracle object recompiles or invalidates everything that depends
11
11
  on it — immediately, schema-wide, for every user of a shared database. The
@@ -51,6 +51,6 @@ program that would be invalidated by an `ALTER`.
51
51
 
52
52
  ## Hand-off
53
53
 
54
- Impact known and acceptable → `plsql-write` to write the change following
55
- the codebase's conventions → `plsql-apply` to land it with preview and
54
+ Impact known and acceptable → `pythia-write` to write the change following
55
+ the codebase's conventions → `pythia-apply` to land it with preview and
56
56
  verification.
@@ -1,11 +1,11 @@
1
1
  ---
2
- name: plsql-review
2
+ name: pythia-review
3
3
  description: Use when reviewing PL/SQL - a changed procedure or package, a proposed file, or an object suspected of causing trouble. Combines the database's own correctness signals with the antipattern checklist, and reports findings anchored to line numbers.
4
4
  ---
5
5
 
6
6
  # Reviewing PL/SQL
7
7
 
8
- **Announce at start:** "Using plsql-review — checking the database's signals first."
8
+ **Announce at start:** "Using pythia-review — checking the database's signals first."
9
9
 
10
10
  Review in two passes: what the database *knows* is wrong, then what the
11
11
  checklist says is *likely* wrong. Machine signals first — they are free and
@@ -45,6 +45,6 @@ style).
45
45
 
46
46
  ## When NOT to use this skill
47
47
 
48
- - Reviewing whether a change is *safe to apply* — that is `plsql-impact`
49
- plus `plsql-apply`'s preview; this skill judges the code itself.
48
+ - Reviewing whether a change is *safe to apply* — that is `pythia-impact`
49
+ plus `pythia-apply`'s preview; this skill judges the code itself.
50
50
  - Reviewing non-PL/SQL application code — outside this skill's scope.
@@ -1,11 +1,11 @@
1
1
  ---
2
- name: plsql-setup
2
+ name: pythia-setup
3
3
  description: Use when setting pythia up for a project or a machine - writing connections.json, creating a least-privilege agent user, wiring the SQLcl MCP server, or diagnosing a connection that will not open. Also the place to start when check prints a privilege warning.
4
4
  ---
5
5
 
6
6
  # Setting Up
7
7
 
8
- **Announce at start:** "Using plsql-setup to configure the database access."
8
+ **Announce at start:** "Using pythia-setup to configure the database access."
9
9
 
10
10
  Set up in this order: connection first (everything else needs it), the
11
11
  least-privilege user second (the only protection that cannot be bypassed),
@@ -1,11 +1,11 @@
1
1
  ---
2
- name: plsql-skill-author
2
+ name: pythia-skill-author
3
3
  description: Use when the developer wants their own way of working captured as a new skill - "make a skill for how we do X", a house convention or preference worth teaching every future session, a team ritual, or the same correction arriving for the second time. Interviews the developer, mines the real conventions from the database, and writes a skill in this pack's format.
4
4
  ---
5
5
 
6
6
  # Authoring a New Skill
7
7
 
8
- **Announce at start:** "Using plsql-skill-author — let's capture how you actually work."
8
+ **Announce at start:** "Using pythia-skill-author — let's capture how you actually work."
9
9
 
10
10
  A skill is a decision captured so it never has to be re-argued. Capture what
11
11
  the team *actually does* — with evidence from the database — not what anyone
@@ -1,11 +1,11 @@
1
1
  ---
2
- name: plsql-write
2
+ name: pythia-write
3
3
  description: Use when writing or modifying PL/SQL source - a procedure, function, package, trigger, or view - after impact is known. The codebase's conventions already exist; copy them instead of inventing style, and anchor every type to the database's reality.
4
4
  ---
5
5
 
6
6
  # Writing PL/SQL
7
7
 
8
- **Announce at start:** "Using plsql-write — mining the codebase's conventions first."
8
+ **Announce at start:** "Using pythia-write — mining the codebase's conventions first."
9
9
 
10
10
  **Before anything:** if the project has `.pythia/conventions.md`, read it —
11
11
  house rules there outrank every generic pattern below, and
@@ -49,7 +49,7 @@ propose style changes to the developer separately, never silently.
49
49
 
50
50
  ## When NOT to use this skill
51
51
 
52
- - Understanding existing code → `plsql-explore`.
53
- - Measuring what a change breaks → `plsql-impact` (must already be done).
54
- - Landing the file on the database → `plsql-apply`, always — never
52
+ - Understanding existing code → `pythia-explore`.
53
+ - Measuring what a change breaks → `pythia-impact` (must already be done).
54
+ - Landing the file on the database → `pythia-apply`, always — never
55
55
  `run-sql`, never `sqlplus`.
@@ -35,8 +35,11 @@ def test_scaffold_creates_config_once_and_never_clobbers():
35
35
 
36
36
 
37
37
  def test_missing_npx_falls_back_to_the_bundled_pack():
38
- """No Node -> the whole kit still installs: run_skills_add signals None
39
- and the bundled skills are copied into both conventional layouts."""
38
+ """No Node -> the whole kit still installs. One target only
39
+ (.agents/skills, the universal layout) a second copy in
40
+ .claude/skills would double every skill in Claude Code's menu. The
41
+ copy merges: foreign skills survive, and stale plsql-* copies of the
42
+ pack's old names are cleaned from both conventional roots."""
40
43
  import shutil
41
44
  old = shutil.which
42
45
  shutil.which = lambda name: None
@@ -45,14 +48,23 @@ def test_missing_npx_falls_back_to_the_bundled_pack():
45
48
  finally:
46
49
  shutil.which = old
47
50
  with tempfile.TemporaryDirectory() as td:
51
+ agents = pathlib.Path(td) / ".agents" / "skills"
52
+ claude = pathlib.Path(td) / ".claude" / "skills"
53
+ # pre-existing content: a custom skill, and stale old-name copies
54
+ (agents / "my-team-skill").mkdir(parents=True)
55
+ (agents / "my-team-skill" / "SKILL.md").write_text("x", encoding="utf-8")
56
+ (agents / "plsql-apply").mkdir()
57
+ (claude / "plsql-review").mkdir(parents=True)
48
58
  targets = pythia.copy_bundled_skills(td)
49
- assert [t.name for t in targets] == ["skills", "skills"]
50
- for root in (pathlib.Path(td) / ".claude" / "skills",
51
- pathlib.Path(td) / ".agents" / "skills"):
52
- assert (root / "plsql-apply" / "SKILL.md").is_file()
53
- assert (root / "plsql-review" / "reference" / "antipatterns.md").is_file()
54
- # second run must not fail on the existing copies
55
- pythia.copy_bundled_skills(td)
59
+ assert targets == [agents]
60
+ assert (agents / "pythia-apply" / "SKILL.md").is_file()
61
+ assert (agents / "pythia-review" / "reference" / "antipatterns.md").is_file()
62
+ assert not (claude / "pythia-apply").exists() # single target
63
+ assert (agents / "my-team-skill" / "SKILL.md").read_text(
64
+ encoding="utf-8") == "x" # merge, not wipe
65
+ assert not (agents / "plsql-apply").exists() # legacy cleaned
66
+ assert not (claude / "plsql-review").exists() # in both roots
67
+ pythia.copy_bundled_skills(td) # idempotent
56
68
 
57
69
 
58
70
  def test_invocation_names_the_dash_m_form():
@@ -72,7 +84,7 @@ def test_invocation_names_the_dash_m_form():
72
84
 
73
85
  def test_pack_dirs_resolve_in_the_source_layout():
74
86
  assert (pythia.QUERY_DIR / "impact.sql").is_file()
75
- assert (pythia.SKILLS_DIR / "plsql-apply" / "SKILL.md").is_file()
87
+ assert (pythia.SKILLS_DIR / "pythia-apply" / "SKILL.md").is_file()
76
88
 
77
89
 
78
90
  def test_one_version_across_every_manifest():
@@ -253,6 +253,45 @@ def test_to_unistr():
253
253
  assert u("ABC 123") == "unistr('ABC 123')"
254
254
 
255
255
 
256
+ def test_agent_user_sql_is_least_privilege():
257
+ sql = pythia.AGENT_USER_SQL.format(owner="APP", agent="APP_AGENT",
258
+ password="x")
259
+ assert "CREATE USER APP_AGENT" in sql
260
+ assert "GRANT CREATE SESSION TO APP_AGENT" in sql
261
+ assert "GRANT CONNECT THROUGH APP_AGENT" in sql
262
+ for bad in ("DBA TO", "RESOURCE TO", "ANY "): # the whole point
263
+ assert bad not in sql.upper().replace("-- ", ""), bad
264
+
265
+
266
+ def test_agent_password_shape():
267
+ for _ in range(20):
268
+ pw = pythia.agent_password()
269
+ assert len(pw) >= 14 and pw[0].isalpha() and "#" in pw
270
+ assert '"' not in pw and "'" not in pw # safe inside quotes
271
+
272
+
273
+ def test_save_agent_connection_keeps_owner_and_sets_default():
274
+ with tempfile.TemporaryDirectory() as td:
275
+ root = pathlib.Path(td)
276
+ (root / ".pythia").mkdir()
277
+ cfg = {"default": "dev",
278
+ "dev": {"host": "h", "port": 1521, "service_name": "svc",
279
+ "user": "PYTHIA", "password": "ownerpw",
280
+ "schema": "PYTHIA"}}
281
+ (root / ".pythia" / "connections.json").write_text(
282
+ json.dumps(cfg), encoding="utf-8")
283
+ name = pythia.save_agent_connection(root, "dev", cfg["dev"],
284
+ "PYTHIA_AGENT", "agpw")
285
+ out = json.loads((root / ".pythia" / "connections.json")
286
+ .read_text(encoding="utf-8"))
287
+ assert name == "dev_agent" and out["default"] == "dev_agent"
288
+ assert out["dev"]["user"] == "PYTHIA" # owner untouched
289
+ assert out["dev"]["password"] == "ownerpw"
290
+ assert out["dev_agent"]["user"] == "pythia_agent[pythia]"
291
+ assert out["dev_agent"]["password"] == "agpw"
292
+ assert out["dev_agent"]["host"] == "h" # connect details copied
293
+
294
+
256
295
  def test_json_envelope():
257
296
  s = pythia.json_envelope("ls", "DEV", "OWNER1", ["A", "B"],
258
297
  [(1, datetime.date(2026, 1, 2))], truncated=True)
@@ -489,6 +489,75 @@ def test_preview_diff_ignores_the_create_header():
489
489
  assert "lines changed" not in out
490
490
 
491
491
 
492
+ def test_apply_builds_plscope_index_by_default():
493
+ """The write session sets plscope_settings before the CREATE, so every
494
+ object applied through pythia carries the semantic index."""
495
+ with tempfile.TemporaryDirectory() as td:
496
+ conn = FakeConn(base_script())
497
+ code = pythia.run_apply(conn, "APP", apply_ns(td, file="f.sql", yes=True),
498
+ NEW_FILE)
499
+ assert code == 0
500
+ stmts = [s for s, _ in conn.executed]
501
+ alter = [i for i, s in enumerate(stmts) if "plscope_settings" in s.lower()]
502
+ create = [i for i, s in enumerate(stmts)
503
+ if s.lstrip().lower().startswith("create")]
504
+ assert alter and create and alter[0] < create[0]
505
+
506
+
507
+ def test_apply_plscope_opt_out_via_settings():
508
+ with tempfile.TemporaryDirectory() as td:
509
+ (pathlib.Path(td) / ".pythia").mkdir()
510
+ (pathlib.Path(td) / ".pythia" / "settings.json").write_text(
511
+ '{"plscope_on_apply": false}', encoding="utf-8")
512
+ conn = FakeConn(base_script())
513
+ code = pythia.run_apply(conn, "APP", apply_ns(td, file="f.sql", yes=True),
514
+ NEW_FILE)
515
+ assert code == 0
516
+ assert not any("plscope_settings" in s.lower()
517
+ for s, _ in conn.executed)
518
+
519
+
520
+ def test_privilege_warning_speaks_proxy():
521
+ """A proxy session inheriting ANY privileges: the warning must blame the
522
+ owner's grants, not the entrance; a clean proxy session warns not at all."""
523
+ dangerous = {"from session_privs": ([("PRIVILEGE",)],
524
+ [("DROP ANY TABLE",), ("SELECT ANY TABLE",)]),
525
+ "proxy_user": ([("P",)], [("CORE_BH_AGENT",)])}
526
+ msg = pythia.privilege_warning(FakeConn(dangerous), "CORE_BH",
527
+ "core_bh_agent[core_bh]")
528
+ assert "Proxy session" in msg and "CORE_BH_AGENT" in msg
529
+ assert "owner holds" in msg and "DROP ANY TABLE" in msg
530
+ clean = {"from session_privs": ([("PRIVILEGE",)], []),
531
+ "proxy_user": ([("P",)], [("CORE_BH_AGENT",)])}
532
+ assert pythia.privilege_warning(FakeConn(clean), "CORE_BH",
533
+ "core_bh_agent[core_bh]") is None
534
+ # no proxy, direct owner: the original warning stands
535
+ direct = {"from session_privs": ([("PRIVILEGE",)], [])}
536
+ assert "schema owner" in pythia.privilege_warning(
537
+ FakeConn(direct), "APP", "APP")
538
+
539
+
540
+ def test_journal_prune_keeps_applied_entries():
541
+ import argparse
542
+ import datetime
543
+ with tempfile.TemporaryDirectory() as td:
544
+ base = datetime.datetime(2026, 8, 26, 12, 0, 0)
545
+ pythia.write_journal_entry(td, "PROCEDURE", "P_PREVIEW", "a", "b",
546
+ {"applied": False}, now=base)
547
+ kept = pythia.write_journal_entry(
548
+ td, "PROCEDURE", "P_APPLIED", "a", "b", {"applied": True},
549
+ now=base.replace(minute=1))
550
+ ns = argparse.Namespace(action="prune", project_root=td, json=False,
551
+ id=None)
552
+ import contextlib
553
+ import io
554
+ buf = io.StringIO()
555
+ with contextlib.redirect_stdout(buf):
556
+ pythia.cmd_journal(None, None, ns)
557
+ assert "pruned 1" in buf.getvalue()
558
+ assert pythia.list_journal_entries(td) == [kept]
559
+
560
+
492
561
  def main():
493
562
  failed = 0
494
563
  for name, fn in sorted(globals().items()):
@@ -10,9 +10,9 @@ import sys
10
10
  ROOT = pathlib.Path(__file__).resolve().parent.parent
11
11
  SKILLS = ROOT / "skills"
12
12
 
13
- EXPECTED = {"plsql-setup", "plsql-explore", "plsql-impact",
14
- "plsql-write", "plsql-apply", "plsql-review",
15
- "plsql-skill-author"}
13
+ EXPECTED = {"pythia-setup", "pythia-explore", "pythia-impact",
14
+ "pythia-write", "pythia-apply", "pythia-review",
15
+ "pythia-skill-author"}
16
16
 
17
17
  # spec: SKILL.md under 150 lines, detail pushed to reference/
18
18
  MAX_LINES = 150
@@ -65,19 +65,19 @@ def test_skill_bodies_stay_within_budget():
65
65
 
66
66
 
67
67
  def test_apply_skill_keeps_the_honest_rollback_table():
68
- path = SKILLS / "plsql-apply" / "SKILL.md"
68
+ path = SKILLS / "pythia-apply" / "SKILL.md"
69
69
  if not path.is_file():
70
70
  return
71
71
  text = path.read_text(encoding="utf-8")
72
72
  for needle in ("Yes — completely", "Flashback Query", "Recycle Bin",
73
73
  "Almost never", "exit 3", "run-sql"):
74
- assert needle in text, f"plsql-apply: missing {needle!r}"
74
+ assert needle in text, f"pythia-apply: missing {needle!r}"
75
75
 
76
76
 
77
77
  def test_reference_files_exist_where_promised():
78
- for rel in ("plsql-explore/reference/data-dictionary.md",
79
- "plsql-write/reference/patterns.md",
80
- "plsql-review/reference/antipatterns.md"):
78
+ for rel in ("pythia-explore/reference/data-dictionary.md",
79
+ "pythia-write/reference/patterns.md",
80
+ "pythia-review/reference/antipatterns.md"):
81
81
  assert (SKILLS / rel).is_file(), f"missing {rel}"
82
82
 
83
83
 
File without changes
File without changes