@erclx/canon 4.125.0 → 4.127.0
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/README.md +2 -2
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/git-pr/references/task-number.md +2 -0
- package/claude/skills/task-board/references/archive.md +1 -0
- package/docs/agents/tasks-archive.md +1 -1
- package/docs/agents/tasks.md +4 -0
- package/docs/index.md +1 -9
- package/docs/target/index.md +16 -0
- package/docs/{target-projects.md → target/target-projects.md} +6 -6
- package/docs/{target-sync.md → target/target-sync.md} +1 -1
- package/docs/workflow/ai-workflow.md +1 -1
- package/docs/workflow/skill-map.md +1 -1
- package/package.json +1 -1
- package/src/commands/labels.ts +3 -3
- package/src/commands/tasks.ts +82 -2
- package/src/gate/measures.ts +8 -12
- package/src/gate/stages.ts +3 -3
- package/src/init/steps.ts +1 -1
- package/src/labels/format.ts +2 -2
- package/src/tasks/archive.ts +30 -0
- package/src/tasks/record.ts +106 -9
- package/standards/commit.md +1 -1
- package/standards/pr.md +1 -1
- package/standards/tasks.md +2 -2
- /package/docs/{target-migrations.md → target/target-migrations.md} +0 -0
- /package/docs/{target-scenarios.md → target/target-scenarios.md} +0 -0
package/README.md
CHANGED
|
@@ -96,7 +96,7 @@ Working with an agent rather than reading this yourself? It loads `CLAUDE.md` at
|
|
|
96
96
|
- [AI workflow](docs/workflow/ai-workflow.md): feature-development loop inside a toolkit-managed project
|
|
97
97
|
- [Operating model](docs/workflow/operating-model.md): orchestrator, planner, and worker roles for building across parallel sessions
|
|
98
98
|
- [Visual design workflow](docs/workflow/visual-design-workflow.md): tiered guide for design and wireframe authoring
|
|
99
|
-
- [Target projects](docs/target-projects.md): scaffold, add a domain later, sync upstream drift
|
|
99
|
+
- [Target projects](docs/target/target-projects.md): scaffold, add a domain later, sync upstream drift
|
|
100
100
|
- [Agents](docs/agents/index.md): CLI flags, exit codes, and JSON output shapes
|
|
101
101
|
- [Docs index](docs/index.md): every reference doc in this repo
|
|
102
102
|
|
|
@@ -152,7 +152,7 @@ canon init
|
|
|
152
152
|
|
|
153
153
|
Pass `--stack <name>` for a framework stack, or `--skip governance` to leave rules out. A snippet resolves the same way, reached at its `@` reference through the plugin's live `claude/snippets` symlink rather than a copy. Run `canon tooling list --json` to see the catalog.
|
|
154
154
|
|
|
155
|
-
For the full journey from scaffold through adding a domain later to syncing upstream drift, see [target projects](docs/target-projects.md).
|
|
155
|
+
For the full journey from scaffold through adding a domain later to syncing upstream drift, see [target projects](docs/target/target-projects.md).
|
|
156
156
|
|
|
157
157
|
### Internal narrative
|
|
158
158
|
|
|
@@ -21,6 +21,8 @@ The verb resolves the board at the main worktree root in-process, adds `Pull req
|
|
|
21
21
|
|
|
22
22
|
The correction reaches only a line that does not read as a list of `#NNN` entries. A line that does takes the new number appended rather than replaced, reported as `appended`, so a task shipped in slices keeps every pull request.
|
|
23
23
|
|
|
24
|
+
The same write drops this branch from the task's `Pending branch:` line, which `canon tasks outcome` wrote when the branch ticked an outcome, so this step is what lets the merge-time archive close the task.
|
|
25
|
+
|
|
24
26
|
Skip this silently when the record is `ok: false` and `reason` is `no-board`, `no-match`, or `ambiguous`. Those are the three cases a guessed write would compound: no board, no task naming the plan, or more than one. One pull request names one task, and a wrong match archives the wrong task unattended once the branch merges. Report any other refusal rather than swallowing it.
|
|
25
27
|
|
|
26
28
|
The number is what lets the merge close the task. Every merge on `main` is a squash carrying it in the subject, so the number survives where a branch name does not, and `post-merge` reads it back to call `canon tasks archive`. Writing it here rather than at worktree time is what makes it a pull request number rather than a branch the squash discards.
|
|
@@ -42,6 +42,7 @@ Each reason has one resolution and none of them is to archive around it:
|
|
|
42
42
|
- `open-outcomes`: the named outcomes are unmarked or genuinely open. Run `context-fold` when the work shipped and nothing marked it. Leave the task on the board when the outcome is real. When the work is being abandoned, cut it by striking the body, `- ~~<outcome>~~ <why>`, whatever the checkbox holds, so the board records what was dropped rather than meeting this refusal a second time.
|
|
43
43
|
- `ambiguous`: two tasks name one pull request, which is the misfile `${CLAUDE_SKILL_DIR}/../../standards/tasks.md` rules out. Resolve the citation by hand, since no sweep repairs it.
|
|
44
44
|
- `earlier-slice`: the number is listed on the task but is not the last, so a later slice is still to merge. Leave the task on the board. Its last slice's merge archives it.
|
|
45
|
+
- `pending-branch`: a branch named in `detail` ticked an outcome before its own pull request number was recorded. Leave the task live, since that branch's merge archives it once `git-pr` records the number. When the operator decides a named branch was abandoned, archive by stem, which is the one case for it.
|
|
45
46
|
- `no-match`: the stem or number names nothing on the board. Check the name against the listed stems.
|
|
46
47
|
- `bad-input`: the command line was wrong rather than the board. Read the message, fix the arguments, and run it again. Nothing on the board needs repair, which is what separates this from the two above.
|
|
47
48
|
|
|
@@ -22,7 +22,7 @@ canon tasks archive --pull-request 673 --json
|
|
|
22
22
|
| `--json` | Emit a machine-readable record on stdout |
|
|
23
23
|
| `--root <path>` | Board root, defaulting to the main worktree |
|
|
24
24
|
|
|
25
|
-
Exit codes: `0` archived, `1` refused. Every gate is a refusal rather than a warning, because `.husky/post-merge` calls this with nobody watching. The `reason` field carries which gate fired: `no-board`, `no-match`, `ambiguous`, `no-outcomes`, `open-outcomes`, `earlier-slice`, or `bad-input`. `earlier-slice` fires when `--pull-request` names a number that is not the last on the task's `Pull request:` line, since outcomes are ticked at ship time and only the last slice's merge closes the task.
|
|
25
|
+
Exit codes: `0` archived, `1` refused. Every gate is a refusal rather than a warning, because `.husky/post-merge` calls this with nobody watching. The `reason` field carries which gate fired: `no-board`, `no-match`, `ambiguous`, `no-outcomes`, `open-outcomes`, `earlier-slice`, `pending-branch`, or `bad-input`. `earlier-slice` fires when `--pull-request` names a number that is not the last on the task's `Pull request:` line, since outcomes are ticked at ship time and only the last slice's merge closes the task. `pending-branch` fires when `--pull-request` meets a `Pending branch:` line, since a branch that ticked an outcome before `git-pr` recorded its number means the last number listed is not yet the last slice. The `detail` array names each pending branch. Selecting by stem skips both checks.
|
|
26
26
|
|
|
27
27
|
An outcome whose body is struck reads as cut rather than open or closed, whatever its checkbox holds: `- ~~<outcome>~~ <why>`. A task carrying only cut outcomes archives, since the gate refuses `no-outcomes` only when both the closed and the cut counts are zero. The success record carries `closed` and `cut` as counts, so a reader tells a shipped task from an abandoned one without opening the file.
|
|
28
28
|
|
package/docs/agents/tasks.md
CHANGED
|
@@ -60,6 +60,8 @@ canon tasks pull-request 673 --plan worktree-scratch-routing --json
|
|
|
60
60
|
|
|
61
61
|
A plan is matched on the token both spellings share, so `worktree-scratch-routing`, `feature-worktree-scratch-routing`, and `.canon/plans/feature-worktree-scratch-routing.md` all select the same task. The `action` field reports `added`, `appended`, `corrected`, or `unchanged`, which makes a rerun against the same number safe.
|
|
62
62
|
|
|
63
|
+
Run from a branch other than `main` or `master`, the verb also drops that branch from the task's `Pending branch:` line, along with every name its reflog says it was renamed from, and removes the line once it is empty. The renamed names matter because `git-ship` renames a nonconforming branch after `context-fold` wrote the marker. It does so on an `unchanged` run too, so a rerun repairs a marker left behind. The `pending` field lists the branches still on the line after the write.
|
|
64
|
+
|
|
63
65
|
Exit codes: `0` recorded, `1` refused. The `reason` field carries `no-board`, `no-match`, or `ambiguous`. `git-pr` skips silently on those three, because each is a case where a guessed write would archive the wrong task once the branch merges.
|
|
64
66
|
|
|
65
67
|
A malformed argument refuses as `bad-input` instead, which sits outside that set on purpose. `git-pr` derives the number by slicing whatever `gh pr create` printed, so a non-numeric value is reachable, and folding it into the swallowed set would lose the number with nothing reporting it.
|
|
@@ -82,6 +84,8 @@ canon tasks outcome --plan worktree-scratch-routing --close 2 --json
|
|
|
82
84
|
|
|
83
85
|
An outcome already closed comes back under `alreadyClosed` rather than refusing, so a rerun against the same positions changes nothing. A position past the end of the list refuses as `out-of-range`, since a caller counting wrong should hear about it rather than mark a neighbor.
|
|
84
86
|
|
|
87
|
+
A close that ticks at least one box from a branch other than `main` or `master` names that branch on a `Pending branch:` line, read from `git branch --show-current` in the working directory. The line holds the task against `canon tasks archive --pull-request` until `canon tasks pull-request` records that branch's own number. A detached HEAD, a trunk checkout, or a failed git read writes no line and refuses nothing. The `pending` field lists the branches on the line after the write.
|
|
88
|
+
|
|
85
89
|
Positions skip fenced blocks. A checkbox inside a sample a task displays is not an outcome the task claims, and counting one would shift every position after it. `canon tasks archive` reads the list through the same walker, so the two verbs cannot disagree about which checkboxes are outcomes.
|
|
86
90
|
|
|
87
91
|
Exit codes: `0` closed, `1` refused. The `reason` field adds `no-outcomes`, `out-of-range`, and `bad-input` to the three above.
|
package/docs/index.md
CHANGED
|
@@ -7,14 +7,6 @@ subtitle: One-line reference for each doc in this folder.
|
|
|
7
7
|
|
|
8
8
|
One-line reference for each doc in this folder.
|
|
9
9
|
|
|
10
|
-
## Agent surface
|
|
11
|
-
|
|
12
|
-
- [Target migrations](target-migrations.md): One-time moves a project scaffolded under an older toolkit runs before its next sync
|
|
13
|
-
- [Target projects](target-projects.md): Scaffold, add domains later, and sync upstream drift in a toolkit-managed project
|
|
14
|
-
- [Target scenarios](target-scenarios.md): Worked setups for a markdown-heavy project, a web application, and a monorepo
|
|
15
|
-
- [Target sync](target-sync.md): Check what drifted in a toolkit-managed project and pull it in, per domain or all at once
|
|
16
|
-
|
|
17
|
-
## Sub-catalogs
|
|
18
|
-
|
|
19
10
|
- [Agents](agents/index.md): CLI catalog and invocation rules for agents, split by command domain. Start with overview.
|
|
11
|
+
- [Target](target/index.md): Scaffold, sync, and migrate a toolkit-managed project, with worked setups for three project shapes.
|
|
20
12
|
- [Workflow](workflow/index.md): How a person and their agents run a feature, from the design tier through the parallel-session loop to the shell that launches them.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Target
|
|
3
|
+
subtitle: Scaffold, sync, and migrate a toolkit-managed project, with worked setups for three project shapes.
|
|
4
|
+
category: Agent surface
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Target
|
|
8
|
+
|
|
9
|
+
Scaffold, sync, and migrate a toolkit-managed project, with worked setups for three project shapes.
|
|
10
|
+
|
|
11
|
+
## Agent surface
|
|
12
|
+
|
|
13
|
+
- [Target migrations](target-migrations.md): One-time moves a project scaffolded under an older toolkit runs before its next sync
|
|
14
|
+
- [Target projects](target-projects.md): Scaffold, add domains later, and sync upstream drift in a toolkit-managed project
|
|
15
|
+
- [Target scenarios](target-scenarios.md): Worked setups for a markdown-heavy project, a web application, and a monorepo
|
|
16
|
+
- [Target sync](target-sync.md): Check what drifted in a toolkit-managed project and pull it in, per domain or all at once
|
|
@@ -8,7 +8,7 @@ category: Agent surface
|
|
|
8
8
|
|
|
9
9
|
How a project outside this repo consumes the toolkit across its lifecycle. Three phases: scaffold once, add a domain later when a new need appears, and sync when the upstream toolkit moves.
|
|
10
10
|
|
|
11
|
-
This doc stays at the narrative layer. For command flags and JSON shapes, see [agents](agents/index.md). For per-domain mechanics, see each `canon/context/<domain>.md`.
|
|
11
|
+
This doc stays at the narrative layer. For command flags and JSON shapes, see [agents](../agents/index.md). For per-domain mechanics, see each `canon/context/<domain>.md`.
|
|
12
12
|
|
|
13
13
|
## Getting the skills
|
|
14
14
|
|
|
@@ -74,7 +74,7 @@ Scaffold installs tooling and seeds. It does not fill the planning docs or the d
|
|
|
74
74
|
1. Fill `canon/REQUIREMENTS.md` and `canon/ARCHITECTURE.md`. The seed provides the files, the scope and decisions are yours to write.
|
|
75
75
|
2. For a UI project, invoke `canon:design-extract` to draft `canon/DESIGN.md`. With no UI code yet it takes the greenfield path and proposes tokens from the requirements and a `## Personality` section. Skip for non-UI projects.
|
|
76
76
|
3. Optionally invoke `canon:draft-diagram` to draft entries under `.canon/diagrams/` from the architecture and the requirements. One file per diagram kind, so a later refresh of one kind leaves the others untouched. It renders each diagram it writes to verify the layout, which downloads the Mermaid CLI on first use and takes about 15 seconds.
|
|
77
|
-
4. Start the feature loop. See [AI workflow](workflow/ai-workflow.md) for the per-feature sequence.
|
|
77
|
+
4. Start the feature loop. See [AI workflow](../workflow/ai-workflow.md) for the per-feature sequence.
|
|
78
78
|
|
|
79
79
|
A machine without a renderer still gets the diagrams and is told which check was skipped.
|
|
80
80
|
|
|
@@ -135,8 +135,8 @@ When the toolkit updates, a target pulls changes per domain. A project scaffolde
|
|
|
135
135
|
|
|
136
136
|
## Related
|
|
137
137
|
|
|
138
|
-
- [agents](agents/index.md): CLI flags, exit codes, and JSON output shapes
|
|
138
|
+
- [agents](../agents/index.md): CLI flags, exit codes, and JSON output shapes
|
|
139
139
|
- [target migrations](target-migrations.md), [target sync](target-sync.md), [target scenarios](target-scenarios.md): the one-time moves, the recurring sync, and worked setups
|
|
140
|
-
- [AI workflow](workflow/ai-workflow.md): feature-development loop inside a toolkit-managed project
|
|
141
|
-
- [tooling](
|
|
142
|
-
- [sandbox](
|
|
140
|
+
- [AI workflow](../workflow/ai-workflow.md): feature-development loop inside a toolkit-managed project
|
|
141
|
+
- [tooling](../../canon/context/tooling/index.md), [governance](../../canon/context/governance/index.md), [claude plugin](../../canon/context/claude-plugin/index.md), [indexes](../../canon/context/indexes.md), [snippets](../../canon/context/snippets.md), [standards](../../canon/context/standards/index.md): per-domain mechanics
|
|
142
|
+
- [sandbox](../../canon/context/sandbox/index.md): scenario catalog for verifying domain flows
|
|
@@ -84,7 +84,7 @@ Use a targeted entry point when only one surface moved upstream. Use the catch-a
|
|
|
84
84
|
|
|
85
85
|
## Verify a sync
|
|
86
86
|
|
|
87
|
-
Before running a sync against a real project, run the relevant sandbox scenario. The sandbox provisions a representative project state and routes `SANDBOX_SCENARIO=sync` through the domain flow. See [sandbox](
|
|
87
|
+
Before running a sync against a real project, run the relevant sandbox scenario. The sandbox provisions a representative project state and routes `SANDBOX_SCENARIO=sync` through the domain flow. See [sandbox](../../canon/context/sandbox/index.md) for the scenario catalog and routing patterns.
|
|
88
88
|
|
|
89
89
|
## Running sync from an agent session
|
|
90
90
|
|
|
@@ -44,7 +44,7 @@ Run `canon init` to seed the `.claude/` directory, a root `CLAUDE.md` file, and
|
|
|
44
44
|
|
|
45
45
|
### Bootstrap a new project
|
|
46
46
|
|
|
47
|
-
See [target projects](../target-projects.md) for the scaffold decision, core domains and skips, and the full lifecycle across scaffold, add-a-domain-later, and upstream sync.
|
|
47
|
+
See [target projects](../target/target-projects.md) for the scaffold decision, core domains and skips, and the full lifecycle across scaffold, add-a-domain-later, and upstream sync.
|
|
48
48
|
|
|
49
49
|
### New feature
|
|
50
50
|
|
|
@@ -6,7 +6,7 @@ category: Workflow
|
|
|
6
6
|
|
|
7
7
|
# Skill map
|
|
8
8
|
|
|
9
|
-
Groups run in the order a project meets them, so a reader at a known point scans to that group and reads across. The set reconciles the scenarios in [AI workflow](ai-workflow.md) with the lifecycle [target projects](../target-projects.md) describes, rather than inventing a third vocabulary beside those two, so a group name matches neither source exactly and every moment either one names has a group. Each row says when to reach for the skill. What it does is the skill's own description.
|
|
9
|
+
Groups run in the order a project meets them, so a reader at a known point scans to that group and reads across. The set reconciles the scenarios in [AI workflow](ai-workflow.md) with the lifecycle [target projects](../target/target-projects.md) describes, rather than inventing a third vocabulary beside those two, so a group name matches neither source exactly and every moment either one names has a group. Each row says when to reach for the skill. What it does is the skill's own description.
|
|
10
10
|
|
|
11
11
|
This page is the corpus the coverage claim is measured against: every name `canon claude skills list --names` reports takes exactly one row here. A skill serving two moments sits at the earlier one, and a mention on any other page is prose rather than routing.
|
|
12
12
|
|
package/package.json
CHANGED
package/src/commands/labels.ts
CHANGED
|
@@ -25,7 +25,7 @@ interface ScanOptions {
|
|
|
25
25
|
|
|
26
26
|
/** What a reader does about each way `checkTitleFormat` graded a title as broken. */
|
|
27
27
|
const TITLE_FORMAT_MESSAGES: Record<TitleFormatIssue, string> = {
|
|
28
|
-
structure: 'does not match <type>(<scope>): <subject>',
|
|
28
|
+
structure: 'does not match <type>(<scope>)[!]: <subject>',
|
|
29
29
|
'casing-type': 'the type is not lowercase',
|
|
30
30
|
'casing-scope': 'the scope is not lowercase',
|
|
31
31
|
'casing-subject': 'the first word of the subject is not lowercase',
|
|
@@ -153,7 +153,7 @@ export function register(program: Command): void {
|
|
|
153
153
|
'forcing a new dependency.',
|
|
154
154
|
'',
|
|
155
155
|
"It also grades the title alone against standards/pr.md's ## Title",
|
|
156
|
-
'section: the `<type>(<scope>): <subject>` structure, lowercase casing',
|
|
156
|
+
'section: the `<type>(<scope>)[!]: <subject>` structure, lowercase casing',
|
|
157
157
|
'for the type, the scope, and the first subject word, and a 72-',
|
|
158
158
|
'character length cap. A scan given no title, whether from a review',
|
|
159
159
|
'or from a bare --body or --body-file invocation, skips this check',
|
|
@@ -374,7 +374,7 @@ async function runScan(opts: ScanOptions): Promise<number> {
|
|
|
374
374
|
logInfo('the scan carries no title, so there is no format to grade')
|
|
375
375
|
} else if (titleFormat.conforms) {
|
|
376
376
|
logInfo(
|
|
377
|
-
'the title matches <type>(<scope>): <subject> and its casing and length rules',
|
|
377
|
+
'the title matches <type>(<scope>)[!]: <subject> and its casing and length rules',
|
|
378
378
|
)
|
|
379
379
|
} else {
|
|
380
380
|
logWarn(
|
package/src/commands/tasks.ts
CHANGED
|
@@ -64,6 +64,8 @@ const EXIT_FINDINGS = 2
|
|
|
64
64
|
/** Matches `trunk.ts`'s bound on a git subprocess this verb also shells out to. */
|
|
65
65
|
const GIT_TIMEOUT_MS = 10_000
|
|
66
66
|
|
|
67
|
+
const TRUNK_BRANCHES: readonly string[] = ['main', 'master']
|
|
68
|
+
|
|
67
69
|
interface ArchiveCommandOptions {
|
|
68
70
|
readonly json?: boolean
|
|
69
71
|
readonly pullRequest?: string
|
|
@@ -164,6 +166,10 @@ export function register(program: Command): void {
|
|
|
164
166
|
'folder that does not resolve, or a destination already taken, moves',
|
|
165
167
|
'nothing and refuses nothing.',
|
|
166
168
|
'',
|
|
169
|
+
'With --pull-request, it refuses earlier-slice when a later number is',
|
|
170
|
+
'listed, and pending-branch while a Pending branch: line names a branch',
|
|
171
|
+
'whose own pull request is not recorded yet. A stem skips both.',
|
|
172
|
+
'',
|
|
167
173
|
'Exit codes:',
|
|
168
174
|
' 0 the task was archived',
|
|
169
175
|
' 1 refused, with the reason on stderr or in the JSON record',
|
|
@@ -420,6 +426,10 @@ export function register(program: Command): void {
|
|
|
420
426
|
'list is replaced. The board is shared scratch, so a linked worktree',
|
|
421
427
|
'records against the same board.',
|
|
422
428
|
'',
|
|
429
|
+
'It also drops the current branch, and each name it was renamed from,',
|
|
430
|
+
'from the Pending branch: line, even when the number was already',
|
|
431
|
+
'listed, and removes the line once empty.',
|
|
432
|
+
'',
|
|
423
433
|
'Examples:',
|
|
424
434
|
' canon tasks pull-request 673 v28.1-trigger-escalation',
|
|
425
435
|
' canon tasks pull-request 673 --plan worktree-scratch-routing --json',
|
|
@@ -497,6 +507,11 @@ export function register(program: Command): void {
|
|
|
497
507
|
'An outcome already closed is reported rather than refused, so a rerun',
|
|
498
508
|
'against the same positions is safe.',
|
|
499
509
|
'',
|
|
510
|
+
'Run from a branch other than main or master, a close that ticks a new',
|
|
511
|
+
'box names that branch on a Pending branch: line, which holds the task',
|
|
512
|
+
'against a merge-time archive until canon tasks pull-request records',
|
|
513
|
+
"the branch's own number.",
|
|
514
|
+
'',
|
|
500
515
|
'Examples:',
|
|
501
516
|
' canon tasks outcome v28.1-trigger-escalation --close 1 --close 3',
|
|
502
517
|
' canon tasks outcome --plan worktree-scratch-routing --close 2 --json',
|
|
@@ -644,11 +659,63 @@ async function runPullRequest(
|
|
|
644
659
|
}
|
|
645
660
|
|
|
646
661
|
const root = opts.root ?? (await mainWorktreeRoot())
|
|
647
|
-
const
|
|
662
|
+
const branch = await workingBranch()
|
|
663
|
+
const outcome = await recordPullRequest(
|
|
664
|
+
root,
|
|
665
|
+
selector,
|
|
666
|
+
Number(number),
|
|
667
|
+
branch ? [branch, ...(await formerNames(branch))] : [],
|
|
668
|
+
)
|
|
648
669
|
|
|
649
670
|
return reportPullRequest(outcome, emitJson, root)
|
|
650
671
|
}
|
|
651
672
|
|
|
673
|
+
const RENAME_PATTERN = /^Branch: renamed refs\/heads\/(.+) to refs\/heads\//
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Names the branch carried before each `git branch -m`, read off its reflog,
|
|
677
|
+
* which moves with a rename and records every earlier one. A marker written
|
|
678
|
+
* under an old name is otherwise one nothing ever clears.
|
|
679
|
+
*/
|
|
680
|
+
async function formerNames(branch: string): Promise<readonly string[]> {
|
|
681
|
+
const result = await execa(
|
|
682
|
+
'git',
|
|
683
|
+
[
|
|
684
|
+
'-C',
|
|
685
|
+
process.cwd(),
|
|
686
|
+
'reflog',
|
|
687
|
+
'show',
|
|
688
|
+
'--format=%gs',
|
|
689
|
+
`refs/heads/${branch}`,
|
|
690
|
+
],
|
|
691
|
+
{ reject: false, timeout: GIT_TIMEOUT_MS, env: gitEnv(), extendEnv: false },
|
|
692
|
+
)
|
|
693
|
+
if (result.exitCode !== 0) return []
|
|
694
|
+
|
|
695
|
+
return result.stdout
|
|
696
|
+
.split('\n')
|
|
697
|
+
.map((line) => RENAME_PATTERN.exec(line)?.[1])
|
|
698
|
+
.filter((name): name is string => name !== undefined)
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* The branch checked out where the command runs, which is where the work is,
|
|
703
|
+
* rather than the board root's. Undefined on trunk, a detached HEAD, or a
|
|
704
|
+
* failed read, where no slice is in flight to hold the task open.
|
|
705
|
+
*/
|
|
706
|
+
async function workingBranch(): Promise<string | undefined> {
|
|
707
|
+
const result = await execa(
|
|
708
|
+
'git',
|
|
709
|
+
['-C', process.cwd(), 'branch', '--show-current'],
|
|
710
|
+
{ reject: false, timeout: GIT_TIMEOUT_MS, env: gitEnv(), extendEnv: false },
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
const branch = result.exitCode === 0 ? result.stdout.trim() : ''
|
|
714
|
+
if (branch.length === 0 || TRUNK_BRANCHES.includes(branch)) return undefined
|
|
715
|
+
|
|
716
|
+
return branch
|
|
717
|
+
}
|
|
718
|
+
|
|
652
719
|
async function runPlanLink(
|
|
653
720
|
task: string,
|
|
654
721
|
plan: string,
|
|
@@ -710,7 +777,12 @@ async function runOutcome(
|
|
|
710
777
|
}
|
|
711
778
|
|
|
712
779
|
const root = opts.root ?? (await mainWorktreeRoot())
|
|
713
|
-
const outcome = await closeOutcomes(
|
|
780
|
+
const outcome = await closeOutcomes(
|
|
781
|
+
root,
|
|
782
|
+
selector,
|
|
783
|
+
raw.map(Number),
|
|
784
|
+
await workingBranch(),
|
|
785
|
+
)
|
|
714
786
|
|
|
715
787
|
return reportOutcome(outcome, emitJson, root)
|
|
716
788
|
}
|
|
@@ -765,6 +837,7 @@ function reportPullRequest(
|
|
|
765
837
|
path: relative(root, outcome.path),
|
|
766
838
|
pullRequest: outcome.number,
|
|
767
839
|
action: outcome.action,
|
|
840
|
+
pending: outcome.pending,
|
|
768
841
|
})}\n`,
|
|
769
842
|
)
|
|
770
843
|
return 0
|
|
@@ -773,6 +846,9 @@ function reportPullRequest(
|
|
|
773
846
|
intro('canon tasks pull-request')
|
|
774
847
|
logStep(outcome.action === 'unchanged' ? 'Already recorded' : 'Recorded')
|
|
775
848
|
logInfo(`${outcome.stem} names pull request #${outcome.number}`)
|
|
849
|
+
if (outcome.pending.length > 0) {
|
|
850
|
+
logInfo(`still pending on ${outcome.pending.join(', ')}`)
|
|
851
|
+
}
|
|
776
852
|
if (outcome.action !== 'unchanged') logAdd(relative(root, outcome.path))
|
|
777
853
|
outro()
|
|
778
854
|
|
|
@@ -831,6 +907,7 @@ function reportOutcome(
|
|
|
831
907
|
path: relative(root, outcome.path),
|
|
832
908
|
closed: outcome.closed,
|
|
833
909
|
alreadyClosed: outcome.alreadyClosed,
|
|
910
|
+
pending: outcome.pending,
|
|
834
911
|
})}\n`,
|
|
835
912
|
)
|
|
836
913
|
return 0
|
|
@@ -841,6 +918,9 @@ function reportOutcome(
|
|
|
841
918
|
|
|
842
919
|
for (const closed of outcome.closed) logAdd(closed)
|
|
843
920
|
for (const already of outcome.alreadyClosed) logInfo(`${already} (already)`)
|
|
921
|
+
if (outcome.pending.length > 0) {
|
|
922
|
+
logInfo(`pending on ${outcome.pending.join(', ')}`)
|
|
923
|
+
}
|
|
844
924
|
|
|
845
925
|
if (outcome.closed.length > 0) logInfo(relative(root, outcome.path))
|
|
846
926
|
outro()
|
package/src/gate/measures.ts
CHANGED
|
@@ -312,10 +312,9 @@ export const skillProvenance: Measure = async (ctx) => {
|
|
|
312
312
|
/**
|
|
313
313
|
* Every tracked markdown document against the whole-document ceiling.
|
|
314
314
|
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
* replaces the warn path rather than sitting beside it.
|
|
315
|
+
* A document past the ceiling fails the stage. The failure names every one with
|
|
316
|
+
* its height rather than the first, so a contributor sees the whole set in one
|
|
317
|
+
* pass, and an exempt document is counted and never named.
|
|
319
318
|
*
|
|
320
319
|
* Read in-process rather than through `canon markdown audit`, whose exit code
|
|
321
320
|
* stays bans and dead links only. The plan and groundwork skills run that audit
|
|
@@ -353,15 +352,12 @@ export const documentCeiling: Measure = async (ctx) => {
|
|
|
353
352
|
}
|
|
354
353
|
}
|
|
355
354
|
|
|
355
|
+
const where = over
|
|
356
|
+
.map((finding) => `${finding.rel} ${finding.renderedLines} rendered lines`)
|
|
357
|
+
.join(', ')
|
|
356
358
|
return {
|
|
357
|
-
emissions: [
|
|
358
|
-
|
|
359
|
-
warn(`${finding.rel} ${finding.renderedLines} rendered lines`),
|
|
360
|
-
),
|
|
361
|
-
info(
|
|
362
|
-
`${plural(over.length, 'document')} past the ${ceiling}-line ceiling, ${exempt} exempt. Reported only, so the push is not held.`,
|
|
363
|
-
),
|
|
364
|
-
],
|
|
359
|
+
emissions: [],
|
|
360
|
+
failure: `${plural(over.length, 'document')} past the ${ceiling}-line ceiling: ${where}. Split each at a seam, or for a verbatim record add a \`<!-- canon-length-exempt: <reason> -->\` line naming why.`,
|
|
365
361
|
}
|
|
366
362
|
}
|
|
367
363
|
|
package/src/gate/stages.ts
CHANGED
|
@@ -338,7 +338,7 @@ export const STAGES: readonly Stage[] = [
|
|
|
338
338
|
// Only the citation half of the audit gates. Its length, depth, table, and
|
|
339
339
|
// index findings are judgment thresholds, and failing a push on one would
|
|
340
340
|
// make the stage something to route around. The one length reading that
|
|
341
|
-
//
|
|
341
|
+
// gates is the whole-document ceiling, which the Document ceiling stage
|
|
342
342
|
// below owns rather than this audit.
|
|
343
343
|
id: 'context-citations',
|
|
344
344
|
label: 'Context citations',
|
|
@@ -360,8 +360,8 @@ export const STAGES: readonly Stage[] = [
|
|
|
360
360
|
checks: [{ kind: 'measure', measure: architectureRecord }],
|
|
361
361
|
},
|
|
362
362
|
{
|
|
363
|
-
// Unscoped, since any branch can grow any document.
|
|
364
|
-
//
|
|
363
|
+
// Unscoped, since any branch can grow any document. A document past the
|
|
364
|
+
// ceiling fails the push.
|
|
365
365
|
id: 'document-ceiling',
|
|
366
366
|
label: 'Document ceiling',
|
|
367
367
|
checks: [{ kind: 'measure', measure: documentCeiling }],
|
package/src/init/steps.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { DomainStep } from '@/init/run'
|
|
|
5
5
|
export type RunFactory = (args: readonly string[]) => () => Promise<boolean>
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* States the same fact as `docs/target-migrations.md`'s backup section in its
|
|
8
|
+
* States the same fact as `docs/target/target-migrations.md`'s backup section in its
|
|
9
9
|
* own words. Keep the two in step by hand; nothing compares them.
|
|
10
10
|
*/
|
|
11
11
|
const RECORDS_SETUP_NOTICE =
|
package/src/labels/format.ts
CHANGED
|
@@ -22,8 +22,8 @@ const TITLE_MAX_LENGTH = 72
|
|
|
22
22
|
|
|
23
23
|
// Casing-agnostic on purpose. Casing is graded separately once the shape
|
|
24
24
|
// matches, so `type` and `scope` accept either case here and `structure`
|
|
25
|
-
// reports only a title with no `<type>(<scope>): <subject>` shape at all.
|
|
26
|
-
const TITLE_SHAPE = /^([A-Za-z]+)\(([A-Za-z0-9][\w.-]*)\)
|
|
25
|
+
// reports only a title with no `<type>(<scope>)[!]: <subject>` shape at all.
|
|
26
|
+
const TITLE_SHAPE = /^([A-Za-z]+)\(([A-Za-z0-9][\w.-]*)\)!?: (.+)$/
|
|
27
27
|
const LEADING_LETTERS = /^[A-Za-z]+/
|
|
28
28
|
|
|
29
29
|
/**
|
package/src/tasks/archive.ts
CHANGED
|
@@ -47,6 +47,7 @@ export const ARCHIVE_REFUSALS = [
|
|
|
47
47
|
'no-outcomes',
|
|
48
48
|
'open-outcomes',
|
|
49
49
|
'earlier-slice',
|
|
50
|
+
'pending-branch',
|
|
50
51
|
'bad-input',
|
|
51
52
|
] as const
|
|
52
53
|
|
|
@@ -251,6 +252,22 @@ export function readPullRequest(text: string): readonly number[] {
|
|
|
251
252
|
return match[1].split(',').map((entry) => Number(entry.trim().slice(1)))
|
|
252
253
|
}
|
|
253
254
|
|
|
255
|
+
/**
|
|
256
|
+
* Reads the `Pending branch:` line `canon tasks outcome` writes when a branch
|
|
257
|
+
* ticks an outcome ahead of its own pull request number reaching the task.
|
|
258
|
+
* `canon tasks pull-request` clears each branch as it records, so a name left
|
|
259
|
+
* here is a slice whose number the `Pull request:` line cannot list yet.
|
|
260
|
+
*/
|
|
261
|
+
export function readPendingBranches(text: string): readonly string[] {
|
|
262
|
+
const match = /^Pending branch:(.*)$/m.exec(text)
|
|
263
|
+
if (!match) return []
|
|
264
|
+
|
|
265
|
+
return match[1]
|
|
266
|
+
.split(',')
|
|
267
|
+
.map((entry) => entry.trim())
|
|
268
|
+
.filter((entry) => entry.length > 0)
|
|
269
|
+
}
|
|
270
|
+
|
|
254
271
|
/** A markdown link on a `Plan:` line, its target captured. */
|
|
255
272
|
const PLAN_LINK_PATTERN = /\[[^\]]*\]\(([^)]+)\)/g
|
|
256
273
|
|
|
@@ -860,6 +877,19 @@ export async function archiveTask(
|
|
|
860
877
|
)
|
|
861
878
|
}
|
|
862
879
|
|
|
880
|
+
// A branch ticks its outcome before `git-pr` appends its number, so in that
|
|
881
|
+
// window the number above reads as last while it is not. The stem selector
|
|
882
|
+
// skips this check the way it skips the one above, since a hand archive is
|
|
883
|
+
// the route out of a pending branch that was abandoned.
|
|
884
|
+
const pending = readPendingBranches(text)
|
|
885
|
+
if (selector.kind === 'pull-request' && pending.length > 0) {
|
|
886
|
+
return refuse(
|
|
887
|
+
'pending-branch',
|
|
888
|
+
`${stem} still waits on ${pending.join(', ')}, which closed an outcome before its own pull request was recorded, so the task closes when that pull request merges.`,
|
|
889
|
+
pending,
|
|
890
|
+
)
|
|
891
|
+
}
|
|
892
|
+
|
|
863
893
|
const { open, closed, cut } = readOutcomes(text)
|
|
864
894
|
|
|
865
895
|
if (open.length > 0) {
|
package/src/tasks/record.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
OUTCOME_PATTERN,
|
|
11
11
|
planLine,
|
|
12
12
|
planLineIndex,
|
|
13
|
+
readPendingBranches,
|
|
13
14
|
readPlanTargets,
|
|
14
15
|
readPullRequest,
|
|
15
16
|
tasksDir,
|
|
@@ -63,6 +64,8 @@ export interface PullRequestRecorded {
|
|
|
63
64
|
readonly path: string
|
|
64
65
|
readonly number: number
|
|
65
66
|
readonly action: LineAction
|
|
67
|
+
/** Branches still on the `Pending branch:` line after this write. */
|
|
68
|
+
readonly pending: readonly string[]
|
|
66
69
|
}
|
|
67
70
|
|
|
68
71
|
export type PullRequestOutcome = PullRequestRecorded | RecordRefused
|
|
@@ -85,6 +88,8 @@ export interface OutcomesClosed {
|
|
|
85
88
|
readonly path: string
|
|
86
89
|
readonly closed: readonly string[]
|
|
87
90
|
readonly alreadyClosed: readonly string[]
|
|
91
|
+
/** Branches on the `Pending branch:` line after this write. */
|
|
92
|
+
readonly pending: readonly string[]
|
|
88
93
|
}
|
|
89
94
|
|
|
90
95
|
export type CloseOutcome = OutcomesClosed | RecordRefused
|
|
@@ -146,16 +151,77 @@ export function writePullRequestLine(
|
|
|
146
151
|
return { text: lines.join('\n'), action: 'added' }
|
|
147
152
|
}
|
|
148
153
|
|
|
149
|
-
function lastOriginLine(
|
|
154
|
+
function lastOriginLine(
|
|
155
|
+
lines: readonly string[],
|
|
156
|
+
prefixes: readonly string[] = ORIGIN_PREFIXES,
|
|
157
|
+
): number | undefined {
|
|
150
158
|
let found: number | undefined
|
|
151
159
|
|
|
152
160
|
for (const [index, line] of lines.entries()) {
|
|
153
|
-
if (
|
|
161
|
+
if (prefixes.some((prefix) => line.startsWith(prefix))) found = index
|
|
154
162
|
}
|
|
155
163
|
|
|
156
164
|
return found
|
|
157
165
|
}
|
|
158
166
|
|
|
167
|
+
const PENDING_PREFIX = 'Pending branch:'
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Names a branch on the `Pending branch:` line, which holds the task open
|
|
171
|
+
* against a merge-time archive until that branch's own number is recorded. The
|
|
172
|
+
* line sits under `Pull request:` when one exists, since a later slice ticks
|
|
173
|
+
* after an earlier slice's number is already listed.
|
|
174
|
+
*/
|
|
175
|
+
export function writePendingBranch(
|
|
176
|
+
text: string,
|
|
177
|
+
branch: string,
|
|
178
|
+
): { readonly text: string; readonly changed: boolean } {
|
|
179
|
+
const lines = text.split('\n')
|
|
180
|
+
const existing = lines.findIndex((entry) => entry.startsWith(PENDING_PREFIX))
|
|
181
|
+
|
|
182
|
+
if (existing !== -1) {
|
|
183
|
+
const pending = readPendingBranches(lines[existing])
|
|
184
|
+
if (pending.includes(branch)) return { text, changed: false }
|
|
185
|
+
lines[existing] = `${PENDING_PREFIX} ${[...pending, branch].join(', ')}`
|
|
186
|
+
return { text: lines.join('\n'), changed: true }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const line = `${PENDING_PREFIX} ${branch}`
|
|
190
|
+
const anchor = lastOriginLine(lines, [...ORIGIN_PREFIXES, 'Pull request:'])
|
|
191
|
+
if (anchor !== undefined) {
|
|
192
|
+
lines.splice(anchor + 1, 0, line)
|
|
193
|
+
return { text: lines.join('\n'), changed: true }
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const heading = lines.findIndex((entry) => entry.startsWith('# '))
|
|
197
|
+
if (heading === -1) return { text: `${line}\n${text}`, changed: true }
|
|
198
|
+
|
|
199
|
+
lines.splice(heading + 1, 0, '', line)
|
|
200
|
+
return { text: lines.join('\n'), changed: true }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Drops a branch from the `Pending branch:` line, and the line once it is empty. */
|
|
204
|
+
export function clearPendingBranch(
|
|
205
|
+
text: string,
|
|
206
|
+
branch: string,
|
|
207
|
+
): { readonly text: string; readonly changed: boolean } {
|
|
208
|
+
const lines = text.split('\n')
|
|
209
|
+
const existing = lines.findIndex((entry) => entry.startsWith(PENDING_PREFIX))
|
|
210
|
+
if (existing === -1) return { text, changed: false }
|
|
211
|
+
|
|
212
|
+
const pending = readPendingBranches(lines[existing])
|
|
213
|
+
if (!pending.includes(branch)) return { text, changed: false }
|
|
214
|
+
|
|
215
|
+
const rest = pending.filter((entry) => entry !== branch)
|
|
216
|
+
if (rest.length > 0) {
|
|
217
|
+
lines[existing] = `${PENDING_PREFIX} ${rest.join(', ')}`
|
|
218
|
+
} else {
|
|
219
|
+
lines.splice(existing, 1)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return { text: lines.join('\n'), changed: true }
|
|
223
|
+
}
|
|
224
|
+
|
|
159
225
|
export type PlanLineWrite =
|
|
160
226
|
| {
|
|
161
227
|
readonly ok: true
|
|
@@ -330,24 +396,43 @@ async function openTask(
|
|
|
330
396
|
* this from a linked worktree, where an in-place edit through the file-editing
|
|
331
397
|
* tools is refused and a shell stream editor is banned, so the write has to
|
|
332
398
|
* resolve the board root in-process.
|
|
399
|
+
*
|
|
400
|
+
* `branches` names the recording branch and every name it carried before a
|
|
401
|
+
* rename, since `git-ship` renames a branch after `context-fold` has already
|
|
402
|
+
* written the marker under the old name. Each comes off the `Pending branch:`
|
|
403
|
+
* line even when the number was already listed, so a rerun repairs a stale
|
|
404
|
+
* marker.
|
|
333
405
|
*/
|
|
334
406
|
export async function recordPullRequest(
|
|
335
407
|
root: string,
|
|
336
408
|
selector: RecordSelector,
|
|
337
409
|
number: number,
|
|
410
|
+
branches: readonly string[] = [],
|
|
338
411
|
): Promise<PullRequestOutcome> {
|
|
339
412
|
const opened = await openTask(root, selector)
|
|
340
413
|
if ('ok' in opened) return opened
|
|
341
414
|
|
|
342
415
|
const { stem, path } = opened
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
416
|
+
const numbered = writePullRequestLine(await readFile(path, 'utf8'), number)
|
|
417
|
+
let text = numbered.text
|
|
418
|
+
let isCleared = false
|
|
419
|
+
|
|
420
|
+
for (const branch of branches) {
|
|
421
|
+
const cleared = clearPendingBranch(text, branch)
|
|
422
|
+
text = cleared.text
|
|
423
|
+
isCleared ||= cleared.changed
|
|
424
|
+
}
|
|
347
425
|
|
|
348
|
-
if (action !== 'unchanged') await writeFile(path, text)
|
|
426
|
+
if (numbered.action !== 'unchanged' || isCleared) await writeFile(path, text)
|
|
349
427
|
|
|
350
|
-
return {
|
|
428
|
+
return {
|
|
429
|
+
ok: true,
|
|
430
|
+
stem,
|
|
431
|
+
path,
|
|
432
|
+
number,
|
|
433
|
+
action: numbered.action,
|
|
434
|
+
pending: readPendingBranches(text),
|
|
435
|
+
}
|
|
351
436
|
}
|
|
352
437
|
|
|
353
438
|
/**
|
|
@@ -399,11 +484,17 @@ export async function recordPlan(
|
|
|
399
484
|
* Marks the named outcomes `[x]` in place. `context-fold` runs this against a
|
|
400
485
|
* board it can read and cannot edit from a linked worktree, and the positions
|
|
401
486
|
* come from the read it already made.
|
|
487
|
+
*
|
|
488
|
+
* A `branch` that ticks at least one new box goes on the `Pending branch:`
|
|
489
|
+
* line, so the merge of an earlier slice cannot archive the task in the window
|
|
490
|
+
* before this branch's own number is recorded. A rerun that ticks nothing adds
|
|
491
|
+
* no marker, since its branch has already recorded or is already listed.
|
|
402
492
|
*/
|
|
403
493
|
export async function closeOutcomes(
|
|
404
494
|
root: string,
|
|
405
495
|
selector: RecordSelector,
|
|
406
496
|
positions: readonly number[],
|
|
497
|
+
branch?: string,
|
|
407
498
|
): Promise<CloseOutcome> {
|
|
408
499
|
const opened = await openTask(root, selector)
|
|
409
500
|
if ('ok' in opened) return opened
|
|
@@ -427,7 +518,12 @@ export async function closeOutcomes(
|
|
|
427
518
|
)
|
|
428
519
|
}
|
|
429
520
|
|
|
430
|
-
|
|
521
|
+
const text =
|
|
522
|
+
result.closed.length > 0 && branch
|
|
523
|
+
? writePendingBranch(result.text, branch).text
|
|
524
|
+
: result.text
|
|
525
|
+
|
|
526
|
+
if (result.closed.length > 0) await writeFile(path, text)
|
|
431
527
|
|
|
432
528
|
return {
|
|
433
529
|
ok: true,
|
|
@@ -435,5 +531,6 @@ export async function closeOutcomes(
|
|
|
435
531
|
path,
|
|
436
532
|
closed: result.closed,
|
|
437
533
|
alreadyClosed: result.alreadyClosed,
|
|
534
|
+
pending: readPendingBranches(text),
|
|
438
535
|
}
|
|
439
536
|
}
|
package/standards/commit.md
CHANGED
|
@@ -17,7 +17,7 @@ Does not govern:
|
|
|
17
17
|
|
|
18
18
|
## Format
|
|
19
19
|
|
|
20
|
-
- Structure: `<type>(<scope>): <subject
|
|
20
|
+
- Structure: `<type>(<scope>): <subject>`, with `!` before the colon for a breaking change
|
|
21
21
|
- Casing: lowercase for `<type>`, `<scope>`, and first word of `<subject>`
|
|
22
22
|
- Subject: 72 characters maximum, no trailing period
|
|
23
23
|
|
package/standards/pr.md
CHANGED
|
@@ -20,7 +20,7 @@ Does not govern:
|
|
|
20
20
|
|
|
21
21
|
## Title
|
|
22
22
|
|
|
23
|
-
- Format: `<type>(<scope>): <subject
|
|
23
|
+
- Format: `<type>(<scope>): <subject>`, with `!` before the colon for a breaking change
|
|
24
24
|
- Casing: lowercase for `<type>`, `<scope>`, and first word of `<subject>`
|
|
25
25
|
- Length: 72 characters maximum
|
|
26
26
|
|
package/standards/tasks.md
CHANGED
|
@@ -72,7 +72,7 @@ Add no third heading. Status stays inline on an outcome rather than becoming an
|
|
|
72
72
|
|
|
73
73
|
Size the outcomes so one pull request closes all of them, and split the task before handing it off rather than after. A task closes whole or not at all, so outcomes spanning two pull requests leave the board showing in-progress work no branch carries.
|
|
74
74
|
|
|
75
|
-
A sliced task is the one allowed exception: a task the operator decided to ship across several pull requests on purpose, one slice each. Its `Pull request:` line lists every slice, so the board records which merged work covered which outcomes, and it closes whole when the last slice's merge finds every outcome ticked.
|
|
75
|
+
A sliced task is the one allowed exception: a task the operator decided to ship across several pull requests on purpose, one slice each. Its `Pull request:` line lists every slice, so the board records which merged work covered which outcomes, and it closes whole when the last slice's merge finds every outcome ticked and no slice left pending.
|
|
76
76
|
|
|
77
77
|
Prefix the H1 with the `vX.Y:` phase label, then a short title whose form depends on the task type:
|
|
78
78
|
|
|
@@ -134,7 +134,7 @@ One plan per task. A plan cited by two tasks is a misfile rather than a shape to
|
|
|
134
134
|
|
|
135
135
|
`Pull request:` records which pull requests carry the task's work, each as a bare `#NNN` the way `Issue:` does. It lists every pull request that shipped part of the task, oldest first and separated by commas, as in `Pull request: #NNN, #NNN`, and a task shipped whole lists one. It is not an origin, so a task without one is well-formed. `git-pr` appends to it when a pull request opens.
|
|
136
136
|
|
|
137
|
-
The line is what lets a merge close its own task. `canon tasks archive --pull-request <n>` finds the task by any number the line lists and archives it only on the last. One pull request, one task: two tasks naming the same number refuse to archive rather than both moving.
|
|
137
|
+
The line is what lets a merge close its own task. `canon tasks archive --pull-request <n>` finds the task by any number the line lists and archives it only on the last. One pull request, one task: two tasks naming the same number refuse to archive rather than both moving. `Pending branch: <branch>` holds that merge back while a branch that ticked an outcome waits on its own number, since `canon tasks outcome` writes it and `canon tasks pull-request` clears it.
|
|
138
138
|
|
|
139
139
|
## What goes in
|
|
140
140
|
|
|
File without changes
|
|
File without changes
|