ticketwright 3.2.0__tar.gz → 3.3.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.3.0/.claude/config/stack.example.multi-warehouse.yaml +104 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/.claude/config/stack.schema.md +54 -4
- {ticketwright-3.2.0 → ticketwright-3.3.0}/.claude/config/stack.yaml +1 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/.claude/hooks/db_write_guard.py +85 -5
- ticketwright-3.3.0/.claude/hooks/session_context.py +179 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/.claude/hooks/ticket_index_context.py +2 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/.claude/settings.json.tmpl +1 -1
- {ticketwright-3.2.0 → ticketwright-3.3.0}/.claude/skills/refresh/index.md +10 -2
- {ticketwright-3.2.0 → ticketwright-3.3.0}/.claude/skills/setup/SKILL.md +11 -5
- {ticketwright-3.2.0 → ticketwright-3.3.0}/.claude/skills/setup/adopt.md +4 -2
- {ticketwright-3.2.0 → ticketwright-3.3.0}/.claude/skills/setup/scaffold.md +9 -2
- {ticketwright-3.2.0 → ticketwright-3.3.0}/.claude/skills/ship/SKILL.md +14 -8
- {ticketwright-3.2.0 → ticketwright-3.3.0}/.claude/skills/spec-and-build/SKILL.md +3 -3
- {ticketwright-3.2.0 → ticketwright-3.3.0}/.claude/statusline.sh +13 -1
- {ticketwright-3.2.0 → ticketwright-3.3.0}/CHANGELOG.md +117 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/PKG-INFO +49 -12
- {ticketwright-3.2.0 → ticketwright-3.3.0}/README.md +48 -11
- {ticketwright-3.2.0 → ticketwright-3.3.0}/adapters/warehouse/bigquery.md +3 -2
- {ticketwright-3.2.0 → ticketwright-3.3.0}/adapters/warehouse/databricks.md +3 -2
- {ticketwright-3.2.0 → ticketwright-3.3.0}/adapters/warehouse/postgres.md +3 -2
- {ticketwright-3.2.0 → ticketwright-3.3.0}/adapters/warehouse/redshift.md +3 -2
- {ticketwright-3.2.0 → ticketwright-3.3.0}/adapters/warehouse/snowflake.md +3 -2
- {ticketwright-3.2.0 → ticketwright-3.3.0}/adapters/warehouse/synapse.md +3 -2
- {ticketwright-3.2.0 → ticketwright-3.3.0}/bin/build_ticket_index.py +186 -10
- ticketwright-3.3.0/bin/bump_version.sh +99 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/bin/recall.py +1 -1
- {ticketwright-3.2.0 → ticketwright-3.3.0}/bin/selftest.sh +354 -4
- ticketwright-3.3.0/bin/verify_stack.sh +146 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/pyproject.toml +8 -1
- {ticketwright-3.2.0 → ticketwright-3.3.0}/templates/AGENTS.md.tmpl +11 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/templates/gitignore.tmpl +8 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/ticketwright/__init__.py +1 -1
- {ticketwright-3.2.0 → ticketwright-3.3.0}/ticketwright/cli.py +4 -3
- 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.3.0}/.claude/agents/qc-reviewer.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/.claude/commands/.gitkeep +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/.claude/config/stack.example.asana-bq.yaml +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/.claude/config/stack.example.azure.yaml +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/.claude/hooks/regenerate_ticket_index.py +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/.claude/skills/productize/SKILL.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/.claude/skills/productize/authoring.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/.claude/skills/refresh/SKILL.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/.claude/skills/refresh/context-pack.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/.claude/skills/review/SKILL.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/.claude/skills/setup/teammate.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/.claude/skills/ticket/SKILL.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/.claude/skills/ticket/priming.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/.gitignore +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/LICENSE +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/adapters/README.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/adapters/chat/slack.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/adapters/chat/teams.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/adapters/docstore/gdrive.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/adapters/docstore/sharepoint.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/adapters/tracker/asana.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/adapters/tracker/azure-devops.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/adapters/tracker/github-issues.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/adapters/tracker/jira.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/adapters/tracker/linear.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/adapters/tracker/monday.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/adapters/vcs/azure-repos.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/adapters/vcs/github.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/adapters/vcs/gitlab.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/bin/enrich_ticket.py +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/bin/ingest_index_records.py +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/bin/render.sh +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/bin/render_and_validate.sh +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/bin/split_and_export.sh +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/templates/CLAUDE.md.tmpl +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/templates/plan.md.tmpl +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/templates/productized-skill/SKILL.md.tmpl +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/templates/productized-skill/bin/drift_check.sh +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/templates/productized-skill/golden/example.json +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/templates/productized-skill/sql/qc.sql.tmpl +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/templates/productized-skill/sql/step.sql.tmpl +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/templates/productized-skill/templates/README.md.tmpl +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/templates/productized-skill/templates/tracker_comment.txt.tmpl +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/templates/roles/analyst.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/templates/roles/engineer.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/templates/roles/generalist.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/templates/roles/scientist.md +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/templates/spec.md.tmpl +0 -0
- {ticketwright-3.2.0 → ticketwright-3.3.0}/templates/ticket-README.md.tmpl +0 -0
|
@@ -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
|
|
@@ -34,10 +34,13 @@ policies: # behavioral rules every skill inherits (the kit's "global rules
|
|
|
34
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
35
|
| `word_limits` | map | `{tracker_comment: 100, chat: 100, pr: 200, ticket: 200}` | Hard caps the comms skills enforce. |
|
|
36
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`. |
|
|
37
38
|
|
|
38
39
|
## `seams`
|
|
39
40
|
|
|
40
|
-
Exactly these five keys: `tracker`, `warehouse`, `chat`, `docstore`, `vcs`. Each
|
|
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:
|
|
41
44
|
|
|
42
45
|
| Field | Type | Meaning |
|
|
43
46
|
|---|---|---|
|
|
@@ -50,6 +53,51 @@ Exactly these five keys: `tracker`, `warehouse`, `chat`, `docstore`, `vcs`. Each
|
|
|
50
53
|
The `warehouse` seam may also be `null`/omitted for non-data repos — `review`, `spec-and-build`,
|
|
51
54
|
and `refresh context` degrade gracefully (skip warehouse steps) when it is.
|
|
52
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
|
+
|
|
53
101
|
## `policies` (the 9 kit policies — see kit README "AI-layer" section)
|
|
54
102
|
|
|
55
103
|
| Policy | Default | Enforced by |
|
|
@@ -71,7 +119,9 @@ and `refresh context` degrade gracefully (skip warehouse steps) when it is.
|
|
|
71
119
|
|
|
72
120
|
## Worked example
|
|
73
121
|
|
|
74
|
-
A worked example lives at [`stack.yaml`](stack.yaml) (Jira/Snowflake/Slack/Drive/GitHub).
|
|
122
|
+
A worked example lives at [`stack.yaml`](stack.yaml) (Jira/Snowflake/Slack/Drive/GitHub). Three more
|
|
75
123
|
prove the abstraction holds with zero skill edits: `stack.example.asana-bq.yaml`
|
|
76
|
-
(Asana/BigQuery/Teams/SharePoint/GitLab)
|
|
77
|
-
(Azure DevOps/Synapse/Teams/SharePoint/Azure Repos)
|
|
124
|
+
(Asana/BigQuery/Teams/SharePoint/GitLab), `stack.example.azure.yaml`
|
|
125
|
+
(Azure DevOps/Synapse/Teams/SharePoint/Azure Repos), and `stack.example.multi-warehouse.yaml`
|
|
126
|
+
(Jira/**Snowflake + Databricks**/Slack/Drive/GitHub — the named-targets shape above). To validate any
|
|
127
|
+
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:
|
|
@@ -51,15 +51,95 @@ def find_stack_yaml(cwd: str) -> Path | None:
|
|
|
51
51
|
return None
|
|
52
52
|
|
|
53
53
|
|
|
54
|
+
_COMMENT = re.compile(r"^\s*#")
|
|
55
|
+
# `note: |`, `- note: |-`, `"note": >2`, … — a key whose value is a literal/folded block scalar.
|
|
56
|
+
_BLOCK_SCALAR = re.compile(
|
|
57
|
+
r"""^\s*(?:-\s+)?(?:"[^"]*"|'[^']*'|[A-Za-z0-9_.-]+):\s*[|>][-+0-9]*\s*(?:#.*)?$"""
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def seam_block(text: str, seam: str) -> str:
|
|
62
|
+
"""The lines under `<seam>:` inside the top-level `seams:` mapping.
|
|
63
|
+
|
|
64
|
+
Three properties matter, all of them about never *narrowing* what gets gated:
|
|
65
|
+
|
|
66
|
+
* The seam-key indent is **inferred** from the file, not assumed to be two spaces — a
|
|
67
|
+
four-space-indented stack.yaml is valid YAML that `yq` reads fine.
|
|
68
|
+
* Comment-only lines carry no indentation and never terminate the scan, so a comment
|
|
69
|
+
between `seams:` and the first seam can't hide it.
|
|
70
|
+
* A config with no `seams:` anchor (malformed or partial) is scanned whole rather than
|
|
71
|
+
skipped. Gating too much only costs a confirmation prompt; gating too little is a
|
|
72
|
+
destructive statement running unreviewed.
|
|
73
|
+
|
|
74
|
+
Block-scalar bodies are skipped so prose can't be read as configuration. No yaml dep — this
|
|
75
|
+
hook has to stay a standalone stdlib script.
|
|
76
|
+
"""
|
|
77
|
+
# A mapping key may carry a YAML anchor/alias/tag before its nested block (`warehouse: &wh`),
|
|
78
|
+
# which yq resolves fine — so the key patterns tolerate one.
|
|
79
|
+
prop = r"(?:[&*!][^\s#]*\s*)?"
|
|
80
|
+
lines = text.splitlines()
|
|
81
|
+
start = next((i for i, ln in enumerate(lines)
|
|
82
|
+
if re.match(rf"^seams:\s*{prop}(?:#.*)?$", ln)), None)
|
|
83
|
+
if start is None:
|
|
84
|
+
body, min_indent = lines, 0 # no anchor: a bare `warehouse:` may sit at column 0
|
|
85
|
+
else:
|
|
86
|
+
body, min_indent = lines[start + 1:], 1
|
|
87
|
+
|
|
88
|
+
indent = depth = skip_deeper_than = None
|
|
89
|
+
out = []
|
|
90
|
+
for ln in body:
|
|
91
|
+
if not ln.strip() or _COMMENT.match(ln):
|
|
92
|
+
if depth is not None and ln.strip() == "":
|
|
93
|
+
out.append(ln)
|
|
94
|
+
continue # comments define neither indent nor an end
|
|
95
|
+
cur = len(ln) - len(ln.lstrip())
|
|
96
|
+
if cur < min_indent:
|
|
97
|
+
break # dedented out of `seams:`
|
|
98
|
+
if skip_deeper_than is not None:
|
|
99
|
+
if cur > skip_deeper_than:
|
|
100
|
+
continue # still inside a block scalar
|
|
101
|
+
skip_deeper_than = None
|
|
102
|
+
if depth is None:
|
|
103
|
+
if indent is None:
|
|
104
|
+
indent = cur # the first real child sets the seam-key column
|
|
105
|
+
if cur == indent:
|
|
106
|
+
km = re.match(rf"^\s*{re.escape(seam)}:\s*(.*)$", ln)
|
|
107
|
+
if km:
|
|
108
|
+
rest = re.sub(r"^[&*!][^\s#]*\s*", "", km.group(1).strip())
|
|
109
|
+
rest = re.sub(r"\s*#.*$", "", rest).strip()
|
|
110
|
+
if rest.startswith("{"):
|
|
111
|
+
return rest # inline flow mapping — the seam is all on this line
|
|
112
|
+
if rest == "":
|
|
113
|
+
depth = cur # normal nested block
|
|
114
|
+
# any other inline scalar (`warehouse: null`) opens nothing
|
|
115
|
+
continue
|
|
116
|
+
if cur <= depth:
|
|
117
|
+
break # next seam at the same level
|
|
118
|
+
if _BLOCK_SCALAR.match(ln):
|
|
119
|
+
skip_deeper_than = cur
|
|
120
|
+
continue
|
|
121
|
+
out.append(ln)
|
|
122
|
+
return "\n".join(out)
|
|
123
|
+
|
|
124
|
+
|
|
54
125
|
def warehouse_clis(stack: Path | None) -> list[str]:
|
|
126
|
+
"""Defaults + every `cli:` declared inside the warehouse seam.
|
|
127
|
+
|
|
128
|
+
Scoped to the seam block for two reasons. A multi-target seam declares one `cli:` per target,
|
|
129
|
+
so all of them must be gated. And the previous `DOTALL` scan was unanchored: on a warehouse
|
|
130
|
+
seam with no `cli:` of its own (only Snowflake requires one) that was listed *before* the
|
|
131
|
+
tracker, it captured the tracker's CLI — making `<tracker-cli> ... create ...` trip the
|
|
132
|
+
destructive-statement check and prompt for approval on a plain ticket edit.
|
|
133
|
+
"""
|
|
55
134
|
clis = list(DEFAULT_WAREHOUSE_CLIS)
|
|
56
135
|
if stack:
|
|
57
136
|
try:
|
|
58
|
-
|
|
59
|
-
#
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
clis
|
|
137
|
+
blk = seam_block(stack.read_text(errors="replace"), "warehouse")
|
|
138
|
+
# Unanchored within the block (which is already scoped to the seam) so a flow mapping
|
|
139
|
+
# — `prod: {tool: trino, cli: trino}` — is read too, not just one-key-per-line style.
|
|
140
|
+
for c in re.findall(r"(?:^|[\s{,])cli:\s*([A-Za-z0-9_.-]+)", blk, re.MULTILINE):
|
|
141
|
+
if c not in clis:
|
|
142
|
+
clis.insert(0, c)
|
|
63
143
|
except OSError:
|
|
64
144
|
pass
|
|
65
145
|
return clis
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""SessionStart hook — prime every session with the configured stack + AI-layer index.
|
|
3
|
+
|
|
4
|
+
Prints a compact summary (becomes session additionalContext) so the agent always knows
|
|
5
|
+
which tools are wired, which skills/commands exist, and the lifecycle — without anyone
|
|
6
|
+
having to load all of AGENTS.md. This is the always-on, *tiny* slice of context; the
|
|
7
|
+
`/prime-*` commands load the rest on demand.
|
|
8
|
+
|
|
9
|
+
Wire it in settings.json:
|
|
10
|
+
"hooks": { "SessionStart": [ { "hooks": [
|
|
11
|
+
{ "type": "command", "command": "python3 .claude/hooks/session_context.py" } ] } ] }
|
|
12
|
+
|
|
13
|
+
Stdlib only. Fails open (prints nothing) if the kit isn't configured yet.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
import re
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def project_root() -> Path:
|
|
24
|
+
if os.environ.get("CLAUDE_PROJECT_DIR"):
|
|
25
|
+
return Path(os.environ["CLAUDE_PROJECT_DIR"])
|
|
26
|
+
# hook lives at <root>/.claude/hooks/
|
|
27
|
+
return Path(__file__).resolve().parent.parent.parent
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
_COMMENT = re.compile(r"^\s*#")
|
|
31
|
+
_BLOCK_SCALAR = re.compile(
|
|
32
|
+
r"""^\s*(?:-\s+)?(?:"[^"]*"|'[^']*'|[A-Za-z0-9_.-]+):\s*[|>][-+0-9]*\s*(?:#.*)?$"""
|
|
33
|
+
)
|
|
34
|
+
_PROP = r"(?:[&*!][^\s#]*\s*)?" # an optional YAML anchor/alias/tag after a mapping key
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def seam_block(text: str, seam: str) -> str:
|
|
38
|
+
"""The lines under `<seam>:` inside the top-level `seams:` mapping.
|
|
39
|
+
|
|
40
|
+
The seam-key indent is inferred from the file rather than assumed, comment-only lines carry no
|
|
41
|
+
indentation, a key may hold a YAML anchor, and block-scalar bodies are skipped. Deliberately
|
|
42
|
+
duplicated from db_write_guard.py — hooks are copied individually on a vendored install, so each
|
|
43
|
+
has to stand alone as a stdlib-only script.
|
|
44
|
+
"""
|
|
45
|
+
lines = text.splitlines()
|
|
46
|
+
start = next((i for i, ln in enumerate(lines)
|
|
47
|
+
if re.match(rf"^seams:\s*{_PROP}(?:#.*)?$", ln)), None)
|
|
48
|
+
if start is None:
|
|
49
|
+
return ""
|
|
50
|
+
|
|
51
|
+
indent = depth = skip_deeper_than = None
|
|
52
|
+
out = []
|
|
53
|
+
for ln in lines[start + 1:]:
|
|
54
|
+
if not ln.strip() or _COMMENT.match(ln):
|
|
55
|
+
continue
|
|
56
|
+
cur = len(ln) - len(ln.lstrip())
|
|
57
|
+
if cur == 0:
|
|
58
|
+
break
|
|
59
|
+
if skip_deeper_than is not None:
|
|
60
|
+
if cur > skip_deeper_than:
|
|
61
|
+
continue
|
|
62
|
+
skip_deeper_than = None
|
|
63
|
+
if depth is None:
|
|
64
|
+
if indent is None:
|
|
65
|
+
indent = cur
|
|
66
|
+
if cur == indent:
|
|
67
|
+
km = re.match(rf"^\s*{re.escape(seam)}:\s*(.*)$", ln)
|
|
68
|
+
if km:
|
|
69
|
+
rest = re.sub(r"^[&*!][^\s#]*\s*", "", km.group(1).strip())
|
|
70
|
+
rest = re.sub(r"\s*#.*$", "", rest).strip()
|
|
71
|
+
if rest.startswith("{"):
|
|
72
|
+
return rest # inline flow mapping — the seam is all on this line
|
|
73
|
+
if rest == "":
|
|
74
|
+
depth = cur
|
|
75
|
+
continue
|
|
76
|
+
if cur <= depth:
|
|
77
|
+
break
|
|
78
|
+
if _BLOCK_SCALAR.match(ln):
|
|
79
|
+
skip_deeper_than = cur
|
|
80
|
+
continue
|
|
81
|
+
out.append(ln)
|
|
82
|
+
return "\n".join(out)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def seam_tools(text: str, seam: str) -> list[str]:
|
|
86
|
+
"""Every `tool:` declared in a seam — one for a single mapping, one per target otherwise.
|
|
87
|
+
|
|
88
|
+
A multi-target seam returns the default's tool FIRST, so the banner never implies the wrong
|
|
89
|
+
warehouse is the active one.
|
|
90
|
+
"""
|
|
91
|
+
blk = seam_block(text, seam)
|
|
92
|
+
if not blk:
|
|
93
|
+
return []
|
|
94
|
+
|
|
95
|
+
# A wholly inline seam (`warehouse: {default: prod, targets: {…}}`) has no lines to walk, so
|
|
96
|
+
# read it positionally: tools in document order, default's tool hoisted to the front.
|
|
97
|
+
if blk.lstrip().startswith("{"):
|
|
98
|
+
tools = re.findall(r"\btool:\s*([A-Za-z0-9_.-]+)", blk)
|
|
99
|
+
dm = re.search(r"\bdefault:\s*([A-Za-z0-9_.-]+)", blk)
|
|
100
|
+
if dm:
|
|
101
|
+
# `<name>: {… tool: X …}` — find the tool belonging to the default target.
|
|
102
|
+
tm = re.search(rf"\b{re.escape(dm.group(1))}:\s*\{{[^}}]*\btool:\s*([A-Za-z0-9_.-]+)", blk)
|
|
103
|
+
if tm and tm.group(1) in tools:
|
|
104
|
+
tools = [tm.group(1)] + [t for t in tools if t != tm.group(1)]
|
|
105
|
+
return tools
|
|
106
|
+
|
|
107
|
+
pairs, cur_target, target_indent = [], None, None
|
|
108
|
+
for ln in blk.splitlines():
|
|
109
|
+
m = re.match(r"^(\s*)([A-Za-z0-9_.-]+):\s*(.*)$", ln)
|
|
110
|
+
if not m:
|
|
111
|
+
continue
|
|
112
|
+
ind, key = len(m.group(1)), m.group(2)
|
|
113
|
+
val = re.sub(r"\s*#.*$", "", m.group(3)).strip()
|
|
114
|
+
if key == "tool" and val:
|
|
115
|
+
pairs.append((cur_target, val))
|
|
116
|
+
continue
|
|
117
|
+
if key == "targets":
|
|
118
|
+
continue
|
|
119
|
+
if val == "" or val.startswith("{"):
|
|
120
|
+
# the header of one target — bare `name:`, or `name: {…}` in flow style
|
|
121
|
+
if target_indent is None or ind <= target_indent:
|
|
122
|
+
target_indent, cur_target = ind, key
|
|
123
|
+
tm = re.search(r"\btool:\s*([A-Za-z0-9_.-]+)", val)
|
|
124
|
+
if tm:
|
|
125
|
+
pairs.append((key, tm.group(1))) # flow style: its tool is on this same line
|
|
126
|
+
dm = re.search(r"^\s+default:\s*([A-Za-z0-9_.-]+)", blk, re.MULTILINE)
|
|
127
|
+
if dm:
|
|
128
|
+
d = dm.group(1)
|
|
129
|
+
pairs.sort(key=lambda p: 0 if p[0] == d else 1) # stable: others keep file order
|
|
130
|
+
return [t for _, t in pairs]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def scan_stack(stack: Path) -> dict:
|
|
134
|
+
text = stack.read_text(errors="replace")
|
|
135
|
+
out = {}
|
|
136
|
+
m = re.search(r"^\s*key_prefix:\s*([A-Za-z0-9_-]+)", text, re.MULTILINE)
|
|
137
|
+
out["key_prefix"] = m.group(1) if m else "?"
|
|
138
|
+
for seam in ("tracker", "warehouse", "chat", "docstore", "vcs"):
|
|
139
|
+
tools = seam_tools(text, seam)
|
|
140
|
+
# A multi-target seam renders as "a+b" (default first) so no target is hidden.
|
|
141
|
+
out[seam] = "+".join(tools) if tools else "—"
|
|
142
|
+
return out
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def main() -> int:
|
|
146
|
+
root = project_root()
|
|
147
|
+
stack = root / ".claude/config/stack.yaml"
|
|
148
|
+
if not stack.is_file():
|
|
149
|
+
return 0 # not configured — say nothing
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
s = scan_stack(stack)
|
|
153
|
+
except OSError:
|
|
154
|
+
return 0
|
|
155
|
+
|
|
156
|
+
skills = sorted(p.parent.name for p in (root / ".claude/skills").glob("*/SKILL.md")) \
|
|
157
|
+
if (root / ".claude/skills").is_dir() else []
|
|
158
|
+
commands = sorted(
|
|
159
|
+
p.stem for p in (root / ".claude/commands").glob("*.md")
|
|
160
|
+
) if (root / ".claude/commands").is_dir() else []
|
|
161
|
+
|
|
162
|
+
lines = [
|
|
163
|
+
"## Ticketwright — session context",
|
|
164
|
+
f"Stack ({s['key_prefix']}-tickets): tracker={s['tracker']} · warehouse={s['warehouse']} · "
|
|
165
|
+
f"chat={s['chat']} · docstore={s['docstore']} · vcs={s['vcs']}.",
|
|
166
|
+
"Lifecycle: /ticket (opens + auto-primes context) → /spec-and-build → /review → /ship.",
|
|
167
|
+
]
|
|
168
|
+
if skills:
|
|
169
|
+
lines.append("Skills: " + ", ".join(skills) + ".")
|
|
170
|
+
if commands:
|
|
171
|
+
lines.append("Commands: " + ", ".join(commands) + ".")
|
|
172
|
+
lines.append("Policies enforced: DB writes & external posts require approval (db_write_guard hook + "
|
|
173
|
+
"skill hard-halts); chat defaults to draft; outputs deterministic. See AGENTS.md.")
|
|
174
|
+
print("\n".join(lines))
|
|
175
|
+
return 0
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
if __name__ == "__main__":
|
|
179
|
+
sys.exit(main())
|
|
@@ -95,6 +95,8 @@ def main() -> int:
|
|
|
95
95
|
lines.append(f"- {t.get('owner')}/{t.get('id')} ({d}) — {title}")
|
|
96
96
|
if total > len(tickets):
|
|
97
97
|
lines.append(f"({total - len(tickets)} newer ticket(s) on disk not yet enriched — run the index workflow.)")
|
|
98
|
+
elif len(tickets) > total:
|
|
99
|
+
lines.append(f"({len(tickets) - total} record(s) have no folder on disk — run /refresh index --prune.)")
|
|
98
100
|
print("\n".join(lines))
|
|
99
101
|
return 0
|
|
100
102
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_README": "Rendered to .claude/settings.json by /setup. The hooks block below is for a VENDORED (cp -r) install, where nothing else wires the kit's hooks. On a PLUGIN install (CLAUDE_PLUGIN_ROOT set), .claude-plugin/plugin.json already wires these same hooks from the plugin dir, so /setup OMITS this hooks block to avoid double-firing (double db-write prompts, double index regen) AND ADDS an 'extraKnownMarketplaces' (ticketwright
|
|
2
|
+
"_README": "Rendered to .claude/settings.json by /setup. The hooks block below is for a VENDORED (cp -r) install, where nothing else wires the kit's hooks. On a PLUGIN install (CLAUDE_PLUGIN_ROOT set), .claude-plugin/plugin.json already wires these same hooks from the plugin dir, so /setup OMITS this hooks block to avoid double-firing (double db-write prompts, double index regen) AND ADDS an 'extraKnownMarketplaces' (ticketwright https url source, autoUpdate:true) + 'enabledPlugins' (ticketwright@ticketwright:true) block so the REPO opts every user into the plugin at PROJECT scope (a plugin cannot set its own install scope — the repo does; teammates are prompted to install), committed with the repo, and self-updates on formal releases only (version-string change; never un-released main commits). Do NOT add those two keys on a vendored install — there is no marketplace to enable from. permissions + statusLine are written in both modes (setup appends stack-specific CLI allows); on a plugin install setup copies statusline.sh into .claude/ so the relative command resolves. The db_write_guard hook turns 'db_write_requires_approval' into a mechanical ask before any destructive warehouse statement.",
|
|
3
3
|
"hooks": {
|
|
4
4
|
"PreToolUse": [
|
|
5
5
|
{
|
|
@@ -22,8 +22,16 @@ Two layers, kept separate so the catalog is reproducible and CI/pre-commit-safe:
|
|
|
22
22
|
marked `▱`).
|
|
23
23
|
|
|
24
24
|
## Phase 2 — Enrich (the model half)
|
|
25
|
-
4. Decide scope
|
|
26
|
-
|
|
25
|
+
4. Decide scope (enrichment is the model (re)writing curated summaries — **never rewrite curated
|
|
26
|
+
summaries silently**):
|
|
27
|
+
- a **specific ticket id (or ids)** — enrich just those;
|
|
28
|
+
- **default** (no flag): only the **un-enriched + stale** set from `--stats` — the safe day-to-day
|
|
29
|
+
scope;
|
|
30
|
+
- **`--all`**: cover every ticket **but skip those already enriched and fresh** — the bootstrap
|
|
31
|
+
scope for a new or newly-adopted backlog (render everything, then enrich only what's missing or
|
|
32
|
+
changed);
|
|
33
|
+
- **`--force`** (a.k.a. `--reenrich-all`): genuinely re-enrich **everything**, rewriting even
|
|
34
|
+
hand-curated, non-stale summaries. Rare, and destructive to curation — only on an explicit ask.
|
|
27
35
|
5. For each target ticket, read its `README.md` and write ONE record:
|
|
28
36
|
`{id, owner, title, status, date, summary (<=180 chars, lead with what was delivered + key
|
|
29
37
|
numbers), tags (1-4 kebab-case), cross_refs (other ticket ids), objects (qualified data objects
|
|
@@ -53,11 +53,17 @@ include the key commented with a `# TODO` and keep going — `verify` will point
|
|
|
53
53
|
`private/` subfolder), the AI-layer index, and the seeded ticket index.
|
|
54
54
|
|
|
55
55
|
### Phase 4 — Verify & hand off
|
|
56
|
-
6.
|
|
57
|
-
`!bash "${CLAUDE_PLUGIN_ROOT:-$CLAUDE_PROJECT_DIR}/bin/
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
6. **Two distinct checks — keep them labeled as such in the report:**
|
|
57
|
+
- `!bash "${CLAUDE_PLUGIN_ROOT:-$CLAUDE_PROJECT_DIR}/bin/selftest.sh"` — **kit integrity**. It
|
|
58
|
+
validates the plugin's *own bundled example* stacks, **not** your repo's config. A failure here
|
|
59
|
+
is fatal.
|
|
60
|
+
- `!bash "${CLAUDE_PLUGIN_ROOT:-$CLAUDE_PROJECT_DIR}/bin/verify_stack.sh" .claude/config/stack.yaml`
|
|
61
|
+
— **your repo's stack** reachability (pass the repo stack path explicitly so it's unambiguous
|
|
62
|
+
which config was checked). An unreachable seam is **not** fatal at setup time; print its
|
|
63
|
+
adapter's auth notes as the fix.
|
|
64
|
+
7. **Report:** name which check is which (selftest = kit integrity; verify_stack = *your* seams), then
|
|
65
|
+
the chosen stack, files written, any `# TODO` keys or stub adapters, and the next step —
|
|
66
|
+
`/ticket <id>` to start work, or `/setup --teammate` for a new person.
|
|
61
67
|
8. **Offer to commit the scaffold.** What setup just wrote (`.claude/config/stack.yaml`, `AGENTS.md`,
|
|
62
68
|
`CLAUDE.md`, `.claude/settings.json`, `.gitignore`, `documentation/AI_LAYER_INDEX.md`, the seeded `tickets/`
|
|
63
69
|
index — plus, on a vendored install, the kit itself) is untracked; if it isn't committed, a later
|
|
@@ -39,5 +39,7 @@ Include: what was auto-configured, what needs a human decision, and the suggeste
|
|
|
39
39
|
real ticket through `/ticket → /review → /ship` before deleting anything custom.
|
|
40
40
|
|
|
41
41
|
## 5 · Verify & report
|
|
42
|
-
Same as Phase 4 of the default mode
|
|
43
|
-
|
|
42
|
+
Same as Phase 4 of the default mode: `selftest.sh` (**kit integrity** — the plugin's own example
|
|
43
|
+
stacks) then `verify_stack.sh .claude/config/stack.yaml` (**your repo's** seams). Label the two in
|
|
44
|
+
the report so the kit's example stack is never mistaken for the repo's config. The report leads with
|
|
45
|
+
the MIGRATION.md path and the trial-ticket suggestion — adoption is incremental by design.
|
|
@@ -33,7 +33,7 @@ itself:
|
|
|
33
33
|
{
|
|
34
34
|
"extraKnownMarketplaces": {
|
|
35
35
|
"ticketwright": {
|
|
36
|
-
"source": { "source": "
|
|
36
|
+
"source": { "source": "url", "url": "https://github.com/kyle-chalmers/ticketwright.git" },
|
|
37
37
|
"autoUpdate": true
|
|
38
38
|
}
|
|
39
39
|
},
|
|
@@ -43,6 +43,10 @@ itself:
|
|
|
43
43
|
}
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
+
The source is an explicit `https://…git` URL, not the `owner/repo` shorthand — the shorthand can
|
|
47
|
+
resolve to SSH and fail for users without GitHub SSH keys, whereas the URL clones over HTTPS via the
|
|
48
|
+
git credential helper. A fork edits just this URL.
|
|
49
|
+
|
|
46
50
|
`autoUpdate` re-installs **only when the plugin's version string changes** — i.e. only on a formal
|
|
47
51
|
release (the release commit bumps `plugin.json`/`marketplace.json`/`__init__.py` in lockstep and tags
|
|
48
52
|
`v*`). Between releases, ordinary commits to the default branch leave the version untouched, so
|
|
@@ -77,4 +81,7 @@ SessionStart surfacing, curated summaries at ship time). An existing backlog get
|
|
|
77
81
|
`/refresh index --all`.
|
|
78
82
|
|
|
79
83
|
That renderer also writes the Obsidian graph layer (`tickets/graph/` + `tickets/objects/`) when
|
|
80
|
-
`project.graph_notes` is on (the default), committed alongside `INDEX.md`/`OBJECTS.md`.
|
|
84
|
+
`project.graph_notes` is on (the default), committed alongside `INDEX.md`/`OBJECTS.md`. It then seeds
|
|
85
|
+
`.obsidian/graph.json` (unless `project.graph_config: false`) with the tickets↔objects filter + color
|
|
86
|
+
groups so the Graph view opens ready-to-read — create/merge-only, so it never clobbers a user's manual
|
|
87
|
+
graph tweaks.
|
|
@@ -40,14 +40,20 @@ authorization, execute in order:
|
|
|
40
40
|
7. **chat.draft** to `seams.chat.default_channel` (policy `chat_default_draft` — the human clicks
|
|
41
41
|
send unless they said "send it", in which case `chat.send`). Smart links for ticket id(s),
|
|
42
42
|
files, PR.
|
|
43
|
-
8. **vcs.commit** —
|
|
44
|
-
|
|
45
|
-
`
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
gitignored
|
|
50
|
-
|
|
43
|
+
8. **vcs.commit** — **first, isolate repo-setup / AI-layer files.** If any are dirty
|
|
44
|
+
(`.claude/settings.json`, `.claude/config/stack.yaml`, `.claude/statusline.sh`, `AGENTS.md`/
|
|
45
|
+
`CLAUDE.md`, `documentation/AI_LAYER_INDEX.md`, `.gitignore`) they belong to the repo's plugin
|
|
46
|
+
setup, not this ticket — give them a separate `chore(plugins): …` commit on this ticket's branch
|
|
47
|
+
(a distinct commit that rides the ticket's one PR — not a second PR, never folded into the ticket
|
|
48
|
+
commit); `.claude/settings.local.json` +
|
|
49
|
+
`.claude/worktrees/` are gitignored, leave them. Then stage this ticket's paths (deliverable files
|
|
50
|
+
included: they're committed by default so results live with the ticket and show in the PR) **plus
|
|
51
|
+
`tickets/INDEX.md` + `tickets/OBJECTS.md` + `tickets/index_data.json`** (all three, or `--check`
|
|
52
|
+
flags drift in CI; semantic message + Co-Authored-By). **Before staging, list the
|
|
53
|
+
`final_deliverables/` files that will be committed and confirm none carry PII/customer data that
|
|
54
|
+
shouldn't be in git** — if any do, have the user rename them `*.private.csv` (etc.) or move them
|
|
55
|
+
under a `private/` subfolder (both gitignored) first. Then **vcs.open_pr** (semantic title; body =
|
|
56
|
+
Business Impact / Deliverables / Technical Notes / QC).
|
|
51
57
|
9. **transition** the ticket toward `project.terminal_status` if appropriate.
|
|
52
58
|
|
|
53
59
|
## Phase C — System-evolution retro (always, even on success)
|
|
@@ -30,8 +30,8 @@ context-engineering core idea: AI fails from missing context, not weak models.
|
|
|
30
30
|
3. **Write the spec** from `${CLAUDE_PLUGIN_ROOT:-$CLAUDE_PROJECT_DIR}/templates/spec.md.tmpl` into the ticket's folder
|
|
31
31
|
(`specs/<id>-<slug>.md` or `final_deliverables/`): operation type (new/alter), data grain,
|
|
32
32
|
sources + join/cast rules, transformation logic, **validation gates** (the exact QC the build must
|
|
33
|
-
pass), downstream impact, dev
|
|
34
|
-
(1–10)**.
|
|
33
|
+
pass), downstream impact, the **dev target** (`seams.warehouse.dev_target`, else the key the
|
|
34
|
+
warehouse adapter names in its `dev_key:` frontmatter), and a **confidence score (1–10)**.
|
|
35
35
|
4. **Reduce assumptions:** before finalizing, list open questions and **ask the user** (don't guess).
|
|
36
36
|
5. **Commit the spec** via vcs `commit` (`docs: <id> spec for <thing>`) — policy
|
|
37
37
|
`commit_plan_before_implement` enables blame-free retry if the build later reveals a gap.
|
|
@@ -41,7 +41,7 @@ context-engineering core idea: AI fails from missing context, not weak models.
|
|
|
41
41
|
6. **Load** the committed spec (path arg or newest in the ticket's `specs/`). Treat it as the source
|
|
42
42
|
of truth, but **validate each step independently** — don't blindly follow; the spec can be wrong.
|
|
43
43
|
7. **Implement in small build-and-check sub-loops:** one object/step at a time. Develop against
|
|
44
|
-
|
|
44
|
+
the warehouse's **dev target** first; parameterize values at the top **via a CTE params row
|
|
45
45
|
(`WITH params AS (SELECT … AS anchor) … CROSS JOIN params`), not a session `DECLARE`/`SET`** —
|
|
46
46
|
CTE params stay portable and keep CSV exports clean; explicit `ORDER BY` on any export
|
|
47
47
|
(deterministic outputs).
|