@amsterdamdatalabs/enact-extensions 0.1.49 → 0.1.51
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.
- package/LICENSE +20 -0
- package/dist/internal/extensions-table.d.ts +16 -6
- package/dist/internal/extensions-table.d.ts.map +1 -1
- package/dist/internal/extensions-table.js +25 -21
- package/dist/internal/extensions-table.js.map +1 -1
- package/extensions/enact-repo/.agents/plugin.json +1 -1
- package/extensions/enact-repo/hooks/hooks.json +0 -1
- package/extensions/enact-repo/skills/to-feature/SKILL.md +91 -0
- package/extensions/enact-repo/skills/to-feature/references/FEATURE-SPEC-TEMPLATE.md +87 -0
- package/extensions/enact-repo/skills/to-workitems/SKILL.md +89 -0
- package/extensions/enact-repo/skills/to-workitems/references/AZURE-DEVOPS-HIERARCHY.md +118 -0
- package/extensions/enact-repo/skills/to-workitems/references/TICKET-TEMPLATES.md +68 -0
- package/extensions/enact-repo/skills/wayfinder/SKILL.md +1 -1
- package/package.json +1 -1
- package/scripts/lib/run-repo-install.mjs +3 -3
- package/extensions/enact-repo/skills/to-spec/SKILL.md +0 -85
- package/extensions/enact-repo/skills/to-tickets/SKILL.md +0 -75
- package/extensions/enact-repo/skills/to-tickets/references/TICKET-TEMPLATES.md +0 -39
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Proprietary License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Amsterdam Data Labs B.V.
|
|
4
|
+
|
|
5
|
+
All rights reserved.
|
|
6
|
+
|
|
7
|
+
This software and its accompanying source code, documentation and configuration
|
|
8
|
+
are the confidential and proprietary property of Amsterdam Data Labs B.V..
|
|
9
|
+
|
|
10
|
+
No part of this work may be reproduced, copied, modified, distributed,
|
|
11
|
+
published, sublicensed or transmitted in any form or by any means, electronic
|
|
12
|
+
or mechanical, without the prior written permission of the copyright holder.
|
|
13
|
+
|
|
14
|
+
Unauthorised use, reproduction or distribution of this work, or any portion of
|
|
15
|
+
it, is a breach of that copyright and may result in civil and criminal
|
|
16
|
+
penalties.
|
|
17
|
+
|
|
18
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
19
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
20
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
*/
|
|
25
25
|
/** The marker table itself, written once when the file gains `[extensions]`. */
|
|
26
26
|
export declare const EXTENSIONS_BLOCK = "[extensions]\nversion = 1\n";
|
|
27
|
-
/** The text declaring ONE extension,
|
|
28
|
-
export declare function extensionBlock(name: string
|
|
27
|
+
/** The text declaring ONE extension: membership, and nothing else. */
|
|
28
|
+
export declare function extensionBlock(name: string): string;
|
|
29
29
|
/**
|
|
30
30
|
* Declared extension names, from the `[extensions.<name>]` sub-tables.
|
|
31
31
|
*
|
|
@@ -44,11 +44,21 @@ export declare function declaredExtensionsFrom(parsed: unknown): string[];
|
|
|
44
44
|
* and comments inside the declaration were silently destroyed by the next run.
|
|
45
45
|
* `enact-config.toml` is the hand-authored statement of what the repo should
|
|
46
46
|
* have — install reads it and reconciles the repo to it, and writes to it only
|
|
47
|
-
* to record an extension that was not declared before.
|
|
48
|
-
*
|
|
49
|
-
*
|
|
47
|
+
* to record an extension that was not declared before.
|
|
48
|
+
*
|
|
49
|
+
* The declaration carries NO version. It said which version was installed, but
|
|
50
|
+
* nothing read it: `runRepoDoctor` compares the lock's version against the
|
|
51
|
+
* bundle's and never opens this file. Keeping that third copy current meant
|
|
52
|
+
* rewriting a protected, hand-authored file on every bundle bump, so a
|
|
53
|
+
* skills-only change dragged `enact-config.toml` into a commit that needs a
|
|
54
|
+
* human to land. Membership belongs here; the version belongs to the bundle
|
|
55
|
+
* that declares it and the lock that records what was installed.
|
|
56
|
+
*
|
|
57
|
+
* A version left by an earlier install is therefore CUT rather than updated —
|
|
58
|
+
* it is install's own key inside install's own block, and leaving it would
|
|
59
|
+
* strand a value nothing maintains.
|
|
50
60
|
*/
|
|
51
|
-
export declare function declareExtension(configText: string, name: string
|
|
61
|
+
export declare function declareExtension(configText: string, name: string): string | null;
|
|
52
62
|
/** Return `configText` with `[extensions.<name>]` removed entirely. */
|
|
53
63
|
export declare function removeExtensionBlock(configText: string, name: string): string;
|
|
54
64
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extensions-table.d.ts","sourceRoot":"","sources":["../../src/internal/extensions-table.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,gFAAgF;AAChF,eAAO,MAAM,gBAAgB,gCAAgC,CAAC;AAE9D,
|
|
1
|
+
{"version":3,"file":"extensions-table.d.ts","sourceRoot":"","sources":["../../src/internal/extensions-table.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,gFAAgF;AAChF,eAAO,MAAM,gBAAgB,gCAAgC,CAAC;AAE9D,sEAAsE;AACtE,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEnD;AAKD;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,EAAE,CAMhE;AAkBD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAoBhF;AAED,uEAAuE;AACvE,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAS7E;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CA0BxF;AAoBD;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAoBtE"}
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
*/
|
|
25
25
|
/** The marker table itself, written once when the file gains `[extensions]`. */
|
|
26
26
|
export const EXTENSIONS_BLOCK = "[extensions]\nversion = 1\n";
|
|
27
|
-
/** The text declaring ONE extension,
|
|
28
|
-
export function extensionBlock(name
|
|
29
|
-
return `[extensions.${name}]\
|
|
27
|
+
/** The text declaring ONE extension: membership, and nothing else. */
|
|
28
|
+
export function extensionBlock(name) {
|
|
29
|
+
return `[extensions.${name}]\n`;
|
|
30
30
|
}
|
|
31
31
|
/** `version = "..."` — the one key install owns inside a declaration. */
|
|
32
32
|
const VERSION_KEY = /^\s*version\s*=/;
|
|
@@ -71,11 +71,21 @@ function tableEnd(lines, headerIndex, isOwnSubTable) {
|
|
|
71
71
|
* and comments inside the declaration were silently destroyed by the next run.
|
|
72
72
|
* `enact-config.toml` is the hand-authored statement of what the repo should
|
|
73
73
|
* have — install reads it and reconciles the repo to it, and writes to it only
|
|
74
|
-
* to record an extension that was not declared before.
|
|
75
|
-
*
|
|
76
|
-
*
|
|
74
|
+
* to record an extension that was not declared before.
|
|
75
|
+
*
|
|
76
|
+
* The declaration carries NO version. It said which version was installed, but
|
|
77
|
+
* nothing read it: `runRepoDoctor` compares the lock's version against the
|
|
78
|
+
* bundle's and never opens this file. Keeping that third copy current meant
|
|
79
|
+
* rewriting a protected, hand-authored file on every bundle bump, so a
|
|
80
|
+
* skills-only change dragged `enact-config.toml` into a commit that needs a
|
|
81
|
+
* human to land. Membership belongs here; the version belongs to the bundle
|
|
82
|
+
* that declares it and the lock that records what was installed.
|
|
83
|
+
*
|
|
84
|
+
* A version left by an earlier install is therefore CUT rather than updated —
|
|
85
|
+
* it is install's own key inside install's own block, and leaving it would
|
|
86
|
+
* strand a value nothing maintains.
|
|
77
87
|
*/
|
|
78
|
-
export function declareExtension(configText, name
|
|
88
|
+
export function declareExtension(configText, name) {
|
|
79
89
|
const lines = configText.split("\n");
|
|
80
90
|
if (!lines.some((l) => l.trim() === "[extensions]"))
|
|
81
91
|
return null;
|
|
@@ -83,23 +93,17 @@ export function declareExtension(configText, name, version) {
|
|
|
83
93
|
const at = lines.findIndex((l) => l.trim() === header);
|
|
84
94
|
if (at < 0) {
|
|
85
95
|
const tail = configText.endsWith("\n") ? "" : "\n";
|
|
86
|
-
return `${configText}${tail}\n${extensionBlock(name
|
|
96
|
+
return `${configText}${tail}\n${extensionBlock(name)}`;
|
|
87
97
|
}
|
|
88
|
-
// Already declared:
|
|
89
|
-
// span between this header and the next is walked line by line rather
|
|
90
|
-
// replaced, so other keys keep their values and comments
|
|
98
|
+
// Already declared: drop the ONE key install owns, if an older run wrote it.
|
|
99
|
+
// The span between this header and the next is walked line by line rather
|
|
100
|
+
// than replaced, so other keys keep their values and comments their places.
|
|
91
101
|
const end = tableEnd(lines, at);
|
|
92
|
-
const
|
|
102
|
+
const keyAt = lines.slice(at + 1, end).findIndex((l) => VERSION_KEY.test(l));
|
|
103
|
+
if (keyAt < 0)
|
|
104
|
+
return null;
|
|
93
105
|
const out = [...lines];
|
|
94
|
-
|
|
95
|
-
if (keyAt >= 0) {
|
|
96
|
-
if (out[at + 1 + keyAt] === rendered)
|
|
97
|
-
return null;
|
|
98
|
-
out[at + 1 + keyAt] = rendered;
|
|
99
|
-
}
|
|
100
|
-
else {
|
|
101
|
-
out.splice(at + 1, 0, rendered);
|
|
102
|
-
}
|
|
106
|
+
out.splice(at + 1 + keyAt, 1);
|
|
103
107
|
return keepTrailingNewline(configText, out.join("\n"));
|
|
104
108
|
}
|
|
105
109
|
/** Return `configText` with `[extensions.<name>]` removed entirely. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extensions-table.js","sourceRoot":"","sources":["../../src/internal/extensions-table.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,gFAAgF;AAChF,MAAM,CAAC,MAAM,gBAAgB,GAAG,6BAA6B,CAAC;AAE9D,
|
|
1
|
+
{"version":3,"file":"extensions-table.js","sourceRoot":"","sources":["../../src/internal/extensions-table.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "enact-repo",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Baseline agent setup for any repository: the full Enact skill catalog plus guard, briefing, and code-review-graph hooks and MCP tooling, installed at repo scope.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "amsterdamdatalabs"
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: to-feature
|
|
3
|
+
description: Turn the current conversation into a feature spec and open it as a Feature work item on Azure DevOps, parented to the plan's Epic — no interview, just synthesis of what you have already discussed.
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# To Feature
|
|
8
|
+
|
|
9
|
+
Open a **Feature** on the board and give it a plan folder to live in.
|
|
10
|
+
|
|
11
|
+
**A plan is a Feature, and a Feature is a directory** — `features/az<id>-<slug>/spec.md`, named for the work item it is. This skill creates that Feature and that directory; `to-workitems` later fills its `items/` with PBIs and Bugs parented under it. Where the repo carries a `plans/AGENTS.md`, it governs; otherwise follow the global `## Plans and work items` contract in your host's agent instructions.
|
|
12
|
+
|
|
13
|
+
Do NOT interview the user — synthesize what you already know from the conversation and the codebase.
|
|
14
|
+
|
|
15
|
+
## Use When
|
|
16
|
+
|
|
17
|
+
- The current conversation, plus codebase exploration, already gives you enough to synthesize a feature spec, and the Epic it belongs under is known.
|
|
18
|
+
|
|
19
|
+
## Do Not Use When
|
|
20
|
+
|
|
21
|
+
- The user's intent is still unclear and needs an interview or clarifying questions first — this skill explicitly skips that; gather requirements before invoking it.
|
|
22
|
+
- The way to the destination is not visible yet — chart the effort with `wayfinder` instead, and come back when the decisions are resolved.
|
|
23
|
+
- `epic:` is unset. **Stop and ask.** `epic:` is authored, never tool-created, and the server refuses `Epic` as a creatable type outright — so a typo must fail here rather than hang the plan off the wrong portfolio branch.
|
|
24
|
+
|
|
25
|
+
## Process
|
|
26
|
+
|
|
27
|
+
### 1. Explore the repo
|
|
28
|
+
|
|
29
|
+
Understand the current state of the codebase, if you haven't already. Use the project's domain glossary vocabulary throughout the spec, and respect any ADRs in the area you're touching.
|
|
30
|
+
|
|
31
|
+
### 2. Sketch the seams
|
|
32
|
+
|
|
33
|
+
Sketch out the seams at which you're going to test the feature. Existing seams should be preferred to new ones. Use the highest seam possible. If new seams are needed, propose them at the highest point you can. The fewer seams across the codebase, the better — the ideal number is one.
|
|
34
|
+
|
|
35
|
+
Check with the user that these seams match their expectations.
|
|
36
|
+
|
|
37
|
+
### 3. Have the human confirm the Epic
|
|
38
|
+
|
|
39
|
+
**Stop here and ask.** Show the user the id in `epic:` and the title of the Feature you are about to create under it, and get an explicit confirmation before going on.
|
|
40
|
+
|
|
41
|
+
This is a human step because **no agent-side check can establish which Epic is the right one — only that the parent named is an Epic at all.** Agents have no access to Epics: fetching one by id is refused, so there is no tool call that reads `epic:` back and confirms it names the Epic you intend. Do not go looking for one, and do not treat a successful create as agreement on identity — it proves the type, never the choice.
|
|
42
|
+
|
|
43
|
+
Be careful with that refusal if you meet it — it reports only that the id is not a tracked type, which is equally true of a Task and of an id you cannot see. It does **not** say "this is an Epic", and reading it that way turns one observation into a proof it cannot carry.
|
|
44
|
+
|
|
45
|
+
The access is asymmetric, and step 5 depends on the half that works: you cannot **read** an Epic, but you can **write** a parent link to one. The server checks that write's parent **type** — a PBI or Bug aimed at the wrong level is refused before the request is sent, and a Feature is no longer the exception: `workitem_create_or_update` refuses a wrong-typed parent by name, on create and on update alike (`Feature must be parented to a Epic; work item <id> is a <type>`). What that check cannot do is tell you whether `<id>` is the **right** Epic, only that it is *an* Epic — a Feature parented to the wrong Epic still creates cleanly, no error, silently on the wrong branch of the portfolio. The confirmation you get here is the only thing standing between you and that.
|
|
46
|
+
|
|
47
|
+
`epic:` is **authored, never tool-created**. If it is unset, that is a stop — ask for it. Never create an Epic to fill the gap; the server refuses `Epic` as a creatable type outright, and a typo must fail loudly rather than quietly spawn a duplicate.
|
|
48
|
+
|
|
49
|
+
### 4. Draft the spec, but do not write it yet
|
|
50
|
+
|
|
51
|
+
Draft the spec against the template in [references/FEATURE-SPEC-TEMPLATE.md](references/FEATURE-SPEC-TEMPLATE.md). Hold it in the conversation — it has no folder to live in yet, because it has no id yet.
|
|
52
|
+
|
|
53
|
+
**The id is the identity: allocate the work item first, then name the file.** A Feature is never written to disk under a placeholder and renamed afterwards. This skill allocates the id itself one step from now, so a placeholder would exist only for the length of a single tool call and buy nothing — it would be a second name for the same thing, and the rename is a step that can fail and leave the folder lying.
|
|
54
|
+
|
|
55
|
+
### 5. Create the Feature
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
workitem_create_or_update({
|
|
59
|
+
type: 'Feature',
|
|
60
|
+
title: <the outcome, as it reads on the board>,
|
|
61
|
+
description: <the spec's ## Problem and ## Solution>,
|
|
62
|
+
acceptanceCriteria: <the spec's ## Acceptance criteria, verbatim>,
|
|
63
|
+
parentId: <the epic>,
|
|
64
|
+
})
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`description` and `acceptanceCriteria` are both advertised arguments — the plan's prose **is** those two fields, so send them on the create call rather than leaving the board holding a title and a parent. If the file and the field differ, the board is lying. `assignedTo`, `priority`, `startDate` and `targetDate` map the same way from the template's frontmatter when the plan sets them.
|
|
68
|
+
|
|
69
|
+
`parentId` is **always** the Epic — the one adjacent level a Feature may hang from. Omitting it is refused as `requires parentId`, whatever the type; there is no root case to fall back on.
|
|
70
|
+
|
|
71
|
+
**Set no state and no gate flags.** A Feature carries neither: the six Build flags apply only to Product Backlog Item and Bug, and a Feature's custom fields are empty. Do not carry a `fields: {...}` idiom across from a skill that works on PBIs — that raw-fields escape hatch reaches `System.State` without the dedicated `state` argument the gate matrix reads, which is the trap it exists to close. A new Feature lands in Azure DevOps's own default state, `New`, and moving it is a later, separate pass.
|
|
72
|
+
|
|
73
|
+
### 6. Record the id, then verify the link landed
|
|
74
|
+
|
|
75
|
+
Create `features/az<id>-<slug>/` — named with the id the create call just returned, which is the only name it ever has — and write the drafted spec into its `spec.md` with `work_item:` set to that id and `epic:` carried through. Do this **immediately**, before anything else: a crash then leaves a folder that says what exists on the board, rather than a Feature nobody can trace back to a plan.
|
|
76
|
+
|
|
77
|
+
Leave `items/` uncreated. A Feature whose items do not exist yet is the normal resting state of a fresh plan, not an unfinished one.
|
|
78
|
+
|
|
79
|
+
Then read the Feature back and confirm `links.parent` is the `epic:` id:
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
workitem_list_or_get({ id: <the new feature>, raw: true })
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`raw: true` is necessary rather than stylistic — the grouped view does not carry `System.WorkItemType` verbatim. This reads the **Feature**, never the Epic, so it stays inside what an agent can see.
|
|
86
|
+
|
|
87
|
+
Be exact about what it proves: that the link landed on the id the human confirmed. The create having succeeded already proved that id is an Epic — a wrong type would have failed loudly at step 5 with `Feature must be parented to a Epic; work item <id> is a <type>`, before this read ever ran. What no agent-side check proves, here or at step 5, is that it is the **right** Epic. If `links.parent` is anything other than the `epic:` id, say so plainly rather than moving on; the plan is already on the board by then, and a wrong parent is worth raising immediately.
|
|
88
|
+
|
|
89
|
+
### 7. Stop
|
|
90
|
+
|
|
91
|
+
Slicing the feature into items is `to-workitems`' job, and it wants item files that do not exist yet. Tell the user the Feature is open and what its id is; do not start creating children in this run.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# The plan document
|
|
2
|
+
|
|
3
|
+
`features/az<id>-<slug>/spec.md`. **This file IS the Feature** — the long form of its work
|
|
4
|
+
item, not a parallel copy. `## Problem` and `## Solution` are its Description; `## Acceptance
|
|
5
|
+
criteria` is its Acceptance Criteria field. If the two ever differ, the board is lying.
|
|
6
|
+
|
|
7
|
+
Where the repo carries `plans/templates/feature.md`, that file is the source and this one is
|
|
8
|
+
only its shape — author from the repo's copy.
|
|
9
|
+
|
|
10
|
+
```markdown
|
|
11
|
+
---
|
|
12
|
+
type: Feature
|
|
13
|
+
title: "[the outcome, as it reads on the board]"
|
|
14
|
+
description: "[one line — becomes System.Description]"
|
|
15
|
+
status: draft
|
|
16
|
+
created: YYYY-MM-DD
|
|
17
|
+
epic: <id>
|
|
18
|
+
work_item: <id>
|
|
19
|
+
assigned_to: <email>
|
|
20
|
+
priority: 2
|
|
21
|
+
start_date: YYYY-MM-DD
|
|
22
|
+
target_date: YYYY-MM-DD
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
# [the outcome]
|
|
26
|
+
|
|
27
|
+
## Problem
|
|
28
|
+
|
|
29
|
+
What is wrong today, and what it costs. Evidence, not assertion — the command whose output
|
|
30
|
+
shows it, the file and line, the number.
|
|
31
|
+
|
|
32
|
+
## Solution
|
|
33
|
+
|
|
34
|
+
The shape of the fix, from the outside in. What changes for whoever uses this. Not a
|
|
35
|
+
layer-by-layer implementation list — that is the items' job.
|
|
36
|
+
|
|
37
|
+
## Acceptance criteria
|
|
38
|
+
|
|
39
|
+
Checkable statements. This is the same text as the work item's Acceptance Criteria field.
|
|
40
|
+
|
|
41
|
+
- [ ] [something a reviewer can confirm without reading the diff]
|
|
42
|
+
|
|
43
|
+
## Scope
|
|
44
|
+
|
|
45
|
+
In: [paths or systems]
|
|
46
|
+
|
|
47
|
+
Non-goals: [what a reader would wrongly assume is included]
|
|
48
|
+
|
|
49
|
+
## Decisions
|
|
50
|
+
|
|
51
|
+
- [decision, and what it rules out]
|
|
52
|
+
|
|
53
|
+
## Items
|
|
54
|
+
|
|
55
|
+
Order and dependencies. What each delivers lives in its own file under `items/`.
|
|
56
|
+
|
|
57
|
+
| # | Item | Work item | Predecessor |
|
|
58
|
+
| --- | --- | --- | --- |
|
|
59
|
+
| 1 | `az<id>-<slug>.md` | <id> | — |
|
|
60
|
+
|
|
61
|
+
## Verification
|
|
62
|
+
|
|
63
|
+
The end-to-end check on the acceptance criteria above. Each item carries its own check — this
|
|
64
|
+
is the one that only means anything once they have all landed.
|
|
65
|
+
|
|
66
|
+
## Done when
|
|
67
|
+
|
|
68
|
+
- [ ] Every item is Done on the board.
|
|
69
|
+
- [ ] Acceptance criteria all met, with observed evidence.
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## `status:` is about the document
|
|
73
|
+
|
|
74
|
+
`status: draft` describes this file, not the work. The work's state is the Feature's board
|
|
75
|
+
state, and it lives there alone — there is no `plan_state`, no `approval_state`, and no
|
|
76
|
+
directory that moves to record progress.
|
|
77
|
+
|
|
78
|
+
## Two rows to leave empty, and one to fill
|
|
79
|
+
|
|
80
|
+
`work_item:` is filled from the create call's return, in the same step that names the folder.
|
|
81
|
+
|
|
82
|
+
`epic:` was **authored before this skill ran** — it is never tool-created. If it is unset,
|
|
83
|
+
that is a stop, not a value to allocate.
|
|
84
|
+
|
|
85
|
+
The `## Items` table's `Work item` column stays empty here. `to-workitems` fills it when it
|
|
86
|
+
creates the children, and that is the only join between the plan and its items: the link runs
|
|
87
|
+
one way, each item naming its `feature:`, and the Feature never lists its items anywhere else.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: to-workitems
|
|
3
|
+
description: Publish a change plan's backlog items to Azure DevOps — one PBI or Bug per item file, parented under the plan's Feature, with each id written back into the file it came from.
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# To Work Items
|
|
8
|
+
|
|
9
|
+
Put a plan's **backlog items** on the board.
|
|
10
|
+
|
|
11
|
+
**A plan is a Feature. An item file is a PBI or a Bug.** One file each. The plan and its `## Items` table are written before this skill runs; the item files are not, because each is named for an id only this skill allocates. Where the repo carries a `plans/AGENTS.md`, it governs the layout; otherwise follow the global `## Plans and work items` contract in your host's agent instructions. [references/AZURE-DEVOPS-HIERARCHY.md](references/AZURE-DEVOPS-HIERARCHY.md) carries the board rules both depend on.
|
|
12
|
+
|
|
13
|
+
## Use When
|
|
14
|
+
|
|
15
|
+
- A plan folder's `spec.md` is approved, its `## Items` table is filled in, and those items need to be on the board.
|
|
16
|
+
|
|
17
|
+
## Do Not Use When
|
|
18
|
+
|
|
19
|
+
- The plan has open decisions under `## Decisions` — resolve them first, or chart the effort with `wayfinder` if the destination itself is still unclear.
|
|
20
|
+
- `epic:` or `feature:` is unset. **Stop and ask.** Never create an Epic or Feature to fill the gap: a typo must fail here, not silently spawn a duplicate. The server refuses a stray Epic outright, and refuses a wrong-*typed* parent for any creatable type — but a right-typed id can still be the **wrong** one, and nothing on the board catches that. Identity is on you.
|
|
21
|
+
|
|
22
|
+
## Process
|
|
23
|
+
|
|
24
|
+
### 1. Read the plan and check the board
|
|
25
|
+
|
|
26
|
+
`workitem_list_or_get({ id: <feature>, raw: true })` and assert:
|
|
27
|
+
|
|
28
|
+
- `fields['System.WorkItemType']` is `Feature`
|
|
29
|
+
- `links.parent` is `epic`
|
|
30
|
+
|
|
31
|
+
One call settles both — there is no need to fetch the Epic itself. `parentId` for every item this skill creates is **always** the Feature — never the Epic.
|
|
32
|
+
|
|
33
|
+
### 2. Read the plan's `## Items` table
|
|
34
|
+
|
|
35
|
+
**The rows are the source, not files.** A file under `items/` is named `az<work item id>-<slug>.md`, so **the id is the identity** — and it is always a real work item id. An item that has no id yet therefore has no file and cannot have one: allocate the work item first, then write the file. Nothing is drafted under a placeholder and renamed into truth later.
|
|
36
|
+
|
|
37
|
+
Each row has to carry the whole item before the board sees it: its title, its `item_type` (`pbi` or `bug`), what it delivers, its falsifiable verification, and its predecessors. A row missing any of those is a stop, not a skip — fix the row first.
|
|
38
|
+
|
|
39
|
+
A row whose `Work item` column is already filled is **already published**. Leave it alone, and confirm its file exists under `items/` with a matching `work_item:`. A filled column with no file means the ids were never written back; a file whose `feature:` names a different plan is a stop.
|
|
40
|
+
|
|
41
|
+
### 3. Check the items are tracer bullets
|
|
42
|
+
|
|
43
|
+
Fix the row if not — do not paper over it in the work item body.
|
|
44
|
+
|
|
45
|
+
<vertical-slice-rules>
|
|
46
|
+
|
|
47
|
+
- Each slice cuts a narrow but COMPLETE path through every layer (schema, API, UI, tests) — vertical, NOT a horizontal slice of one layer
|
|
48
|
+
- A completed slice is demoable or verifiable on its own
|
|
49
|
+
- Each slice is sized to fit in a single fresh context window
|
|
50
|
+
- Any prefactoring should be done first
|
|
51
|
+
|
|
52
|
+
</vertical-slice-rules>
|
|
53
|
+
|
|
54
|
+
**Wide refactors are the exception.** A **wide refactor** is one mechanical change — rename a column, retype a shared symbol — whose **blast radius** fans across the whole codebase, so a single edit breaks thousands of call sites at once and no vertical slice can land green. Sequence it as **expand–contract**: add the new form beside the old, migrate call sites in batches sized by blast radius (each batch its own item, blocked by the expand), then delete the old form in an item blocked by every batch.
|
|
55
|
+
|
|
56
|
+
### 4. Quiz the user
|
|
57
|
+
|
|
58
|
+
Present the items as a numbered list — title, blocked by, what it delivers. Ask whether the granularity and the blocking edges are right. Iterate until approved.
|
|
59
|
+
|
|
60
|
+
### 5. Create the work items
|
|
61
|
+
|
|
62
|
+
In `## Items` order, predecessors first — that is what makes `predecessors` expressible, since a row's blockers already have ids by the time it is reached.
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
workitem_create_or_update({
|
|
66
|
+
type: <item_type: pbi -> 'Product Backlog Item', bug -> 'Bug'>,
|
|
67
|
+
title: <the item's title>,
|
|
68
|
+
description: <the body, per references/TICKET-TEMPLATES.md>,
|
|
69
|
+
parentId: <the plan's feature>,
|
|
70
|
+
predecessors: [<ids of the rows this one lists under Predecessor>],
|
|
71
|
+
related: [<for a Bug: the id of the PBI it was found in>],
|
|
72
|
+
})
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The body is the row's own what-it-delivers and verification, shaped by [references/TICKET-TEMPLATES.md](references/TICKET-TEMPLATES.md). It goes in on the create call — `description` is an advertised argument that becomes a `System.Description` field op, so there is no follow-up write and nothing to confirm afterwards.
|
|
76
|
+
|
|
77
|
+
A Bug parents to the **Feature**, exactly as a PBI does, so `related` is the only way to record which item it was found while building.
|
|
78
|
+
|
|
79
|
+
An item with no `parentId` is refused (`requires parentId`), and one parented to anything but the Feature is refused before the request is sent (see [references/AZURE-DEVOPS-HIERARCHY.md](references/AZURE-DEVOPS-HIERARCHY.md)) — so a wrong or missing feature id in the plan now stops the create rather than silently landing an orphan.
|
|
80
|
+
|
|
81
|
+
Act on each returned id **immediately**, before creating the next — that is step 6, run once per row rather than once per plan. A crash then leaves a tree that says exactly what exists, rather than a board full of orphans nobody can trace.
|
|
82
|
+
|
|
83
|
+
### 6. Write the item file the id now names
|
|
84
|
+
|
|
85
|
+
The create call has returned an id, so the item finally has a name. Write `items/az<id>-<slug>.md` from the repo's `plans/templates/backlog-item.md`, with `work_item:` set to that id, `feature:` naming this plan, and `item_type:` the row's. Then fill that id into the plan's `## Items` row.
|
|
86
|
+
|
|
87
|
+
The file is **born with its real id** and is never renamed. The `## Items` index is the only join between a plan and its items, and the link runs one way — the item names its `feature:`, the Feature never lists its items anywhere else.
|
|
88
|
+
|
|
89
|
+
Nothing here moves between directories afterwards, and no directory name records progress. From here, progress lives on the work item as the six Build flags, and setting them is a later, separate pass — not this run, which already created the items and so is the wrong party to also confirm `Plan written` on them. That pass's own mechanics — reference names, what each state requires, who must set a flag independently of whom, and how to read a fenced-write refusal — belong to whichever skill runs it, not to this one.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# How Azure DevOps organises Epic, Feature, PBI, Bug
|
|
2
|
+
|
|
3
|
+
From the official docs, not inference:
|
|
4
|
+
- [About work items and work item types](https://learn.microsoft.com/en-us/azure/devops/boards/work-items/about-work-items?view=azure-devops)
|
|
5
|
+
- [Organize your product backlog](https://learn.microsoft.com/en-us/azure/devops/boards/backlogs/organize-backlog?view=azure-devops)
|
|
6
|
+
|
|
7
|
+
## The hierarchy is four levels, by category
|
|
8
|
+
|
|
9
|
+
| Category | Type (Scrum process) | Appears on |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| Epic | `Epic` | Epic portfolio backlog |
|
|
12
|
+
| Feature | `Feature` | Feature portfolio backlog |
|
|
13
|
+
| Requirement | `Product Backlog Item` | Product backlog and Sprints backlog |
|
|
14
|
+
| Task | `Task` | Sprint backlog and Taskboard |
|
|
15
|
+
| Bug | `Bug` | **depends on a team setting** — see Rule 3 |
|
|
16
|
+
|
|
17
|
+
> "Epics group Features, Features group Requirements (User Stories, Product
|
|
18
|
+
> Backlog Items, Issues, or Requirements), and Requirements group Tasks."
|
|
19
|
+
|
|
20
|
+
## Rule 1 — parenting is restricted to adjacent levels
|
|
21
|
+
|
|
22
|
+
> "You can only reparent backlog items under other features, and features under
|
|
23
|
+
> other epics."
|
|
24
|
+
|
|
25
|
+
So `parentId` for a Product Backlog Item **or a Bug** is always a Feature,
|
|
26
|
+
never an Epic. Creating either with an Epic's id as `parentId` is refused
|
|
27
|
+
before the request is sent, naming the mismatch:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
must be parented to a Feature; work item 320 is a Epic
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Reaching the Epic is the Feature's job. In a plan, `feature:` is the parent
|
|
34
|
+
link and `epic:` is a cross-check on that Feature's own parent — read the
|
|
35
|
+
Feature once (`workitem_list_or_get({ id: feature, raw: true })`) and compare
|
|
36
|
+
`links.parent` to `epic:`; there is no need for a second call that fetches the
|
|
37
|
+
Epic directly.
|
|
38
|
+
|
|
39
|
+
**Every tracked type requires a parent — there is no root case.** Omitting
|
|
40
|
+
`parentId` on a Feature, a Product Backlog Item, or a Bug is refused as
|
|
41
|
+
`requires parentId`, whatever the type.
|
|
42
|
+
|
|
43
|
+
## Rule 2 — one parent per work item
|
|
44
|
+
|
|
45
|
+
Parent-child is single-valued. Ordering and blocking therefore **cannot** be
|
|
46
|
+
expressed by parenting; they need `System.LinkTypes.Dependency-Reverse` links,
|
|
47
|
+
which `workitem_create_or_update`'s `predecessors` argument creates — one link
|
|
48
|
+
per id, in the order given.
|
|
49
|
+
|
|
50
|
+
`predecessors` is the only argument that creates them. It has no alias at the
|
|
51
|
+
tool level: `blockedBy` is not read.
|
|
52
|
+
|
|
53
|
+
The complete set of link kinds this server will write is `parent`,
|
|
54
|
+
`predecessors`, `related` — nothing else, including `children` and
|
|
55
|
+
`successors`, which the board derives from the other end of `parent` and
|
|
56
|
+
`predecessors` and so are never a second way to say the same thing.
|
|
57
|
+
|
|
58
|
+
## Rule 3 — a Bug parents to the Feature, the same as a Product Backlog Item
|
|
59
|
+
|
|
60
|
+
Creating a Bug with the plan's Feature as `parentId` succeeds. A Bug can also
|
|
61
|
+
carry `related: [<id>]` to record which item it was found while building,
|
|
62
|
+
alongside its `System.LinkTypes.Hierarchy-Reverse` parent link.
|
|
63
|
+
|
|
64
|
+
## Rule 4 — states are exact, and per work item type
|
|
65
|
+
|
|
66
|
+
Product Backlog Item and Bug share one state list, in board order:
|
|
67
|
+
|
|
68
|
+
| State | Category |
|
|
69
|
+
| --- | --- |
|
|
70
|
+
| New | Proposed |
|
|
71
|
+
| Approved | Proposed |
|
|
72
|
+
| Committed | InProgress |
|
|
73
|
+
| In Progress | InProgress |
|
|
74
|
+
| In Testing | InProgress |
|
|
75
|
+
| Done | Completed |
|
|
76
|
+
| Removed | Removed |
|
|
77
|
+
|
|
78
|
+
This list was read from the board, not transcribed from generic Scrum
|
|
79
|
+
documentation — treat it as exhaustive. A state outside this list is not a
|
|
80
|
+
looser spelling of one of these; it is wrong. `Removed` hides the item from
|
|
81
|
+
the backlog and is otherwise unconstrained. What each state requires of the
|
|
82
|
+
six Build flags is out of scope here — to-workitems never sets a flag or
|
|
83
|
+
changes state (see [`../SKILL.md`](../SKILL.md), Step 6); that belongs to
|
|
84
|
+
whichever skill runs that pass. Read `System.State` verbatim — never infer a
|
|
85
|
+
local status from a remote string.
|
|
86
|
+
|
|
87
|
+
## What can be created, and what can only be a parent
|
|
88
|
+
|
|
89
|
+
Creating a work item is restricted to `Feature`, `Product Backlog Item`,
|
|
90
|
+
`Bug`. Both `Epic` and `Task` are refused with the same message:
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
type must be one of ...
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Epic is still a **valid parent** — a Feature's own `parentId` — it simply
|
|
97
|
+
cannot be the type of a new work item.
|
|
98
|
+
|
|
99
|
+
**The parent-type rule covers all three creatable types**, Feature included:
|
|
100
|
+
|
|
101
|
+
```js
|
|
102
|
+
PARENT_TYPE_REQUIREMENT = { Feature: "Epic", "Product Backlog Item": "Feature", Bug: "Feature" }
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
So a Feature parented to anything but an Epic is refused by name, on create and
|
|
106
|
+
on update alike — `Feature must be parented to a Epic; work item <id> is a
|
|
107
|
+
<type>`. The check reads the parent through the raw work-item endpoint rather
|
|
108
|
+
than the tracked-type tool, which is how it can inspect an Epic that
|
|
109
|
+
`workitem_list_or_get` refuses to return.
|
|
110
|
+
|
|
111
|
+
What the server still cannot check is **which** Epic. A Feature parented to the
|
|
112
|
+
wrong Epic is the right type and creates cleanly, so an unset or mistyped
|
|
113
|
+
`epic:` is caught by type but never by intent — that part stays on the agent,
|
|
114
|
+
and on the human it asks. See [`../SKILL.md`](../SKILL.md)'s "Do Not Use When".
|
|
115
|
+
|
|
116
|
+
`Task` stays out for a design reason, not just a server one: an Execution row
|
|
117
|
+
is already sized to one item, and a fourth level would need a second place to
|
|
118
|
+
record progress, which is the duplication this design exists to avoid.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Backlog item body
|
|
2
|
+
|
|
3
|
+
## The work item's `description`
|
|
4
|
+
|
|
5
|
+
Sent on the create call for every PBI and Bug. It is the item's own two sections — no more:
|
|
6
|
+
|
|
7
|
+
```markdown
|
|
8
|
+
## What it delivers
|
|
9
|
+
|
|
10
|
+
The end-to-end behaviour this makes work. Not a layer-by-layer list.
|
|
11
|
+
|
|
12
|
+
## Verification
|
|
13
|
+
|
|
14
|
+
The falsifiable check. A command and its expected result, not "tested".
|
|
15
|
+
|
|
16
|
+
## Source
|
|
17
|
+
|
|
18
|
+
Plan: the Feature's `spec.md`, `## Items` row <n>.
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## The item file
|
|
22
|
+
|
|
23
|
+
Written after the create call returns, because the file is named `az<work item id>-<slug>.md`
|
|
24
|
+
and **the id is the identity** — allocate the work item first, then write the file. Where the
|
|
25
|
+
repo carries `plans/templates/backlog-item.md`, author from that file; this is only its shape.
|
|
26
|
+
|
|
27
|
+
```markdown
|
|
28
|
+
---
|
|
29
|
+
type: Backlog Item
|
|
30
|
+
title: "[what it delivers]"
|
|
31
|
+
description: "[one line]"
|
|
32
|
+
status: draft
|
|
33
|
+
created: YYYY-MM-DD
|
|
34
|
+
item_type: pbi
|
|
35
|
+
feature: az<feature work item id>
|
|
36
|
+
work_item: <id>
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
# [what it delivers]
|
|
40
|
+
|
|
41
|
+
## What it delivers
|
|
42
|
+
|
|
43
|
+
## Verification
|
|
44
|
+
|
|
45
|
+
## Predecessors
|
|
46
|
+
|
|
47
|
+
Item files that must finish first, by filename — or "None". These become Predecessor links on
|
|
48
|
+
the board.
|
|
49
|
+
|
|
50
|
+
## Related
|
|
51
|
+
|
|
52
|
+
For a Bug: the item file of the PBI it was found in. A Bug's parent is the Feature, so Related
|
|
53
|
+
is the only way to record where it came from.
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`item_type: pbi` maps to `Product Backlog Item` and `bug` to `Bug`. `status: draft` describes
|
|
57
|
+
the document, never the work — the work's state is the board's, and an item file that also
|
|
58
|
+
claimed it would be a second place to update and the one that goes stale.
|
|
59
|
+
|
|
60
|
+
## What does not go in either
|
|
61
|
+
|
|
62
|
+
Do not restate the six Build flags. They live on the work item's fields, not in prose anyone
|
|
63
|
+
has to keep in sync by hand. to-workitems never sets a flag or changes state (see
|
|
64
|
+
[`../SKILL.md`](../SKILL.md), Step 6): the reference names, what each state requires of them,
|
|
65
|
+
who must set one independently of whom, the failure-reason table, and the revision fence all
|
|
66
|
+
belong to whichever skill runs that later pass, not to this file. They are removed from here
|
|
67
|
+
for that reason, not merely trimmed for length — re-author them where that skill lives; do not
|
|
68
|
+
restore them to this file.
|
|
@@ -15,7 +15,7 @@ The destination varies per effort, and naming it is the first act of charting
|
|
|
15
15
|
|
|
16
16
|
## Do Not Use When
|
|
17
17
|
|
|
18
|
-
- The work is already small enough for one session, or is execution-shaped (slicing an already-decided plan into build tickets) rather than decision-shaped — use `to-
|
|
18
|
+
- The work is already small enough for one session, or is execution-shaped (slicing an already-decided plan into build tickets) rather than decision-shaped — use `to-workitems` instead.
|
|
19
19
|
|
|
20
20
|
## Plan, don't do
|
|
21
21
|
|
package/package.json
CHANGED
|
@@ -1329,9 +1329,9 @@ export function runRepoInstall(pluginRoot, options = {}) {
|
|
|
1329
1329
|
text = base;
|
|
1330
1330
|
config = prev.config ?? { created: false, appended: null };
|
|
1331
1331
|
}
|
|
1332
|
-
// Insert-once: null means already declared
|
|
1333
|
-
//
|
|
1334
|
-
const declared = declareExtension(text, bundle.name
|
|
1332
|
+
// Insert-once: null means nothing to change (already declared with no
|
|
1333
|
+
// stale version key, or no [extensions] to write into) -- see declareExtension.
|
|
1334
|
+
const declared = declareExtension(text, bundle.name);
|
|
1335
1335
|
if (declared !== null) text = declared;
|
|
1336
1336
|
if (text !== configText) {
|
|
1337
1337
|
parseConfigText(text, "refusing to write invalid TOML");
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: to-spec
|
|
3
|
-
description: Turn the current conversation into a spec and publish it to the project issue tracker — no interview, just synthesis of what you've already discussed.
|
|
4
|
-
disable-model-invocation: true
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# To Spec
|
|
8
|
-
This skill takes the current conversation context and codebase understanding and produces a spec (you may know this document as a PRD). Do NOT interview the user — just synthesize what you already know.
|
|
9
|
-
|
|
10
|
-
The issue tracker and triage label vocabulary should have been provided to you — run `/setup-matt-pocock-skills` if not.
|
|
11
|
-
|
|
12
|
-
## Use When
|
|
13
|
-
|
|
14
|
-
- The current conversation, plus codebase exploration, already gives you enough to synthesize a spec/PRD and publish it to the project issue tracker.
|
|
15
|
-
|
|
16
|
-
## Do Not Use When
|
|
17
|
-
|
|
18
|
-
- The user's intent is still unclear and needs an interview or clarifying questions first — this skill explicitly skips that; gather requirements before invoking it.
|
|
19
|
-
- No issue tracker or triage label vocabulary has been configured (run `/setup-matt-pocock-skills` first).
|
|
20
|
-
|
|
21
|
-
## Process
|
|
22
|
-
|
|
23
|
-
1. Explore the repo to understand the current state of the codebase, if you haven't already. Use the project's domain glossary vocabulary throughout the spec, and respect any ADRs in the area you're touching.
|
|
24
|
-
|
|
25
|
-
2. Sketch out the seams at which you're going to test the feature. Existing seams should be preferred to new ones. Use the highest seam possible. If new seams are needed, propose them at the highest point you can. The fewer seams across the codebase, the better - the ideal number is one.
|
|
26
|
-
|
|
27
|
-
Check with the user that these seams match their expectations.
|
|
28
|
-
|
|
29
|
-
3. Write the spec using the template below, then publish it to the project issue tracker. Apply the `ready-for-agent` triage label - no need for additional triage.
|
|
30
|
-
|
|
31
|
-
<spec-template>
|
|
32
|
-
|
|
33
|
-
## Problem Statement
|
|
34
|
-
|
|
35
|
-
The problem that the user is facing, from the user's perspective.
|
|
36
|
-
|
|
37
|
-
## Solution
|
|
38
|
-
|
|
39
|
-
The solution to the problem, from the user's perspective.
|
|
40
|
-
|
|
41
|
-
## User Stories
|
|
42
|
-
|
|
43
|
-
A LONG, numbered list of user stories. Each user story should be in the format of:
|
|
44
|
-
|
|
45
|
-
1. As an <actor>, I want a <feature>, so that <benefit>
|
|
46
|
-
|
|
47
|
-
<user-story-example>
|
|
48
|
-
1. As a mobile bank customer, I want to see balance on my accounts, so that I can make better informed decisions about my spending
|
|
49
|
-
</user-story-example>
|
|
50
|
-
|
|
51
|
-
This list of user stories should be extremely extensive and cover all aspects of the feature.
|
|
52
|
-
|
|
53
|
-
## Implementation Decisions
|
|
54
|
-
|
|
55
|
-
A list of implementation decisions that were made. This can include:
|
|
56
|
-
|
|
57
|
-
- The modules that will be built/modified
|
|
58
|
-
- The interfaces of those modules that will be modified
|
|
59
|
-
- Technical clarifications from the developer
|
|
60
|
-
- Architectural decisions
|
|
61
|
-
- Schema changes
|
|
62
|
-
- API contracts
|
|
63
|
-
- Specific interactions
|
|
64
|
-
|
|
65
|
-
Do NOT include specific file paths or code snippets. They may end up being outdated very quickly.
|
|
66
|
-
|
|
67
|
-
Exception: if a prototype produced a snippet that encodes a decision more precisely than prose can (state machine, reducer, schema, type shape), inline it within the relevant decision and note briefly that it came from a prototype. Trim to the decision-rich parts — not a working demo, just the important bits.
|
|
68
|
-
|
|
69
|
-
## Testing Decisions
|
|
70
|
-
|
|
71
|
-
A list of testing decisions that were made. Include:
|
|
72
|
-
|
|
73
|
-
- A description of what makes a good test (only test external behavior, not implementation details)
|
|
74
|
-
- Which modules will be tested
|
|
75
|
-
- Prior art for the tests (i.e. similar types of tests in the codebase)
|
|
76
|
-
|
|
77
|
-
## Out of Scope
|
|
78
|
-
|
|
79
|
-
A description of the things that are out of scope for this spec.
|
|
80
|
-
|
|
81
|
-
## Further Notes
|
|
82
|
-
|
|
83
|
-
Any further notes about the feature.
|
|
84
|
-
|
|
85
|
-
</spec-template>
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: to-tickets
|
|
3
|
-
description: Break a plan, spec, or the current conversation into a set of tracer-bullet tickets, each declaring its blocking edges, published to the configured tracker — edges as text in one file per ticket locally, or native blocking links on a real tracker.
|
|
4
|
-
disable-model-invocation: true
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# To Tickets
|
|
8
|
-
|
|
9
|
-
Break a plan, spec, or conversation into a set of **tickets** — tracer-bullet vertical slices, each declaring the tickets that **block** it.
|
|
10
|
-
|
|
11
|
-
The issue tracker and triage label vocabulary should have been provided to you — run `/setup-matt-pocock-skills` if not.
|
|
12
|
-
|
|
13
|
-
## Use When
|
|
14
|
-
|
|
15
|
-
- You have a plan, spec, or conversation ready to break into tracer-bullet tickets with blocking edges, to publish on the configured tracker.
|
|
16
|
-
|
|
17
|
-
## Do Not Use When
|
|
18
|
-
|
|
19
|
-
- The work is still a loose idea with open decisions to resolve first — chart it with `wayfinder` before slicing it into tickets.
|
|
20
|
-
|
|
21
|
-
## Process
|
|
22
|
-
|
|
23
|
-
### 1. Gather context
|
|
24
|
-
|
|
25
|
-
Work from whatever is already in the conversation context. If the user passes a reference (a spec path, an issue number or URL) as an argument, fetch it and read its full body and comments.
|
|
26
|
-
|
|
27
|
-
### 2. Explore the codebase (optional)
|
|
28
|
-
|
|
29
|
-
If you have not already explored the codebase, do so to understand the current state of the code. Ticket titles and descriptions should use the project's domain glossary vocabulary, and respect ADRs in the area you're touching.
|
|
30
|
-
|
|
31
|
-
Look for opportunities to prefactor the code to make the implementation easier. "Make the change easy, then make the easy change."
|
|
32
|
-
|
|
33
|
-
### 3. Draft vertical slices
|
|
34
|
-
|
|
35
|
-
Break the work into **tracer bullet** tickets.
|
|
36
|
-
|
|
37
|
-
<vertical-slice-rules>
|
|
38
|
-
|
|
39
|
-
- Each slice cuts a narrow but COMPLETE path through every layer (schema, API, UI, tests) — vertical, NOT a horizontal slice of one layer
|
|
40
|
-
- A completed slice is demoable or verifiable on its own
|
|
41
|
-
- Each slice is sized to fit in a single fresh context window
|
|
42
|
-
- Any prefactoring should be done first
|
|
43
|
-
|
|
44
|
-
</vertical-slice-rules>
|
|
45
|
-
|
|
46
|
-
Give each ticket its **blocking edges** — the other tickets that must complete before it can start. A ticket with no blockers can start immediately.
|
|
47
|
-
|
|
48
|
-
**Wide refactors are the exception to vertical slicing.** A **wide refactor** is one mechanical change — rename a column, retype a shared symbol — whose **blast radius** fans across the whole codebase, so a single edit breaks thousands of call sites at once and no vertical slice can land green. Don't force it into a tracer bullet; sequence it as **expand–contract**. First expand: add the new form beside the old so nothing breaks. Then migrate the call sites over in batches sized by blast radius (per package, per directory), each batch its own ticket blocked by the expand, keeping CI green batch to batch because the old form still exists. Finally contract: delete the old form once no caller remains, in a ticket blocked by every migrate batch. When even the batches can't stay green alone, keep the sequence but let them share an integration branch that all block a final integrate-and-verify ticket — green is promised only there.
|
|
49
|
-
|
|
50
|
-
### 4. Quiz the user
|
|
51
|
-
|
|
52
|
-
Present the proposed breakdown as a numbered list. For each ticket, show:
|
|
53
|
-
|
|
54
|
-
- **Title**: short descriptive name
|
|
55
|
-
- **Blocked by**: which other tickets (if any) must complete first
|
|
56
|
-
- **What it delivers**: the end-to-end behaviour this ticket makes work
|
|
57
|
-
|
|
58
|
-
Ask the user:
|
|
59
|
-
|
|
60
|
-
- Does the granularity feel right? (too coarse / too fine)
|
|
61
|
-
- Are the blocking edges correct — does each ticket only depend on tickets that genuinely gate it?
|
|
62
|
-
- Should any tickets be merged or split further?
|
|
63
|
-
|
|
64
|
-
Iterate until the user approves the breakdown.
|
|
65
|
-
|
|
66
|
-
### 5. Publish the tickets to the configured tracker
|
|
67
|
-
|
|
68
|
-
Publish the approved tickets. **How** depends on the tracker `/setup-matt-pocock-skills` configured — the tickets are the same either way, only the shape of the blocking edges changes:
|
|
69
|
-
|
|
70
|
-
- **Local files** → write one file per ticket under `.enact/<feature-slug>/issues/<NN>-<slug>.md`, numbered from `01` in dependency order (blockers first). Each file's "Blocked by" lists the numbers/titles it depends on. Use the local-ticket template in [references/TICKET-TEMPLATES.md](references/TICKET-TEMPLATES.md) — one ticket per file, never a single combined file.
|
|
71
|
-
- **A real issue tracker (GitHub, Linear, …)** → publish one issue per ticket in dependency order (blockers first), using the issue template in [references/TICKET-TEMPLATES.md](references/TICKET-TEMPLATES.md) so each ticket's blocking edges can reference real identifiers. Use the platform's native blocking / sub-issue relationship where it has one; otherwise set each ticket's "Blocked by" to the blocking issues. Apply the `ready-for-agent` triage label unless instructed otherwise — the tickets are agent-grabbable by construction.
|
|
72
|
-
|
|
73
|
-
Work the **frontier**: any ticket whose blockers are all done. For a purely linear chain that means top to bottom.
|
|
74
|
-
|
|
75
|
-
Do NOT close or modify any parent issue. In either form, avoid specific file paths or code snippets — they go stale fast. Exception: if a prototype produced a snippet that encodes a decision more precisely than prose can (state machine, reducer, schema, type shape), inline it and note briefly that it came from a prototype. Trim to the decision-rich parts — not a working demo, just the important bits.
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
# Ticket Templates
|
|
2
|
-
|
|
3
|
-
<local-ticket-template>
|
|
4
|
-
|
|
5
|
-
```markdown
|
|
6
|
-
# <NN> — <Ticket title>
|
|
7
|
-
|
|
8
|
-
**What to build:** the end-to-end behaviour this ticket makes work, from the user's perspective — not a layer-by-layer implementation list.
|
|
9
|
-
|
|
10
|
-
**Blocked by:** the numbers/titles of the tickets that gate this one, or "None — can start immediately".
|
|
11
|
-
|
|
12
|
-
**Status:** ready-for-agent
|
|
13
|
-
|
|
14
|
-
- [ ] Acceptance criterion 1
|
|
15
|
-
- [ ] Acceptance criterion 2
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
</local-ticket-template>
|
|
19
|
-
|
|
20
|
-
<issue-template>
|
|
21
|
-
|
|
22
|
-
## Parent
|
|
23
|
-
|
|
24
|
-
A reference to the parent issue on the tracker (if the source was an existing issue, otherwise omit this section).
|
|
25
|
-
|
|
26
|
-
## What to build
|
|
27
|
-
|
|
28
|
-
The end-to-end behaviour this ticket makes work, from the user's perspective — not layer-by-layer implementation.
|
|
29
|
-
|
|
30
|
-
## Acceptance criteria
|
|
31
|
-
|
|
32
|
-
- [ ] Criterion 1
|
|
33
|
-
- [ ] Criterion 2
|
|
34
|
-
|
|
35
|
-
## Blocked by
|
|
36
|
-
|
|
37
|
-
- A reference to each blocking ticket, or "None — can start immediately".
|
|
38
|
-
|
|
39
|
-
</issue-template>
|