ticketwright 3.2.0__tar.gz → 3.4.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.
- {ticketwright-3.2.0 → ticketwright-3.4.0}/.claude/config/stack.example.asana-bq.yaml +1 -1
- {ticketwright-3.2.0 → ticketwright-3.4.0}/.claude/config/stack.example.azure.yaml +1 -1
- ticketwright-3.4.0/.claude/config/stack.example.multi-warehouse.yaml +104 -0
- ticketwright-3.4.0/.claude/config/stack.schema.md +157 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/.claude/config/stack.yaml +2 -1
- ticketwright-3.4.0/.claude/hooks/_stack.py +139 -0
- ticketwright-3.4.0/.claude/hooks/db_write_guard.py +381 -0
- ticketwright-3.4.0/.claude/hooks/session_context.py +192 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/.claude/hooks/ticket_index_context.py +2 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/.claude/settings.json.tmpl +1 -1
- {ticketwright-3.2.0 → ticketwright-3.4.0}/.claude/skills/refresh/index.md +10 -2
- {ticketwright-3.2.0 → ticketwright-3.4.0}/.claude/skills/setup/SKILL.md +11 -5
- {ticketwright-3.2.0 → ticketwright-3.4.0}/.claude/skills/setup/adopt.md +4 -2
- {ticketwright-3.2.0 → ticketwright-3.4.0}/.claude/skills/setup/scaffold.md +9 -2
- {ticketwright-3.2.0 → ticketwright-3.4.0}/.claude/skills/ship/SKILL.md +14 -8
- {ticketwright-3.2.0 → ticketwright-3.4.0}/.claude/skills/spec-and-build/SKILL.md +8 -5
- {ticketwright-3.2.0 → ticketwright-3.4.0}/.claude/statusline.sh +13 -1
- {ticketwright-3.2.0 → ticketwright-3.4.0}/CHANGELOG.md +166 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/PKG-INFO +60 -14
- {ticketwright-3.2.0 → ticketwright-3.4.0}/README.md +59 -13
- {ticketwright-3.2.0 → ticketwright-3.4.0}/adapters/warehouse/bigquery.md +7 -3
- {ticketwright-3.2.0 → ticketwright-3.4.0}/adapters/warehouse/databricks.md +7 -3
- {ticketwright-3.2.0 → ticketwright-3.4.0}/adapters/warehouse/postgres.md +7 -3
- {ticketwright-3.2.0 → ticketwright-3.4.0}/adapters/warehouse/redshift.md +7 -3
- {ticketwright-3.2.0 → ticketwright-3.4.0}/adapters/warehouse/snowflake.md +7 -4
- {ticketwright-3.2.0 → ticketwright-3.4.0}/adapters/warehouse/synapse.md +7 -3
- {ticketwright-3.2.0 → ticketwright-3.4.0}/bin/build_ticket_index.py +186 -10
- ticketwright-3.4.0/bin/bump_version.sh +99 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/bin/recall.py +1 -1
- {ticketwright-3.2.0 → ticketwright-3.4.0}/bin/selftest.sh +471 -24
- ticketwright-3.4.0/bin/verify_stack.sh +146 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/pyproject.toml +8 -1
- {ticketwright-3.2.0 → ticketwright-3.4.0}/templates/AGENTS.md.tmpl +32 -7
- {ticketwright-3.2.0 → ticketwright-3.4.0}/templates/gitignore.tmpl +8 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/templates/productized-skill/SKILL.md.tmpl +2 -1
- {ticketwright-3.2.0 → ticketwright-3.4.0}/ticketwright/__init__.py +1 -1
- {ticketwright-3.2.0 → ticketwright-3.4.0}/ticketwright/cli.py +4 -3
- ticketwright-3.2.0/.claude/config/stack.schema.md +0 -77
- ticketwright-3.2.0/.claude/hooks/db_write_guard.py +0 -145
- ticketwright-3.2.0/.claude/hooks/session_context.py +0 -76
- ticketwright-3.2.0/bin/verify_stack.sh +0 -83
- {ticketwright-3.2.0 → ticketwright-3.4.0}/.claude/agents/qc-reviewer.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/.claude/commands/.gitkeep +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/.claude/hooks/regenerate_ticket_index.py +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/.claude/skills/productize/SKILL.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/.claude/skills/productize/authoring.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/.claude/skills/refresh/SKILL.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/.claude/skills/refresh/context-pack.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/.claude/skills/review/SKILL.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/.claude/skills/setup/teammate.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/.claude/skills/ticket/SKILL.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/.claude/skills/ticket/priming.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/.gitignore +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/LICENSE +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/adapters/README.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/adapters/chat/slack.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/adapters/chat/teams.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/adapters/docstore/gdrive.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/adapters/docstore/sharepoint.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/adapters/tracker/asana.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/adapters/tracker/azure-devops.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/adapters/tracker/github-issues.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/adapters/tracker/jira.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/adapters/tracker/linear.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/adapters/tracker/monday.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/adapters/vcs/azure-repos.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/adapters/vcs/github.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/adapters/vcs/gitlab.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/bin/enrich_ticket.py +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/bin/ingest_index_records.py +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/bin/render.sh +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/bin/render_and_validate.sh +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/bin/split_and_export.sh +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/templates/CLAUDE.md.tmpl +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/templates/plan.md.tmpl +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/templates/productized-skill/bin/drift_check.sh +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/templates/productized-skill/golden/example.json +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/templates/productized-skill/sql/qc.sql.tmpl +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/templates/productized-skill/sql/step.sql.tmpl +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/templates/productized-skill/templates/README.md.tmpl +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/templates/productized-skill/templates/tracker_comment.txt.tmpl +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/templates/roles/analyst.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/templates/roles/engineer.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/templates/roles/generalist.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/templates/roles/scientist.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/templates/spec.md.tmpl +0 -0
- {ticketwright-3.2.0 → ticketwright-3.4.0}/templates/ticket-README.md.tmpl +0 -0
|
@@ -59,7 +59,7 @@ seams:
|
|
|
59
59
|
|
|
60
60
|
policies:
|
|
61
61
|
hard_halt_before_external_posts: true
|
|
62
|
-
db_write_requires_approval:
|
|
62
|
+
db_write_requires_approval: high_risk # off | high_risk | all — see stack.schema.md
|
|
63
63
|
chat_default_draft: true
|
|
64
64
|
hyperlink_everything: true
|
|
65
65
|
skillify_everything: true
|
|
@@ -64,7 +64,7 @@ seams:
|
|
|
64
64
|
|
|
65
65
|
policies:
|
|
66
66
|
hard_halt_before_external_posts: true
|
|
67
|
-
db_write_requires_approval:
|
|
67
|
+
db_write_requires_approval: high_risk # off | high_risk | all — see stack.schema.md
|
|
68
68
|
chat_default_draft: true
|
|
69
69
|
hyperlink_everything: true
|
|
70
70
|
skillify_everything: true
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# stack.example.multi-warehouse.yaml — a team that must reach TWO warehouses.
|
|
2
|
+
#
|
|
3
|
+
# Jira / Snowflake + Databricks / Slack / Google Drive / GitHub. Everything except the warehouse
|
|
4
|
+
# seam is identical to stack.yaml; this file exists to prove one claim — one warehouse seam can
|
|
5
|
+
# hold several named targets, and every target is independently reachable.
|
|
6
|
+
#
|
|
7
|
+
# Skills resolve which target to use per `adapters/README.md` § Multi-target seams; a `.sql` file
|
|
8
|
+
# names its own target in a `-- warehouse-target: <name>` header comment.
|
|
9
|
+
# Field docs: stack.schema.md · Validate: bash bin/verify_stack.sh .claude/config/stack.example.multi-warehouse.yaml
|
|
10
|
+
|
|
11
|
+
project:
|
|
12
|
+
key_prefix: ENG
|
|
13
|
+
key_prefixes: [ENG]
|
|
14
|
+
assignee_dir: alice
|
|
15
|
+
ticket_path: "tickets/{assignee}/{id}"
|
|
16
|
+
ticket_subdirs: [source_materials, final_deliverables, qc_queries, exploratory_analysis]
|
|
17
|
+
default_epic: ENG-100
|
|
18
|
+
terminal_status: Done
|
|
19
|
+
ticket_url_template: "https://acme.atlassian.net/browse/{id}"
|
|
20
|
+
word_limits: {tracker_comment: 100, chat: 100, pr: 200, ticket: 200}
|
|
21
|
+
graph_notes: true
|
|
22
|
+
graph_config: true
|
|
23
|
+
|
|
24
|
+
seams:
|
|
25
|
+
tracker:
|
|
26
|
+
tool: jira
|
|
27
|
+
adapter: adapters/tracker/jira.md
|
|
28
|
+
transport: both
|
|
29
|
+
site: acme.atlassian.net
|
|
30
|
+
cli: acli
|
|
31
|
+
mcp: atlassian
|
|
32
|
+
verify: 'acli jira workitem search --jql "project = {key_prefix}" --limit 1'
|
|
33
|
+
|
|
34
|
+
# ── MULTI-TARGET warehouse seam ───────────────────────────────────────────────────────────────
|
|
35
|
+
# The presence of `targets:` is the discriminator — NOT `default:`, because other seams already
|
|
36
|
+
# use default_channel / default_mode / default_branch and `default` must never carry type meaning.
|
|
37
|
+
#
|
|
38
|
+
# List the DEFAULT target FIRST. Readers that predate this feature (an un-relaunched session's
|
|
39
|
+
# statusline and SessionStart banner) display the first target they find, so first == default
|
|
40
|
+
# keeps them honest instead of quietly naming the wrong warehouse.
|
|
41
|
+
#
|
|
42
|
+
# Scalar keys at the SEAM level are inherited by every target, and a target's own key wins —
|
|
43
|
+
# handy when two targets share an account (one `cli:` up here, differing `default_warehouse:`
|
|
44
|
+
# per target).
|
|
45
|
+
warehouse:
|
|
46
|
+
default: prod
|
|
47
|
+
targets:
|
|
48
|
+
prod:
|
|
49
|
+
tool: snowflake
|
|
50
|
+
adapter: adapters/warehouse/snowflake.md
|
|
51
|
+
transport: both
|
|
52
|
+
cli: snow
|
|
53
|
+
default_warehouse: ANALYTICS_WH
|
|
54
|
+
pii_role: PII_READER
|
|
55
|
+
dev_target: ANALYTICS_DEV # canonical; a legacy `dev_db:` is still honored
|
|
56
|
+
verify: "snow connection test"
|
|
57
|
+
lake:
|
|
58
|
+
tool: databricks
|
|
59
|
+
adapter: adapters/warehouse/databricks.md
|
|
60
|
+
transport: cli
|
|
61
|
+
cli: dbsqlcli
|
|
62
|
+
warehouse_id: 0a1b2c3d4e5f6789
|
|
63
|
+
catalog: main
|
|
64
|
+
schema: analytics
|
|
65
|
+
dev_target: main_dev # was `dev_catalog:`
|
|
66
|
+
profile: DEFAULT
|
|
67
|
+
verify: "databricks --profile {profile} current-user me"
|
|
68
|
+
|
|
69
|
+
chat:
|
|
70
|
+
tool: slack
|
|
71
|
+
adapter: adapters/chat/slack.md
|
|
72
|
+
transport: mcp
|
|
73
|
+
mcp: slack
|
|
74
|
+
default_channel: C0XXXXXXXXX
|
|
75
|
+
default_mode: draft
|
|
76
|
+
always_include: [Alice]
|
|
77
|
+
verify: null
|
|
78
|
+
|
|
79
|
+
docstore:
|
|
80
|
+
tool: gdrive
|
|
81
|
+
adapter: adapters/docstore/gdrive.md
|
|
82
|
+
transport: cli
|
|
83
|
+
base_path: "/path/to/your/Shared drives/Tickets"
|
|
84
|
+
verify: "test -d \"{base_path}\""
|
|
85
|
+
|
|
86
|
+
vcs:
|
|
87
|
+
tool: github
|
|
88
|
+
adapter: adapters/vcs/github.md
|
|
89
|
+
transport: cli
|
|
90
|
+
default_branch: main
|
|
91
|
+
semantic_pr: true
|
|
92
|
+
worktree_root: ".claude/worktrees"
|
|
93
|
+
verify: "gh auth status"
|
|
94
|
+
|
|
95
|
+
policies:
|
|
96
|
+
hard_halt_before_external_posts: true
|
|
97
|
+
db_write_requires_approval: true
|
|
98
|
+
chat_default_draft: true
|
|
99
|
+
hyperlink_everything: true
|
|
100
|
+
skillify_everything: true
|
|
101
|
+
reduce_assumptions: true
|
|
102
|
+
commit_plan_before_implement: true
|
|
103
|
+
system_evolution: true
|
|
104
|
+
deterministic_outputs: true
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# `stack.yaml` — the tool registry (schema + policies)
|
|
2
|
+
|
|
3
|
+
`stack.yaml` is the **single source of truth** for which concrete tool fills each abstract "seam"
|
|
4
|
+
and for the project facts the skills need. Skills never hardcode `acli`, `snow`, `slack`, channel
|
|
5
|
+
IDs, epics, or paths — those live **here** and in the per-tool adapters. Swapping Jira→Asana or
|
|
6
|
+
Snowflake→BigQuery means editing this file and pointing at a different adapter; **no skill changes.**
|
|
7
|
+
|
|
8
|
+
The `setup` skill writes this file by interviewing you and detecting installed tooling.
|
|
9
|
+
`bin/verify_stack.sh` reads it to smoke-test every seam. Every skill reads it at preflight.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Top-level shape
|
|
14
|
+
|
|
15
|
+
```yaml
|
|
16
|
+
project: # facts about this workspace, tool-independent
|
|
17
|
+
seams: # one entry per abstract seam → concrete tool + adapter + verify
|
|
18
|
+
policies: # behavioral rules every skill inherits (the kit's "global rules")
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## `project`
|
|
24
|
+
|
|
25
|
+
| Field | Type | Example | Meaning |
|
|
26
|
+
|---|---|---|---|
|
|
27
|
+
| `key_prefix` | string | `ENG` | Ticket-ID prefix. Branch names = `{key_prefix}-NNNN`. |
|
|
28
|
+
| `key_prefixes` | list | `[ENG]` | Prefixes the ticket index recognizes in folder names. Optional; defaults to `[key_prefix]`. Use when one repo holds tickets from several trackers (e.g. `[ENG, OPS]`). |
|
|
29
|
+
| `assignee_dir` | string | `alice` | Default owner subdir under `tickets/`. |
|
|
30
|
+
| `ticket_path` | template | `tickets/{assignee}/{id}` | Where a ticket folder lives. `{assignee}` `{id}` tokens. |
|
|
31
|
+
| `ticket_subdirs` | list | `[source_materials, final_deliverables, qc_queries, exploratory_analysis]` | Scaffolded per ticket. |
|
|
32
|
+
| `default_epic` | string \| null | `ENG-100` | Parent epic for newly created tickets (null if tracker has no epics). |
|
|
33
|
+
| `terminal_status` | string | `Done` | The "done" workflow state (not always "Done"). |
|
|
34
|
+
| `ticket_url_template` | template \| null | `https://acme.atlassian.net/browse/{id}` | How `tickets/INDEX.md` links each ticket (`{id}` token). Null/omitted → the index renders no per-ticket link. |
|
|
35
|
+
| `word_limits` | map | `{tracker_comment: 100, chat: 100, pr: 200, ticket: 200}` | Hard caps the comms skills enforce. |
|
|
36
|
+
| `graph_notes` | bool | `true` | Generate the Obsidian graph layer (`tickets/graph/` + `tickets/objects/`). On by default; set `false` to disable. |
|
|
37
|
+
| `graph_config` | bool | `true` | Also write/merge `.obsidian/graph.json` (tickets↔objects filter + color groups) so the Graph view opens ready-to-read. Create/merge-only — never clobbers manual tweaks. On by default; set `false` to keep the nodes but not manage the Obsidian config. Ignored when `graph_notes` is `false`. |
|
|
38
|
+
|
|
39
|
+
## `seams`
|
|
40
|
+
|
|
41
|
+
Exactly these five keys: `tracker`, `warehouse`, `chat`, `docstore`, `vcs`. Each is **either** a
|
|
42
|
+
single mapping (below) **or** a *multi-target* mapping — see "Multiple warehouses". Fields of a
|
|
43
|
+
single mapping:
|
|
44
|
+
|
|
45
|
+
| Field | Type | Meaning |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `tool` | string | The concrete tool, e.g. `jira` / `asana` / `monday` / `linear`. |
|
|
48
|
+
| `adapter` | path | The playbook that maps the verb contract → this tool's commands. |
|
|
49
|
+
| `verify` | string \| null | A **read-only** smoke-test command. `{token}` interpolation from this seam's own keys + `project`. `null` = skip (skills warn). Non-zero exit ⇒ seam "unreachable". |
|
|
50
|
+
| `transport` | enum | `cli` \| `mcp` \| `both` — how the adapter talks to the tool. Drives the verify fallback. |
|
|
51
|
+
| *(extra keys)* | any | Tool-specific config the adapter reads (site, warehouse, role, channel, base_path, …). |
|
|
52
|
+
|
|
53
|
+
The `warehouse` seam may also be `null`/omitted for non-data repos — `review`, `spec-and-build`,
|
|
54
|
+
and `refresh context` degrade gracefully (skip warehouse steps) when it is.
|
|
55
|
+
|
|
56
|
+
### Multiple warehouses (named targets)
|
|
57
|
+
|
|
58
|
+
A repo that must reach more than one warehouse — prod Snowflake plus a Databricks lakehouse, or two
|
|
59
|
+
Snowflake accounts — declares **named targets** instead of one flat mapping:
|
|
60
|
+
|
|
61
|
+
```yaml
|
|
62
|
+
warehouse:
|
|
63
|
+
default: prod # REQUIRED when `targets:` is present; must name a key below
|
|
64
|
+
cli: snow # seam-level scalars are inherited by every target
|
|
65
|
+
targets:
|
|
66
|
+
prod: { tool: snowflake, adapter: adapters/warehouse/snowflake.md, verify: "snow connection test" }
|
|
67
|
+
lake: { tool: databricks, adapter: adapters/warehouse/databricks.md, verify: "…" }
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
| Field | Type | Meaning |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| `targets` | map | Named targets. **Its presence is the discriminator** for a multi-target seam. |
|
|
73
|
+
| `default` | string | Which target skills use when nothing else selects one. Required with `targets`. |
|
|
74
|
+
|
|
75
|
+
Rules:
|
|
76
|
+
|
|
77
|
+
- **Inheritance.** A target inherits any key it doesn't define itself, including `tool` / `adapter` /
|
|
78
|
+
`verify` — so two targets on one account can share all three and differ only in, say,
|
|
79
|
+
`default_warehouse`. A target's own key wins. Inheritance is keyed on *absence*: an explicit
|
|
80
|
+
`verify: null` on a target means "skip", it does not fall back to the seam's command.
|
|
81
|
+
- **List the default first.** Readers that predate this feature (an un-relaunched session's statusline
|
|
82
|
+
and SessionStart banner) show the first target they find, so first == default keeps them honest.
|
|
83
|
+
`bin/verify_stack.sh` warns when the default isn't first, and fails when `default` is missing or
|
|
84
|
+
names an unknown target.
|
|
85
|
+
- **Which target is active** is resolved per `adapters/README.md` § Multi-target seams. A `.sql` file
|
|
86
|
+
names its own target in a `-- warehouse-target: <name>` header comment; that never goes in a CSV,
|
|
87
|
+
whose header must stay on row 1 with no preamble.
|
|
88
|
+
- v1 implements multi-target for **`warehouse`** only. `verify_stack.sh` handles the shape for any
|
|
89
|
+
seam, but no skill resolves targets for the other four, so don't rely on it there yet.
|
|
90
|
+
|
|
91
|
+
**Known limitation:** `tickets/OBJECTS.md` and the graph layer fold object names case-insensitively
|
|
92
|
+
and are warehouse-blind, so `ANALYTICS.CUSTOMERS` on one target and `analytics.customers` on another
|
|
93
|
+
collapse into one node. Usually that's the useful reading (a genuine cross-system relationship), but
|
|
94
|
+
it is not a per-target index.
|
|
95
|
+
|
|
96
|
+
**Dev target.** The dev environment is `seams.warehouse[.targets.<name>].dev_target`. When that key
|
|
97
|
+
is absent it falls back to the key named by the warehouse adapter's `dev_key:` frontmatter (`dev_db`
|
|
98
|
+
for Snowflake, `dev_dataset` for BigQuery, `dev_catalog` for Databricks, `dev_schema` for
|
|
99
|
+
Postgres/Redshift/Synapse) — so configs written before `dev_target` existed keep working untouched.
|
|
100
|
+
|
|
101
|
+
## `policies` (the 9 kit policies — see kit README "AI-layer" section)
|
|
102
|
+
|
|
103
|
+
| Policy | Type | Default | Enforced by |
|
|
104
|
+
|---|---|---|---|
|
|
105
|
+
| `hard_halt_before_external_posts` | bool | `true` | `ship`, every productized skill — pause for human go before any tracker/chat/docstore write. |
|
|
106
|
+
| `db_write_requires_approval` | enum | `high_risk` | the `db_write_guard` hook (Claude Code) + any skill issuing a non-SELECT. See below. |
|
|
107
|
+
| `chat_default_draft` | `true` | `chat.draft` not `chat.send` unless the user says "send it". |
|
|
108
|
+
| `hyperlink_everything` | `true` | comms skills wrap every ticket-ID / file / PR in a smart link. |
|
|
109
|
+
| `skillify_everything` | `true` | recurring work → a `/productize` skill the agent can invoke, not a one-off. |
|
|
110
|
+
| `reduce_assumptions` | `true` | ask before building; still document every assumption in the ticket README. |
|
|
111
|
+
| `commit_plan_before_implement` | `true` | `spec-and-build` commits the spec/plan artifact before `build` (blame-free retry). |
|
|
112
|
+
| `system_evolution` | `true` | `ship` retro: a failure fixes the AI layer (rule/context/command/adapter), not just the ticket. |
|
|
113
|
+
| `deterministic_outputs` | `true` | data exports use explicit `ORDER BY`; productized skills ship golden-replay diffs. |
|
|
114
|
+
|
|
115
|
+
All are booleans except `db_write_requires_approval`.
|
|
116
|
+
|
|
117
|
+
### `db_write_requires_approval` — the one enum
|
|
118
|
+
|
|
119
|
+
| Value | Behavior | Also accepts |
|
|
120
|
+
|---|---|---|
|
|
121
|
+
| `off` | Never ask. | `false`, `none`, `null` |
|
|
122
|
+
| `high_risk` | **Default.** Ask only for irreversible, access-changing, or unrecognized SQL. | `true`, `destructive` |
|
|
123
|
+
| `all` | Ask for any mutation at all. | `strict`, `always` |
|
|
124
|
+
|
|
125
|
+
Reads (`SELECT`/`SHOW`/`DESCRIBE`/`EXPLAIN`/`WITH … SELECT`/`LIST`/`USE`) pass in every value;
|
|
126
|
+
`all` means every *write*, not every command.
|
|
127
|
+
|
|
128
|
+
Classification is **default-deny**. Only a narrow allowlist is treated as additive — plain
|
|
129
|
+
`CREATE` (no `OR REPLACE`), `INSERT INTO` (no `OVERWRITE`), `ALTER … ADD`, and `COMMENT ON`.
|
|
130
|
+
Everything else that mutates is high-risk, *including anything the scanner does not recognize*.
|
|
131
|
+
Enumerating dangerous forms instead would leave holes: `ALTER TABLE … MODIFY COLUMN` can change
|
|
132
|
+
a type and truncate data while matching no plausible "destructive verb" list.
|
|
133
|
+
|
|
134
|
+
A missing, malformed, or unrecognized value resolves to `all`, never to something weaker —
|
|
135
|
+
unparseable config must not quietly widen what runs unprompted. Note the asymmetry: an explicit
|
|
136
|
+
legacy `true` *does* relax to `high_risk`, because it is a value someone chose rather than a
|
|
137
|
+
value the parser failed on.
|
|
138
|
+
|
|
139
|
+
Under `bypassPermissions` the hook stays silent and emits a `systemMessage` instead of asking —
|
|
140
|
+
the operator has already opted out of prompting, so a prompt there is incoherent. Every other
|
|
141
|
+
permission mode gets a normal `ask`. For agents other than Claude Code this policy is
|
|
142
|
+
**guidance, not enforcement**: they read it in `AGENTS.md` and are trusted to honor it, since
|
|
143
|
+
hooks are the only mechanically enforced layer.
|
|
144
|
+
|
|
145
|
+
`always_include` (under `seams.chat`) — names always added to a chat message (e.g. `[Alice]`); the
|
|
146
|
+
"never solo-DM a stakeholder" rule.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Worked example
|
|
151
|
+
|
|
152
|
+
A worked example lives at [`stack.yaml`](stack.yaml) (Jira/Snowflake/Slack/Drive/GitHub). Three more
|
|
153
|
+
prove the abstraction holds with zero skill edits: `stack.example.asana-bq.yaml`
|
|
154
|
+
(Asana/BigQuery/Teams/SharePoint/GitLab), `stack.example.azure.yaml`
|
|
155
|
+
(Azure DevOps/Synapse/Teams/SharePoint/Azure Repos), and `stack.example.multi-warehouse.yaml`
|
|
156
|
+
(Jira/**Snowflake + Databricks**/Slack/Drive/GitHub — the named-targets shape above). To validate any
|
|
157
|
+
config: `bash bin/verify_stack.sh`.
|
|
@@ -18,6 +18,7 @@ project:
|
|
|
18
18
|
ticket_url_template: "https://acme.atlassian.net/browse/{id}" # how INDEX.md links a ticket ({id} token); omit/null = no link
|
|
19
19
|
word_limits: {tracker_comment: 100, chat: 100, pr: 200, ticket: 200}
|
|
20
20
|
graph_notes: true # Obsidian graph layer (tickets/graph + tickets/objects); false to disable
|
|
21
|
+
graph_config: true # also write .obsidian/graph.json (tickets↔objects filter + colors); create/merge-only, never clobbers manual tweaks
|
|
21
22
|
|
|
22
23
|
seams:
|
|
23
24
|
tracker:
|
|
@@ -67,7 +68,7 @@ seams:
|
|
|
67
68
|
|
|
68
69
|
policies:
|
|
69
70
|
hard_halt_before_external_posts: true
|
|
70
|
-
db_write_requires_approval:
|
|
71
|
+
db_write_requires_approval: high_risk # off | high_risk | all — see stack.schema.md
|
|
71
72
|
chat_default_draft: true
|
|
72
73
|
hyperlink_everything: true
|
|
73
74
|
skillify_everything: true
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Shared stack.yaml resolution + reading for the hooks. Stdlib only, no yaml dep.
|
|
2
|
+
|
|
3
|
+
The four hooks each grew their own root-resolution and their own mini-YAML scanner,
|
|
4
|
+
three mutually inconsistent ways. This module is the seam they should converge on;
|
|
5
|
+
`db_write_guard` uses it today, the others can migrate without behavior change.
|
|
6
|
+
|
|
7
|
+
Two properties matter more than convenience here:
|
|
8
|
+
|
|
9
|
+
1. **No kit-relative fallback.** Resolution stops at the project. A hook that falls
|
|
10
|
+
back to the kit's own shipped `stack.yaml` silently activates outside a
|
|
11
|
+
configured repo and reads the worked example's policy instead of the user's —
|
|
12
|
+
which is exactly how a "repo-gated" guard ends up firing everywhere.
|
|
13
|
+
2. **Block-scoped lookups.** `_block_lines` walks indentation rather than pattern-matching
|
|
14
|
+
across the whole document, so a key is read from the block that actually owns it.
|
|
15
|
+
|
|
16
|
+
Seam blocks are *not* handled here — `db_write_guard.seam_block` already does that, with
|
|
17
|
+
harder-won handling for inferred indent, anchors, flow mappings, and block scalars. This
|
|
18
|
+
module covers the top-level `policies:` mapping only; duplicating a second seam parser
|
|
19
|
+
would just create two things to keep correct.
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import os
|
|
24
|
+
import re
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
STACK_REL = ".claude/config/stack.yaml"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _iter_up(start: Path):
|
|
31
|
+
"""Yield `start` and its ancestors, stopping at (and including) the repo root.
|
|
32
|
+
|
|
33
|
+
Bounded by `.git` so a subdirectory invocation still finds the project's config
|
|
34
|
+
without escaping into a parent checkout that happens to have one.
|
|
35
|
+
"""
|
|
36
|
+
try:
|
|
37
|
+
cur = start.resolve()
|
|
38
|
+
except OSError:
|
|
39
|
+
return
|
|
40
|
+
while True:
|
|
41
|
+
yield cur
|
|
42
|
+
if (cur / ".git").exists() or cur.parent == cur:
|
|
43
|
+
return
|
|
44
|
+
cur = cur.parent
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def find_stack(cwd: str | None) -> Path | None:
|
|
48
|
+
"""Locate the *project's* stack.yaml, or None. None means "not a ticketwright repo"."""
|
|
49
|
+
project_dir = os.environ.get("CLAUDE_PROJECT_DIR")
|
|
50
|
+
if project_dir:
|
|
51
|
+
candidate = Path(project_dir) / STACK_REL
|
|
52
|
+
if candidate.is_file():
|
|
53
|
+
return candidate
|
|
54
|
+
if cwd:
|
|
55
|
+
for directory in _iter_up(Path(cwd)):
|
|
56
|
+
candidate = directory / STACK_REL
|
|
57
|
+
if candidate.is_file():
|
|
58
|
+
return candidate
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def read_text(stack: Path | None) -> str:
|
|
63
|
+
if stack is None:
|
|
64
|
+
return ""
|
|
65
|
+
try:
|
|
66
|
+
return stack.read_text(errors="replace")
|
|
67
|
+
except OSError:
|
|
68
|
+
return ""
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _block_lines(text: str, key: str) -> list[str]:
|
|
72
|
+
"""Return the lines nested under `key:`, by indentation.
|
|
73
|
+
|
|
74
|
+
Stops at the first non-blank, non-comment line indented at or below the key's own
|
|
75
|
+
level — i.e. the next sibling. Blank and comment lines inside the block are kept
|
|
76
|
+
so they never terminate it early.
|
|
77
|
+
"""
|
|
78
|
+
base: int | None = None
|
|
79
|
+
header = re.compile(rf"^(\s*){re.escape(key)}\s*:\s*(?:#.*)?$")
|
|
80
|
+
out: list[str] = []
|
|
81
|
+
for line in text.splitlines():
|
|
82
|
+
if base is None:
|
|
83
|
+
match = header.match(line)
|
|
84
|
+
if match:
|
|
85
|
+
base = len(match.group(1))
|
|
86
|
+
continue
|
|
87
|
+
if not line.strip() or line.lstrip().startswith("#"):
|
|
88
|
+
out.append(line)
|
|
89
|
+
continue
|
|
90
|
+
if len(line) - len(line.lstrip()) <= base:
|
|
91
|
+
break
|
|
92
|
+
out.append(line)
|
|
93
|
+
return out
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _unquote(value: str) -> str:
|
|
97
|
+
value = value.strip()
|
|
98
|
+
if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'":
|
|
99
|
+
return value[1:-1]
|
|
100
|
+
# Only strip a trailing comment from an unquoted scalar; a quoted one may contain '#'.
|
|
101
|
+
return value.split("#", 1)[0].strip()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _scalar(lines: list[str], key: str) -> str | None:
|
|
105
|
+
pattern = re.compile(rf"^\s*{re.escape(key)}\s*:\s*(\S.*)$")
|
|
106
|
+
for line in lines:
|
|
107
|
+
match = pattern.match(line)
|
|
108
|
+
if match:
|
|
109
|
+
return _unquote(match.group(1))
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ---- the db_write_requires_approval policy -------------------------------------
|
|
114
|
+
|
|
115
|
+
MODE_OFF = "off"
|
|
116
|
+
MODE_HIGH_RISK = "high_risk"
|
|
117
|
+
MODE_ALL = "all"
|
|
118
|
+
|
|
119
|
+
POLICY_KEY = "db_write_requires_approval"
|
|
120
|
+
|
|
121
|
+
# Legacy `true` relaxes to high_risk (v3.4 behavior change). A missing, malformed, or
|
|
122
|
+
# unrecognized value does NOT — unparseable config must never widen what runs
|
|
123
|
+
# unprompted, so it falls back to `all`.
|
|
124
|
+
_ALIASES = {
|
|
125
|
+
"off": MODE_OFF, "false": MODE_OFF, "no": MODE_OFF, "none": MODE_OFF, "null": MODE_OFF,
|
|
126
|
+
"high_risk": MODE_HIGH_RISK, "high-risk": MODE_HIGH_RISK,
|
|
127
|
+
"destructive": MODE_HIGH_RISK, "true": MODE_HIGH_RISK, "yes": MODE_HIGH_RISK,
|
|
128
|
+
"all": MODE_ALL, "strict": MODE_ALL, "always": MODE_ALL,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def db_write_mode(stack: Path | None) -> str:
|
|
133
|
+
text = read_text(stack)
|
|
134
|
+
if not text:
|
|
135
|
+
return MODE_ALL
|
|
136
|
+
raw = _scalar(_block_lines(text, "policies"), POLICY_KEY)
|
|
137
|
+
if raw is None:
|
|
138
|
+
return MODE_ALL
|
|
139
|
+
return _ALIASES.get(raw.strip().lower(), MODE_ALL)
|