@magnusekdahl/parallix 1.0.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 +140 -0
- package/LICENSE +661 -0
- package/README.md +196 -0
- package/config/agents.json +25 -0
- package/config/agents.local.json.template +8 -0
- package/config/state-map.json +4 -0
- package/config/state-map.json.template +31 -0
- package/config/workflow.config.schema.json +98 -0
- package/data/.gitkeep +0 -0
- package/docs/adr/0031-ai-agent-instruction-boundary-and-command-floor.md +114 -0
- package/docs/adr/0032-mission-refinement-state-and-usage-budget-signals.md +135 -0
- package/docs/adr/0034-module-and-skill-invocation-model.md +202 -0
- package/docs/adr/0036-mission-sizing-and-dependency-wave-heuristics.md +79 -0
- package/docs/adr/0037-ai-workflow-coordination-architecture.md +162 -0
- package/docs/adr/0041-integration-pipeline-gates.md +165 -0
- package/docs/adr/0042-workflow-cli-color-rendering-approach.md +106 -0
- package/docs/adr/0043-git-target-resolution-strategy.md +185 -0
- package/docs/adr/0044-workflow-distribution-model.md +277 -0
- package/docs/adr/0045-parallax-branch-model.md +182 -0
- package/docs/adr/0046-npm-publish-process-and-security.md +138 -0
- package/docs/adr/index.md +20 -0
- package/docs/agents.md +212 -0
- package/docs/authority-reference.md +298 -0
- package/docs/forgejo-setup.md +31 -0
- package/docs/migration/extraction.md +61 -0
- package/docs/migration/task-classification.md +36 -0
- package/docs/operator-setup.md +76 -0
- package/docs/readme-rewrite-benchmark.md +188 -0
- package/docs/use-cases.md +105 -0
- package/examples/README.md +62 -0
- package/examples/run-enterprise-tarball-workflow-smoke.sh +257 -0
- package/examples/run-verify-env-smoke.sh +40 -0
- package/index.js +250 -0
- package/lib/README.md +13 -0
- package/lib/agents/agents.js +867 -0
- package/lib/agents/claude-telemetry.js +233 -0
- package/lib/agents/claude.js +139 -0
- package/lib/agents/codex-telemetry.js +202 -0
- package/lib/agents/codex.js +219 -0
- package/lib/agents/limit-hit.js +252 -0
- package/lib/agents/mistral-telemetry.js +44 -0
- package/lib/agents/mistral.js +68 -0
- package/lib/agents/opencode-export.js +110 -0
- package/lib/agents/opencode-telemetry.js +356 -0
- package/lib/agents/opencode.js +218 -0
- package/lib/agents/stage-telemetry.js +37 -0
- package/lib/commands/active.js +625 -0
- package/lib/commands/checkpoint.js +76 -0
- package/lib/commands/config.js +39 -0
- package/lib/commands/coverage-gate.js +358 -0
- package/lib/commands/diff.js +119 -0
- package/lib/commands/draft.js +854 -0
- package/lib/commands/handoff.js +501 -0
- package/lib/commands/integrate.js +1528 -0
- package/lib/commands/mission-start.js +246 -0
- package/lib/commands/rebase.js +597 -0
- package/lib/commands/repair-handoff.js +227 -0
- package/lib/commands/resolve-conflict.js +109 -0
- package/lib/commands/review.js +13 -0
- package/lib/commands/setup-review.js +13 -0
- package/lib/commands/setup.js +3 -0
- package/lib/commands/stats-backfill.js +395 -0
- package/lib/commands/stats.js +1601 -0
- package/lib/commands/status.js +183 -0
- package/lib/commands/verify.js +1 -0
- package/lib/core/fmt.js +202 -0
- package/lib/core/git.js +73 -0
- package/lib/core/gitignore.js +110 -0
- package/lib/core/mission-utils.js +1017 -0
- package/lib/core/persistent-data-migration.js +201 -0
- package/lib/core/product-config.js +508 -0
- package/lib/core/runtime-matrix.js +82 -0
- package/lib/core/spawn-tee.js +173 -0
- package/lib/core/state-map.js +89 -0
- package/lib/core/storage.js +165 -0
- package/lib/core/verification.js +149 -0
- package/lib/index.js +77 -0
- package/lib/review/rebase.js +163 -0
- package/lib/review/review-adapter.js +135 -0
- package/lib/review/review-artifacts.js +619 -0
- package/lib/review/review-commands.js +1375 -0
- package/lib/review/review-events.js +1007 -0
- package/lib/review/review-loop.js +1004 -0
- package/lib/review/review-polling.js +141 -0
- package/lib/review/review-prompts.js +212 -0
- package/lib/review/review-state.js +280 -0
- package/lib/review/review.js +96 -0
- package/lib/tools/backlog.js +680 -0
- package/lib/tools/forgejo.js +1585 -0
- package/lib/tools/gatekeeper.js +106 -0
- package/lib/tools/sessions.js +74 -0
- package/lib/tools/setup-review.js +1053 -0
- package/package.json +56 -0
- package/prompts/act-on-review-verbose.md +20 -0
- package/prompts/act-on-review.md +22 -0
- package/prompts/draft.md +20 -0
- package/prompts/execute.md +24 -0
- package/prompts/portfolio.md +30 -0
- package/prompts/review-verbose.md +20 -0
- package/prompts/review.md +17 -0
- package/px.js +236 -0
- package/templates/AGENTS-snippet.md +14 -0
- package/templates/AGENTS.md.template +34 -0
- package/templates/CLAUDE.md.template +27 -0
- package/templates/CODEX.md.template +38 -0
- package/templates/MISTRAL.md.template +24 -0
- package/templates/claude-commands/act-on-review.md +3 -0
- package/templates/claude-commands/area-review.md +3 -0
- package/templates/claude-commands/draft.md +6 -0
- package/templates/claude-commands/execute.md +6 -0
- package/templates/claude-commands/integrate.md +4 -0
- package/templates/claude-commands/portfolio.md +5 -0
- package/templates/claude-commands/review.md +4 -0
- package/templates/codex/config.toml +6 -0
- package/templates/mission-scaffold.md +39 -0
- package/templates/vibe/skills/act-on-review/SKILL.md +16 -0
- package/templates/vibe/skills/area-review/SKILL.md +16 -0
- package/templates/vibe/skills/draft/SKILL.md +16 -0
- package/templates/vibe/skills/execute/SKILL.md +16 -0
- package/templates/vibe/skills/integrate/SKILL.md +16 -0
- package/templates/vibe/skills/portfolio/SKILL.md +21 -0
- package/templates/vibe/skills/review/SKILL.md +16 -0
- package/tools/setup-forgejo-docker.sh +84 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
const { getCurrentBranch, getUncommittedCount, getLastThreeCommits, run } = require('../core/git');
|
|
2
|
+
const { findTaskFile, getTaskStatus } = require('../tools/backlog');
|
|
3
|
+
const { findMissionDir, findCheckpoints, getFirstLine, inferSlug, missionBranchPrefix, missionBranchName } = require('../core/mission-utils');
|
|
4
|
+
const { WORKFLOW_AGENT_NAMES, eligibleAgentsForStep, readAgentConfigOrExit, workflowLauncherStatus } = require('../agents/agents');
|
|
5
|
+
const { getPrStatus } = require('../tools/forgejo');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const fmt = require('../core/fmt');
|
|
9
|
+
|
|
10
|
+
function parseWorktreeList(porcelain) {
|
|
11
|
+
const entries = [];
|
|
12
|
+
let current = null;
|
|
13
|
+
|
|
14
|
+
for (const line of porcelain.split('\n')) {
|
|
15
|
+
if (!line.trim()) {
|
|
16
|
+
if (current) {
|
|
17
|
+
entries.push(current);
|
|
18
|
+
current = null;
|
|
19
|
+
}
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (line.startsWith('worktree ')) {
|
|
24
|
+
if (current) entries.push(current);
|
|
25
|
+
current = { path: line.slice('worktree '.length).trim(), branch: null };
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (line.startsWith('branch ') && current) {
|
|
30
|
+
current.branch = line.slice('branch '.length).trim();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (current) entries.push(current);
|
|
35
|
+
return entries;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function findStaleMissionWorktrees({
|
|
39
|
+
gitRun = run,
|
|
40
|
+
findTaskFileFn = findTaskFile,
|
|
41
|
+
getTaskStatusFn = getTaskStatus,
|
|
42
|
+
primaryWorktree = null
|
|
43
|
+
} = {}) {
|
|
44
|
+
const result = gitRun('git', ['worktree', 'list', '--porcelain']);
|
|
45
|
+
if (result.status !== 0) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let resolvedPrimary = primaryWorktree;
|
|
50
|
+
if (!resolvedPrimary) {
|
|
51
|
+
try {
|
|
52
|
+
const { getPrimaryWorktree } = require('../core/mission-utils');
|
|
53
|
+
resolvedPrimary = getPrimaryWorktree();
|
|
54
|
+
} catch (_) {
|
|
55
|
+
resolvedPrimary = null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return parseWorktreeList(result.stdout)
|
|
60
|
+
.filter(entry => entry.path !== resolvedPrimary)
|
|
61
|
+
.map(entry => {
|
|
62
|
+
const branchRef = entry.branch || '';
|
|
63
|
+
const prefix = missionBranchPrefix(resolvedPrimary || process.cwd());
|
|
64
|
+
const normalizedRefPrefix = `refs/heads/${prefix}`;
|
|
65
|
+
const match = branchRef.startsWith(normalizedRefPrefix)
|
|
66
|
+
? [branchRef, branchRef.slice(normalizedRefPrefix.length)]
|
|
67
|
+
: null;
|
|
68
|
+
if (!match) return null;
|
|
69
|
+
|
|
70
|
+
const slug = match[1];
|
|
71
|
+
const taskFile = findTaskFileFn(slug);
|
|
72
|
+
const taskStatus = taskFile ? getTaskStatusFn(taskFile) : null;
|
|
73
|
+
if (taskStatus !== 'done' && taskFile) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
slug,
|
|
79
|
+
path: entry.path,
|
|
80
|
+
taskStatus: taskStatus || 'missing',
|
|
81
|
+
cleanupCommand: taskStatus === 'done'
|
|
82
|
+
? `scripts/cleanup-mission-worktree.sh ${slug}`
|
|
83
|
+
: `git worktree remove ${entry.path} && git branch -D ${missionBranchName(slug, resolvedPrimary || process.cwd())}`
|
|
84
|
+
};
|
|
85
|
+
})
|
|
86
|
+
.filter(Boolean);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function status(args, options = {}) {
|
|
90
|
+
const exit = options.exit || process.exit;
|
|
91
|
+
const log = options.log || fmt.log.plain;
|
|
92
|
+
const inferSlugFn = options.inferSlugFn || inferSlug;
|
|
93
|
+
const getCurrentBranchFn = options.getCurrentBranchFn || getCurrentBranch;
|
|
94
|
+
const findTaskFileFn = options.findTaskFileFn || findTaskFile;
|
|
95
|
+
const getTaskStatusFn = options.getTaskStatusFn || getTaskStatus;
|
|
96
|
+
const findMissionDirFn = options.findMissionDirFn || findMissionDir;
|
|
97
|
+
const findCheckpointsFn = options.findCheckpointsFn || findCheckpoints;
|
|
98
|
+
const getFirstLineFn = options.getFirstLineFn || getFirstLine;
|
|
99
|
+
const getPrStatusFn = options.getPrStatusFn || getPrStatus;
|
|
100
|
+
const findStaleMissionWorktreesFn = options.findStaleMissionWorktreesFn || findStaleMissionWorktrees;
|
|
101
|
+
const readAgentConfigOrExitFn = options.readAgentConfigOrExitFn || readAgentConfigOrExit;
|
|
102
|
+
const eligibleAgentsForStepFn = options.eligibleAgentsForStepFn || eligibleAgentsForStep;
|
|
103
|
+
const allWorkflowAgentNamesFn = options.allWorkflowAgentNamesFn || (() => WORKFLOW_AGENT_NAMES);
|
|
104
|
+
const workflowLauncherStatusFn = options.workflowLauncherStatusFn || workflowLauncherStatus;
|
|
105
|
+
const getLastThreeCommitsFn = options.getLastThreeCommitsFn || getLastThreeCommits;
|
|
106
|
+
const getUncommittedCountFn = options.getUncommittedCountFn || getUncommittedCount;
|
|
107
|
+
|
|
108
|
+
const explicitSlug = args[0];
|
|
109
|
+
const slug = inferSlugFn(explicitSlug);
|
|
110
|
+
|
|
111
|
+
log(fmt.bold('--- Mission Status ---'));
|
|
112
|
+
log(`Branch: ${fmt.branch(getCurrentBranchFn())}`);
|
|
113
|
+
log(`Worktree: ${fmt.path(process.cwd())}`);
|
|
114
|
+
|
|
115
|
+
if (slug) {
|
|
116
|
+
const taskFile = findTaskFileFn(slug);
|
|
117
|
+
const taskStatus = getTaskStatusFn(taskFile);
|
|
118
|
+
log(`Backlog status: ${taskStatus || 'unknown'}`);
|
|
119
|
+
|
|
120
|
+
const missionDir = findMissionDirFn(slug);
|
|
121
|
+
if (missionDir) {
|
|
122
|
+
const checkpoints = findCheckpointsFn(missionDir);
|
|
123
|
+
if (checkpoints.length > 0) {
|
|
124
|
+
const lastCP = checkpoints[checkpoints.length - 1];
|
|
125
|
+
const firstLine = getFirstLineFn(lastCP);
|
|
126
|
+
log(`Last checkpoint: ${path.basename(lastCP)} - ${firstLine}`);
|
|
127
|
+
} else {
|
|
128
|
+
log('Last checkpoint: none');
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
log('Last checkpoint: unknown');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Forgejo PR state
|
|
135
|
+
const pr = getPrStatusFn(missionBranchName(slug));
|
|
136
|
+
if (pr.exists) {
|
|
137
|
+
log(`Forgejo PR: #${pr.number} (${pr.state})`);
|
|
138
|
+
} else if (pr.raw) {
|
|
139
|
+
log(`Forgejo PR: unavailable (${pr.raw})`);
|
|
140
|
+
} else {
|
|
141
|
+
log('Forgejo PR: none');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!explicitSlug) {
|
|
146
|
+
const staleWorktrees = findStaleMissionWorktreesFn();
|
|
147
|
+
staleWorktrees.forEach(entry => {
|
|
148
|
+
log(`Stale worktree: ${fmt.path(entry.path)} (task: ${entry.taskStatus})`);
|
|
149
|
+
log(` Cleanup: ${fmt.command(entry.cleanupCommand)}`);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Agent eligibility matrix
|
|
154
|
+
const config = readAgentConfigOrExitFn();
|
|
155
|
+
const draftEligible = eligibleAgentsForStepFn('draft', { config });
|
|
156
|
+
const activeEligible = eligibleAgentsForStepFn('active', { config });
|
|
157
|
+
const baseAgents = allWorkflowAgentNamesFn();
|
|
158
|
+
const allAgents = [...new Set([...baseAgents, ...draftEligible, ...activeEligible])].sort();
|
|
159
|
+
const envOverride = process.env.WORKFLOW_AGENT;
|
|
160
|
+
log('Agent launcher matrix:');
|
|
161
|
+
for (const agent of allAgents) {
|
|
162
|
+
const launcher = workflowLauncherStatusFn(agent);
|
|
163
|
+
const support = launcher.supported ? 'supported' : 'blocked';
|
|
164
|
+
const draftMark = draftEligible.includes(agent) ? 'draft' : '-';
|
|
165
|
+
const activeMark = activeEligible.includes(agent) ? 'active' : '-';
|
|
166
|
+
log(` ${fmt.agent(agent)}: ${support} | eligible: ${draftMark},${activeMark}`);
|
|
167
|
+
}
|
|
168
|
+
if (envOverride) {
|
|
169
|
+
log(` (WORKFLOW_AGENT override: ${fmt.agent(envOverride)})`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const lastThree = getLastThreeCommitsFn();
|
|
173
|
+
log('Last 3 commits:');
|
|
174
|
+
lastThree.forEach(c => log(` - ${c}`));
|
|
175
|
+
|
|
176
|
+
log(`Uncommitted files: ${getUncommittedCountFn()}`);
|
|
177
|
+
log(fmt.bold('----------------------'));
|
|
178
|
+
exit(0);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = status;
|
|
182
|
+
module.exports.parseWorktreeList = parseWorktreeList;
|
|
183
|
+
module.exports.findStaleMissionWorktrees = findStaleMissionWorktrees;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('../core/verification');
|
package/lib/core/fmt.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared formatting and palette layer for workflow-owned output.
|
|
3
|
+
* Uses Node.js built-in util.styleText for color rendering (ADR 0042).
|
|
4
|
+
* No external dependencies.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { styleText } = require('node:util');
|
|
8
|
+
|
|
9
|
+
const colors = {
|
|
10
|
+
reset: 'reset',
|
|
11
|
+
bold: 'bold',
|
|
12
|
+
red: 'red',
|
|
13
|
+
green: 'green',
|
|
14
|
+
yellow: 'yellow',
|
|
15
|
+
blue: 'blue',
|
|
16
|
+
magenta: 'magenta',
|
|
17
|
+
cyan: 'cyan',
|
|
18
|
+
white: 'white',
|
|
19
|
+
dim: 'gray',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const statusMap = {
|
|
23
|
+
PASS: 'green',
|
|
24
|
+
FAIL: 'red',
|
|
25
|
+
WARN: 'yellow',
|
|
26
|
+
INFO: 'cyan',
|
|
27
|
+
DEBUG: 'gray',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const agentMap = {
|
|
31
|
+
codex: 'magenta',
|
|
32
|
+
claude: 'blue',
|
|
33
|
+
gemini: 'cyan',
|
|
34
|
+
qwen: 'yellow',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function colorize(format, text) {
|
|
38
|
+
return styleText(format, String(text ?? ''));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function stripAnsi(text) {
|
|
42
|
+
return String(text ?? '').replace(/\x1B\[[0-9;]*m/g, '');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function visibleWidth(text) {
|
|
46
|
+
return stripAnsi(text).length;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function padVisibleEnd(text, width) {
|
|
50
|
+
const stringValue = String(text ?? '');
|
|
51
|
+
const padLength = Math.max(0, width - visibleWidth(stringValue));
|
|
52
|
+
return stringValue + ' '.repeat(padLength);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Formats a status line prefix (e.g. "[PASS]").
|
|
57
|
+
*/
|
|
58
|
+
function status(type, text) {
|
|
59
|
+
const format = statusMap[type];
|
|
60
|
+
if (!format) return `[${type}] ${text}`;
|
|
61
|
+
return `${colorize(format, `[${type}]`)} ${text}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Formats an agent family name.
|
|
66
|
+
*/
|
|
67
|
+
function agent(family, text = family) {
|
|
68
|
+
let label = text;
|
|
69
|
+
if (family === 'qwen' && text === 'qwen') {
|
|
70
|
+
label = 'qwen (opencode)';
|
|
71
|
+
}
|
|
72
|
+
const format = agentMap[family];
|
|
73
|
+
if (!format) return label;
|
|
74
|
+
return colorize(format, label);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function bold(text) {
|
|
78
|
+
return colorize('bold', text);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function dim(text) {
|
|
82
|
+
return colorize('gray', text);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Aligned key-value pair.
|
|
87
|
+
*/
|
|
88
|
+
function kv(key, value, width = 30) {
|
|
89
|
+
return `${bold(key.padEnd(width))} ${value}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Aligned table of columns.
|
|
94
|
+
*/
|
|
95
|
+
function table(rows, { indent = 2, colPadding = 2 } = {}) {
|
|
96
|
+
if (rows.length === 0) return '';
|
|
97
|
+
const colWidths = rows[0].map((_, i) => Math.max(...rows.map(row => visibleWidth(row[i] ?? ''))));
|
|
98
|
+
const padding = ' '.repeat(indent);
|
|
99
|
+
return rows.map(row => padding + row.map((col, i) => padVisibleEnd(col ?? '', colWidths[i] + colPadding)).join('')).join('\n');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Bulleted list.
|
|
104
|
+
*/
|
|
105
|
+
function list(items, { bullet = '-', indent = 2 } = {}) {
|
|
106
|
+
const padding = ' '.repeat(indent);
|
|
107
|
+
return items.map(item => `${padding}${bullet} ${item}`).join('\n');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const semantic = {
|
|
111
|
+
path: (text) => colorize('blue', text),
|
|
112
|
+
slug: (text) => colorize('cyan', bold(text)),
|
|
113
|
+
branch: (text) => colorize('magenta', text),
|
|
114
|
+
sha: (text) => colorize('yellow', text),
|
|
115
|
+
command: (text) => colorize('green', text),
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
let currentLogger = {
|
|
119
|
+
log: (...args) => console.log(...args),
|
|
120
|
+
error: (...args) => console.error(...args),
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const log = {
|
|
124
|
+
info: (text) => {
|
|
125
|
+
return text.toString().split('\n').map(line => {
|
|
126
|
+
const s = status('INFO', line);
|
|
127
|
+
currentLogger.log(s);
|
|
128
|
+
return s;
|
|
129
|
+
}).join('\n');
|
|
130
|
+
},
|
|
131
|
+
pass: (text) => {
|
|
132
|
+
return text.toString().split('\n').map(line => {
|
|
133
|
+
const s = status('PASS', line);
|
|
134
|
+
currentLogger.log(s);
|
|
135
|
+
return s;
|
|
136
|
+
}).join('\n');
|
|
137
|
+
},
|
|
138
|
+
fail: (text) => {
|
|
139
|
+
return text.toString().split('\n').map(line => {
|
|
140
|
+
const s = status('FAIL', line);
|
|
141
|
+
currentLogger.error(s);
|
|
142
|
+
return s;
|
|
143
|
+
}).join('\n');
|
|
144
|
+
},
|
|
145
|
+
warn: (text) => {
|
|
146
|
+
return text.toString().split('\n').map(line => {
|
|
147
|
+
const s = status('WARN', line);
|
|
148
|
+
currentLogger.log(s);
|
|
149
|
+
return s;
|
|
150
|
+
}).join('\n');
|
|
151
|
+
},
|
|
152
|
+
error: (text) => {
|
|
153
|
+
return text.toString().split('\n').map(line => {
|
|
154
|
+
const s = status('FAIL', line);
|
|
155
|
+
currentLogger.error(s);
|
|
156
|
+
return s;
|
|
157
|
+
}).join('\n');
|
|
158
|
+
},
|
|
159
|
+
debug: (text) => {
|
|
160
|
+
if (!process.env.DEBUG) return null;
|
|
161
|
+
return text.toString().split('\n').map(line => {
|
|
162
|
+
const s = status('DEBUG', line);
|
|
163
|
+
currentLogger.log(s);
|
|
164
|
+
return s;
|
|
165
|
+
}).join('\n');
|
|
166
|
+
},
|
|
167
|
+
plain: (text) => {
|
|
168
|
+
text.toString().split('\n').forEach(line => currentLogger.log(line));
|
|
169
|
+
return text;
|
|
170
|
+
},
|
|
171
|
+
plainError: (text) => {
|
|
172
|
+
text.toString().split('\n').forEach(line => currentLogger.error(line));
|
|
173
|
+
return text;
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
function setLogger(newLogger) {
|
|
178
|
+
const old = currentLogger;
|
|
179
|
+
currentLogger = {
|
|
180
|
+
log: newLogger.log || newLogger,
|
|
181
|
+
error: newLogger.error || newLogger.log || newLogger,
|
|
182
|
+
};
|
|
183
|
+
return old;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = {
|
|
187
|
+
colors,
|
|
188
|
+
status,
|
|
189
|
+
agent,
|
|
190
|
+
bold,
|
|
191
|
+
dim,
|
|
192
|
+
colorize,
|
|
193
|
+
stripAnsi,
|
|
194
|
+
visibleWidth,
|
|
195
|
+
padVisibleEnd,
|
|
196
|
+
kv,
|
|
197
|
+
table,
|
|
198
|
+
list,
|
|
199
|
+
...semantic,
|
|
200
|
+
log,
|
|
201
|
+
setLogger,
|
|
202
|
+
};
|
package/lib/core/git.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const childProcess = require('child_process');
|
|
2
|
+
|
|
3
|
+
function git(args, options = {}) {
|
|
4
|
+
const spawnOptions = {
|
|
5
|
+
encoding: 'utf8',
|
|
6
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
7
|
+
...options
|
|
8
|
+
};
|
|
9
|
+
const result = childProcess.spawnSync('git', args, spawnOptions);
|
|
10
|
+
if (result.error && result.status == null) {
|
|
11
|
+
throw result.error;
|
|
12
|
+
}
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function run(command, args, options = {}) {
|
|
17
|
+
const spawnOptions = {
|
|
18
|
+
encoding: 'utf8',
|
|
19
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
20
|
+
...options
|
|
21
|
+
};
|
|
22
|
+
const result = childProcess.spawnSync(command, args, spawnOptions);
|
|
23
|
+
if (result.error && result.status == null) {
|
|
24
|
+
throw result.error;
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getCurrentBranch(cwd = process.cwd()) {
|
|
30
|
+
const result = git(['-C', cwd, 'branch', '--show-current']);
|
|
31
|
+
return result.stdout.trim();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getWorktreeStatus(cwd = process.cwd()) {
|
|
35
|
+
const result = git(['-C', cwd, 'status', '--porcelain']);
|
|
36
|
+
return result.stdout
|
|
37
|
+
.split('\n')
|
|
38
|
+
.map(line => line.trimEnd())
|
|
39
|
+
.filter(Boolean);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isDirty(cwd = process.cwd()) {
|
|
43
|
+
const result = git(['-C', cwd, 'status', '--porcelain']);
|
|
44
|
+
return result.stdout.trim().length > 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getUncommittedCount(cwd = process.cwd()) {
|
|
48
|
+
const result = git(['-C', cwd, 'status', '--porcelain']);
|
|
49
|
+
if (!result.stdout.trim()) return 0;
|
|
50
|
+
return result.stdout.trim().split('\n').length;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getLastCommit() {
|
|
54
|
+
const result = git(['log', '-1', '--format=%H|%ad|%s']);
|
|
55
|
+
const [sha, date, subject] = result.stdout.trim().split('|');
|
|
56
|
+
return { sha, date, subject };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getLastThreeCommits() {
|
|
60
|
+
const result = git(['log', '-3', '--format=%s']);
|
|
61
|
+
return result.stdout.trim().split('\n');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = {
|
|
65
|
+
git,
|
|
66
|
+
run,
|
|
67
|
+
getCurrentBranch,
|
|
68
|
+
getWorktreeStatus,
|
|
69
|
+
isDirty,
|
|
70
|
+
getUncommittedCount,
|
|
71
|
+
getLastCommit,
|
|
72
|
+
getLastThreeCommits
|
|
73
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Workflow-generated paths that must be gitignored in enterprise repositories.
|
|
6
|
+
* These are paths created by the px workflow toolkit that should never be
|
|
7
|
+
* committed to version control.
|
|
8
|
+
*/
|
|
9
|
+
const WORKFLOW_ENTRIES = [
|
|
10
|
+
'.workflow/',
|
|
11
|
+
'.sessions/',
|
|
12
|
+
'.forgejo-local/',
|
|
13
|
+
'workflow/.cache/',
|
|
14
|
+
'workflow/.sessions/',
|
|
15
|
+
'workflow/config/agents.local.json',
|
|
16
|
+
'agents.local.json',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Ensure that a repository's .gitignore contains all workflow-generated paths.
|
|
21
|
+
*
|
|
22
|
+
* Handles:
|
|
23
|
+
* - Missing .gitignore: creates one with all 7 entries
|
|
24
|
+
* - Existing .gitignore: appends only missing entries, skips duplicates
|
|
25
|
+
* - Symlinked .gitignore: skips modification, logs warning
|
|
26
|
+
* - Non-git directories: skips gracefully without crashing
|
|
27
|
+
*
|
|
28
|
+
* @param {string} rootDir - Path to the repository root
|
|
29
|
+
* @param {object} options - Injected dependencies
|
|
30
|
+
* @param {Function} [options.existsSyncFn] - fs.existsSync replacement
|
|
31
|
+
* @param {Function} [options.lstatSyncFn] - fs.lstatSync replacement
|
|
32
|
+
* @param {Function} [options.readFileSyncFn] - fs.readFileSync replacement
|
|
33
|
+
* @param {Function} [options.writeFileSyncFn] - fs.writeFileSync replacement
|
|
34
|
+
* @param {Function} [options.logFn] - Logger function
|
|
35
|
+
* @returns {{ ok: boolean, created: boolean, appended: number, skipped: boolean, reason?: string }}
|
|
36
|
+
*/
|
|
37
|
+
function ensureWorkflowGitignore(rootDir, options = {}) {
|
|
38
|
+
const {
|
|
39
|
+
existsSyncFn = fs.existsSync,
|
|
40
|
+
lstatSyncFn = fs.lstatSync,
|
|
41
|
+
readFileSyncFn = fs.readFileSync,
|
|
42
|
+
writeFileSyncFn = fs.writeFileSync,
|
|
43
|
+
logFn = () => {},
|
|
44
|
+
} = options;
|
|
45
|
+
|
|
46
|
+
const gitignorePath = path.join(rootDir, '.gitignore');
|
|
47
|
+
const gitDirPath = path.join(rootDir, '.git');
|
|
48
|
+
|
|
49
|
+
// Check if this is a git repository
|
|
50
|
+
if (!existsSyncFn(gitDirPath)) {
|
|
51
|
+
logFn(`[gitignore] Skipping ${rootDir}: not a git repository (no .git directory)`);
|
|
52
|
+
return { ok: true, created: false, appended: 0, skipped: true, reason: 'not-a-git-repo' };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Check if .gitignore exists
|
|
56
|
+
if (!existsSyncFn(gitignorePath)) {
|
|
57
|
+
// Create new .gitignore with all entries
|
|
58
|
+
const content = WORKFLOW_ENTRIES.join('\n') + '\n';
|
|
59
|
+
writeFileSyncFn(gitignorePath, content, 'utf8');
|
|
60
|
+
logFn(`[gitignore] Created ${gitignorePath} with ${WORKFLOW_ENTRIES.length} entries`);
|
|
61
|
+
return { ok: true, created: true, appended: WORKFLOW_ENTRIES.length, skipped: false };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// .gitignore exists - check for symlink
|
|
65
|
+
try {
|
|
66
|
+
const stat = lstatSyncFn(gitignorePath);
|
|
67
|
+
if (stat.isSymbolicLink()) {
|
|
68
|
+
logFn(`[gitignore] Skipping ${gitignorePath}: symbolic link, not modifying`);
|
|
69
|
+
return { ok: true, created: false, appended: 0, skipped: true, reason: 'symlink' };
|
|
70
|
+
}
|
|
71
|
+
} catch (_) {
|
|
72
|
+
// If we can't stat, fall through to read - it might exist but be inaccessible
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Read existing .gitignore
|
|
76
|
+
let existingContent;
|
|
77
|
+
try {
|
|
78
|
+
existingContent = readFileSyncFn(gitignorePath, 'utf8');
|
|
79
|
+
} catch (err) {
|
|
80
|
+
logFn(`[gitignore] Could not read ${gitignorePath}: ${err.message}`);
|
|
81
|
+
return { ok: false, created: false, appended: 0, skipped: false, reason: `read-error: ${err.message}` };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Parse existing entries
|
|
85
|
+
const existingLines = existingContent.split(/\r?\n/);
|
|
86
|
+
const existingEntries = new Set(
|
|
87
|
+
existingLines
|
|
88
|
+
.map(line => line.trim())
|
|
89
|
+
.filter(line => line.length > 0 && !line.startsWith('#'))
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// Find missing entries
|
|
93
|
+
const missingEntries = WORKFLOW_ENTRIES.filter(entry => !existingEntries.has(entry));
|
|
94
|
+
|
|
95
|
+
if (missingEntries.length === 0) {
|
|
96
|
+
logFn(`[gitignore] ${gitignorePath} already contains all ${WORKFLOW_ENTRIES.length} workflow entries`);
|
|
97
|
+
return { ok: true, created: false, appended: 0, skipped: false };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Append missing entries
|
|
101
|
+
const newContent = existingContent + '\n' + missingEntries.join('\n') + '\n';
|
|
102
|
+
writeFileSyncFn(gitignorePath, newContent, 'utf8');
|
|
103
|
+
logFn(`[gitignore] Appended ${missingEntries.length} missing entries to ${gitignorePath}`);
|
|
104
|
+
|
|
105
|
+
return { ok: true, created: false, appended: missingEntries.length, skipped: false };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = ensureWorkflowGitignore;
|
|
109
|
+
module.exports.ensureWorkflowGitignore = ensureWorkflowGitignore;
|
|
110
|
+
module.exports.WORKFLOW_ENTRIES = WORKFLOW_ENTRIES;
|