devflow-kit 2.3.0 → 2.5.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/CHANGELOG.md +177 -0
- package/README.md +86 -18
- package/dist/agents/git.md +824 -0
- package/dist/cli/commands/agents.js +6 -1
- package/dist/cli/commands/attribution-prompts.js +144 -0
- package/dist/cli/commands/compliance-prompts.js +7 -11
- package/dist/cli/commands/compliance.js +23 -1
- package/dist/cli/commands/init-seed.js +69 -30
- package/dist/cli/commands/init.js +551 -77
- package/dist/cli/commands/install-report.js +205 -0
- package/dist/cli/commands/knowledge/index.js +2 -2
- package/dist/cli/commands/knowledge/toggle.js +27 -37
- package/dist/cli/commands/learning.js +37 -30
- package/dist/cli/commands/memory.js +79 -69
- package/dist/cli/commands/prompt-io.js +38 -0
- package/dist/cli/commands/proxy.js +91 -31
- package/dist/cli/commands/security.js +76 -16
- package/dist/cli/commands/skills.js +53 -7
- package/dist/cli/commands/tracker-prompts.js +145 -0
- package/dist/cli/commands/tracker.js +405 -0
- package/dist/cli/commands/uninstall.js +233 -77
- package/dist/cli.js +2 -0
- package/dist/commands/bug-analysis.md +22 -4
- package/dist/commands/code-review.md +44 -15
- package/dist/commands/debug.md +20 -6
- package/dist/commands/dynamic-build.md +289 -67
- package/dist/commands/dynamic-plan.md +60 -21
- package/dist/commands/dynamic-profile.md +1 -1
- package/dist/commands/dynamic-tickets.md +58 -8
- package/dist/commands/explore.md +2 -2
- package/dist/commands/implement.md +241 -53
- package/dist/commands/plan.md +88 -17
- package/dist/commands/release.md +64 -17
- package/dist/commands/resolve.md +138 -58
- package/dist/commands/self-review.md +2 -2
- package/dist/core/agent-models.js +55 -12
- package/dist/core/assets.js +58 -2
- package/dist/core/evidence-policy.js +147 -0
- package/dist/core/feature-config.js +130 -64
- package/dist/core/feature-switch.js +112 -0
- package/dist/core/flags.js +149 -8
- package/dist/core/manifest.js +33 -7
- package/dist/core/mds-variants.js +861 -0
- package/dist/core/model-discovery.js +12 -1
- package/dist/core/plugins.js +357 -9
- package/dist/core/project-paths.js +1 -1
- package/dist/core/proxy-log.js +8 -4
- package/dist/core/proxy-state.js +100 -43
- package/dist/core/reference-sweep.js +136 -0
- package/dist/core/tracker.js +407 -0
- package/dist/skills/git/references/decision-markers.md +19 -0
- package/dist/skills/git/references/learn-conventions.md +56 -0
- package/dist/skills/git/references/pr/check-ci-status.md +14 -0
- package/dist/skills/git/references/pr/check-merge-readiness.md +28 -0
- package/dist/skills/git/references/pr/ensure-pr-ready.md +24 -0
- package/dist/skills/git/references/pr/fetch-review-threads.md +22 -0
- package/dist/skills/git/references/pr/post-resolution-summary.md +40 -0
- package/dist/skills/git/references/pr/post-review-summary.md +42 -0
- package/dist/skills/git/references/pr/resolve-review-threads.md +35 -0
- package/dist/skills/git/references/pr/update-pr-evidence.md +14 -0
- package/dist/skills/git/references/pr/validate-branch.md +18 -0
- package/dist/skills/git/references/publication-gate.md +13 -0
- package/dist/skills/git/references/tracker/_mcp.md +153 -0
- package/dist/skills/git/references/tracker/github/associate-release.md +18 -0
- package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +40 -0
- package/dist/skills/git/references/tracker/github/create-release.md +11 -0
- package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +16 -0
- package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +69 -0
- package/dist/skills/git/references/tracker/github/fetch-issue.md +32 -0
- package/dist/skills/git/references/tracker/github/fetch-issues-batch.md +17 -0
- package/dist/skills/git/references/tracker/github/gather-release-evidence.md +19 -0
- package/dist/skills/git/references/tracker/github/manage-debt.md +101 -0
- package/dist/skills/git/references/tracker/github/post-wave-report.md +28 -0
- package/dist/skills/git/references/tracker/github/setup-task.md +26 -0
- package/dist/skills/git/references/tracker/jira/associate-release.md +18 -0
- package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +49 -0
- package/dist/skills/git/references/tracker/jira/create-release.md +17 -0
- package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +22 -0
- package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +53 -0
- package/dist/skills/git/references/tracker/jira/fetch-issue.md +14 -0
- package/dist/skills/git/references/tracker/jira/fetch-issues-batch.md +15 -0
- package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +18 -0
- package/dist/skills/git/references/tracker/jira/manage-debt.md +37 -0
- package/dist/skills/git/references/tracker/jira/post-wave-report.md +33 -0
- package/dist/skills/git/references/tracker/jira/setup-task.md +31 -0
- package/dist/skills/git/references/tracker/linear/associate-release.md +18 -0
- package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +53 -0
- package/dist/skills/git/references/tracker/linear/create-release.md +17 -0
- package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +22 -0
- package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +53 -0
- package/dist/skills/git/references/tracker/linear/fetch-issue.md +14 -0
- package/dist/skills/git/references/tracker/linear/fetch-issues-batch.md +15 -0
- package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +18 -0
- package/dist/skills/git/references/tracker/linear/manage-debt.md +37 -0
- package/dist/skills/git/references/tracker/linear/post-wave-report.md +33 -0
- package/dist/skills/git/references/tracker/linear/setup-task.md +32 -0
- package/dist/skills/git/references/trust-rule.md +7 -0
- package/dist/targets/claude-code/installer.js +1213 -31
- package/dist/targets/claude-code/legacy.js +5 -0
- package/dist/targets/claude-code/post-install.js +303 -104
- package/dist/targets/claude-code/tracker-install.js +161 -0
- package/package.json +4 -3
- package/src/assets/agents/code.md +42 -4
- package/src/assets/agents/design.md +1 -1
- package/src/assets/agents/git.mds +827 -0
- package/src/assets/agents/knowledge.md +1 -1
- package/src/assets/agents/learning.md +11 -0
- package/src/assets/agents/synthesize.md +1 -1
- package/src/assets/agents/test.md +16 -5
- package/src/assets/agents/tracker.md +467 -0
- package/src/assets/agents/validate.md +7 -5
- package/src/assets/commands/_partials/_engine.mds +11 -9
- package/src/assets/commands/_partials/_evidence_policy.mds +30 -0
- package/src/assets/commands/_partials/_knowledge.mds +2 -2
- package/src/assets/commands/_partials/_plan_contract.mds +22 -7
- package/src/assets/commands/_partials/_preamble.mds +1 -1
- package/src/assets/commands/_partials/_publication.mds +3 -1
- package/src/assets/commands/_partials/_ticket_template.mds +3 -2
- package/src/assets/commands/_partials/_tracker.mds +18 -0
- package/src/assets/commands/_partials/_wave.mds +16 -10
- package/src/assets/commands/bug-analysis.mds +15 -5
- package/src/assets/commands/code-review.mds +34 -14
- package/src/assets/commands/debug.mds +11 -4
- package/src/assets/commands/dynamic-build.mds +227 -41
- package/src/assets/commands/dynamic-plan.mds +35 -13
- package/src/assets/commands/dynamic-tickets.mds +47 -5
- package/src/assets/commands/implement.mds +206 -52
- package/src/assets/commands/plan.mds +70 -17
- package/src/assets/commands/release.md +64 -17
- package/src/assets/commands/resolve.mds +126 -56
- package/src/assets/mds/git/_pr.mds +331 -0
- package/src/assets/mds/git/_references.mds +135 -0
- package/src/assets/mds/tracker/_common.mds +156 -0
- package/src/assets/mds/tracker/_github.mds +472 -0
- package/src/assets/mds/tracker/_jira.mds +407 -0
- package/src/assets/mds/tracker/_linear.mds +449 -0
- package/src/assets/mds/tracker/_mcp.mds +299 -0
- package/src/assets/scripts/hooks/assets/orchestrator-charter.md +5 -8
- package/src/assets/scripts/hooks/background-memory-update +14 -9
- package/src/assets/scripts/hooks/capture-prompt +6 -2
- package/src/assets/scripts/hooks/capture-question +6 -2
- package/src/assets/scripts/hooks/capture-turn +6 -2
- package/src/assets/scripts/hooks/ensure-devflow-init +1 -1
- package/src/assets/scripts/hooks/ensure-proxy +64 -5
- package/src/assets/scripts/hooks/ensure-root-gitignore +161 -60
- package/src/assets/scripts/hooks/hook-log-init +3 -1
- package/src/assets/scripts/hooks/json-helper.cjs +223 -5
- package/src/assets/scripts/hooks/lib/project-paths.cjs +1 -1
- package/src/assets/scripts/hooks/memory-worker +15 -8
- package/src/assets/scripts/hooks/pre-compact-memory +12 -8
- package/src/assets/scripts/hooks/preamble +1 -4
- package/src/assets/scripts/hooks/queue-append +68 -24
- package/src/assets/scripts/hooks/session-start-context +355 -8
- package/src/assets/scripts/hooks/session-start-memory +12 -8
- package/src/assets/scripts/pr-evidence.cjs +1961 -0
- package/src/assets/scripts/redact-secrets.cjs +490 -62
- package/src/assets/scripts/release-trace.cjs +1143 -0
- package/src/assets/scripts/resolve-evidence-policy.cjs +1065 -0
- package/src/assets/scripts/verify-evidence.cjs +1822 -0
- package/src/assets/skills/compliance/SKILL.md +2 -0
- package/src/assets/skills/docs-framework/SKILL.md +5 -3
- package/src/assets/skills/git/SKILL.md +8 -82
- package/src/assets/skills/git/references/github-api.md +179 -141
- package/src/assets/skills/git/references/patterns.md +11 -12
- package/src/assets/skills/review-methodology/SKILL.md +1 -1
- package/src/assets/skills/review-methodology/references/patterns.md +6 -61
- package/src/assets/skills/review-methodology/references/violations.md +14 -22
- package/src/targets/claude-code/templates/settings.json +0 -4
- package/src/assets/agents/git.md +0 -938
|
@@ -6,7 +6,9 @@ import * as p from '@clack/prompts';
|
|
|
6
6
|
import color from 'picocolors';
|
|
7
7
|
import { getInstallationPaths, getClaudeDirectory, getManagedSettingsPath } from '../../targets/claude-code/claude-paths.js';
|
|
8
8
|
import { getGitRoot } from '../../core/git.js';
|
|
9
|
-
import { DEVFLOW_PLUGINS, SKILL_NAMESPACE, getAllSkillNames, getAllAgentNames, getAllCommandNames, parsePluginSelection, resolveFeatureRedirect, prefixSkillName, unprefixSkillName, FEATURE_OWNED_SKILLS } from '../../core/plugins.js';
|
|
9
|
+
import { DEVFLOW_PLUGINS, SKILL_NAMESPACE, getAllSkillNames, getAllAgentNames, getAllCommandNames, parsePluginSelection, resolveFeatureRedirect, prefixSkillName, unprefixSkillName, skillsOf, FEATURE_OWNED_SKILLS } from '../../core/plugins.js';
|
|
10
|
+
import { readManifest } from '../../core/manifest.js';
|
|
11
|
+
import { TRACKER_ATTEMPTS_FILE, TRACKER_CLAIM_FILE, TRACKER_ENABLED_FILE } from '../../core/tracker.js';
|
|
10
12
|
import { sweepOrphanedAssets, mdFileName, mdEntryName } from '../../core/orphan-sweep.js';
|
|
11
13
|
import { LEGACY_SKILL_NAMES } from '../../targets/claude-code/legacy.js';
|
|
12
14
|
import { removeAmbientHook } from './ambient.js';
|
|
@@ -15,9 +17,10 @@ import { removeCaptureHooks } from './capture.js';
|
|
|
15
17
|
import { removeDreamHook } from './legacy-hooks.js';
|
|
16
18
|
import { removeHudStatusLine } from './hud.js';
|
|
17
19
|
import { removeContextHook } from './context.js';
|
|
18
|
-
import {
|
|
19
|
-
import { readProxyState,
|
|
20
|
+
import { applyProxyTeardownToSettings } from './proxy.js';
|
|
21
|
+
import { readProxyState, proxyJsonExists } from '../../core/proxy-state.js';
|
|
20
22
|
import { hudCacheDir } from '../../core/cache.js';
|
|
23
|
+
import { TRACKER_CONVENTIONS_FILE, TRACKER_CONVENTIONS_BACKUP_NAMES, TRACKER_STAGED_PREFIX, } from '../../core/tracker.js';
|
|
21
24
|
import { revertExternalAgents } from '../../core/agent-models.js';
|
|
22
25
|
import { detectShell, getProfilePath } from '../../core/safe-delete.js';
|
|
23
26
|
import { isAlreadyInstalled, removeFromProfile } from '../../core/safe-delete-install.js';
|
|
@@ -26,20 +29,51 @@ import { writeFileAtomicExclusive } from '../../core/fs-atomic.js';
|
|
|
26
29
|
import { stripFlags } from '../../core/flags.js';
|
|
27
30
|
import { stripDevflowTeammateModeFromJson } from '../../core/teammate-mode-cleanup.js';
|
|
28
31
|
import { getPackageRoot, isContainedIn } from '../../core/paths.js';
|
|
32
|
+
/**
|
|
33
|
+
* The plugins the manifest records as installed, as registry definitions.
|
|
34
|
+
*
|
|
35
|
+
* Falls back to the whole registry when there is no readable manifest, or when
|
|
36
|
+
* it names nothing this registry still has: that is the pre-manifest and the
|
|
37
|
+
* corrupt-manifest case, and retaining too much is the safe direction for a
|
|
38
|
+
* removal. Names the manifest carries that the registry has since dropped are
|
|
39
|
+
* skipped rather than invented — a definition is what the retained-set
|
|
40
|
+
* arithmetic needs, and there is none for a deleted plugin.
|
|
41
|
+
*/
|
|
42
|
+
export async function resolveInstalledPlugins(devflowDir) {
|
|
43
|
+
const manifest = await readManifest(devflowDir).catch(() => null);
|
|
44
|
+
const names = new Set(manifest?.plugins ?? []);
|
|
45
|
+
if (names.size === 0)
|
|
46
|
+
return DEVFLOW_PLUGINS;
|
|
47
|
+
const resolved = DEVFLOW_PLUGINS.filter(plugin => names.has(plugin.name));
|
|
48
|
+
return resolved.length === 0 ? DEVFLOW_PLUGINS : resolved;
|
|
49
|
+
}
|
|
29
50
|
/**
|
|
30
51
|
* Compute which assets should be removed during selective plugin uninstall.
|
|
31
52
|
* Skills and agents shared by remaining plugins are retained.
|
|
32
53
|
* Rules shared by remaining plugins are also retained.
|
|
54
|
+
*
|
|
55
|
+
* D-RETAIN-FROM-MANIFEST: `installedPlugins` is what the MANIFEST records as
|
|
56
|
+
* installed, not the whole registry. Once skills are plugin-scoped the two stop
|
|
57
|
+
* agreeing, and taking the registry retains assets on behalf of plugins the user
|
|
58
|
+
* never installed — so `devflow uninstall --plugin=X` keeps X's skills alive
|
|
59
|
+
* because some unselected plugin also declares them, and the user is left with
|
|
60
|
+
* exactly the files they asked to remove. Skills are retained across the CLOSURE
|
|
61
|
+
* (`skills ∪ requires`) of the remaining plugins, for the same reason the
|
|
62
|
+
* install set is a closure: a skill another installed plugin merely requires is
|
|
63
|
+
* still a skill it needs.
|
|
64
|
+
*
|
|
65
|
+
* @param installedPlugins - The plugins the manifest records. Callers pass the
|
|
66
|
+
* registry only when there is no manifest to read, and the dry-run and the
|
|
67
|
+
* real removal must always be given the SAME list or they describe different
|
|
68
|
+
* outcomes.
|
|
33
69
|
*/
|
|
34
|
-
export function computeAssetsToRemove(selectedPlugins,
|
|
70
|
+
export function computeAssetsToRemove(selectedPlugins, installedPlugins) {
|
|
35
71
|
const selectedNames = new Set(selectedPlugins.map(p => p.name));
|
|
36
|
-
const remainingPlugins =
|
|
37
|
-
const retainedSkills =
|
|
72
|
+
const remainingPlugins = installedPlugins.filter(p => !selectedNames.has(p.name));
|
|
73
|
+
const retainedSkills = skillsOf(remainingPlugins);
|
|
38
74
|
const retainedAgents = new Set();
|
|
39
75
|
const retainedRules = new Set();
|
|
40
76
|
for (const rp of remainingPlugins) {
|
|
41
|
-
for (const s of rp.skills)
|
|
42
|
-
retainedSkills.add(s);
|
|
43
77
|
for (const a of rp.agents)
|
|
44
78
|
retainedAgents.add(a);
|
|
45
79
|
for (const r of rp.rules)
|
|
@@ -49,11 +83,11 @@ export function computeAssetsToRemove(selectedPlugins, allPlugins) {
|
|
|
49
83
|
const agents = [];
|
|
50
84
|
const commands = [];
|
|
51
85
|
const rules = [];
|
|
86
|
+
for (const skill of skillsOf(selectedPlugins)) {
|
|
87
|
+
if (!retainedSkills.has(skill))
|
|
88
|
+
skills.push(skill);
|
|
89
|
+
}
|
|
52
90
|
for (const plugin of selectedPlugins) {
|
|
53
|
-
for (const skill of plugin.skills) {
|
|
54
|
-
if (!retainedSkills.has(skill))
|
|
55
|
-
skills.push(skill);
|
|
56
|
-
}
|
|
57
91
|
for (const agent of plugin.agents) {
|
|
58
92
|
if (!retainedAgents.has(agent))
|
|
59
93
|
agents.push(agent);
|
|
@@ -183,21 +217,69 @@ export function resolveDevflowDirCleanup(opts) {
|
|
|
183
217
|
return 'prompt';
|
|
184
218
|
}
|
|
185
219
|
/**
|
|
186
|
-
*
|
|
187
|
-
* full cleanup of the directory.
|
|
220
|
+
* Single source of truth for user-authored content under `devflowDir`.
|
|
188
221
|
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
* devflowDir/hud.json — HUD enable/disable preference and display config
|
|
222
|
+
* The counterpart to `installArtifactPaths`: together the two lists describe
|
|
223
|
+
* every Devflow-created entry in `~/.devflow`, and they never overlap (@D8).
|
|
224
|
+
* Membership here means "survives every path but a confirmed full-dir rm";
|
|
225
|
+
* membership there means "removed on decline, cancel, non-interactive and
|
|
226
|
+
* --keep-docs alike".
|
|
195
227
|
*
|
|
196
228
|
* NOT listed here: agent-models.json — reclassified as an INSTALL ARTIFACT (AC-P1-F4).
|
|
197
229
|
* Stale per-agent model overrides silently re-apply to renamed/deleted agents on reinstall,
|
|
198
230
|
* so it must be cleaned up by removeDevFlowInstallArtifacts, not preserved behind a confirm gate.
|
|
199
231
|
*
|
|
200
|
-
*
|
|
232
|
+
* @param devflowDir - Absolute path to ~/.devflow (labels quote the shadow dirs).
|
|
233
|
+
*/
|
|
234
|
+
export function userContentPaths(devflowDir) {
|
|
235
|
+
return [
|
|
236
|
+
// Skill shadow overrides (~/.devflow/skills/{name}/)
|
|
237
|
+
{ relPath: 'skills', isDir: true, label: `skill shadows (${path.join(devflowDir, 'skills')})` },
|
|
238
|
+
// Rule shadow overrides (~/.devflow/rules/{name}.md)
|
|
239
|
+
{ relPath: 'rules', isDir: true, label: `rule shadows (${path.join(devflowDir, 'rules')})` },
|
|
240
|
+
// preference-profile.md — user-curated decision-preference profile
|
|
241
|
+
{ relPath: 'preference-profile.md', label: 'preference-profile.md' },
|
|
242
|
+
// tracker.md — the inferred, hand-editable issue-tracker conventions file.
|
|
243
|
+
//
|
|
244
|
+
// USER CONTENT (OD-15), classified the same way as preference-profile.md above
|
|
245
|
+
// rather than as an install artifact like agent-models.json, because it is
|
|
246
|
+
// inferred ONCE per machine and then hand-editable: absence is the trigger that
|
|
247
|
+
// re-runs inference, so deleting it on every decline/cancel/--keep-docs path
|
|
248
|
+
// would silently discard work the user may have corrected by hand.
|
|
249
|
+
//
|
|
250
|
+
// REVERSAL CONDITION, recorded: this classification is CONDITIONAL on the
|
|
251
|
+
// provider-mismatch guard shipping. agent-models.json was reclassified to an
|
|
252
|
+
// artifact precisely because stale overrides re-apply *silently*; "silently" is
|
|
253
|
+
// the load-bearing word. A stale tracker.md whose frontmatter provider
|
|
254
|
+
// disagrees with the resolved provider produces
|
|
255
|
+
// `TRACEABILITY: DEGRADED (tracker configuration mismatch (conventions file))`
|
|
256
|
+
// and no tracker call — that is what removes the silence, and the reason names
|
|
257
|
+
// THIS file rather than the per-repo override so the user is told which of the
|
|
258
|
+
// two to edit. If that guard is ever dropped,
|
|
259
|
+
// reclassify tracker.md to an install artifact IN THE SAME CHANGE, otherwise a
|
|
260
|
+
// silently-authoritative stale file survives uninstall.
|
|
261
|
+
{ relPath: TRACKER_CONVENTIONS_FILE, label: `${TRACKER_CONVENTIONS_FILE} (issue tracker conventions)` },
|
|
262
|
+
// tracker.md.{provider}.bak — what renameStaleTrackerConventions leaves behind
|
|
263
|
+
// on a provider change. Same inferred content as tracker.md (the user's site
|
|
264
|
+
// and project key), so the same classification: named by the confirm prompt,
|
|
265
|
+
// kept by every artifacts-only pass. The names come from the registry, so a
|
|
266
|
+
// fourth provider is covered the day it lands.
|
|
267
|
+
...TRACKER_CONVENTIONS_BACKUP_NAMES.map(name => ({
|
|
268
|
+
relPath: name,
|
|
269
|
+
label: `${name} (previous issue tracker conventions)`,
|
|
270
|
+
})),
|
|
271
|
+
// learning.json — global learning agent tuning config
|
|
272
|
+
{ relPath: 'learning.json', label: 'learning.json' },
|
|
273
|
+
// hud.json — user HUD enable/disable preference and display config
|
|
274
|
+
{ relPath: 'hud.json', label: 'hud.json (HUD configuration)' },
|
|
275
|
+
];
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Enumerate user-authored content in devflowDir that would be deleted by a
|
|
279
|
+
* full cleanup of the directory.
|
|
280
|
+
*
|
|
281
|
+
* Walks `userContentPaths` and returns the label of every entry that exists on
|
|
282
|
+
* disk — a directory counts only when it is non-empty. Empty array means nothing
|
|
201
283
|
* user-authored is present in the directory.
|
|
202
284
|
*
|
|
203
285
|
* Pure I/O — no side effects, no output, fully testable.
|
|
@@ -207,41 +289,54 @@ export function resolveDevflowDirCleanup(opts) {
|
|
|
207
289
|
*/
|
|
208
290
|
export async function enumerateUserDevFlowContent(devflowDir) {
|
|
209
291
|
const items = [];
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
292
|
+
for (const entry of userContentPaths(devflowDir)) {
|
|
293
|
+
const fullPath = path.join(devflowDir, entry.relPath);
|
|
294
|
+
try {
|
|
295
|
+
if (entry.isDir) {
|
|
296
|
+
const children = await fs.readdir(fullPath);
|
|
297
|
+
if (children.length === 0)
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
await fs.access(fullPath);
|
|
302
|
+
}
|
|
303
|
+
items.push(entry.label);
|
|
215
304
|
}
|
|
305
|
+
catch { /* absent or unreadable */ }
|
|
216
306
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
307
|
+
return items;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Expand `installArtifactPaths` against real disk.
|
|
311
|
+
*
|
|
312
|
+
* Exact entries pass through untouched; a prefix entry becomes one entry per
|
|
313
|
+
* matching direct child of `devflowDir`. This is the ONE place a prefix turns
|
|
314
|
+
* into paths, so the removal loop and the dry-run preview cannot disagree about
|
|
315
|
+
* what a prefix covers. An unreadable or absent `devflowDir` yields the exact
|
|
316
|
+
* entries alone — absent is the ordinary case here, never an error.
|
|
317
|
+
*/
|
|
318
|
+
export async function resolveInstallArtifactPaths(devflowDir) {
|
|
319
|
+
const resolved = [];
|
|
320
|
+
let children = null;
|
|
321
|
+
for (const entry of installArtifactPaths(devflowDir)) {
|
|
322
|
+
if (entry.isPrefix !== true) {
|
|
323
|
+
resolved.push({ relPath: entry.relPath, isDir: entry.isDir });
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (children === null) {
|
|
327
|
+
try {
|
|
328
|
+
children = await fs.readdir(devflowDir);
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
children = [];
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
for (const child of children) {
|
|
335
|
+
if (child.startsWith(entry.relPath))
|
|
336
|
+
resolved.push({ relPath: child, isDir: entry.isDir });
|
|
223
337
|
}
|
|
224
338
|
}
|
|
225
|
-
|
|
226
|
-
// preference-profile.md — user-curated decision-preference profile
|
|
227
|
-
try {
|
|
228
|
-
await fs.access(path.join(devflowDir, 'preference-profile.md'));
|
|
229
|
-
items.push('preference-profile.md');
|
|
230
|
-
}
|
|
231
|
-
catch { /* absent */ }
|
|
232
|
-
// learning.json — global learning agent tuning config
|
|
233
|
-
try {
|
|
234
|
-
await fs.access(path.join(devflowDir, 'learning.json'));
|
|
235
|
-
items.push('learning.json');
|
|
236
|
-
}
|
|
237
|
-
catch { /* absent */ }
|
|
238
|
-
// hud.json — user HUD enable/disable preference and display config
|
|
239
|
-
try {
|
|
240
|
-
await fs.access(path.join(devflowDir, 'hud.json'));
|
|
241
|
-
items.push('hud.json (HUD configuration)');
|
|
242
|
-
}
|
|
243
|
-
catch { /* absent */ }
|
|
244
|
-
return items;
|
|
339
|
+
return resolved;
|
|
245
340
|
}
|
|
246
341
|
/**
|
|
247
342
|
* Single source of truth for Devflow-owned install artifacts under `devflowDir`.
|
|
@@ -252,9 +347,18 @@ export async function enumerateUserDevFlowContent(devflowDir) {
|
|
|
252
347
|
* lets callers (dry-run display, tests) enumerate the artifact set without
|
|
253
348
|
* duplicating the list.
|
|
254
349
|
*
|
|
255
|
-
* @D8 Nothing returned here may overlap with the
|
|
256
|
-
*
|
|
257
|
-
*
|
|
350
|
+
* @D8 Nothing returned here may overlap with `userContentPaths` — the two lists
|
|
351
|
+
* are the whole of what Devflow puts in `~/.devflow`, and a name in both is
|
|
352
|
+
* deleted whatever the user answers to the full-wipe prompt. Checked two ways:
|
|
353
|
+
* mechanically, by intersecting the `relPath` sets of the two functions and
|
|
354
|
+
* checking no user path falls UNDER a prefix entry (so an entry added to either
|
|
355
|
+
* list is covered the day it lands), and behaviourally, by test 9f — every
|
|
356
|
+
* enumerated user item survives an artifact-only removal.
|
|
357
|
+
*
|
|
358
|
+
* Entries marked `isPrefix` name a FAMILY of per-invocation basenames rather
|
|
359
|
+
* than one path; `resolveInstallArtifactPaths` is what turns them into paths.
|
|
360
|
+
* Every consumer goes through that resolver, so a prefix entry is never treated
|
|
361
|
+
* as a literal filename.
|
|
258
362
|
*
|
|
259
363
|
* @param devflowDir - Absolute path to ~/.devflow (used to resolve cache dir).
|
|
260
364
|
*/
|
|
@@ -274,6 +378,20 @@ export function installArtifactPaths(devflowDir) {
|
|
|
274
378
|
{ relPath: 'proxy-routing.json' },
|
|
275
379
|
{ relPath: 'proxy.pid' },
|
|
276
380
|
{ relPath: '.proxy-spawn.lock', isDir: true },
|
|
381
|
+
// tracker runtime artifacts — the Tracker agent's atomic claim file, its
|
|
382
|
+
// inference attempt counter, and the provider presence sentinel the
|
|
383
|
+
// SessionStart hook stats. All three are machine state with no user-authored
|
|
384
|
+
// content, so they go on this list; `tracker.md` beside them and the
|
|
385
|
+
// `tracker.md.{provider}.bak` a provider change leaves are USER CONTENT
|
|
386
|
+
// (OD-15) and are deliberately NOT here (@D8: the two lists stay disjoint).
|
|
387
|
+
{ relPath: TRACKER_CLAIM_FILE },
|
|
388
|
+
{ relPath: TRACKER_ATTEMPTS_FILE },
|
|
389
|
+
{ relPath: TRACKER_ENABLED_FILE },
|
|
390
|
+
// The agent's scrubbed staging file, one per invocation under a mktemp name
|
|
391
|
+
// it removes from a trap — a SIGKILL outruns the trap and leaves it behind.
|
|
392
|
+
// A prefix, because the names exist only on disk. Content is a scrubbed copy
|
|
393
|
+
// that was never placed, so it is machine state like the three above.
|
|
394
|
+
{ relPath: TRACKER_STAGED_PREFIX, isPrefix: true },
|
|
277
395
|
// per-project hook logs (logs/{project-slug}/) AND global logs — remove the
|
|
278
396
|
// whole logs/ tree; covers proxy.log, debug logs, and any project-slug dirs.
|
|
279
397
|
{ relPath: 'logs', isDir: true },
|
|
@@ -295,7 +413,8 @@ export function installArtifactPaths(devflowDir) {
|
|
|
295
413
|
* @D8 Nothing enumerated by enumerateUserDevFlowContent may appear in this list.
|
|
296
414
|
* This function runs on the decline, cancel, non-interactive AND --keep-docs paths,
|
|
297
415
|
* so an entry here is deleted even when the user answers "no" to the full wipe.
|
|
298
|
-
* User-authored state (skill/rule shadows,
|
|
416
|
+
* User-authored state (everything in `userContentPaths`: skill/rule shadows,
|
|
417
|
+
* preference-profile.md, tracker.md and its provider backups, learning.json,
|
|
299
418
|
* hud.json) is removed only by the confirmed full-dir rm.
|
|
300
419
|
* agent-models.json is an INSTALL ARTIFACT (stale per-agent overrides silently
|
|
301
420
|
* re-apply to renamed/deleted agents on reinstall — AC-P1-F4) and therefore
|
|
@@ -327,8 +446,10 @@ export async function removeDevFlowInstallArtifacts(devflowDir, verbose) {
|
|
|
327
446
|
}
|
|
328
447
|
}
|
|
329
448
|
catch { /* proxy.pid absent or unreadable — non-fatal */ }
|
|
330
|
-
// All install artifacts removed non-fatally (avoids PF-009).
|
|
331
|
-
|
|
449
|
+
// All install artifacts removed non-fatally (avoids PF-009). Resolved against
|
|
450
|
+
// disk first, so a per-run staging basename is a real path by the time the
|
|
451
|
+
// containment guard below sees it.
|
|
452
|
+
for (const artifact of await resolveInstallArtifactPaths(devflowDir)) {
|
|
332
453
|
const fullPath = path.join(devflowDir, artifact.relPath);
|
|
333
454
|
// Containment invariant: every artifact must resolve to a path STRICTLY inside
|
|
334
455
|
// devflowDir. A derived relPath that ever collapsed to '' or '..' would turn the
|
|
@@ -456,7 +577,7 @@ export async function enumerateDryRunExtras(claudeDir, devflowDir) {
|
|
|
456
577
|
// Guard with fs.access so files that never existed don't pollute the preview.
|
|
457
578
|
// (F7: previously pushed unconditionally, inflating the dry-run list with
|
|
458
579
|
// paths that were never on disk.)
|
|
459
|
-
for (const artifact of
|
|
580
|
+
for (const artifact of await resolveInstallArtifactPaths(devflowDir)) {
|
|
460
581
|
const fullPath = path.join(devflowDir, artifact.relPath);
|
|
461
582
|
try {
|
|
462
583
|
await fs.access(fullPath);
|
|
@@ -476,13 +597,18 @@ export async function enumerateDryRunExtras(claudeDir, devflowDir) {
|
|
|
476
597
|
* @param opts.scopesToUninstall - Scopes detected in the setup phase.
|
|
477
598
|
* @param opts.isSelectiveUninstall - true when --plugin was given.
|
|
478
599
|
* @param opts.selectedPlugins - The plugin subset for selective mode.
|
|
600
|
+
* @param opts.installedPlugins - What the manifest records as installed. The
|
|
601
|
+
* dry-run and the real removal MUST receive the same list, or the preview
|
|
602
|
+
* describes an outcome the removal does not produce.
|
|
479
603
|
*/
|
|
480
604
|
export async function runDryRunPhase(opts) {
|
|
481
|
-
const { scopesToUninstall, isSelectiveUninstall, selectedPlugins } = opts;
|
|
605
|
+
const { scopesToUninstall, isSelectiveUninstall, selectedPlugins, installedPlugins } = opts;
|
|
482
606
|
p.log.info(`Scope(s): ${[...scopesToUninstall].join(', ')} (dry-run shows all detected scopes)`);
|
|
483
607
|
if (isSelectiveUninstall) {
|
|
484
|
-
// Selective:
|
|
485
|
-
|
|
608
|
+
// Selective: computed against the INSTALLED list, the same argument the real
|
|
609
|
+
// removal is given below — a preview computed from a different list is a
|
|
610
|
+
// preview of a different uninstall.
|
|
611
|
+
const assets = computeAssetsToRemove(selectedPlugins, installedPlugins);
|
|
486
612
|
const plan = formatDryRunPlan(assets);
|
|
487
613
|
for (const line of plan.split('\n')) {
|
|
488
614
|
p.log.info(line);
|
|
@@ -527,6 +653,7 @@ export async function runDryRunPhase(opts) {
|
|
|
527
653
|
*/
|
|
528
654
|
export async function runSelectivePhaseForScope(opts) {
|
|
529
655
|
const { claudeDir, devflowDir, selectedPlugins, verbose } = opts;
|
|
656
|
+
const installedPlugins = opts.installedPlugins ?? DEVFLOW_PLUGINS;
|
|
530
657
|
// Revert GPT agent frontmatter BEFORE removing agent files — strips GPT model
|
|
531
658
|
// lines from installed agent frontmatter while the files are still present.
|
|
532
659
|
// Non-fatal: tolerate missing agents dir or revert errors.
|
|
@@ -543,7 +670,7 @@ export async function runSelectivePhaseForScope(opts) {
|
|
|
543
670
|
}
|
|
544
671
|
catch { /* agents dir absent or revert failed — non-fatal */ }
|
|
545
672
|
}
|
|
546
|
-
await removeSelectedPlugins(claudeDir, selectedPlugins, verbose);
|
|
673
|
+
await removeSelectedPlugins(claudeDir, selectedPlugins, verbose, installedPlugins);
|
|
547
674
|
// Clean up ambient hook if ambient plugin is being removed
|
|
548
675
|
if (selectedPlugins.some(sp => sp.name === 'devflow-ambient')) {
|
|
549
676
|
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
@@ -768,14 +895,16 @@ export async function runCleanupPhase(opts) {
|
|
|
768
895
|
settingsContent = stripFlags(settingsContent); // also strips viewMode via view-mode registry entry
|
|
769
896
|
settingsContent = stripDevflowTeammateModeFromJson(settingsContent);
|
|
770
897
|
// Remove proxy hooks and ANTHROPIC_BASE_URL env in a single parse-mutate-serialize pass.
|
|
771
|
-
//
|
|
772
|
-
//
|
|
773
|
-
//
|
|
774
|
-
//
|
|
898
|
+
// D-STRIP-1: an absent entry in managedProxyPorts means proxy.json did not exist
|
|
899
|
+
// before artifact removal — no evidence Devflow ever wrote the env vars, so
|
|
900
|
+
// applyProxyTeardownToSettings removes only our hooks and leaves the user's
|
|
901
|
+
// ANTHROPIC_BASE_URL / CLAUDE_CODE_DISABLE_UNKNOWN_MODEL_WINDOW_ENFORCEMENT alone.
|
|
902
|
+
// Hook removal is never gated: our hooks must not survive an uninstall.
|
|
903
|
+
// REG-1: the port-scoped URL strip protects any foreign localhost gateway on a
|
|
904
|
+
// different port — the managedPort value is from the pre-captured proxy.json port.
|
|
775
905
|
{
|
|
776
|
-
const managedPort = opts.managedProxyPorts?.get(scope) ?? DEFAULT_PROXY_PORT;
|
|
777
906
|
const parsedSettings = JSON.parse(settingsContent);
|
|
778
|
-
|
|
907
|
+
applyProxyTeardownToSettings(parsedSettings, opts.managedProxyPorts?.get(scope));
|
|
779
908
|
settingsContent = JSON.stringify(parsedSettings, null, 2) + '\n';
|
|
780
909
|
}
|
|
781
910
|
if (settingsContent !== originalContent) {
|
|
@@ -970,23 +1099,44 @@ export const uninstallCommand = new Command('uninstall')
|
|
|
970
1099
|
}
|
|
971
1100
|
// === DRY RUN: show plan and exit ===
|
|
972
1101
|
if (dryRun) {
|
|
973
|
-
|
|
1102
|
+
// One resolution, handed to the dry-run and (below) to the real removal,
|
|
1103
|
+
// so the preview and the outcome are computed from the same list.
|
|
1104
|
+
let dryRunInstalled = DEVFLOW_PLUGINS;
|
|
1105
|
+
try {
|
|
1106
|
+
const paths = await getInstallationPaths(scopesToUninstall[0]);
|
|
1107
|
+
dryRunInstalled = await resolveInstalledPlugins(paths.devflowDir);
|
|
1108
|
+
}
|
|
1109
|
+
catch { /* scope path resolution failed — fall back to the registry */ }
|
|
1110
|
+
await runDryRunPhase({
|
|
1111
|
+
scopesToUninstall,
|
|
1112
|
+
isSelectiveUninstall,
|
|
1113
|
+
selectedPlugins,
|
|
1114
|
+
installedPlugins: dryRunInstalled,
|
|
1115
|
+
});
|
|
974
1116
|
p.outro(color.dim('No changes made (dry run)'));
|
|
975
1117
|
return;
|
|
976
1118
|
}
|
|
977
1119
|
// Pre-capture managed proxy ports for each scope BEFORE artifact removal.
|
|
978
1120
|
// proxy.json is deleted by removeDevFlowInstallArtifacts (inside runFullPhaseForScope),
|
|
979
1121
|
// so reading it after the full phase would always yield DEFAULT_PROXY_PORT. (F6)
|
|
1122
|
+
//
|
|
1123
|
+
// D-STRIP-1: only populate managedProxyPorts when proxy.json exists — evidence that
|
|
1124
|
+
// Devflow has managed the proxy on this machine. runCleanupPhase gates the
|
|
1125
|
+
// applyProxyTeardownToSettings call on the port being present in the map, so when the file
|
|
1126
|
+
// is absent (never managed), no env strip is attempted and a user's own gateway vars
|
|
1127
|
+
// are left untouched.
|
|
980
1128
|
const managedProxyPorts = new Map();
|
|
981
1129
|
if (!isSelectiveUninstall) {
|
|
982
1130
|
for (const scope of scopesToUninstall) {
|
|
983
1131
|
try {
|
|
984
1132
|
const paths = await getInstallationPaths(scope);
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1133
|
+
if (await proxyJsonExists(paths.devflowDir)) {
|
|
1134
|
+
const proxyState = await readProxyState(paths.devflowDir);
|
|
1135
|
+
if (proxyState.ok)
|
|
1136
|
+
managedProxyPorts.set(scope, proxyState.value.port);
|
|
1137
|
+
}
|
|
988
1138
|
}
|
|
989
|
-
catch { /* non-fatal
|
|
1139
|
+
catch { /* non-fatal */ }
|
|
990
1140
|
}
|
|
991
1141
|
}
|
|
992
1142
|
// Uninstall from each scope
|
|
@@ -1011,7 +1161,13 @@ export const uninstallCommand = new Command('uninstall')
|
|
|
1011
1161
|
continue;
|
|
1012
1162
|
}
|
|
1013
1163
|
if (isSelectiveUninstall) {
|
|
1014
|
-
await runSelectivePhaseForScope({
|
|
1164
|
+
await runSelectivePhaseForScope({
|
|
1165
|
+
claudeDir,
|
|
1166
|
+
devflowDir,
|
|
1167
|
+
selectedPlugins,
|
|
1168
|
+
verbose,
|
|
1169
|
+
installedPlugins: await resolveInstalledPlugins(devflowDir),
|
|
1170
|
+
});
|
|
1015
1171
|
}
|
|
1016
1172
|
else {
|
|
1017
1173
|
await runFullPhaseForScope({ scope, claudeDir, devflowDir, devflowScriptsDir, verbose, keepDocs: !!options.keepDocs, isTTY: !!process.stdin.isTTY });
|
|
@@ -1151,8 +1307,8 @@ export async function sweepDevflowNamespaces(claudeDir, verbose) {
|
|
|
1151
1307
|
* registry (retired agents/commands survive indefinitely without the sweep).
|
|
1152
1308
|
* For skills: only remove skills that are NOT used by any remaining plugin.
|
|
1153
1309
|
*/
|
|
1154
|
-
export async function removeSelectedPlugins(claudeDir, plugins, verbose) {
|
|
1155
|
-
const { skills, agents, commands, rules } = computeAssetsToRemove(plugins,
|
|
1310
|
+
export async function removeSelectedPlugins(claudeDir, plugins, verbose, installedPlugins = DEVFLOW_PLUGINS) {
|
|
1311
|
+
const { skills, agents, commands, rules } = computeAssetsToRemove(plugins, installedPlugins);
|
|
1156
1312
|
const commandsDir = path.join(claudeDir, 'commands', 'devflow');
|
|
1157
1313
|
for (const cmd of commands) {
|
|
1158
1314
|
const cmdFileName = mdFileName(cmd.replace(/^\//, ''));
|
package/dist/cli.js
CHANGED
|
@@ -19,6 +19,7 @@ import { safeDeleteCommand } from './cli/commands/safe-delete.js';
|
|
|
19
19
|
import { proxyCommand } from './cli/commands/proxy.js';
|
|
20
20
|
import { agentsCommand } from './cli/commands/agents.js';
|
|
21
21
|
import { complianceCommand } from './cli/commands/compliance.js';
|
|
22
|
+
import { trackerCommand } from './cli/commands/tracker.js';
|
|
22
23
|
const __filename = fileURLToPath(import.meta.url);
|
|
23
24
|
const __dirname = dirname(__filename);
|
|
24
25
|
// Read version from package.json
|
|
@@ -47,6 +48,7 @@ program.addCommand(safeDeleteCommand);
|
|
|
47
48
|
program.addCommand(proxyCommand);
|
|
48
49
|
program.addCommand(agentsCommand);
|
|
49
50
|
program.addCommand(complianceCommand);
|
|
51
|
+
program.addCommand(trackerCommand);
|
|
50
52
|
// Handle no command (bare `devflow`) or unknown subcommand.
|
|
51
53
|
// When Commander sees an unrecognised first argument it does not route to any
|
|
52
54
|
// registered subcommand; instead the root action fires with that argument
|
|
@@ -17,17 +17,35 @@ Run a proactive bug analysis on the current branch by combining static analysis
|
|
|
17
17
|
|
|
18
18
|
### Phase 1: Pre-flight
|
|
19
19
|
|
|
20
|
-
**Produces:** BRANCH_INFO, PR_DESCRIPTION,
|
|
20
|
+
**Produces:** BRANCH_INFO, PR_DESCRIPTION, EVIDENCE_POLICY, ISSUE_REQUIRED, APPLY_CONVENTIONS, REQUIRE_NON_AUTHOR_APPROVAL
|
|
21
21
|
|
|
22
|
-
**Resolve
|
|
23
|
-
|
|
22
|
+
**Resolve the evidence policy once per run**, from the repository root, before any step reads the values:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
node "${DEVFLOW_DIR:-$HOME/.devflow}/scripts/resolve-evidence-policy.cjs" 2>/dev/null; echo "exit=$?"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Accept the output only when it is exactly two lines: `exit=0` last and, before it, one line of the form `EVIDENCE_POLICY=<required|standard> SOURCE=<file|worktree|default|invalid|error> REF=<branch|none>[ WARN=<remote-unavailable|invalid-file|raised-by-compliance|pr-changes-policy>[,…]] ISSUE_REQUIRED=<true|false> APPLY_CONVENTIONS=<true|false> REQUIRE_NON_AUTHOR_APPROVAL=<true|false>` — these fields, in this order, nothing else, where `<branch>` is a branch name such as `main`. **Anything else** (a non-zero exit, no line, extra text, or a missing, reordered or unlisted field or value) ⇒ use `EVIDENCE_POLICY=required SOURCE=error REF=none ISSUE_REQUIRED=true APPLY_CONVENTIONS=true REQUIRE_NON_AUTHOR_APPROVAL=true` instead.
|
|
29
|
+
|
|
30
|
+
Set `EVIDENCE_POLICY`, `ISSUE_REQUIRED`, `APPLY_CONVENTIONS` and `REQUIRE_NON_AUTHOR_APPROVAL` from the accepted line. Pass agents only the three mechanism inputs, never `EVIDENCE_POLICY`. Report `Evidence policy: {EVIDENCE_POLICY} (source: {SOURCE})`, plus any `WARN` tokens as advisory, once in the final report.
|
|
31
|
+
|
|
32
|
+
Render the test-plan block from `/implement`'s evidence file, and from nothing else:
|
|
33
|
+
1. `branch_slug` is `git branch --show-current` with every `/` replaced by `-`.
|
|
34
|
+
2. Only when `branch_slug` matches `^[A-Za-z0-9._-]{1,200}$` and the file exists, run (the path double-quoted):
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
node "${DEVFLOW_DIR:-$HOME/.devflow}/scripts/verify-evidence.cjs" render --plan ".devflow/docs/evidence-{branch_slug}.md"; echo "exit=$?"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
3. On `exit=0`, `PR_TEST_PLAN_BLOCK` is its stdout byte for byte without that `exit=` line; in every other case it is `(none)`. Nothing here waits on it: the Git agent pastes it only behind its own check, and only into a PR it creates.
|
|
24
41
|
|
|
25
42
|
Spawn Git agent:
|
|
26
43
|
|
|
27
44
|
```
|
|
28
45
|
Agent(subagent_type="Git", run_in_background=false):
|
|
29
46
|
"OPERATION: ensure-pr-ready
|
|
30
|
-
|
|
47
|
+
PR_TEST_PLAN_BLOCK: {PR_TEST_PLAN_BLOCK verbatim, or (none)}
|
|
48
|
+
APPLY_CONVENTIONS: {APPLY_CONVENTIONS}
|
|
31
49
|
Validate branch, commit if needed, push, create PR if needed.
|
|
32
50
|
Return: branch, base_branch, branch-slug, PR#"
|
|
33
51
|
```
|