pan-wizard 3.21.1 → 3.24.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 +38 -24
- package/agents/pan-conductor.md +3 -3
- package/agents/pan-design-checker.md +83 -0
- package/agents/pan-designer.md +67 -0
- package/agents/pan-document_code.md +3 -2
- package/agents/pan-executor.md +2 -2
- package/agents/pan-plan-checker.md +19 -6
- package/agents/pan-planner.md +4 -1
- package/agents/pan-previewer.md +1 -1
- package/agents/pan-roadmapper.md +3 -1
- package/agents/pan-verifier.md +10 -10
- package/bin/install-lib.cjs +128 -17
- package/bin/install.js +480 -74
- package/commands/pan/army.md +20 -14
- package/commands/pan/audit-deployment.md +17 -14
- package/commands/pan/cost.md +2 -2
- package/commands/pan/debug.md +1 -1
- package/commands/pan/design-phase.md +77 -0
- package/commands/pan/exec-phase.md +2 -2
- package/commands/pan/experiment.md +18 -14
- package/commands/pan/focus-auto.md +5 -5
- package/commands/pan/focus-design.md +13 -1
- package/commands/pan/focus-exec.md +1 -1
- package/commands/pan/learn.md +1 -1
- package/commands/pan/links.md +3 -1
- package/commands/pan/map-codebase.md +4 -4
- package/commands/pan/mcp-bridge.md +1 -1
- package/commands/pan/milestone-new.md +3 -1
- package/commands/pan/optimize.md +2 -2
- package/commands/pan/patches.md +1 -1
- package/commands/pan/plan-phase.md +3 -1
- package/commands/pan/preview.md +3 -3
- package/commands/pan/profile.md +3 -3
- package/commands/pan/research-phase.md +1 -1
- package/commands/pan/retro.md +4 -1
- package/commands/pan/review-deep.md +1 -1
- package/commands/pan/settings.md +2 -2
- package/commands/pan/what-if.md +1 -1
- package/hooks/dist/pan-check-update.js +149 -38
- package/hooks/dist/pan-context-monitor.js +155 -81
- package/hooks/dist/pan-cost-logger.js +288 -13
- package/hooks/dist/pan-statusline.js +24 -8
- package/hooks/dist/pan-stop-guard.js +160 -0
- package/hooks/dist/pan-trace-logger.js +261 -19
- package/package.json +2 -2
- package/pan-wizard-core/bin/lib/bridge.cjs +1 -1
- package/pan-wizard-core/bin/lib/bus.cjs +19 -4
- package/pan-wizard-core/bin/lib/campaign.cjs +8 -3
- package/pan-wizard-core/bin/lib/codebase.cjs +15 -5
- package/pan-wizard-core/bin/lib/commands.cjs +88 -6
- package/pan-wizard-core/bin/lib/config.cjs +27 -2
- package/pan-wizard-core/bin/lib/constants.cjs +1 -0
- package/pan-wizard-core/bin/lib/core.cjs +113 -8
- package/pan-wizard-core/bin/lib/cost.cjs +61 -24
- package/pan-wizard-core/bin/lib/distill.cjs +22 -4
- package/pan-wizard-core/bin/lib/doc-lint.cjs +25 -7
- package/pan-wizard-core/bin/lib/experiment.cjs +25 -1
- package/pan-wizard-core/bin/lib/focus.cjs +42 -7
- package/pan-wizard-core/bin/lib/frontmatter.cjs +62 -31
- package/pan-wizard-core/bin/lib/git.cjs +51 -16
- package/pan-wizard-core/bin/lib/hud.cjs +17 -2
- package/pan-wizard-core/bin/lib/init.cjs +2 -5
- package/pan-wizard-core/bin/lib/knowledge.cjs +20 -1
- package/pan-wizard-core/bin/lib/learn-lint.cjs +50 -0
- package/pan-wizard-core/bin/lib/links.cjs +9 -6
- package/pan-wizard-core/bin/lib/lock.cjs +23 -4
- package/pan-wizard-core/bin/lib/memory-optimize.cjs +101 -17
- package/pan-wizard-core/bin/lib/memory-rebuild.cjs +26 -2
- package/pan-wizard-core/bin/lib/milestone.cjs +11 -5
- package/pan-wizard-core/bin/lib/optimize.cjs +18 -7
- package/pan-wizard-core/bin/lib/phase-remove.cjs +41 -2
- package/pan-wizard-core/bin/lib/phase.cjs +82 -11
- package/pan-wizard-core/bin/lib/preview.cjs +23 -4
- package/pan-wizard-core/bin/lib/review-deep.cjs +14 -6
- package/pan-wizard-core/bin/lib/roadmap.cjs +6 -2
- package/pan-wizard-core/bin/lib/runner.cjs +17 -7
- package/pan-wizard-core/bin/lib/squads.cjs +43 -11
- package/pan-wizard-core/bin/lib/state.cjs +32 -11
- package/pan-wizard-core/bin/lib/template.cjs +8 -3
- package/pan-wizard-core/bin/lib/verify-deploy.cjs +14 -2
- package/pan-wizard-core/bin/lib/verify-drift.cjs +15 -1
- package/pan-wizard-core/bin/lib/verify-preflight.cjs +9 -3
- package/pan-wizard-core/bin/lib/verify.cjs +84 -39
- package/pan-wizard-core/bin/lib/whatif.cjs +8 -1
- package/pan-wizard-core/bin/pan-tools.cjs +42 -19
- package/pan-wizard-core/learnings/index.json +7 -7
- package/pan-wizard-core/learnings/universal/autonomous-loop.md +5 -3
- package/pan-wizard-core/learnings/universal/concurrency.md +1 -1
- package/pan-wizard-core/references/design-methodology.md +94 -0
- package/pan-wizard-core/references/git-integration.md +9 -9
- package/pan-wizard-core/references/guardrails.md +4 -1
- package/pan-wizard-core/references/model-profile-resolution.md +1 -1
- package/pan-wizard-core/references/model-profiles.md +44 -34
- package/pan-wizard-core/references/planning-config.md +5 -5
- package/pan-wizard-core/references/verification-patterns.md +67 -0
- package/pan-wizard-core/templates/codebase/architecture.md +1 -1
- package/pan-wizard-core/templates/codebase/concerns.md +1 -1
- package/pan-wizard-core/templates/codebase/conventions.md +1 -1
- package/pan-wizard-core/templates/codebase/integrations.md +1 -1
- package/pan-wizard-core/templates/codebase/stack.md +1 -1
- package/pan-wizard-core/templates/codebase/structure.md +1 -1
- package/pan-wizard-core/templates/codebase/testing.md +1 -1
- package/pan-wizard-core/templates/design.md +146 -0
- package/pan-wizard-core/templates/discovery.md +3 -3
- package/pan-wizard-core/templates/milestone-archive.md +2 -2
- package/pan-wizard-core/templates/playbook.md +1 -1
- package/pan-wizard-core/templates/uat.md +1 -1
- package/pan-wizard-core/workflows/diagnose-issues.md +9 -7
- package/pan-wizard-core/workflows/exec-phase.md +28 -8
- package/pan-wizard-core/workflows/execute-plan.md +2 -3
- package/pan-wizard-core/workflows/health.md +24 -1
- package/pan-wizard-core/workflows/help.md +5 -4
- package/pan-wizard-core/workflows/learn.md +5 -5
- package/pan-wizard-core/workflows/map-codebase.md +1 -1
- package/pan-wizard-core/workflows/milestone-audit.md +1 -1
- package/pan-wizard-core/workflows/milestone-new.md +4 -4
- package/pan-wizard-core/workflows/new-project.md +47 -32
- package/pan-wizard-core/workflows/optimize.md +8 -8
- package/pan-wizard-core/workflows/pause.md +1 -1
- package/pan-wizard-core/workflows/phase-tests.md +2 -2
- package/pan-wizard-core/workflows/plan-phase.md +4 -0
- package/pan-wizard-core/workflows/profile.md +13 -10
- package/pan-wizard-core/workflows/research-phase.md +1 -1
- package/pan-wizard-core/workflows/resume-project.md +1 -1
- package/pan-wizard-core/workflows/settings.md +9 -9
- package/pan-wizard-core/workflows/transition.md +24 -8
- package/pan-wizard-core/workflows/update.md +23 -20
- package/pan-wizard-core/workflows/verify-phase.md +7 -2
- package/pan-zcode/KNOWN-BETA-RISKS.md +8 -0
- package/pan-zcode/README.md +14 -2
- package/pan-zcode/bin/install-zcode.js +1 -1
- package/pan-zcode/mcp/native-tools.cjs +1 -1
- package/pan-zcode/mcp/server.cjs +76 -14
- package/scripts/build-hooks.js +2 -1
- package/scripts/generate-skills-docs.py +0 -4
- package/scripts/run-tests.cjs +11 -4
|
@@ -21,9 +21,21 @@ const SCANNABLE_EXTS = ['.js', '.cjs', '.mjs', '.ts', '.tsx', '.jsx', '.py'];
|
|
|
21
21
|
|
|
22
22
|
// ─── Pass 1: Deterministic static analysis ───────────────────────────────────
|
|
23
23
|
|
|
24
|
+
// Calls that genuinely CANNOT throw when given any single argument, so a
|
|
25
|
+
// try/catch wrapping only such a call is truly phantom (dead catch clause).
|
|
26
|
+
// Deliberately EXCLUDES throwing calls — JSON.parse (SyntaxError on bad input),
|
|
27
|
+
// JSON.stringify (TypeError on circular refs / BigInt), new RegExp, fs.*Sync,
|
|
28
|
+
// etc. — because their try/catch is load-bearing and must not be flagged for
|
|
29
|
+
// removal. (M14: earlier the pattern listed JSON.parse/JSON.stringify and
|
|
30
|
+
// wrongly flagged their guards as phantom.)
|
|
31
|
+
const NON_THROWING_CALLS = ['Number', 'String', 'Boolean', 'parseInt', 'parseFloat'];
|
|
32
|
+
|
|
24
33
|
function findPhantomTryCatch(content, filePath) {
|
|
25
34
|
const findings = [];
|
|
26
|
-
const re =
|
|
35
|
+
const re = new RegExp(
|
|
36
|
+
'try\\s*\\{\\s*(?:return\\s+)?(' + NON_THROWING_CALLS.join('|') + ')\\([^)]*\\)\\s*;?\\s*\\}\\s*catch',
|
|
37
|
+
'g'
|
|
38
|
+
);
|
|
27
39
|
let match;
|
|
28
40
|
while ((match = re.exec(content)) !== null) {
|
|
29
41
|
const line = content.slice(0, match.index).split('\n').length;
|
|
@@ -372,6 +384,12 @@ function readPatternsMemory(cwd) {
|
|
|
372
384
|
return { patterns, file: filePath };
|
|
373
385
|
}
|
|
374
386
|
|
|
387
|
+
// M15: the patterns-memory file (distill-patterns.md) records DETECTIONS, not
|
|
388
|
+
// resolutions — there is no resolution/fix marker in the entry schema
|
|
389
|
+
// ({ date, pattern, file, note }). So a prior entry only proves the pattern was
|
|
390
|
+
// SEEN before, not that it was ever fixed. Labeling a re-detection "REGRESSED —
|
|
391
|
+
// last resolved <date>" was a false claim. We now report the honest signal:
|
|
392
|
+
// the pattern is RECURRING (still present across sessions / previously detected).
|
|
375
393
|
function detectRegressedPatterns(currentFindings, memory) {
|
|
376
394
|
const regressed = [];
|
|
377
395
|
for (const f of currentFindings) {
|
|
@@ -380,8 +398,8 @@ function detectRegressedPatterns(currentFindings, memory) {
|
|
|
380
398
|
regressed.push({
|
|
381
399
|
...f,
|
|
382
400
|
regressed: true,
|
|
383
|
-
|
|
384
|
-
message: f.message + ` (
|
|
401
|
+
previously_detected: prior.date,
|
|
402
|
+
message: f.message + ` (RECURRING — previously detected ${prior.date})`,
|
|
385
403
|
});
|
|
386
404
|
}
|
|
387
405
|
}
|
|
@@ -426,7 +444,7 @@ function writePatternsMemory(cwd, findings, opts) {
|
|
|
426
444
|
fs.writeFileSync(filePath, lines.join('\n'), 'utf-8');
|
|
427
445
|
return { written: true, file: filePath, count: all.length };
|
|
428
446
|
} catch (e) {
|
|
429
|
-
return { written: false, error: e.message };
|
|
447
|
+
return { written: false, error: e.message || 'write_failed' };
|
|
430
448
|
}
|
|
431
449
|
}
|
|
432
450
|
|
|
@@ -39,7 +39,10 @@ const DEFAULT_SCHEMA_PATH = path.join(
|
|
|
39
39
|
* @param {string} cwd - working directory (used to resolve relative paths)
|
|
40
40
|
* @param {string} dir - directory to scan (relative to cwd or absolute)
|
|
41
41
|
* @param {object} opts - { schema: string, format: 'json'|'human', strict: bool, exclude: string[], raw: bool }
|
|
42
|
-
* @returns {void} — writes to stdout
|
|
42
|
+
* @returns {void} — writes to stdout, then exits. Exit code: 0 clean, 1 violations
|
|
43
|
+
* found, 2 the schema itself is malformed. output() exits, so every branch that
|
|
44
|
+
* calls it must pass its verdict as output()'s 4th argument; a bare
|
|
45
|
+
* process.exit() after an output() call is unreachable.
|
|
43
46
|
*/
|
|
44
47
|
function cmdDocLint(cwd, dir, opts = {}) {
|
|
45
48
|
const targetDir = path.isAbsolute(dir) ? dir : path.join(cwd, dir);
|
|
@@ -63,8 +66,16 @@ function cmdDocLint(cwd, dir, opts = {}) {
|
|
|
63
66
|
process.stderr.write(` ${schemaPath}:${e.line} — ${e.message}\n`);
|
|
64
67
|
}
|
|
65
68
|
} else {
|
|
66
|
-
output(
|
|
69
|
+
// Exit 2 is passed to output() because output() EXITS. The `process.exit(2)`
|
|
70
|
+
// below it was dead code on this branch, so `doc-lint --format json` against a
|
|
71
|
+
// malformed schema printed the errors and reported SUCCESS -- nothing was
|
|
72
|
+
// linted, and the caller was told everything was fine. The exit code is not
|
|
73
|
+
// derived here: `schema_errors` is a plural collection, deliberately outside
|
|
74
|
+
// output()'s error family (an empty array is truthy), so gates state their
|
|
75
|
+
// verdict explicitly.
|
|
76
|
+
output({ schema_errors: schemaErrors, schema: schemaPath }, false, undefined, 2);
|
|
67
77
|
}
|
|
78
|
+
// Reached only on the --raw branch above, which does not call output().
|
|
68
79
|
process.exit(2);
|
|
69
80
|
}
|
|
70
81
|
|
|
@@ -102,6 +113,9 @@ function cmdDocLint(cwd, dir, opts = {}) {
|
|
|
102
113
|
process.stdout.write(summaryLine(violations, fileCount) + '\n');
|
|
103
114
|
}
|
|
104
115
|
} else {
|
|
116
|
+
// Same verdict the --raw branch reports via the process.exit() below; passed
|
|
117
|
+
// explicitly because output() exits and would otherwise discard it, making
|
|
118
|
+
// `doc-lint --format json` a linter that never fails.
|
|
105
119
|
output({
|
|
106
120
|
directory: dir,
|
|
107
121
|
schema: schemaPath,
|
|
@@ -109,7 +123,7 @@ function cmdDocLint(cwd, dir, opts = {}) {
|
|
|
109
123
|
error_count: errorCount,
|
|
110
124
|
warning_count: warningCount,
|
|
111
125
|
violations,
|
|
112
|
-
}, false);
|
|
126
|
+
}, false, undefined, errorCount > 0 ? 1 : 0);
|
|
113
127
|
}
|
|
114
128
|
|
|
115
129
|
process.exit(errorCount > 0 ? 1 : 0);
|
|
@@ -131,8 +145,9 @@ function cmdDocLintSchemaCheck(cwd, schemaPath, opts = {}) {
|
|
|
131
145
|
error_count: errors.length,
|
|
132
146
|
errors,
|
|
133
147
|
};
|
|
134
|
-
output(
|
|
135
|
-
|
|
148
|
+
// output() is called on both branches here, so the process.exit() below it was
|
|
149
|
+
// unconditionally dead: schema-check reported a malformed schema and exited 0.
|
|
150
|
+
output(result, opts.raw, undefined, result.ok ? 0 : 1);
|
|
136
151
|
}
|
|
137
152
|
|
|
138
153
|
// ─── Count-drift lint (IMPROVEMENT-TODO P1, v3.7.10) ────────────────────────
|
|
@@ -178,6 +193,7 @@ const COUNT_ALLOWED_RE = /(^|[\\/])(CLAUDE\.md|CHANGELOG\.md|MEMORY\.md|SKILLS-F
|
|
|
178
193
|
const COUNT_ALLOWED_DIR_SEGMENTS = [
|
|
179
194
|
'decisions', // ADRs — frozen
|
|
180
195
|
'specs', // feature specs — frozen
|
|
196
|
+
'audits', // audit reports — point-in-time snapshots, exempt by their own preamble
|
|
181
197
|
'experiments', // harvested experiment artifacts
|
|
182
198
|
'learnings', // AI-derived patterns; evidence quotes reference numbers
|
|
183
199
|
'archive', // archived old docs
|
|
@@ -278,8 +294,9 @@ function cmdDocLintCounts(cwd, dir, opts = {}) {
|
|
|
278
294
|
process.stdout.write(`\n${violations.length} violation(s) across ${fileCount} files\n`);
|
|
279
295
|
}
|
|
280
296
|
} else {
|
|
281
|
-
output(result, false);
|
|
297
|
+
output(result, false, undefined, violations.length > 0 ? 1 : 0);
|
|
282
298
|
}
|
|
299
|
+
// Reached only on the --raw branch (release-check gate 4 relies on this code).
|
|
283
300
|
process.exit(violations.length > 0 ? 1 : 0);
|
|
284
301
|
}
|
|
285
302
|
|
|
@@ -355,8 +372,9 @@ function cmdDocLintFlags(cwd, opts = {}, raw) {
|
|
|
355
372
|
process.stdout.write(`\n${r.violation_count} aspirational flag(s)\n`);
|
|
356
373
|
}
|
|
357
374
|
} else {
|
|
358
|
-
output(r, false);
|
|
375
|
+
output(r, false, undefined, r.violation_count > 0 ? 1 : 0);
|
|
359
376
|
}
|
|
377
|
+
// Reached only on the --raw branch.
|
|
360
378
|
process.exit(r.violation_count > 0 ? 1 : 0);
|
|
361
379
|
}
|
|
362
380
|
|
|
@@ -156,7 +156,31 @@ function newExperiment(slug, opts = {}) {
|
|
|
156
156
|
// going because the workflow-level commit step exit code was 0.
|
|
157
157
|
initExperimentGit(expPath);
|
|
158
158
|
|
|
159
|
-
// Optional: invoke installer for the chosen runtime in the experiment folder
|
|
159
|
+
// Optional: invoke installer for the chosen runtime in the experiment folder.
|
|
160
|
+
// M16: the installer only ships in the PAN SOURCE repo (bin/install.js). In an
|
|
161
|
+
// INSTALLED host project PAN_SOURCE_ROOT resolves to the config dir (e.g.
|
|
162
|
+
// .claude/), which never contains bin/install.js — so the installer step would
|
|
163
|
+
// always fail. Detect the missing installer up front and default to skip with a
|
|
164
|
+
// clear note, rather than attempting a spawn that is doomed to error.
|
|
165
|
+
const installerPresent = fs.existsSync(path.join(PAN_SOURCE_ROOT, 'bin', 'install.js'));
|
|
166
|
+
if (!opts.skipInstaller && !installerPresent) {
|
|
167
|
+
manifest.status = 'scaffolded';
|
|
168
|
+
manifest.installer_skipped = 'installer not available (bin/install.js absent — PAN is installed, not running from source); run the PAN installer manually in the experiment folder if a runtime is needed';
|
|
169
|
+
try {
|
|
170
|
+
fs.writeFileSync(
|
|
171
|
+
path.join(expPath, '.planning', 'experiment.json'),
|
|
172
|
+
JSON.stringify(manifest, null, 2)
|
|
173
|
+
);
|
|
174
|
+
} catch { /* best effort — non-fatal */ }
|
|
175
|
+
return {
|
|
176
|
+
experiment_id: slug,
|
|
177
|
+
path: expPath,
|
|
178
|
+
runtime,
|
|
179
|
+
idea_path: manifest.idea_path,
|
|
180
|
+
created_at: createdAt,
|
|
181
|
+
installer_skipped: manifest.installer_skipped,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
160
184
|
if (!opts.skipInstaller) {
|
|
161
185
|
const installerError = runInstaller(expPath, runtime);
|
|
162
186
|
if (installerError) {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
const fs = require('fs');
|
|
10
10
|
const path = require('path');
|
|
11
|
-
const { output, error, safeReadFile, loadConfig, scanPendingTodos, scanSourceTodos, toPosix, isGitRepo, execGit, escapeRegex } = require('./core.cjs');
|
|
11
|
+
const { output, EXIT_OK, error, safeReadFile, loadConfig, scanPendingTodos, scanSourceTodos, toPosix, isGitRepo, execGit, escapeRegex, normalizePhaseName } = require('./core.cjs');
|
|
12
12
|
const {
|
|
13
13
|
PLANNING_DIR, PHASES_DIR, ROADMAP_FILE, PATTERNS_FILE, EFFORT_POINTS, PRIORITY_LEVELS, EFFORT_SIZES,
|
|
14
14
|
FOCUS_MODES, FOCUS_TIERS, FOCUS_DIR,
|
|
@@ -46,7 +46,17 @@ function collectWorkItems(cwd) {
|
|
|
46
46
|
try { dirs = fs.readdirSync(phasesDir); } catch { dirs = []; }
|
|
47
47
|
|
|
48
48
|
for (const phase of phases) {
|
|
49
|
-
|
|
49
|
+
// Normalize before matching. Roadmap headings are UNPADDED — the shipped
|
|
50
|
+
// templates/roadmap.md writes "### Phase 1:" and `phase add` appends the same —
|
|
51
|
+
// while phase directories are zero-padded (`01-foundation`). Matching the raw
|
|
52
|
+
// heading number meant '1-' never matched '01-foundation', so focus scan found
|
|
53
|
+
// ZERO work items on a roadmap in PAN's own format, and the whole focus family
|
|
54
|
+
// (scan/plan/classify-stages/exec/auto) was a no-op on any default-shaped
|
|
55
|
+
// project. normalizePhaseName pads and is idempotent, so a roadmap that already
|
|
56
|
+
// says "Phase 01" still matches. Same normalization core.cjs searchPhaseInDir
|
|
57
|
+
// applies, which is why `find-phase 1` always worked where this did not.
|
|
58
|
+
const normalized = normalizePhaseName(phase.number);
|
|
59
|
+
const dirName = dirs.find(d => d.startsWith(normalized + '-') || d === normalized);
|
|
50
60
|
if (!dirName) continue;
|
|
51
61
|
|
|
52
62
|
const phaseDir = path.join(phasesDir, dirName);
|
|
@@ -344,7 +354,13 @@ function cmdFocusPlan(cwd, raw, ...args) {
|
|
|
344
354
|
// Collect items
|
|
345
355
|
const { items, sources } = collectWorkItems(cwd);
|
|
346
356
|
if (items.length === 0) {
|
|
347
|
-
|
|
357
|
+
// EXIT_OK: an empty backlog is a legitimate answer, not a failure — it is the
|
|
358
|
+
// desired end state of a burn-down loop. `focus scan` already reports zero items
|
|
359
|
+
// as a success payload ({ items: [], total: 0 }) and the shipped scenario test
|
|
360
|
+
// asserts `focus plan` succeeds on a project with no TODOs
|
|
361
|
+
// (tests/scenarios/workflow-focus.test.cjs), so exiting non-zero here would make
|
|
362
|
+
// "nothing left to do" indistinguishable from a crash for the same caller.
|
|
363
|
+
output({ error: 'No work items found. Run focus scan first or add phases/todos.' }, raw, undefined, EXIT_OK);
|
|
348
364
|
return;
|
|
349
365
|
}
|
|
350
366
|
|
|
@@ -404,23 +420,35 @@ function checkDocStaleness(cwd, opts) {
|
|
|
404
420
|
const current = [];
|
|
405
421
|
const options = opts || {};
|
|
406
422
|
|
|
407
|
-
// Count actuals
|
|
423
|
+
// Count actuals. M17: a filesystem count only reconciles a documented count
|
|
424
|
+
// when the corresponding SOURCE directory actually exists. In an INSTALLED
|
|
425
|
+
// host project commands/pan, agents/, and pan-wizard-core/bin/lib are all
|
|
426
|
+
// absent — reconciling a documented "N commands" against an actual of 0 there
|
|
427
|
+
// is a false positive, so the per-entity check is SKIPPED when its dir is
|
|
428
|
+
// missing (tracked via *Present flags below). Explicit --tests/--suites and the
|
|
429
|
+
// version cross-reference are layout-independent and always run.
|
|
408
430
|
let commandCount = 0;
|
|
431
|
+
let commandsPresent = false;
|
|
409
432
|
try {
|
|
410
433
|
const cmdDir = path.join(cwd, 'commands', 'pan');
|
|
411
434
|
commandCount = fs.readdirSync(cmdDir).filter(f => f.endsWith('.md')).length;
|
|
435
|
+
commandsPresent = true;
|
|
412
436
|
} catch { /* no commands dir */ }
|
|
413
437
|
|
|
414
438
|
let agentCount = 0;
|
|
439
|
+
let agentsPresent = false;
|
|
415
440
|
try {
|
|
416
441
|
const agentDir = path.join(cwd, 'agents');
|
|
417
442
|
agentCount = fs.readdirSync(agentDir).filter(f => f.endsWith('.md')).length;
|
|
443
|
+
agentsPresent = true;
|
|
418
444
|
} catch { /* no agents dir */ }
|
|
419
445
|
|
|
420
446
|
let moduleCount = 0;
|
|
447
|
+
let modulesPresent = false;
|
|
421
448
|
try {
|
|
422
449
|
const libDir = path.join(cwd, 'pan-wizard-core', 'bin', 'lib');
|
|
423
450
|
moduleCount = fs.readdirSync(libDir).filter(f => f.endsWith('.cjs')).length;
|
|
451
|
+
modulesPresent = true;
|
|
424
452
|
} catch { /* no lib dir */ }
|
|
425
453
|
|
|
426
454
|
const actuals = { commands: commandCount, agents: agentCount, modules: moduleCount };
|
|
@@ -429,9 +457,9 @@ function checkDocStaleness(cwd, opts) {
|
|
|
429
457
|
for (const relFile of DOC_SYNC_FILES) {
|
|
430
458
|
const content = safeReadFile(path.join(cwd, relFile));
|
|
431
459
|
if (!content) continue;
|
|
432
|
-
checkCount(content, relFile, 'commands', commandCount, stale, current);
|
|
433
|
-
checkCount(content, relFile, 'agents', agentCount, stale, current);
|
|
434
|
-
checkCount(content, relFile, 'modules', moduleCount, stale, current);
|
|
460
|
+
if (commandsPresent) checkCount(content, relFile, 'commands', commandCount, stale, current);
|
|
461
|
+
if (agentsPresent) checkCount(content, relFile, 'agents', agentCount, stale, current);
|
|
462
|
+
if (modulesPresent) checkCount(content, relFile, 'modules', moduleCount, stale, current);
|
|
435
463
|
|
|
436
464
|
// Check test/suite counts if provided
|
|
437
465
|
if (options.tests != null) {
|
|
@@ -790,6 +818,13 @@ function focusAutoUpdate(cwd, raw, getVal) {
|
|
|
790
818
|
timestamp: new Date().toISOString(),
|
|
791
819
|
};
|
|
792
820
|
|
|
821
|
+
// M18: wire --prompts-remaining so the `prompts_complete` stop reason is
|
|
822
|
+
// reachable (focus-auto.md step 8 instructs recording it). Absent → null, so
|
|
823
|
+
// determineStopReason's `prompts_remaining === 0` check never fires spuriously
|
|
824
|
+
// for non-prompts categories or when the flag isn't supplied.
|
|
825
|
+
const promptsRemainingRaw = getVal('--prompts-remaining', null);
|
|
826
|
+
cycle.prompts_remaining = promptsRemainingRaw != null ? Number(promptsRemainingRaw) : null;
|
|
827
|
+
|
|
793
828
|
// Anti-fake: re-run the suite when verification is enabled; else record that
|
|
794
829
|
// this count was self-reported (tests_verified:false) so the trust is visible.
|
|
795
830
|
const tv = verifyTestCount(cwd, loadConfig(cwd), cycle.tests_after);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
|
-
const { safeReadFile, output, error } = require('./core.cjs');
|
|
7
|
+
const { safeReadFile, output, error, escapeRegex } = require('./core.cjs');
|
|
8
8
|
const { FIELD_VALUE_RE, PRIORITY_LEVELS, EFFORT_SIZES } = require('./constants.cjs');
|
|
9
9
|
|
|
10
10
|
// --- Inline array rendering thresholds -------------------------------------------
|
|
@@ -157,7 +157,10 @@ function renderYamlEntry(lines, key, value, depth) {
|
|
|
157
157
|
if (Array.isArray(value)) {
|
|
158
158
|
if (value.length === 0) {
|
|
159
159
|
lines.push(`${indent}${key}: []`);
|
|
160
|
-
} else if (value.every(item => typeof item === 'string') && value.length <= MAX_INLINE_ARRAY_ITEMS && value.join(', ').length < MAX_INLINE_ARRAY_WIDTH) {
|
|
160
|
+
} else if (value.every(item => typeof item === 'string') && value.length <= MAX_INLINE_ARRAY_ITEMS && value.join(', ').length < MAX_INLINE_ARRAY_WIDTH && value.every(item => !item.includes(','))) {
|
|
161
|
+
// Inline form `[a, b, c]` is delimited by ', ', so an item that itself
|
|
162
|
+
// contains a comma would be re-split into multiple items on parse (lossy).
|
|
163
|
+
// Force multi-line ("- item") for those arrays so each item survives whole.
|
|
161
164
|
lines.push(`${indent}${key}: [${value.join(', ')}]`);
|
|
162
165
|
} else {
|
|
163
166
|
lines.push(`${indent}${key}:`);
|
|
@@ -226,56 +229,84 @@ function parseMustHavesBlock(content, blockName) {
|
|
|
226
229
|
if (!fmMatch) return [];
|
|
227
230
|
|
|
228
231
|
const yaml = fmMatch[1];
|
|
232
|
+
const lines = yaml.split('\n');
|
|
233
|
+
const indentOf = (line) => line.match(/^(\s*)/)[1].length;
|
|
234
|
+
|
|
235
|
+
// Find `blockName:` as a child of `must_haves:` — by RELATIVE indent, never a
|
|
236
|
+
// fixed column. This function used to require exactly 4 spaces
|
|
237
|
+
// (`^\s{4}artifacts:`) with list items at exactly 6 and continuations at 8+.
|
|
238
|
+
// Nothing PAN ships is written that way: `template fill plan`,
|
|
239
|
+
// templates/phase-prompt.md, agents/pan-planner.md and agents/pan-verifier.md
|
|
240
|
+
// all put `must_haves:` at column 0 with its children at 2. So every real plan
|
|
241
|
+
// parsed as ZERO must_haves, and `verify reconcile` — the anti-rubber-stamp gate
|
|
242
|
+
// — reported "no must_haves declared … verdict trusted" and exited 0 on a stub.
|
|
243
|
+
// The 2026-08 audit chain missed it for five rounds because its fixtures were
|
|
244
|
+
// hand-written at 4-space, i.e. it verified the parser against itself.
|
|
245
|
+
const mustHavesIdx = lines.findIndex(l => /^\s*must_haves:\s*$/.test(l));
|
|
246
|
+
if (mustHavesIdx === -1) return [];
|
|
247
|
+
const mustHavesIndent = indentOf(lines[mustHavesIdx]);
|
|
248
|
+
|
|
249
|
+
// The block header is the first `blockName:` deeper than must_haves and before
|
|
250
|
+
// the next line at must_haves' own level or shallower (which ends the section).
|
|
251
|
+
let blockIdx = -1;
|
|
252
|
+
for (let i = mustHavesIdx + 1; i < lines.length; i++) {
|
|
253
|
+
const line = lines[i];
|
|
254
|
+
if (line.trim() === '') continue;
|
|
255
|
+
const ind = indentOf(line);
|
|
256
|
+
if (ind <= mustHavesIndent) break; // left the must_haves section
|
|
257
|
+
if (new RegExp(`^\\s*${escapeRegex(blockName)}:\\s*$`).test(line)) { blockIdx = i; break; }
|
|
258
|
+
}
|
|
259
|
+
if (blockIdx === -1) return [];
|
|
260
|
+
const blockIndent = indentOf(lines[blockIdx]);
|
|
229
261
|
|
|
230
|
-
|
|
231
|
-
const blockPattern = new RegExp(`^\\s{4}${blockName}:\\s*$`, 'm');
|
|
232
|
-
const blockStart = yaml.search(blockPattern);
|
|
233
|
-
if (blockStart === -1) return [];
|
|
234
|
-
|
|
235
|
-
const afterBlock = yaml.slice(blockStart);
|
|
236
|
-
// Skip the header line itself, then process remaining lines
|
|
237
|
-
const blockLines = afterBlock.split('\n').slice(1);
|
|
262
|
+
const blockLines = lines.slice(blockIdx + 1);
|
|
238
263
|
|
|
239
264
|
const items = [];
|
|
240
265
|
let currentItem = null;
|
|
266
|
+
// Indent of the list-item dashes, learned from the first one we see, so that
|
|
267
|
+
// continuation lines can be told apart from items without assuming a width.
|
|
268
|
+
let itemIndent = null;
|
|
241
269
|
|
|
242
270
|
for (const line of blockLines) {
|
|
243
271
|
// Skip blank lines within the block
|
|
244
272
|
if (line.trim() === '') continue;
|
|
245
273
|
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if (indent <= 4 && line.trim() !== '') break;
|
|
274
|
+
// Leaving the block: back to the block header's level or shallower (a sibling
|
|
275
|
+
// block such as key_links, or back out to must_haves).
|
|
276
|
+
const indent = indentOf(line);
|
|
277
|
+
if (indent <= blockIndent) break;
|
|
251
278
|
|
|
252
|
-
|
|
253
|
-
|
|
279
|
+
const dashMatch = line.match(/^(\s*)-\s+(.*)$/);
|
|
280
|
+
const isNewItem = dashMatch && (itemIndent === null || dashMatch[1].length <= itemIndent);
|
|
281
|
+
|
|
282
|
+
if (isNewItem) {
|
|
283
|
+
if (itemIndent === null) itemIndent = dashMatch[1].length;
|
|
284
|
+
// New list item (direct child of the block)
|
|
254
285
|
if (currentItem) items.push(currentItem);
|
|
255
286
|
currentItem = {};
|
|
256
287
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
288
|
+
const body = dashMatch[2];
|
|
289
|
+
// A plain string item — no `key: value` shape.
|
|
290
|
+
const kvMatch = body.match(/^(\w+):\s*"?([^"]*)"?\s*$/);
|
|
291
|
+
if (kvMatch) {
|
|
292
|
+
currentItem = {};
|
|
293
|
+
currentItem[kvMatch[1]] = kvMatch[2];
|
|
261
294
|
} else {
|
|
262
|
-
|
|
263
|
-
const kvMatch = line.match(/^\s{6}-\s+(\w+):\s*"?([^"]*)"?\s*$/);
|
|
264
|
-
if (kvMatch) {
|
|
265
|
-
currentItem = {};
|
|
266
|
-
currentItem[kvMatch[1]] = kvMatch[2];
|
|
267
|
-
}
|
|
295
|
+
currentItem = body.replace(/^"|"$/g, '').trim();
|
|
268
296
|
}
|
|
269
297
|
} else if (currentItem && typeof currentItem === 'object') {
|
|
270
|
-
// Continuation key-value
|
|
271
|
-
const kvMatch = line.match(/^\s
|
|
298
|
+
// Continuation key-value: deeper than the item's dash (properties of it)
|
|
299
|
+
const kvMatch = line.match(/^\s+(\w+):\s*"?([^"]*)"?\s*$/);
|
|
272
300
|
if (kvMatch) {
|
|
273
301
|
const val = kvMatch[2];
|
|
274
302
|
// Coerce pure-integer strings to numbers for convenience
|
|
275
303
|
currentItem[kvMatch[1]] = /^\d+$/.test(val) ? parseInt(val, 10) : val;
|
|
276
304
|
}
|
|
277
|
-
// Array items nested under a property
|
|
278
|
-
|
|
305
|
+
// Array items nested under a property. Any dash line reaching this branch is
|
|
306
|
+
// already deeper than the list-item dash (a shallower one would have been
|
|
307
|
+
// treated as a new item above), so relative depth alone identifies it —
|
|
308
|
+
// no fixed column.
|
|
309
|
+
const arrMatch = line.match(/^\s+-\s+"?([^"]+)"?\s*$/);
|
|
279
310
|
if (arrMatch) {
|
|
280
311
|
// Convert the most recently added key's scalar value into an array,
|
|
281
312
|
// then append this item to that array
|
|
@@ -3,6 +3,25 @@
|
|
|
3
3
|
const { output, error, isGitRepo, execGit, loadConfig } = require('./core.cjs');
|
|
4
4
|
const { runCommitSafetyChecks, VALID_COMMIT_TYPES } = require('./commands.cjs');
|
|
5
5
|
|
|
6
|
+
// ─── Failure reporting ────────────────────────────────────────────────────────
|
|
7
|
+
//
|
|
8
|
+
// Every subcommand here asks git to DO something. When git refuses, the payload
|
|
9
|
+
// says `<verb>: false` — a shape output() cannot classify on its own, because the
|
|
10
|
+
// same shape also carries legitimate negative answers elsewhere in PAN (see the
|
|
11
|
+
// exit-code contract in core.cjs). So each failure site names itself with an
|
|
12
|
+
// error-family key, which is what output() derives a non-zero exit from.
|
|
13
|
+
//
|
|
14
|
+
// Whatever the value is, it must never be able to come out EMPTY: git does not
|
|
15
|
+
// always write to stderr, and an empty string is falsy — it would launder the
|
|
16
|
+
// failure back into exit 0. Two safe forms are used here:
|
|
17
|
+
// • a stable code (`error: 'push_failed'`) — the form the query subcommands in
|
|
18
|
+
// this file already used (`error: 'status_failed', detail: r.stderr`);
|
|
19
|
+
// • stderr with a fallback (`error: r.stderr || 'unknown git error'`) — used by
|
|
20
|
+
// `commit` only, so all three of PAN's commit paths (this one, cmdCommit and
|
|
21
|
+
// cmdBatchCommit in commands.cjs) report a refused commit identically.
|
|
22
|
+
// The stderr text stays in `detail` where it always was, so existing consumers
|
|
23
|
+
// keep reading the same field.
|
|
24
|
+
|
|
6
25
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
7
26
|
|
|
8
27
|
function getCurrentBranch(cwd) {
|
|
@@ -52,7 +71,10 @@ function cmdGitCommit(cwd, opts, raw) {
|
|
|
52
71
|
const config = loadConfig(cwd);
|
|
53
72
|
const safety = runCommitSafetyChecks(cwd, config, force);
|
|
54
73
|
if (safety.blocked) {
|
|
55
|
-
|
|
74
|
+
// A blocked commit is a refusal that protected something (staged deletions, a
|
|
75
|
+
// secret) — the commit did NOT happen, so the caller must be able to stop. Same
|
|
76
|
+
// classification CLI-REFERENCE already documents for `dirty_working_tree`.
|
|
77
|
+
output({ committed: false, reason: safety.reason, error: 'commit_blocked', safety_checks: safety.safetyChecks, hint: safety.hint }, raw, 'blocked');
|
|
56
78
|
return;
|
|
57
79
|
}
|
|
58
80
|
|
|
@@ -61,10 +83,16 @@ function cmdGitCommit(cwd, opts, raw) {
|
|
|
61
83
|
const r = execGit(cwd, commitArgs);
|
|
62
84
|
if (r.exitCode !== 0) {
|
|
63
85
|
if (r.stdout.includes('nothing to commit') || r.stderr.includes('nothing to commit')) {
|
|
86
|
+
// No error key, exit 0: nothing to commit means no change was NEEDED, not that
|
|
87
|
+
// a change failed. Pinned as a success in CLI-REFERENCE ("Error Shape").
|
|
64
88
|
output({ committed: false, reason: 'nothing_to_commit' }, raw, 'nothing to commit');
|
|
65
89
|
return;
|
|
66
90
|
}
|
|
67
|
-
|
|
91
|
+
// `|| 'unknown git error'` is load-bearing: the exit code is derived from a
|
|
92
|
+
// TRUTHY error key and git does not always write to stderr, so an empty string
|
|
93
|
+
// would launder this real failure back into exit 0. Same shape as the two
|
|
94
|
+
// sibling commit paths in commands.cjs (cmdCommit, cmdBatchCommit).
|
|
95
|
+
output({ committed: false, reason: 'commit_failed', error: r.stderr || 'unknown git error', detail: r.stderr }, raw, 'commit failed');
|
|
68
96
|
return;
|
|
69
97
|
}
|
|
70
98
|
const hash = execGit(cwd, ['rev-parse', '--short', 'HEAD']).stdout || null;
|
|
@@ -90,7 +118,7 @@ function cmdGitBranch(cwd, sub, opts, raw) {
|
|
|
90
118
|
if (!branchName) { error('--name or --phase required for branch create'); }
|
|
91
119
|
const r = execGit(cwd, ['checkout', '-b', branchName]);
|
|
92
120
|
if (r.exitCode !== 0) {
|
|
93
|
-
output({ created: false, branch: branchName, detail: r.stderr }, raw, 'failed');
|
|
121
|
+
output({ created: false, branch: branchName, error: 'branch_create_failed', detail: r.stderr }, raw, 'failed');
|
|
94
122
|
return;
|
|
95
123
|
}
|
|
96
124
|
output({ created: true, branch: branchName }, raw, branchName);
|
|
@@ -100,7 +128,7 @@ function cmdGitBranch(cwd, sub, opts, raw) {
|
|
|
100
128
|
if (!name) { error('--name required for branch switch'); }
|
|
101
129
|
const r = execGit(cwd, ['checkout', name]);
|
|
102
130
|
if (r.exitCode !== 0) {
|
|
103
|
-
output({ switched: false, branch: name, detail: r.stderr }, raw, 'failed');
|
|
131
|
+
output({ switched: false, branch: name, error: 'branch_switch_failed', detail: r.stderr }, raw, 'failed');
|
|
104
132
|
return;
|
|
105
133
|
}
|
|
106
134
|
output({ switched: true, branch: name }, raw, name);
|
|
@@ -111,7 +139,7 @@ function cmdGitBranch(cwd, sub, opts, raw) {
|
|
|
111
139
|
const flag = force ? '-D' : '-d';
|
|
112
140
|
const r = execGit(cwd, ['branch', flag, name]);
|
|
113
141
|
if (r.exitCode !== 0) {
|
|
114
|
-
output({ deleted: false, branch: name, detail: r.stderr, hint: force ? null : 'Use --force to delete unmerged branches' }, raw, 'failed');
|
|
142
|
+
output({ deleted: false, branch: name, error: 'branch_delete_failed', detail: r.stderr, hint: force ? null : 'Use --force to delete unmerged branches' }, raw, 'failed');
|
|
115
143
|
return;
|
|
116
144
|
}
|
|
117
145
|
output({ deleted: true, branch: name }, raw, name);
|
|
@@ -134,7 +162,7 @@ function cmdGitPush(cwd, opts, raw) {
|
|
|
134
162
|
|
|
135
163
|
const r = execGit(cwd, pushArgs);
|
|
136
164
|
if (r.exitCode !== 0) {
|
|
137
|
-
output({ pushed: false, remote, branch, detail: r.stderr }, raw, 'push failed');
|
|
165
|
+
output({ pushed: false, remote, branch, error: 'push_failed', detail: r.stderr }, raw, 'push failed');
|
|
138
166
|
return;
|
|
139
167
|
}
|
|
140
168
|
output({ pushed: true, remote, branch, force: !!force }, raw, remote + '/' + branch);
|
|
@@ -182,14 +210,14 @@ function cmdGitStash(cwd, sub, opts, raw) {
|
|
|
182
210
|
if (sub === 'save') {
|
|
183
211
|
const args = name ? ['stash', 'push', '-m', name] : ['stash', 'push'];
|
|
184
212
|
const r = execGit(cwd, args);
|
|
185
|
-
if (r.exitCode !== 0) { output({ stashed: false, detail: r.stderr }, raw, 'stash failed'); return; }
|
|
213
|
+
if (r.exitCode !== 0) { output({ stashed: false, error: 'stash_save_failed', detail: r.stderr }, raw, 'stash failed'); return; }
|
|
186
214
|
output({ stashed: true, name: name || null }, raw, 'stashed');
|
|
187
215
|
return;
|
|
188
216
|
}
|
|
189
217
|
if (sub === 'pop') {
|
|
190
218
|
const args = index != null ? ['stash', 'pop', 'stash@{' + index + '}'] : ['stash', 'pop'];
|
|
191
219
|
const r = execGit(cwd, args);
|
|
192
|
-
if (r.exitCode !== 0) { output({ popped: false, detail: r.stderr }, raw, 'pop failed'); return; }
|
|
220
|
+
if (r.exitCode !== 0) { output({ popped: false, error: 'stash_pop_failed', detail: r.stderr }, raw, 'pop failed'); return; }
|
|
193
221
|
output({ popped: true }, raw, 'popped');
|
|
194
222
|
return;
|
|
195
223
|
}
|
|
@@ -202,7 +230,7 @@ function cmdGitStash(cwd, sub, opts, raw) {
|
|
|
202
230
|
if (sub === 'drop') {
|
|
203
231
|
const args = index != null ? ['stash', 'drop', 'stash@{' + index + '}'] : ['stash', 'drop'];
|
|
204
232
|
const r = execGit(cwd, args);
|
|
205
|
-
if (r.exitCode !== 0) { output({ dropped: false, detail: r.stderr }, raw, 'drop failed'); return; }
|
|
233
|
+
if (r.exitCode !== 0) { output({ dropped: false, error: 'stash_drop_failed', detail: r.stderr }, raw, 'drop failed'); return; }
|
|
206
234
|
output({ dropped: true }, raw, 'dropped');
|
|
207
235
|
return;
|
|
208
236
|
}
|
|
@@ -250,7 +278,7 @@ function cmdGitRollback(cwd, opts, raw) {
|
|
|
250
278
|
}
|
|
251
279
|
const r = execGit(cwd, ['reset', '--hard', targetTag]);
|
|
252
280
|
if (r.exitCode !== 0) {
|
|
253
|
-
output({ rolled_back: false, tag: targetTag, detail: r.stderr }, raw, 'rollback failed');
|
|
281
|
+
output({ rolled_back: false, tag: targetTag, error: 'rollback_failed', detail: r.stderr }, raw, 'rollback failed');
|
|
254
282
|
return;
|
|
255
283
|
}
|
|
256
284
|
}
|
|
@@ -280,7 +308,7 @@ function cmdGitTag(cwd, sub, opts, raw) {
|
|
|
280
308
|
: ['-c', 'tag.gpgsign=false', 'tag', name];
|
|
281
309
|
const r = execGit(cwd, args);
|
|
282
310
|
if (r.exitCode !== 0) {
|
|
283
|
-
output({ created: false, tag: name, detail: r.stderr }, raw, 'tag create failed');
|
|
311
|
+
output({ created: false, tag: name, error: 'tag_create_failed', detail: r.stderr }, raw, 'tag create failed');
|
|
284
312
|
return;
|
|
285
313
|
}
|
|
286
314
|
output({ created: true, tag: name }, raw, name);
|
|
@@ -290,7 +318,7 @@ function cmdGitTag(cwd, sub, opts, raw) {
|
|
|
290
318
|
if (!name) { error('--name required for tag delete'); }
|
|
291
319
|
const r = execGit(cwd, ['tag', '-d', name]);
|
|
292
320
|
if (r.exitCode !== 0) {
|
|
293
|
-
output({ deleted: false, tag: name, detail: r.stderr }, raw, 'tag delete failed');
|
|
321
|
+
output({ deleted: false, tag: name, error: 'tag_delete_failed', detail: r.stderr }, raw, 'tag delete failed');
|
|
294
322
|
return;
|
|
295
323
|
}
|
|
296
324
|
output({ deleted: true, tag: name }, raw, name);
|
|
@@ -310,14 +338,14 @@ function cmdGitSync(cwd, opts, raw) {
|
|
|
310
338
|
|
|
311
339
|
const fetchR = execGit(cwd, ['fetch', remote]);
|
|
312
340
|
if (fetchR.exitCode !== 0) {
|
|
313
|
-
output({ synced: false, detail: fetchR.stderr }, raw, 'fetch failed');
|
|
341
|
+
output({ synced: false, error: 'fetch_failed', detail: fetchR.stderr }, raw, 'fetch failed');
|
|
314
342
|
return;
|
|
315
343
|
}
|
|
316
344
|
|
|
317
345
|
const pullArgs = rebase ? ['pull', '--rebase', remote, branch] : ['pull', remote, branch];
|
|
318
346
|
const pullR = execGit(cwd, pullArgs);
|
|
319
347
|
if (pullR.exitCode !== 0) {
|
|
320
|
-
output({ synced: false, detail: pullR.stderr }, raw, 'pull failed');
|
|
348
|
+
output({ synced: false, error: 'pull_failed', detail: pullR.stderr }, raw, 'pull failed');
|
|
321
349
|
return;
|
|
322
350
|
}
|
|
323
351
|
|
|
@@ -337,15 +365,22 @@ function cmdGit(cwd, subcommand, args, raw) {
|
|
|
337
365
|
const hasFlag = flag => args.includes(flag);
|
|
338
366
|
|
|
339
367
|
switch (subcommand) {
|
|
340
|
-
case 'commit':
|
|
368
|
+
case 'commit': {
|
|
369
|
+
// M20: filter positional file paths from args AFTER dropping the leading
|
|
370
|
+
// subcommand token ('commit'). Operating on the full args left 'commit'
|
|
371
|
+
// itself in the files list, so a path literally named 'commit' got staged.
|
|
372
|
+
// Also drop the value that follows --type / --message so a flag's argument
|
|
373
|
+
// is never mistaken for a file path.
|
|
374
|
+
const commitArgs = args.slice(1);
|
|
341
375
|
return cmdGitCommit(cwd, {
|
|
342
376
|
type: getOpt('--type', null),
|
|
343
377
|
message: getOpt('--message', null),
|
|
344
378
|
all: hasFlag('--all'),
|
|
345
379
|
amend: hasFlag('--amend'),
|
|
346
380
|
force: hasFlag('--force'),
|
|
347
|
-
files:
|
|
381
|
+
files: commitArgs.filter((a, i) => !a.startsWith('--') && commitArgs[i - 1] !== '--type' && commitArgs[i - 1] !== '--message'),
|
|
348
382
|
}, raw);
|
|
383
|
+
}
|
|
349
384
|
case 'branch':
|
|
350
385
|
return cmdGitBranch(cwd, sub2, {
|
|
351
386
|
name: getOpt('--name', null),
|