@erclx/canon 5.27.0 → 5.28.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/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/draft-ready/SKILL.md +3 -1
- package/claude/skills/git-pr/references/evidence.md +1 -1
- package/claude/skills/review-address/REQUIREMENT.md +4 -2
- package/claude/skills/review-address/SKILL.md +16 -7
- package/claude/skills/review-address/references/rebase-conflicts.md +4 -2
- package/claude/skills/review-craft/references/ui.md +4 -4
- package/claude/skills/review-ui/REQUIREMENT.md +2 -2
- package/claude/skills/review-ui/SKILL.md +13 -11
- package/claude/skills/review-ui/references/driving.md +8 -8
- package/claude/skills/review-ui/references/post.md +13 -3
- package/claude/skills/review-ui/references/tick.md +22 -0
- package/claude/skills/role-orchestrator/REQUIREMENT.md +15 -15
- package/claude/skills/role-orchestrator/SKILL.md +24 -26
- package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +18 -16
- package/claude/skills/role-orchestrator/references/orchestrator-handback.md +12 -2
- package/claude/skills/role-orchestrator/references/orchestrator-inbound.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-launch.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-output.md +4 -4
- package/claude/skills/role-orchestrator/references/orchestrator-parked.md +5 -5
- package/claude/skills/role-orchestrator/references/orchestrator-poll.md +11 -2
- package/claude/skills/role-orchestrator/references/orchestrator-refill.md +6 -6
- package/claude/skills/role-orchestrator/references/orchestrator-sweep.md +2 -2
- package/claude/skills/role-orchestrator/scripts/poll.ts +304 -10
- package/claude/skills/role-planner/REQUIREMENT.md +1 -1
- package/claude/skills/role-planner/SKILL.md +2 -2
- package/claude/skills/role-reviewer/SKILL.md +1 -1
- package/claude/skills/role-worker/SKILL.md +3 -2
- package/claude/skills/ui-checklist/SKILL.md +1 -1
- package/docs/agents/canvas.md +4 -4
- package/docs/agents/gate.md +3 -1
- package/docs/agents/index.md +2 -1
- package/docs/agents/pr-evidence.md +9 -8
- package/docs/agents/pr-tick.md +72 -0
- package/docs/agents/sandbox.md +2 -2
- package/docs/agents/tasks-plans.md +1 -1
- package/docs/agents/tasks-validate.md +18 -18
- package/docs/agents/tasks.md +1 -1
- package/docs/workflow/operating-model.md +10 -15
- package/docs/workflow/parallel-features.md +7 -7
- package/package.json +1 -1
- package/src/audits/baseline.ts +1 -1
- package/src/audits/run.ts +1 -1
- package/src/canvas/capture.ts +1 -1
- package/src/canvas/client/inspector/field.tsx +61 -2
- package/src/canvas/client/inspector/fields.css +78 -0
- package/src/canvas/client/inspector/token-select.tsx +109 -0
- package/src/canvas/client/inspector/values.ts +74 -2
- package/src/canvas/client/inspector.tsx +121 -11
- package/src/canvas/client/keys.ts +54 -0
- package/src/canvas/client/selection.tsx +3 -1
- package/src/canvas/client/shell.css +108 -2
- package/src/canvas/client/state.ts +96 -5
- package/src/canvas/client/surface.tsx +194 -17
- package/src/canvas/client/tools.tsx +64 -0
- package/src/canvas/content.ts +1 -1
- package/src/canvas/edit.ts +12 -2
- package/src/canvas/tokens.ts +1 -1
- package/src/claude/seeds.ts +2 -2
- package/src/claude/skills-reach.ts +1 -1
- package/src/commands/canvas.ts +1 -1
- package/src/commands/claude.ts +3 -3
- package/src/commands/design.ts +3 -3
- package/src/commands/docs.ts +1 -1
- package/src/commands/feedback.ts +2 -2
- package/src/commands/gate.ts +9 -2
- package/src/commands/gov.ts +2 -2
- package/src/commands/init.ts +1 -1
- package/src/commands/migrate.ts +1 -1
- package/src/commands/pr.ts +183 -7
- package/src/commands/sandbox.ts +10 -13
- package/src/commands/slides.ts +1 -1
- package/src/commands/sync.ts +1 -1
- package/src/commands/tasks.ts +19 -2
- package/src/commands/tooling.ts +1 -1
- package/src/commands/transcripts.ts +1 -1
- package/src/commands/upgrade.ts +1 -1
- package/src/commands/wiki.ts +1 -1
- package/src/context/architecture.ts +1 -1
- package/src/context/citations.ts +2 -2
- package/src/context/classify/extract.ts +1 -1
- package/src/context/folders.ts +2 -2
- package/src/context/requirements.ts +1 -1
- package/src/docs/read.ts +1 -1
- package/src/exec.ts +1 -1
- package/src/feedback/github.ts +1 -1
- package/src/gate/measures.ts +1 -1
- package/src/gate/sequencer.ts +55 -3
- package/src/gate/stages.ts +8 -0
- package/src/gate/suite-lock.ts +214 -0
- package/src/intake/folder.ts +1 -1
- package/src/labels/map.ts +1 -1
- package/src/labels/phase.ts +3 -3
- package/src/migrate/record-layout.ts +1 -1
- package/src/migrate/records.ts +11 -11
- package/src/migrate/scratch-evidence.ts +1 -1
- package/src/migrate/surface-roots.ts +6 -6
- package/src/pr/evidence.ts +154 -16
- package/src/pr/tick.ts +56 -0
- package/src/process/harness.ts +1 -1
- package/src/records/backup.ts +1 -1
- package/src/records/ordinal.ts +1 -1
- package/src/records/prune.ts +1 -1
- package/src/records/size.ts +1 -1
- package/src/records/stale-canonical.ts +1 -1
- package/src/records/stale.ts +1 -1
- package/src/records/validate.ts +2 -2
- package/src/{project-root.ts → roots/project.ts} +1 -1
- package/src/{record-root.ts → roots/record.ts} +1 -1
- package/src/sandbox/coverage.ts +11 -12
- package/src/sandbox/equivalence.ts +57 -26
- package/src/sandbox/hooks.ts +1 -1
- package/src/sandbox/provision.ts +78 -28
- package/src/sandbox/scenario.ts +588 -0
- package/src/sessions/live.ts +4 -1
- package/src/sessions/transfer/bundle.ts +1 -1
- package/src/slides/decks.ts +1 -1
- package/src/standards/read.ts +1 -1
- package/src/sync/engine.ts +1 -1
- package/src/sync/layout.ts +2 -2
- package/src/sync/stamp.ts +1 -1
- package/src/targets/validate.ts +1 -1
- package/src/tasks/answers.ts +1 -1
- package/src/tasks/archive.ts +1 -1
- package/src/tasks/label.ts +1 -1
- package/src/tasks/reach.ts +1 -1
- package/src/tasks/validate.ts +38 -14
- package/src/teach/nav.ts +1 -1
- package/src/teach/workspace.ts +1 -1
- package/src/tooling/inject.ts +1 -1
- package/src/tooling/read.ts +1 -1
- package/src/tooling/scan.ts +1 -1
- package/src/version/installed.ts +1 -1
- package/src/version/skew.ts +1 -1
- package/standards/board.md +3 -3
- package/standards/plan.md +13 -7
- package/tsconfig.json +1 -1
- /package/src/{legacy-path.ts → roots/legacy.ts} +0 -0
- /package/src/{surface-root.ts → roots/surface.ts} +0 -0
package/src/gate/measures.ts
CHANGED
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
SHIPPED_CORPORA,
|
|
38
38
|
type ShippedReference,
|
|
39
39
|
} from '@/shipped/references'
|
|
40
|
-
import { surfaceDir } from '@/surface
|
|
40
|
+
import { surfaceDir } from '@/roots/surface'
|
|
41
41
|
import { seedRoots } from '@/tooling/seeds'
|
|
42
42
|
import { plural } from '@/ui'
|
|
43
43
|
import {
|
package/src/gate/sequencer.ts
CHANGED
|
@@ -7,8 +7,13 @@ import type {
|
|
|
7
7
|
RunCommand,
|
|
8
8
|
} from '@/gate/measures'
|
|
9
9
|
import type { Check, Stage } from '@/gate/stages'
|
|
10
|
+
import {
|
|
11
|
+
acquireSuiteLock,
|
|
12
|
+
type LockHolder,
|
|
13
|
+
machineLockPath,
|
|
14
|
+
} from '@/gate/suite-lock'
|
|
10
15
|
import { gitEnv } from '@/git/env'
|
|
11
|
-
import { PROJECT_ROOT } from '@/project
|
|
16
|
+
import { PROJECT_ROOT } from '@/roots/project'
|
|
12
17
|
|
|
13
18
|
export type StageStatus =
|
|
14
19
|
/** Every check ran and none found a fact. */
|
|
@@ -27,8 +32,16 @@ export interface StageResult {
|
|
|
27
32
|
readonly emissions: readonly Emission[]
|
|
28
33
|
/** The remedy line a failed stage prints, naming what to do about it. */
|
|
29
34
|
readonly failure?: string
|
|
30
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Wall time the stage took, including every process spawn and, on a stage
|
|
37
|
+
* holding a machine lock, the time it queued behind another worktree.
|
|
38
|
+
*/
|
|
31
39
|
readonly ms: number
|
|
40
|
+
/**
|
|
41
|
+
* The part of `ms` spent queued behind another worktree's hold on the stage's
|
|
42
|
+
* machine lock. Absent where the stage never waited.
|
|
43
|
+
*/
|
|
44
|
+
readonly queuedMs?: number
|
|
32
45
|
}
|
|
33
46
|
|
|
34
47
|
export interface GateContext extends MeasureContext {
|
|
@@ -39,6 +52,12 @@ export interface GateContext extends MeasureContext {
|
|
|
39
52
|
* scoping is off and every stage runs.
|
|
40
53
|
*/
|
|
41
54
|
readonly changed?: readonly string[]
|
|
55
|
+
/**
|
|
56
|
+
* Told when a stage starts queuing on its machine lock. A stage prints only
|
|
57
|
+
* once it returns, so without this a queued gate is silent for the whole
|
|
58
|
+
* wait and reads as a hung suite.
|
|
59
|
+
*/
|
|
60
|
+
readonly onQueue?: (label: string, holder: LockHolder) => void
|
|
42
61
|
}
|
|
43
62
|
|
|
44
63
|
export interface ChangedSet {
|
|
@@ -157,7 +176,40 @@ async function executeStage(
|
|
|
157
176
|
}
|
|
158
177
|
}
|
|
159
178
|
|
|
160
|
-
|
|
179
|
+
// CI runs one suite per runner, so a lock there queues behind nothing.
|
|
180
|
+
if (stage.lock === undefined || ctx.ci) return runChecks(stage, ctx, [])
|
|
181
|
+
|
|
182
|
+
const held = await acquireSuiteLock({
|
|
183
|
+
path: machineLockPath(stage.lock),
|
|
184
|
+
root: ctx.root,
|
|
185
|
+
onWait: (holder) => ctx.onQueue?.(stage.label, holder),
|
|
186
|
+
})
|
|
187
|
+
const queued: Emission[] =
|
|
188
|
+
held.waitedOn === undefined
|
|
189
|
+
? []
|
|
190
|
+
: [
|
|
191
|
+
{
|
|
192
|
+
kind: 'info',
|
|
193
|
+
text: `Waited ${Math.round(held.waitedMs / 1000)}s for the ${stage.label} stage another worktree held, at ${held.waitedOn.root}`,
|
|
194
|
+
},
|
|
195
|
+
]
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
const outcome = await runChecks(stage, ctx, queued)
|
|
199
|
+
return held.waitedOn === undefined
|
|
200
|
+
? outcome
|
|
201
|
+
: { ...outcome, queuedMs: held.waitedMs }
|
|
202
|
+
} finally {
|
|
203
|
+
held.release()
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function runChecks(
|
|
208
|
+
stage: Stage,
|
|
209
|
+
ctx: GateContext,
|
|
210
|
+
leading: readonly Emission[],
|
|
211
|
+
): Promise<StageOutcome> {
|
|
212
|
+
const emissions: Emission[] = [...leading]
|
|
161
213
|
|
|
162
214
|
for (const check of stage.checks) {
|
|
163
215
|
const outcome = await runCheck(check, ctx)
|
package/src/gate/stages.ts
CHANGED
|
@@ -88,6 +88,11 @@ export interface Stage {
|
|
|
88
88
|
*/
|
|
89
89
|
readonly when?: (options: { readonly write: boolean }) => boolean
|
|
90
90
|
readonly checks: readonly Check[]
|
|
91
|
+
/**
|
|
92
|
+
* The machine lock this stage holds while its checks run, so one box runs it
|
|
93
|
+
* for one worktree at a time. Absent means the stage never queues.
|
|
94
|
+
*/
|
|
95
|
+
readonly lock?: string
|
|
91
96
|
/** The line a clean stage prints, where the checks emit none of their own. */
|
|
92
97
|
readonly success?: string
|
|
93
98
|
}
|
|
@@ -579,6 +584,9 @@ export const STAGES: readonly Stage[] = [
|
|
|
579
584
|
label: 'Tests',
|
|
580
585
|
scope: TESTS_SCOPE,
|
|
581
586
|
skipped: 'Skipped, no TypeScript or asserted-corpus changes',
|
|
587
|
+
// Concurrent suites from a wave of worktrees starve each other's spawn
|
|
588
|
+
// and browser tests past their timeouts, so the box runs one at a time.
|
|
589
|
+
lock: 'tests',
|
|
582
590
|
checks: [
|
|
583
591
|
{
|
|
584
592
|
kind: 'command',
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import {
|
|
2
|
+
linkSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
renameSync,
|
|
6
|
+
unlinkSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from 'node:fs'
|
|
9
|
+
import { dirname, join } from 'node:path'
|
|
10
|
+
import { liveness, type LivenessProbes, SYSTEM_PROBES } from '@/sessions/live'
|
|
11
|
+
import { stateDir } from '@/targets/registry'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Who holds a machine lock, as the file records it.
|
|
15
|
+
*
|
|
16
|
+
* The start time is what separates the holder from whatever inherits its pid
|
|
17
|
+
* after a crash, so a takeover never waits on a recycled number.
|
|
18
|
+
*/
|
|
19
|
+
export interface LockHolder {
|
|
20
|
+
readonly pid: number
|
|
21
|
+
readonly procStart: string | undefined
|
|
22
|
+
/** The worktree whose gate holds the lock, which the wait line names. */
|
|
23
|
+
readonly root: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SuiteLockOptions {
|
|
27
|
+
readonly path: string
|
|
28
|
+
readonly root: string
|
|
29
|
+
readonly pid?: number
|
|
30
|
+
readonly probes?: LivenessProbes
|
|
31
|
+
readonly pollMs?: number
|
|
32
|
+
readonly sleep?: (ms: number) => Promise<void>
|
|
33
|
+
/** Called once, when the first poll finds a live holder, so a wait is never silent. */
|
|
34
|
+
readonly onWait?: (holder: LockHolder) => void
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface HeldLock {
|
|
38
|
+
readonly release: () => void
|
|
39
|
+
/** The holder this acquirer queued behind, absent when it never waited. */
|
|
40
|
+
readonly waitedOn?: LockHolder
|
|
41
|
+
readonly waitedMs: number
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type AcquireOutcome =
|
|
45
|
+
| { readonly acquired: true; readonly release: () => void }
|
|
46
|
+
| { readonly acquired: false; readonly holder: LockHolder | undefined }
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The file a named machine lock lives at, resolved at call time so a test run
|
|
50
|
+
* that redirects the state folder never touches the real one.
|
|
51
|
+
*/
|
|
52
|
+
export function machineLockPath(name: string): string {
|
|
53
|
+
return join(stateDir(), 'locks', `${name}.lock`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isHolder(value: unknown): value is LockHolder {
|
|
57
|
+
if (typeof value !== 'object' || value === null) return false
|
|
58
|
+
const fields = value as Record<string, unknown>
|
|
59
|
+
return typeof fields.pid === 'number' && typeof fields.root === 'string'
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function readText(path: string): string | undefined {
|
|
63
|
+
try {
|
|
64
|
+
return readFileSync(path, 'utf8')
|
|
65
|
+
} catch {
|
|
66
|
+
return undefined
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function parseHolder(text: string): LockHolder | undefined {
|
|
71
|
+
try {
|
|
72
|
+
const value: unknown = JSON.parse(text)
|
|
73
|
+
return isHolder(value) ? value : undefined
|
|
74
|
+
} catch {
|
|
75
|
+
return undefined
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isCode(error: unknown, code: string): boolean {
|
|
80
|
+
return (error as NodeJS.ErrnoException).code === code
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Publishes the holder's file whole or not at all.
|
|
85
|
+
*
|
|
86
|
+
* `link` refuses an existing name the way an exclusive open does, and the
|
|
87
|
+
* linked file is already written, so no reader meets an empty lock and mistakes
|
|
88
|
+
* a holder mid-write for a corrupt file it may take over.
|
|
89
|
+
*/
|
|
90
|
+
function publish(path: string, text: string, pid: number): boolean {
|
|
91
|
+
const staged = `${path}.${pid}.tmp`
|
|
92
|
+
writeFileSync(staged, text)
|
|
93
|
+
try {
|
|
94
|
+
linkSync(staged, path)
|
|
95
|
+
return true
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (isCode(error, 'EEXIST')) return false
|
|
98
|
+
throw error
|
|
99
|
+
} finally {
|
|
100
|
+
unlinkSync(staged)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Moves a dead holder's file aside, or puts back one that changed under it.
|
|
106
|
+
*
|
|
107
|
+
* Unlink then create is two calls, and two acquirers judging one stale file
|
|
108
|
+
* would both get through. A rename moves the file to a name only this acquirer
|
|
109
|
+
* uses, so only one of them gets it, and comparing what moved against what was
|
|
110
|
+
* judged catches the case where another acquirer already replaced it.
|
|
111
|
+
*
|
|
112
|
+
* One interleaving still gets through. A third acquirer publishing between the
|
|
113
|
+
* rename and the restore leaves the moved holder's file with nowhere to go, so
|
|
114
|
+
* two suites run at once. That costs one oversubscribed run and never a hang,
|
|
115
|
+
* and closing it needs a lock the kernel releases, which `node:fs` does not
|
|
116
|
+
* offer.
|
|
117
|
+
*/
|
|
118
|
+
function displace(path: string, judged: string, pid: number): void {
|
|
119
|
+
const aside = `${path}.${pid}.stale`
|
|
120
|
+
try {
|
|
121
|
+
renameSync(path, aside)
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (isCode(error, 'ENOENT')) return
|
|
124
|
+
throw error
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (readText(aside) !== judged) {
|
|
128
|
+
try {
|
|
129
|
+
linkSync(aside, path)
|
|
130
|
+
} catch (error) {
|
|
131
|
+
if (!isCode(error, 'EEXIST')) throw error
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
unlinkSync(aside)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* One attempt at the lock: take it free, take it over from a dead holder, or
|
|
139
|
+
* report who holds it.
|
|
140
|
+
*/
|
|
141
|
+
export function tryAcquire(options: SuiteLockOptions): AcquireOutcome {
|
|
142
|
+
const pid = options.pid ?? process.pid
|
|
143
|
+
const probes = options.probes ?? SYSTEM_PROBES
|
|
144
|
+
const own: LockHolder = {
|
|
145
|
+
pid,
|
|
146
|
+
procStart: probes.procStartOf(pid) ?? undefined,
|
|
147
|
+
root: options.root,
|
|
148
|
+
}
|
|
149
|
+
const text = JSON.stringify(own)
|
|
150
|
+
|
|
151
|
+
mkdirSync(dirname(options.path), { recursive: true })
|
|
152
|
+
|
|
153
|
+
if (publish(options.path, text, pid)) {
|
|
154
|
+
return { acquired: true, release: () => release(options.path, text) }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const judged = readText(options.path)
|
|
158
|
+
if (judged === undefined) return { acquired: false, holder: undefined }
|
|
159
|
+
|
|
160
|
+
const holder = parseHolder(judged)
|
|
161
|
+
if (holder !== undefined && liveness(holder, probes).alive) {
|
|
162
|
+
return { acquired: false, holder }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
displace(options.path, judged, pid)
|
|
166
|
+
|
|
167
|
+
if (publish(options.path, text, pid)) {
|
|
168
|
+
return { acquired: true, release: () => release(options.path, text) }
|
|
169
|
+
}
|
|
170
|
+
return { acquired: false, holder: parseHolder(readText(options.path) ?? '') }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Removes the file only while it is still this holder's own. */
|
|
174
|
+
function release(path: string, text: string): void {
|
|
175
|
+
if (readText(path) !== text) return
|
|
176
|
+
try {
|
|
177
|
+
unlinkSync(path)
|
|
178
|
+
} catch (error) {
|
|
179
|
+
if (!isCode(error, 'ENOENT')) throw error
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const wait = (ms: number) =>
|
|
184
|
+
new Promise<void>((resolve) => setTimeout(resolve, ms))
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Waits for the lock with no deadline while its holder lives. A deadline would
|
|
188
|
+
* turn a long queue back into the intermittent failure the lock removes, and a
|
|
189
|
+
* dead holder is taken over on the next poll rather than waited out.
|
|
190
|
+
*/
|
|
191
|
+
export async function acquireSuiteLock(
|
|
192
|
+
options: SuiteLockOptions,
|
|
193
|
+
): Promise<HeldLock> {
|
|
194
|
+
const sleep = options.sleep ?? wait
|
|
195
|
+
const pollMs = options.pollMs ?? 500
|
|
196
|
+
const startedAt = performance.now()
|
|
197
|
+
let waitedOn: LockHolder | undefined
|
|
198
|
+
|
|
199
|
+
for (;;) {
|
|
200
|
+
const outcome = tryAcquire(options)
|
|
201
|
+
if (outcome.acquired) {
|
|
202
|
+
return {
|
|
203
|
+
release: outcome.release,
|
|
204
|
+
waitedOn,
|
|
205
|
+
waitedMs: waitedOn === undefined ? 0 : performance.now() - startedAt,
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (waitedOn === undefined && outcome.holder !== undefined) {
|
|
209
|
+
options.onWait?.(outcome.holder)
|
|
210
|
+
}
|
|
211
|
+
waitedOn = outcome.holder ?? waitedOn
|
|
212
|
+
await sleep(pollMs)
|
|
213
|
+
}
|
|
214
|
+
}
|
package/src/intake/folder.ts
CHANGED
package/src/labels/map.ts
CHANGED
package/src/labels/phase.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { linesOutsideFences, maskCodeSpans } from '@/markdown/scan'
|
|
2
|
-
import { RECORD_ENTRIES, RECORD_ROOTS } from '@/record
|
|
3
|
-
import type { RecordRoot } from '@/record
|
|
2
|
+
import { RECORD_ENTRIES, RECORD_ROOTS } from '@/roots/record'
|
|
3
|
+
import type { RecordRoot } from '@/roots/record'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* The two version namespaces `standards/versioning.md` keeps apart, and why a
|
|
@@ -114,7 +114,7 @@ const ROOT_READING: Record<RecordRoot, 'whole' | 'entries'> = {
|
|
|
114
114
|
* `.claude/` is tracked and holds `rules`, `skills`, `hooks`, and `context`,
|
|
115
115
|
* so a rule path resolves in any clone and is not a board reference, and only
|
|
116
116
|
* the entries the record move relocated are unreadable there. Reading the
|
|
117
|
-
* roots and the relocated entries from `src/record
|
|
117
|
+
* roots and the relocated entries from `src/roots/record.ts` is what makes a
|
|
118
118
|
* folder added there matched here without an edit, and the list above is what
|
|
119
119
|
* covers the one thing that module deliberately does not carry.
|
|
120
120
|
*
|
|
@@ -40,7 +40,7 @@ import {
|
|
|
40
40
|
slugOf,
|
|
41
41
|
} from '@/migrate/evidence-ordinal'
|
|
42
42
|
import { presentFolders } from '@/records/backup'
|
|
43
|
-
import { recordDir, SCRATCH, spell, type RecordRoot } from '@/record
|
|
43
|
+
import { recordDir, SCRATCH, spell, type RecordRoot } from '@/roots/record'
|
|
44
44
|
|
|
45
45
|
const CANON: RecordRoot = '.canon'
|
|
46
46
|
const CLAUDE: RecordRoot = '.claude'
|
package/src/migrate/records.ts
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
RECORD_ENTRIES,
|
|
16
16
|
RECORD_ROOTS,
|
|
17
17
|
spell,
|
|
18
|
-
} from '@/record
|
|
18
|
+
} from '@/roots/record'
|
|
19
19
|
|
|
20
20
|
/** The root the entries below leave, exported so the writer can prune it. */
|
|
21
21
|
export const FROM_ROOT: RecordRoot = '.claude'
|
|
@@ -26,7 +26,7 @@ const TO_ROOT: RecordRoot = '.canon'
|
|
|
26
26
|
/**
|
|
27
27
|
* Every entry the move relocates, at the name `.claude/` gave it.
|
|
28
28
|
*
|
|
29
|
-
* The list is `RECORD_ENTRIES`, which `record
|
|
29
|
+
* The list is `RECORD_ENTRIES`, which `roots/record.ts` owns because a seed
|
|
30
30
|
* destination and a superseded-layout report ask the same question. Restating
|
|
31
31
|
* it here would let the sweep and the resolver disagree about what moved.
|
|
32
32
|
*/
|
|
@@ -76,12 +76,12 @@ function escape(value: string): string {
|
|
|
76
76
|
* paths a session actually opened. Rewriting either makes it testify to
|
|
77
77
|
* something that never happened.
|
|
78
78
|
*
|
|
79
|
-
* This module and `src/record
|
|
80
|
-
* root on purpose. Sweeping them turns every citation this expression is
|
|
81
|
-
* from into its own replacement, leaving a rewriter that maps `.canon/`
|
|
82
|
-
* `.canon/` and matches nothing. `src/surface
|
|
83
|
-
* early, for the same reason: it spells `.claude` as data the moment
|
|
84
|
-
* exists, and excluding it later would leave one release where a records
|
|
79
|
+
* This module and `src/roots/record.ts` are two of the sources that state the
|
|
80
|
+
* old root on purpose. Sweeping them turns every citation this expression is
|
|
81
|
+
* built from into its own replacement, leaving a rewriter that maps `.canon/`
|
|
82
|
+
* to `.canon/` and matches nothing. `src/roots/surface.ts` joins the list a
|
|
83
|
+
* release early, for the same reason: it spells `.claude` as data the moment
|
|
84
|
+
* it exists, and excluding it later would leave one release where a records
|
|
85
85
|
* migration in a target could rewrite the resolver that migration itself
|
|
86
86
|
* depends on.
|
|
87
87
|
*
|
|
@@ -112,8 +112,8 @@ const EXCLUDED_PREFIXES: readonly string[] = [
|
|
|
112
112
|
|
|
113
113
|
const EXCLUDED_PATHS: readonly string[] = [
|
|
114
114
|
'CHANGELOG.md',
|
|
115
|
-
'src/record
|
|
116
|
-
'src/surface
|
|
115
|
+
'src/roots/record.ts',
|
|
116
|
+
'src/roots/surface.ts',
|
|
117
117
|
]
|
|
118
118
|
|
|
119
119
|
const EXCLUDED_SUFFIXES: readonly string[] = ['.test.ts']
|
|
@@ -171,7 +171,7 @@ export const RECORD_ONLY_ROOTS: readonly RecordRoot[] = RECORD_ROOTS.filter(
|
|
|
171
171
|
* under `.claude/`, and a target's installed `.claude/rules/canon/core/035-tasks.md`
|
|
172
172
|
* is the file the sweep exists to repoint, so a bare `.claude/` prefix strands
|
|
173
173
|
* it silently. The old root is therefore entry-scoped, through `spell` so the
|
|
174
|
-
* one naming variant stays decided in `record
|
|
174
|
+
* one naming variant stays decided in `roots/record.ts`.
|
|
175
175
|
*
|
|
176
176
|
* Joined with a literal separator rather than through `join`, the way
|
|
177
177
|
* `EXCLUDED_PREFIXES` already is. These are matched against what `git ls-files`
|
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
slugOf,
|
|
35
35
|
} from '@/migrate/evidence-ordinal'
|
|
36
36
|
import { presentFolders } from '@/records/backup'
|
|
37
|
-
import { recordDir, SCRATCH } from '@/record
|
|
37
|
+
import { recordDir, SCRATCH } from '@/roots/record'
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
40
|
* Every folder this promotion moves, at the name it carries under scratch.
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { join } from 'node:path'
|
|
12
|
-
import { SURFACE_ENTRIES, spell, type SurfaceRoot } from '@/surface
|
|
12
|
+
import { SURFACE_ENTRIES, spell, type SurfaceRoot } from '@/roots/surface'
|
|
13
13
|
|
|
14
14
|
/** The root the entries below leave. */
|
|
15
15
|
export const FROM_ROOT: SurfaceRoot = '.claude'
|
|
@@ -80,8 +80,8 @@ function escape(value: string): string {
|
|
|
80
80
|
*
|
|
81
81
|
* The changelog is release history, and an eval result is a transcript of
|
|
82
82
|
* paths a session actually opened; rewriting either makes it testify to
|
|
83
|
-
* something that never happened. `src/surface
|
|
84
|
-
* `src/record
|
|
83
|
+
* something that never happened. `src/roots/surface.ts` and
|
|
84
|
+
* `src/roots/record.ts` are the two sources that spell the old root as data on
|
|
85
85
|
* purpose, so sweeping either would turn the very literals this verb reads
|
|
86
86
|
* into their own replacement. A hook is the third such source, guarding on a
|
|
87
87
|
* `case` carrying an arm per root, so `.claude/hooks/` and the seed's copy of
|
|
@@ -102,8 +102,8 @@ const EXCLUDED_PREFIXES: readonly string[] = [
|
|
|
102
102
|
|
|
103
103
|
const EXCLUDED_PATHS: readonly string[] = [
|
|
104
104
|
'CHANGELOG.md',
|
|
105
|
-
'src/surface
|
|
106
|
-
'src/record
|
|
105
|
+
'src/roots/surface.ts',
|
|
106
|
+
'src/roots/record.ts',
|
|
107
107
|
]
|
|
108
108
|
|
|
109
109
|
export function isExcludedPath(path: string): boolean {
|
|
@@ -380,7 +380,7 @@ export interface SurfaceRootsPlan {
|
|
|
380
380
|
* moved entry, and whether its content cites one. Either alone is enough to
|
|
381
381
|
* produce an entry, and a file excluded from the citation rewrite still moves
|
|
382
382
|
* when its path itself is inside a moved entry, since the two entries excluded
|
|
383
|
-
* today, `src/surface
|
|
383
|
+
* today, `src/roots/surface.ts` and `src/roots/record.ts`, both sit outside
|
|
384
384
|
* every moved entry and never take that branch in practice.
|
|
385
385
|
*/
|
|
386
386
|
export function planSurfaceRootsMove(
|