@mjasnikovs/pi-task 0.40.50 → 0.41.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 +7 -4
- package/dist/config/config.d.ts +61 -18
- package/dist/config/config.js +50 -6
- package/dist/config/register.js +18 -5
- package/dist/shared/content-tokens.d.ts +16 -0
- package/dist/shared/content-tokens.js +137 -0
- package/dist/task/accept-debt.d.ts +15 -1
- package/dist/task/accept-debt.js +4 -0
- package/dist/task/artifact-closure.js +7 -1
- package/dist/task/auto-io.d.ts +59 -7
- package/dist/task/auto-io.js +140 -47
- package/dist/task/auto-orchestrator.d.ts +22 -19
- package/dist/task/auto-orchestrator.js +126 -112
- package/dist/task/child-runner.d.ts +18 -0
- package/dist/task/child-runner.js +7 -8
- package/dist/task/command-run.d.ts +4 -0
- package/dist/task/command-run.js +4 -2
- package/dist/task/constraint-policy.d.ts +52 -0
- package/dist/task/constraint-policy.js +66 -0
- package/dist/task/context-silence.d.ts +5 -4
- package/dist/task/context-silence.js +13 -5
- package/dist/task/coverage-loop.d.ts +7 -4
- package/dist/task/coverage-loop.js +16 -138
- package/dist/task/debug-log.d.ts +25 -2
- package/dist/task/debug-log.js +64 -10
- package/dist/task/decompose-fidelity.d.ts +17 -4
- package/dist/task/decompose-fidelity.js +95 -55
- package/dist/task/decompose-granularity.d.ts +7 -7
- package/dist/task/decompose-granularity.js +8 -8
- package/dist/task/env-notes.d.ts +65 -19
- package/dist/task/env-notes.js +152 -50
- package/dist/task/external-context.js +25 -5
- package/dist/task/fix-context.d.ts +39 -0
- package/dist/task/fix-context.js +58 -0
- package/dist/task/gate-deps.d.ts +49 -0
- package/dist/task/gate-deps.js +199 -38
- package/dist/task/gate-evidence.d.ts +51 -0
- package/dist/task/gate-evidence.js +90 -0
- package/dist/task/gate-resolution.d.ts +119 -0
- package/dist/task/gate-resolution.js +120 -0
- package/dist/task/git-state-guard.js +3 -26
- package/dist/task/handoff.d.ts +35 -0
- package/dist/task/handoff.js +100 -0
- package/dist/task/health-baseline.d.ts +87 -0
- package/dist/task/health-baseline.js +158 -0
- package/dist/task/ledger.d.ts +10 -0
- package/dist/task/ledger.js +22 -11
- package/dist/task/lint-fix.d.ts +11 -0
- package/dist/task/lint-fix.js +14 -8
- package/dist/task/loop-detector.d.ts +52 -3
- package/dist/task/loop-detector.js +81 -5
- package/dist/task/mentions.d.ts +17 -0
- package/dist/task/mentions.js +86 -0
- package/dist/task/orchestrator.d.ts +53 -9
- package/dist/task/orchestrator.js +104 -28
- package/dist/task/orientation.d.ts +135 -29
- package/dist/task/orientation.js +259 -67
- package/dist/task/owned-freeze-reassign.d.ts +7 -3
- package/dist/task/owned-freeze-reassign.js +13 -5
- package/dist/task/parsers.d.ts +6 -1
- package/dist/task/parsers.js +21 -4
- package/dist/task/phases.d.ts +1 -1
- package/dist/task/phases.js +125 -153
- package/dist/task/plan-orchestrator.js +5 -6
- package/dist/task/plan-rounds.d.ts +1 -0
- package/dist/task/plan-rounds.js +8 -1
- package/dist/task/prohibition-probe.d.ts +6 -1
- package/dist/task/prohibition-probe.js +26 -6
- package/dist/task/prompts.js +13 -2
- package/dist/task/qa-transcript.d.ts +17 -0
- package/dist/task/qa-transcript.js +28 -0
- package/dist/task/question-source.d.ts +16 -2
- package/dist/task/question-source.js +21 -8
- package/dist/task/repo-health-check.d.ts +27 -0
- package/dist/task/repo-health-check.js +39 -5
- package/dist/task/requirements.d.ts +67 -13
- package/dist/task/requirements.js +129 -67
- package/dist/task/research-worker.d.ts +43 -2
- package/dist/task/research-worker.js +103 -8
- package/dist/task/run-bracket.d.ts +9 -4
- package/dist/task/run-bracket.js +9 -0
- package/dist/task/run-context.d.ts +152 -0
- package/dist/task/run-context.js +277 -0
- package/dist/task/spec-doc.d.ts +78 -0
- package/dist/task/spec-doc.js +205 -0
- package/dist/task/spec-model.d.ts +70 -0
- package/dist/task/spec-model.js +139 -0
- package/dist/task/stall-detector.d.ts +1 -1
- package/dist/task/stall-detector.js +9 -5
- package/dist/task/state-dir.d.ts +29 -0
- package/dist/task/state-dir.js +80 -0
- package/dist/task/suppression-probe.d.ts +67 -0
- package/dist/task/suppression-probe.js +78 -0
- package/dist/task/task-gates.d.ts +39 -57
- package/dist/task/task-gates.js +115 -195
- package/dist/task/task-io.d.ts +10 -0
- package/dist/task/task-io.js +69 -18
- package/dist/task/task-parsers.js +6 -6
- package/dist/task/task-types.d.ts +7 -0
- package/dist/task/timings.d.ts +11 -0
- package/dist/task/timings.js +21 -0
- package/dist/task/tree-hash.d.ts +8 -0
- package/dist/task/tree-hash.js +55 -0
- package/dist/task/verify-reconcile.d.ts +4 -3
- package/dist/task/verify-reconcile.js +7 -8
- package/dist/task/verify-resolution.d.ts +28 -5
- package/dist/task/verify-resolution.js +70 -13
- package/dist/task/verify-work.d.ts +98 -35
- package/dist/task/verify-work.js +227 -60
- package/dist/task/yolo.d.ts +0 -13
- package/dist/task/yolo.js +9 -21
- package/dist/workers/pi-worker-core.d.ts +20 -0
- package/dist/workers/pi-worker-core.js +16 -11
- package/dist/workers/pi-worker-docs.d.ts +6 -5
- package/dist/workers/pi-worker-docs.js +8 -7
- package/dist/workers/pi-worker-fetch.d.ts +3 -2
- package/dist/workers/pi-worker-fetch.js +5 -4
- package/dist/workers/pi-worker-search.js +5 -4
- package/dist/workers/research-cache.d.ts +23 -1
- package/dist/workers/research-cache.js +63 -3
- package/dist/workers/shared.d.ts +18 -0
- package/dist/workers/shared.js +38 -22
- package/package.json +1 -1
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/** Attempts a class whose defect an implementation re-run can actually move. */
|
|
2
|
+
const REATTEMPTABLE_BUDGET = 3;
|
|
3
|
+
/**
|
|
4
|
+
* How many UNATTENDED implementation re-runs each FAIL class is worth.
|
|
5
|
+
*
|
|
6
|
+
* Zero is a statement, not a disabled feature: an absent tool is not installed by
|
|
7
|
+
* re-running the work that needed it, and a verification pass that could not run
|
|
8
|
+
* is not repaired by changing the code it failed to judge. Spending three turns
|
|
9
|
+
* on either is how a run burns an hour to arrive where round one already was.
|
|
10
|
+
*/
|
|
11
|
+
export const AUTOFIX_BUDGET = {
|
|
12
|
+
'repo-health': REATTEMPTABLE_BUDGET,
|
|
13
|
+
'static-checks': REATTEMPTABLE_BUDGET,
|
|
14
|
+
'model-verdict': REATTEMPTABLE_BUDGET,
|
|
15
|
+
unobserved: 0,
|
|
16
|
+
'harness-fault': 0
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* How many times the /task-auto loop will START one plan entry before abandoning it.
|
|
20
|
+
*
|
|
21
|
+
* Derived, not chosen: one run to reach the gate at all, plus the re-runs the most
|
|
22
|
+
* forgiving fail class is worth inside that run. An entry that comes back for a
|
|
23
|
+
* further attempt has already spent a full autofix budget without converging, and
|
|
24
|
+
* the plan's remaining entries are worth more than its next round. Raising a class
|
|
25
|
+
* budget raises this with it, which is the relationship that should hold.
|
|
26
|
+
*/
|
|
27
|
+
export const ENTRY_ATTEMPT_BUDGET = 1 + Math.max(...Object.values(AUTOFIX_BUDGET));
|
|
28
|
+
/** The budget in force, honouring an UNOBSERVED flag on any class. */
|
|
29
|
+
export function autofixBudget(i) {
|
|
30
|
+
return i.unobserved ? 0 : AUTOFIX_BUDGET[i.failClass];
|
|
31
|
+
}
|
|
32
|
+
export function contradictionReason(c) {
|
|
33
|
+
const criterion = c.criterion.trim();
|
|
34
|
+
return (`spec contradiction — \`${c.frozenPath}\` is frozen by this task's spec and `
|
|
35
|
+
+ `${criterion.length > 0 ? criterion : 'the failing check'} cannot be satisfied `
|
|
36
|
+
+ 'without editing it; no re-run under the same freeze converges');
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The two ends of the ladder that are NOT an autofix: ask a human, or — when
|
|
40
|
+
* there is no human — accept and write the defect down under the origin that says
|
|
41
|
+
* a machine decided.
|
|
42
|
+
*/
|
|
43
|
+
function terminal(i, rule, reason) {
|
|
44
|
+
return i.unattended ?
|
|
45
|
+
{ rule, action: 'accept', debtOrigin: 'yolo-accepted', reason }
|
|
46
|
+
: { rule, action: 'ask', debtOrigin: null, reason };
|
|
47
|
+
}
|
|
48
|
+
function noBudgetReason(i) {
|
|
49
|
+
return i.unobserved || i.failClass === 'unobserved' ?
|
|
50
|
+
'verify UNOBSERVED — the spec-required check could not run (tooling absent) and an '
|
|
51
|
+
+ 'unattended re-run cannot provision it'
|
|
52
|
+
: 'the verification pass itself could not run — re-running the implementation cannot '
|
|
53
|
+
+ 'fix a harness fault';
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The ordered ladder. FIRST MATCH WINS, and this order is the only statement of
|
|
57
|
+
* the policy:
|
|
58
|
+
*
|
|
59
|
+
* 1. `spec-contradiction` — the fix the check demands is the edit the spec
|
|
60
|
+
* forbids. First, because every row below would otherwise spend a budget,
|
|
61
|
+
* a human's attention or both on a loop that cannot converge (0053 ran three
|
|
62
|
+
* rounds past this exact fact).
|
|
63
|
+
* 2. `judge-accept` — before any budget arithmetic, because that arithmetic is
|
|
64
|
+
* precisely what used to override a correct ACCEPT with a rescue attempt.
|
|
65
|
+
* 3. `no-autofix-budget` — a class worth zero attempts. Its own row, not folded
|
|
66
|
+
* into the one below, because a budget that was never offered must not be
|
|
67
|
+
* reported as one that was spent.
|
|
68
|
+
* 4. `budget-spent` — consecutive unattended attempts that all still FAIL. A
|
|
69
|
+
* person breaks the loop, or (unattended) it terminates with a debt.
|
|
70
|
+
* 5. `autofix` — unconditional, so the table is total.
|
|
71
|
+
*/
|
|
72
|
+
export const RESOLUTION_RULES = [
|
|
73
|
+
{
|
|
74
|
+
id: 'spec-contradiction',
|
|
75
|
+
match: i => i.contradiction === null ?
|
|
76
|
+
null
|
|
77
|
+
: {
|
|
78
|
+
rule: 'spec-contradiction',
|
|
79
|
+
action: 'accept',
|
|
80
|
+
debtOrigin: 'spec-contradiction',
|
|
81
|
+
reason: contradictionReason(i.contradiction)
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: 'judge-accept',
|
|
86
|
+
match: i => i.recommend === 'accept' ?
|
|
87
|
+
terminal(i, 'judge-accept', `judge recommended ACCEPT after investigating the workspace (autofix budget `
|
|
88
|
+
+ `${i.attempts}/${autofixBudget(i)})`)
|
|
89
|
+
: null
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: 'no-autofix-budget',
|
|
93
|
+
match: i => autofixBudget(i) === 0 ? terminal(i, 'no-autofix-budget', noBudgetReason(i)) : null
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: 'budget-spent',
|
|
97
|
+
match: i => i.attempts >= autofixBudget(i) ?
|
|
98
|
+
terminal(i, 'budget-spent', `autofix budget spent (${i.attempts}/${autofixBudget(i)})`)
|
|
99
|
+
: null
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: 'autofix',
|
|
103
|
+
match: i => ({
|
|
104
|
+
rule: 'autofix',
|
|
105
|
+
action: 'autofix',
|
|
106
|
+
debtOrigin: null,
|
|
107
|
+
reason: `autofix recommended, unattended ${i.attempts + 1}/${autofixBudget(i)}`
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
];
|
|
111
|
+
export function resolveDisposition(i) {
|
|
112
|
+
for (const rule of RESOLUTION_RULES) {
|
|
113
|
+
const d = rule.match(i);
|
|
114
|
+
if (d !== null)
|
|
115
|
+
return d;
|
|
116
|
+
}
|
|
117
|
+
// Unreachable while the last row is unconditional; a reordering that breaks
|
|
118
|
+
// that fails loudly here instead of returning undefined into the gate loop.
|
|
119
|
+
throw new Error('RESOLUTION_RULES must end in an unconditional row');
|
|
120
|
+
}
|
|
@@ -46,8 +46,7 @@ import * as os from 'node:os';
|
|
|
46
46
|
import * as path from 'node:path';
|
|
47
47
|
import { makeGit } from '../shared/git-runner.js';
|
|
48
48
|
import { isRegenerableArtifact } from './regenerable-artifacts.js';
|
|
49
|
-
|
|
50
|
-
const EXCLUDE_TASKS_DIR = ':(exclude).pi-tasks';
|
|
49
|
+
import { worktreeTreeHash } from './tree-hash.js';
|
|
51
50
|
/**
|
|
52
51
|
* Untracked paths that are regenerable test/build OUTPUT, not graded source. A gate
|
|
53
52
|
* child creating or rewriting one of these has not mutated the work under
|
|
@@ -115,28 +114,6 @@ function isAlwaysRegenerable(relPath, ctCacheDirs) {
|
|
|
115
114
|
return true;
|
|
116
115
|
return ctCacheDirs.some(d => p === d || p.startsWith(d + '/'));
|
|
117
116
|
}
|
|
118
|
-
/**
|
|
119
|
-
* Snapshot the worktree content into a tree object via a THROWAWAY index file, so
|
|
120
|
-
* neither the real index nor the stash is touched. Returns null when git cannot
|
|
121
|
-
* build the tree (odd states — the guard then skips tree reconciliation).
|
|
122
|
-
*/
|
|
123
|
-
async function captureWorktreeTree(git) {
|
|
124
|
-
const tmpIndex = path.join(os.tmpdir(), `pi-task-guard-index-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
125
|
-
const env = { GIT_INDEX_FILE: tmpIndex };
|
|
126
|
-
try {
|
|
127
|
-
const empty = await git(['read-tree', '--empty'], env);
|
|
128
|
-
if (empty.exitCode !== 0)
|
|
129
|
-
return null;
|
|
130
|
-
const add = await git(['add', '-A', '--', '.', EXCLUDE_TASKS_DIR], env);
|
|
131
|
-
if (add.exitCode !== 0)
|
|
132
|
-
return null;
|
|
133
|
-
const tree = await git(['write-tree'], env);
|
|
134
|
-
return tree.exitCode === 0 ? tree.stdout.trim() : null;
|
|
135
|
-
}
|
|
136
|
-
finally {
|
|
137
|
-
await fsp.rm(tmpIndex, { force: true }).catch(() => { });
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
117
|
/** Capture the repo state a gate child must leave untouched. */
|
|
141
118
|
export async function captureGitState(cwd, signal, spawnFn) {
|
|
142
119
|
const git = makeGit(cwd, signal, spawnFn);
|
|
@@ -161,7 +138,7 @@ export async function captureGitState(cwd, signal, spawnFn) {
|
|
|
161
138
|
headSha: head.stdout.trim(),
|
|
162
139
|
branchRef: branch.exitCode === 0 ? branch.stdout.trim() : null,
|
|
163
140
|
stashSha: stash.exitCode === 0 ? stash.stdout.trim() : null,
|
|
164
|
-
treeSha: await
|
|
141
|
+
treeSha: await worktreeTreeHash(git)
|
|
165
142
|
};
|
|
166
143
|
}
|
|
167
144
|
/** Paths tracked in the commit `headSha` points at — the "graded" codebase a gate
|
|
@@ -309,7 +286,7 @@ export async function reconcileGitState(cwd, before, signal, spawnFn) {
|
|
|
309
286
|
}
|
|
310
287
|
// 2. Worktree content.
|
|
311
288
|
if (before.treeSha) {
|
|
312
|
-
const afterTree = await
|
|
289
|
+
const afterTree = await worktreeTreeHash(git);
|
|
313
290
|
if (afterTree && afterTree !== before.treeSha) {
|
|
314
291
|
const tracked = await trackedPathsAt(git, before.headSha);
|
|
315
292
|
const ctCacheDirs = readCtCacheDirs(cwd);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { FixContext } from './fix-context.js';
|
|
2
|
+
import type { Disposition } from './gate-resolution.js';
|
|
3
|
+
import type { VerifyFailClass } from './verify-work.js';
|
|
4
|
+
/** The part of a {@link FixContext} worth keeping after the run that used it. */
|
|
5
|
+
export interface HandoffFixSummary {
|
|
6
|
+
/** The rule of the decision table that sent the work back. */
|
|
7
|
+
rule: Disposition['rule'];
|
|
8
|
+
failClass?: VerifyFailClass;
|
|
9
|
+
/** Which re-run the fix context itself was built for, 1-based. */
|
|
10
|
+
fixAttempt: number;
|
|
11
|
+
/** Probe names that found something; the findings live in the gate trail. */
|
|
12
|
+
probes: string[];
|
|
13
|
+
frozenPath?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface HandoffRecord {
|
|
16
|
+
/** Which delivery of this task's spec this is, 1-based. */
|
|
17
|
+
attempt: number;
|
|
18
|
+
specHash: string;
|
|
19
|
+
delivered: 'fresh' | 'reattempt';
|
|
20
|
+
fixContext?: HandoffFixSummary;
|
|
21
|
+
at?: string;
|
|
22
|
+
}
|
|
23
|
+
/** Short content hash of the spec as delivered. */
|
|
24
|
+
export declare function specHash(spec: string): string;
|
|
25
|
+
export declare function summariseFixContext(ctx: FixContext): HandoffFixSummary;
|
|
26
|
+
export declare function formatHandoff(r: HandoffRecord): string;
|
|
27
|
+
/**
|
|
28
|
+
* Every record in a `## handoff` body, oldest first. A block without the
|
|
29
|
+
* `attempt` field is one of the single-timestamp sections this grammar replaced:
|
|
30
|
+
* it is still a delivery, so it is returned as attempt 1 with an unknown hash
|
|
31
|
+
* rather than dropped, and the next append counts from it.
|
|
32
|
+
*/
|
|
33
|
+
export declare function parseHandoff(body: string | null): HandoffRecord[];
|
|
34
|
+
/** The section body with `record` appended after everything already in `prev`. */
|
|
35
|
+
export declare function appendHandoff(prev: string | null, record: HandoffRecord): string;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* handoff — what was delivered to the implementer, per attempt.
|
|
3
|
+
*
|
|
4
|
+
* The `## handoff` section used to be one `handoff_at:` timestamp, overwritten on
|
|
5
|
+
* every re-entry, which answered neither of the questions asked of it after a run:
|
|
6
|
+
* how many times was this task's spec delivered, and was the spec that came back
|
|
7
|
+
* the same one that failed? A record per delivery answers both — the spec hash
|
|
8
|
+
* makes a re-delivery that silently changed the spec visible, and the fix summary
|
|
9
|
+
* says which gate decision paid for it.
|
|
10
|
+
*
|
|
11
|
+
* Key-value lines because this section is read by humans as often as by code, and
|
|
12
|
+
* a blank line between records is the only separator the grammar needs.
|
|
13
|
+
*/
|
|
14
|
+
import { createHash } from 'node:crypto';
|
|
15
|
+
/** Short content hash of the spec as delivered. */
|
|
16
|
+
export function specHash(spec) {
|
|
17
|
+
return createHash('sha256').update(spec).digest('hex').slice(0, 12);
|
|
18
|
+
}
|
|
19
|
+
export function summariseFixContext(ctx) {
|
|
20
|
+
const probes = Object.entries(ctx.probes)
|
|
21
|
+
.filter(([, findings]) => Array.isArray(findings) && findings.length > 0)
|
|
22
|
+
.map(([name]) => name);
|
|
23
|
+
return {
|
|
24
|
+
rule: ctx.disposition.rule,
|
|
25
|
+
...(ctx.outcome.failClass ? { failClass: ctx.outcome.failClass } : {}),
|
|
26
|
+
fixAttempt: ctx.attempt,
|
|
27
|
+
probes,
|
|
28
|
+
...(ctx.contradiction ? { frozenPath: ctx.contradiction.frozenPath } : {})
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const FIELD_RE = /^([a-z_]+):\s*(.*)$/;
|
|
32
|
+
export function formatHandoff(r) {
|
|
33
|
+
const lines = [
|
|
34
|
+
`handoff_at: ${r.at ?? new Date().toISOString()}`,
|
|
35
|
+
`attempt: ${r.attempt}`,
|
|
36
|
+
`spec_hash: ${r.specHash}`,
|
|
37
|
+
`delivered: ${r.delivered}`
|
|
38
|
+
];
|
|
39
|
+
const f = r.fixContext;
|
|
40
|
+
if (f) {
|
|
41
|
+
lines.push(`fix_rule: ${f.rule}`);
|
|
42
|
+
if (f.failClass)
|
|
43
|
+
lines.push(`fix_class: ${f.failClass}`);
|
|
44
|
+
lines.push(`fix_attempt: ${f.fixAttempt}`);
|
|
45
|
+
if (f.probes.length > 0)
|
|
46
|
+
lines.push(`fix_probes: ${f.probes.join(', ')}`);
|
|
47
|
+
if (f.frozenPath)
|
|
48
|
+
lines.push(`fix_frozen_path: ${f.frozenPath}`);
|
|
49
|
+
}
|
|
50
|
+
return lines.join('\n');
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Every record in a `## handoff` body, oldest first. A block without the
|
|
54
|
+
* `attempt` field is one of the single-timestamp sections this grammar replaced:
|
|
55
|
+
* it is still a delivery, so it is returned as attempt 1 with an unknown hash
|
|
56
|
+
* rather than dropped, and the next append counts from it.
|
|
57
|
+
*/
|
|
58
|
+
export function parseHandoff(body) {
|
|
59
|
+
const out = [];
|
|
60
|
+
for (const block of (body ?? '').split(/\n\s*\n/)) {
|
|
61
|
+
const fields = new Map();
|
|
62
|
+
for (const line of block.split('\n')) {
|
|
63
|
+
const m = FIELD_RE.exec(line.trim());
|
|
64
|
+
if (m)
|
|
65
|
+
fields.set(m[1], m[2].trim());
|
|
66
|
+
}
|
|
67
|
+
if (fields.size === 0)
|
|
68
|
+
continue;
|
|
69
|
+
const attempt = parseInt(fields.get('attempt') ?? '', 10);
|
|
70
|
+
const delivered = fields.get('delivered');
|
|
71
|
+
const rule = fields.get('fix_rule');
|
|
72
|
+
const probes = fields.get('fix_probes');
|
|
73
|
+
const failClass = fields.get('fix_class');
|
|
74
|
+
const frozenPath = fields.get('fix_frozen_path');
|
|
75
|
+
out.push({
|
|
76
|
+
attempt: Number.isNaN(attempt) ? out.length + 1 : attempt,
|
|
77
|
+
specHash: fields.get('spec_hash') ?? '',
|
|
78
|
+
delivered: delivered === 'reattempt' ? 'reattempt' : 'fresh',
|
|
79
|
+
...(fields.has('handoff_at') ? { at: fields.get('handoff_at') } : {}),
|
|
80
|
+
...(rule ?
|
|
81
|
+
{
|
|
82
|
+
fixContext: {
|
|
83
|
+
rule: rule,
|
|
84
|
+
...(failClass ? { failClass: failClass } : {}),
|
|
85
|
+
fixAttempt: parseInt(fields.get('fix_attempt') ?? '1', 10) || 1,
|
|
86
|
+
probes: probes ? probes.split(',').map(p => p.trim()) : [],
|
|
87
|
+
...(frozenPath ? { frozenPath } : {})
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
: {})
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
/** The section body with `record` appended after everything already in `prev`. */
|
|
96
|
+
export function appendHandoff(prev, record) {
|
|
97
|
+
const before = (prev ?? '').trim();
|
|
98
|
+
const line = formatHandoff(record);
|
|
99
|
+
return before.length === 0 ? line : `${before}\n\n${line}`;
|
|
100
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { GitRunner } from '../shared/git-runner.js';
|
|
2
|
+
import type { HealthCommandResult, HealthOutcome } from './repo-health-check.js';
|
|
3
|
+
/**
|
|
4
|
+
* The permissive read of a health result: everything the differential needs and
|
|
5
|
+
* nothing else. The gate deps hand around structurally-typed health signals (and
|
|
6
|
+
* tests fake them), so the differential asks for the two fields it reads rather
|
|
7
|
+
* than for the full `HealthOutcome`.
|
|
8
|
+
*/
|
|
9
|
+
export interface HealthSignal {
|
|
10
|
+
ok: boolean;
|
|
11
|
+
/** Absent → the result predates per-command recording; the differential then
|
|
12
|
+
* has only the overall verdict to compare. */
|
|
13
|
+
commands?: readonly HealthCommandResult[];
|
|
14
|
+
}
|
|
15
|
+
export interface HealthBaseline {
|
|
16
|
+
/** When the baseline was taken (ISO). */
|
|
17
|
+
at: string;
|
|
18
|
+
/** The tree it describes, so a reader can tell whether it still applies. */
|
|
19
|
+
treeHash: string | null;
|
|
20
|
+
outcome: HealthOutcome;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Whose fault the current health result is.
|
|
24
|
+
* clean — nothing is failing now.
|
|
25
|
+
* regressed — a check fails that the baseline did not have failing. THIS task.
|
|
26
|
+
* pre-existing — every failing check failed the same way before the task ran.
|
|
27
|
+
*/
|
|
28
|
+
export type HealthDelta = 'clean' | 'regressed' | 'pre-existing';
|
|
29
|
+
/**
|
|
30
|
+
* Classify the current health result against the baseline.
|
|
31
|
+
*
|
|
32
|
+
* A NULL baseline is `regressed`, deliberately. An unestablished baseline is not
|
|
33
|
+
* evidence that the breakage predates the task — it is the absence of evidence —
|
|
34
|
+
* and the two errors are not symmetrical: calling it pre-existing SHIPS a real
|
|
35
|
+
* regression as inherited debt, while calling it a regression routes the failure
|
|
36
|
+
* through the resolution table, where a judge, a bounded lint fix or a human can
|
|
37
|
+
* still say otherwise. The same reasoning retires the `healthBefore?.ok ?? true`
|
|
38
|
+
* default the enforce site used to carry.
|
|
39
|
+
*/
|
|
40
|
+
export declare function classifyHealthDelta(baseline: HealthSignal | null, after: HealthSignal): HealthDelta;
|
|
41
|
+
/** The failing commands, as prompt/trail lines naming the exit code. */
|
|
42
|
+
export declare function inheritedHealthFindings(after: HealthSignal): string[];
|
|
43
|
+
export declare const HEALTH_BASELINE_SECTION = "health baseline";
|
|
44
|
+
/**
|
|
45
|
+
* Render for `## health baseline`. JSON in a fence rather than a hand-rolled
|
|
46
|
+
* grammar: this round-trips through a committed file that a later run parses, and
|
|
47
|
+
* a second grammar is a second thing to drift.
|
|
48
|
+
*
|
|
49
|
+
* The captured `output` is dropped — up to 40 lines of a linter's report, in a
|
|
50
|
+
* file committed with every task, for a field the differential never reads. The
|
|
51
|
+
* live run's own trail already carries it.
|
|
52
|
+
*/
|
|
53
|
+
export declare function formatHealthBaseline(b: HealthBaseline): string;
|
|
54
|
+
/** Parse a `## health baseline` section back. Null on anything unreadable — an
|
|
55
|
+
* unparseable baseline is no baseline, never a fabricated clean one. */
|
|
56
|
+
export declare function parseHealthBaseline(section: string | null): HealthBaseline | null;
|
|
57
|
+
export interface CaptureDeps {
|
|
58
|
+
runHealth: () => Promise<HealthOutcome>;
|
|
59
|
+
treeHash: () => Promise<string | null>;
|
|
60
|
+
/** Undo whatever a `--fix`-style health command wrote. */
|
|
61
|
+
discardEdits: () => Promise<void>;
|
|
62
|
+
now?: () => Date;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Capture the baseline for the tree as it stands. The hash is taken FIRST: a
|
|
66
|
+
* health command that writes would otherwise be described by the hash of its own
|
|
67
|
+
* output rather than of the tree the task starts from.
|
|
68
|
+
*/
|
|
69
|
+
export declare function captureHealthBaseline(deps: CaptureDeps): Promise<HealthBaseline>;
|
|
70
|
+
export interface LazyCaptureDeps {
|
|
71
|
+
git: GitRunner;
|
|
72
|
+
/** Run the health check in the given directory — the throwaway worktree. */
|
|
73
|
+
runHealthIn: (dir: string) => Promise<HealthOutcome>;
|
|
74
|
+
tmpDir?: () => string;
|
|
75
|
+
now?: () => Date;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Establish a baseline for a task file that has none, from a DETACHED WORKTREE at
|
|
79
|
+
* HEAD. HEAD is the last commit before the task's uncommitted work, so it is the
|
|
80
|
+
* state the task started from — and checking it out elsewhere means the real index,
|
|
81
|
+
* the real working tree and the stash are all untouched, which no `stash` or
|
|
82
|
+
* `checkout` of the live tree could promise.
|
|
83
|
+
*
|
|
84
|
+
* Null when git cannot produce the worktree; the caller then has no baseline, and
|
|
85
|
+
* `classifyHealthDelta` says what that means.
|
|
86
|
+
*/
|
|
87
|
+
export declare function lazyHealthBaseline(deps: LazyCaptureDeps): Promise<HealthBaseline | null>;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* health-baseline — what the repo's own static checks said BEFORE the task ran,
|
|
3
|
+
* and the differential that decides whose fault a red check is.
|
|
4
|
+
*
|
|
5
|
+
* THE FAILURE THIS CLOSES. The verify gate ran `runRepoHealthCheck` and failed on
|
|
6
|
+
* its absolute exit code. So a task that inherited a repo whose lint was already
|
|
7
|
+
* red failed its gate for somebody else's defect (AUTO_0002: 0034 absorbed 0033's
|
|
8
|
+
* lint debt), and — worse — that FAIL short-circuited before the probes, so the
|
|
9
|
+
* deterministic findings about the task's OWN work were never computed at all.
|
|
10
|
+
*
|
|
11
|
+
* THE COMPARISON IS PER COMMAND, not per overall verdict. `ok` is one boolean over
|
|
12
|
+
* a list of checks that short-circuits on the first failure, so "red before, red
|
|
13
|
+
* after" is true of a repo whose lint was already broken AND of a task that broke
|
|
14
|
+
* typecheck on top of it. Only the (cmd, exitCode) pairs can tell those apart.
|
|
15
|
+
*
|
|
16
|
+
* WHERE THE BASELINE COMES FROM. `/task-auto` captures it at the pre-task
|
|
17
|
+
* checkpoint commit, where the tree is clean and therefore describes exactly the
|
|
18
|
+
* state the task starts from; `/task` captures at its own start. Health commands
|
|
19
|
+
* may WRITE (`--fix` is a common `lint` script), so the capture discards edits
|
|
20
|
+
* afterwards — a baseline that silently fixed the repo would make the task's gate
|
|
21
|
+
* judge a tree nobody authored. A task file that has no baseline (it predates this,
|
|
22
|
+
* or the capture failed) gets one lazily at verify time from a detached worktree at
|
|
23
|
+
* HEAD: no stash, and the real index is never touched.
|
|
24
|
+
*/
|
|
25
|
+
import * as fsp from 'node:fs/promises';
|
|
26
|
+
import * as os from 'node:os';
|
|
27
|
+
import * as path from 'node:path';
|
|
28
|
+
import { commitTreeHash } from './tree-hash.js';
|
|
29
|
+
function failures(signal) {
|
|
30
|
+
return (signal.commands ?? []).filter(c => c.outcome === 'fail');
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Classify the current health result against the baseline.
|
|
34
|
+
*
|
|
35
|
+
* A NULL baseline is `regressed`, deliberately. An unestablished baseline is not
|
|
36
|
+
* evidence that the breakage predates the task — it is the absence of evidence —
|
|
37
|
+
* and the two errors are not symmetrical: calling it pre-existing SHIPS a real
|
|
38
|
+
* regression as inherited debt, while calling it a regression routes the failure
|
|
39
|
+
* through the resolution table, where a judge, a bounded lint fix or a human can
|
|
40
|
+
* still say otherwise. The same reasoning retires the `healthBefore?.ok ?? true`
|
|
41
|
+
* default the enforce site used to carry.
|
|
42
|
+
*/
|
|
43
|
+
export function classifyHealthDelta(baseline, after) {
|
|
44
|
+
if (after.ok)
|
|
45
|
+
return 'clean';
|
|
46
|
+
if (!baseline)
|
|
47
|
+
return 'regressed';
|
|
48
|
+
const before = failures(baseline);
|
|
49
|
+
const now = failures(after);
|
|
50
|
+
// A failing result that names no failing command carries no per-command detail
|
|
51
|
+
// (a legacy record, or an injected signal); the overall verdict is all there is.
|
|
52
|
+
const detailed = now.length > 0 && (baseline.ok || before.length > 0);
|
|
53
|
+
if (!detailed)
|
|
54
|
+
return baseline.ok ? 'regressed' : 'pre-existing';
|
|
55
|
+
const key = (c) => JSON.stringify([c.cmd, c.exitCode]);
|
|
56
|
+
const wasFailing = new Set(before.map(key));
|
|
57
|
+
return now.every(c => wasFailing.has(key(c))) ? 'pre-existing' : 'regressed';
|
|
58
|
+
}
|
|
59
|
+
/** The failing commands, as prompt/trail lines naming the exit code. */
|
|
60
|
+
export function inheritedHealthFindings(after) {
|
|
61
|
+
return failures(after).map(c => `\`${c.cmd}\` exits ${c.exitCode} (and did before this task)`);
|
|
62
|
+
}
|
|
63
|
+
// ─── The task-file section ───────────────────────────────────────────────────
|
|
64
|
+
export const HEALTH_BASELINE_SECTION = 'health baseline';
|
|
65
|
+
/**
|
|
66
|
+
* Render for `## health baseline`. JSON in a fence rather than a hand-rolled
|
|
67
|
+
* grammar: this round-trips through a committed file that a later run parses, and
|
|
68
|
+
* a second grammar is a second thing to drift.
|
|
69
|
+
*
|
|
70
|
+
* The captured `output` is dropped — up to 40 lines of a linter's report, in a
|
|
71
|
+
* file committed with every task, for a field the differential never reads. The
|
|
72
|
+
* live run's own trail already carries it.
|
|
73
|
+
*/
|
|
74
|
+
export function formatHealthBaseline(b) {
|
|
75
|
+
const { output: _output, ...outcome } = b.outcome;
|
|
76
|
+
return ['```json', JSON.stringify({ ...b, outcome }, null, 2), '```'].join('\n');
|
|
77
|
+
}
|
|
78
|
+
/** Parse a `## health baseline` section back. Null on anything unreadable — an
|
|
79
|
+
* unparseable baseline is no baseline, never a fabricated clean one. */
|
|
80
|
+
export function parseHealthBaseline(section) {
|
|
81
|
+
if (!section)
|
|
82
|
+
return null;
|
|
83
|
+
const json = section.replace(/^```(?:json)?\s*/, '').replace(/```\s*$/, '');
|
|
84
|
+
try {
|
|
85
|
+
const parsed = JSON.parse(json);
|
|
86
|
+
const outcome = parsed.outcome;
|
|
87
|
+
if (!outcome || typeof outcome.ok !== 'boolean')
|
|
88
|
+
return null;
|
|
89
|
+
return {
|
|
90
|
+
at: typeof parsed.at === 'string' ? parsed.at : '',
|
|
91
|
+
treeHash: typeof parsed.treeHash === 'string' ? parsed.treeHash : null,
|
|
92
|
+
outcome: {
|
|
93
|
+
ok: outcome.ok,
|
|
94
|
+
reason: outcome.reason ?? '',
|
|
95
|
+
ecosystem: outcome.ecosystem ?? null,
|
|
96
|
+
commands: outcome.commands ?? [],
|
|
97
|
+
output: ''
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Capture the baseline for the tree as it stands. The hash is taken FIRST: a
|
|
107
|
+
* health command that writes would otherwise be described by the hash of its own
|
|
108
|
+
* output rather than of the tree the task starts from.
|
|
109
|
+
*/
|
|
110
|
+
export async function captureHealthBaseline(deps) {
|
|
111
|
+
const treeHash = await deps.treeHash();
|
|
112
|
+
try {
|
|
113
|
+
const outcome = await deps.runHealth();
|
|
114
|
+
return { at: (deps.now?.() ?? new Date()).toISOString(), treeHash, outcome };
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
await deps.discardEdits();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function lazyWorktreePath() {
|
|
121
|
+
return path.join(os.tmpdir(), `pi-task-health-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Establish a baseline for a task file that has none, from a DETACHED WORKTREE at
|
|
125
|
+
* HEAD. HEAD is the last commit before the task's uncommitted work, so it is the
|
|
126
|
+
* state the task started from — and checking it out elsewhere means the real index,
|
|
127
|
+
* the real working tree and the stash are all untouched, which no `stash` or
|
|
128
|
+
* `checkout` of the live tree could promise.
|
|
129
|
+
*
|
|
130
|
+
* Null when git cannot produce the worktree; the caller then has no baseline, and
|
|
131
|
+
* `classifyHealthDelta` says what that means.
|
|
132
|
+
*/
|
|
133
|
+
export async function lazyHealthBaseline(deps) {
|
|
134
|
+
const dir = (deps.tmpDir ?? lazyWorktreePath)();
|
|
135
|
+
const added = await deps.git(['worktree', 'add', '--detach', dir, 'HEAD']);
|
|
136
|
+
if (added.exitCode !== 0)
|
|
137
|
+
return null;
|
|
138
|
+
try {
|
|
139
|
+
const outcome = await deps.runHealthIn(dir);
|
|
140
|
+
return {
|
|
141
|
+
at: (deps.now?.() ?? new Date()).toISOString(),
|
|
142
|
+
treeHash: await commitTreeHash(deps.git, 'HEAD'),
|
|
143
|
+
outcome
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
// `--force` because the health run may have written into it (a `--fix`
|
|
148
|
+
// script, a build cache), which plain `worktree remove` refuses. If git
|
|
149
|
+
// still declines, drop the directory and prune the admin record by hand —
|
|
150
|
+
// a stale worktree entry makes every later `worktree add` in this repo
|
|
151
|
+
// noisier, and nothing else will clean it up.
|
|
152
|
+
const removed = await deps.git(['worktree', 'remove', '--force', dir]);
|
|
153
|
+
if (removed.exitCode !== 0) {
|
|
154
|
+
await fsp.rm(dir, { recursive: true, force: true }).catch(() => { });
|
|
155
|
+
await deps.git(['worktree', 'prune']);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
package/dist/task/ledger.d.ts
CHANGED
|
@@ -22,6 +22,16 @@ export interface LedgerSpec<T> {
|
|
|
22
22
|
* writer produces, so it is an option, not a unification.
|
|
23
23
|
*/
|
|
24
24
|
onNoop?: 'rewrite' | 'skip';
|
|
25
|
+
/**
|
|
26
|
+
* What `append` does when an incoming record's key is already stored:
|
|
27
|
+
* 'keep-first' (default) — the stored record wins, so provenance traces to
|
|
28
|
+
* whoever established it first;
|
|
29
|
+
* 'replace' — the incoming record wins and moves to the end of the file, so
|
|
30
|
+
* the freshest statement of a fact is the one that survives the cap.
|
|
31
|
+
* env-notes is the second kind: a re-measured environment fact SUPERSEDES the
|
|
32
|
+
* earlier one, and keeping the first is how a stale fact becomes permanent.
|
|
33
|
+
*/
|
|
34
|
+
conflict?: 'keep-first' | 'replace';
|
|
25
35
|
}
|
|
26
36
|
export interface Ledger<T> {
|
|
27
37
|
/** Absolute path of the ledger file for this cwd. */
|
package/dist/task/ledger.js
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
* requirements, requirements-owned).
|
|
5
5
|
*
|
|
6
6
|
* The ritual is seven steps: read the file (ANY error → ''), parse it into records,
|
|
7
|
-
* key the records,
|
|
8
|
-
* newest MAX (oldest dropped), mkdir the tasks dir, write
|
|
9
|
-
* (`lines.join('\n') + '\n'`, plain `writeFile`, NOT atomic).
|
|
10
|
-
* swallowed — a ledger is a sharpener or an auditing aid, never a
|
|
11
|
-
* phase or gate that calls it.
|
|
7
|
+
* key the records, settle an incoming item whose key is already present (see
|
|
8
|
+
* `conflict`), cap to the newest MAX (oldest dropped), mkdir the tasks dir, write
|
|
9
|
+
* the whole file back (`lines.join('\n') + '\n'`, plain `writeFile`, NOT atomic).
|
|
10
|
+
* Every fault is swallowed — a ledger is a sharpener or an auditing aid, never a
|
|
11
|
+
* blocker of the phase or gate that calls it.
|
|
12
12
|
*
|
|
13
13
|
* What varies per site is the DATA SHAPE (file name, cap, key, line format, parser)
|
|
14
14
|
* and exactly one RULE — what an append does when it adds nothing new (see
|
|
@@ -21,8 +21,9 @@
|
|
|
21
21
|
* • `read` is `parse(readRaw)`; every adapter's parser skips blank lines and lines
|
|
22
22
|
* it cannot read, so a corrupt line is dropped, never thrown on.
|
|
23
23
|
* • `append` with an empty batch is a no-op — it does not even create `.pi-tasks/`.
|
|
24
|
-
*
|
|
25
|
-
* the batch
|
|
24
|
+
* Under the default `conflict` the first item with a key wins within a batch,
|
|
25
|
+
* and a key already stored wins over the batch; under `'replace'` the last
|
|
26
|
+
* statement of a key wins and moves to the end.
|
|
26
27
|
* • `write` overwrites with exactly these records; an empty list writes an empty
|
|
27
28
|
* file (this is how a drained queue and a fully-resolved debt ledger look).
|
|
28
29
|
* • Neither `append` nor `write` ever throws.
|
|
@@ -33,6 +34,7 @@ import { tasksDir } from './task-io.js';
|
|
|
33
34
|
export function makeLedger(spec) {
|
|
34
35
|
const { file, max, key, serialize, parse } = spec;
|
|
35
36
|
const onNoop = spec.onNoop ?? 'rewrite';
|
|
37
|
+
const conflict = spec.conflict ?? 'keep-first';
|
|
36
38
|
const filePath = (cwd) => path.join(tasksDir(cwd), file);
|
|
37
39
|
async function readRaw(cwd) {
|
|
38
40
|
try {
|
|
@@ -57,14 +59,23 @@ export function makeLedger(spec) {
|
|
|
57
59
|
const existing = await read(cwd);
|
|
58
60
|
const seen = new Set(existing.map(key));
|
|
59
61
|
const merged = [...existing];
|
|
62
|
+
let added = 0;
|
|
60
63
|
for (const item of items) {
|
|
61
64
|
const k = key(item);
|
|
62
|
-
if (seen.has(k))
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
if (seen.has(k)) {
|
|
66
|
+
if (conflict === 'keep-first')
|
|
67
|
+
continue;
|
|
68
|
+
const at = merged.findIndex(m => key(m) === k);
|
|
69
|
+
if (at !== -1)
|
|
70
|
+
merged.splice(at, 1);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
seen.add(k);
|
|
74
|
+
added++;
|
|
75
|
+
}
|
|
65
76
|
merged.push(item);
|
|
66
77
|
}
|
|
67
|
-
if (
|
|
78
|
+
if (added === 0 && onNoop === 'skip')
|
|
68
79
|
return;
|
|
69
80
|
const kept = max === undefined ? merged : merged.slice(-max);
|
|
70
81
|
await persist(cwd, kept);
|
package/dist/task/lint-fix.d.ts
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
|
+
import type { SpecContradiction } from './gate-resolution.js';
|
|
1
2
|
export interface LintFixResult {
|
|
2
3
|
/** true → findings fixed, repo health passes, work preserved. */
|
|
3
4
|
ok: boolean;
|
|
5
|
+
/**
|
|
6
|
+
* WHAT this pass proved, as data. `frozen-path` used to travel as a
|
|
7
|
+
* `frozen-path:` prefix on `reason`, which the gate loop recovered by
|
|
8
|
+
* re-typing the literal — a reword of either side silently disarmed the
|
|
9
|
+
* cross-task-contradiction routing.
|
|
10
|
+
*/
|
|
11
|
+
class: 'converged' | 'frozen-path' | 'not-applied';
|
|
4
12
|
/** why the fix was not applied (guard trip, no convergence, child error). */
|
|
5
13
|
reason?: string;
|
|
14
|
+
/** Set exactly on `frozen-path`: the criterion and the frozen path whose edit
|
|
15
|
+
* is the only way to meet it, for the gate's decision table. */
|
|
16
|
+
contradiction?: SpecContradiction;
|
|
6
17
|
}
|
|
7
18
|
export interface LintFixDeps {
|
|
8
19
|
cwd: string;
|