universal-dev-standards 6.7.4 → 6.8.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/bin/uds.js +7 -0
- package/bundled/ai/standards/ai-instruction-standards.ai.yaml +6 -6
- package/bundled/core/ai-instruction-standards.md +9 -7
- package/bundled/locales/zh-CN/CHANGELOG.md +32 -3
- package/bundled/locales/zh-CN/README.md +6 -6
- package/bundled/locales/zh-CN/SECURITY.md +1 -1
- package/bundled/locales/zh-CN/core/ai-instruction-standards.md +10 -8
- package/bundled/locales/zh-CN/docs/CHEATSHEET.md +20 -6
- package/bundled/locales/zh-CN/docs/FEATURE-REFERENCE.md +149 -11
- package/bundled/locales/zh-CN/skills/agents/README.md +1 -1
- package/bundled/locales/zh-CN/skills/reverse-engineer/tdd-analysis.md +13 -23
- package/bundled/locales/zh-CN/skills/spec-derivation/SKILL.md +5 -1
- package/bundled/locales/zh-CN/skills/workflows/README.md +2 -11
- package/bundled/locales/zh-TW/CHANGELOG.md +32 -3
- package/bundled/locales/zh-TW/README.md +6 -6
- package/bundled/locales/zh-TW/SECURITY.md +1 -1
- package/bundled/locales/zh-TW/core/ai-instruction-standards.md +10 -8
- package/bundled/locales/zh-TW/docs/CHEATSHEET.md +20 -6
- package/bundled/locales/zh-TW/docs/FEATURE-REFERENCE.md +149 -11
- package/bundled/locales/zh-TW/skills/agents/README.md +1 -1
- package/bundled/locales/zh-TW/skills/reverse-engineer/tdd-analysis.md +13 -23
- package/bundled/locales/zh-TW/skills/spec-derivation/SKILL.md +5 -1
- package/bundled/locales/zh-TW/skills/workflows/README.md +2 -11
- package/bundled/skills/agents/README.md +1 -1
- package/bundled/skills/audit-assistant/SKILL.md +24 -1
- package/bundled/skills/changelog-guide/SKILL.md +24 -1
- package/bundled/skills/commit-standards/SKILL.md +24 -1
- package/bundled/skills/docs-generator/SKILL.md +24 -1
- package/bundled/skills/project-discovery/SKILL.md +24 -1
- package/bundled/skills/release-standards/SKILL.md +24 -1
- package/bundled/skills/reverse-engineer/SKILL.md +24 -1
- package/bundled/skills/reverse-engineer/tdd-analysis.md +16 -23
- package/bundled/skills/spec-derivation/SKILL.md +24 -1
- package/bundled/skills/workflows/README.md +2 -11
- package/package.json +4 -2
- package/src/commands/lint.js +96 -0
- package/src/commands/quickstart.js +16 -13
- package/src/i18n/messages.js +3 -3
- package/src/reconciler/actual-state-scanner.js +14 -3
- package/src/utils/hasher.js +63 -8
- package/src/utils/integration-generator.js +21 -3
- package/src/utils/skills-installer.js +17 -3
- package/src/utils/spec-linter.js +35 -76
- package/standards-registry.json +7 -7
- package/src/commands/sync.js +0 -133
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
getCommandsSupportedAgents,
|
|
20
20
|
getCommandFileExtension
|
|
21
21
|
} from '../config/ai-agent-paths.js';
|
|
22
|
-
import { computeDirectoryHashes, computeFileHash } from './hasher.js';
|
|
22
|
+
import { computeDirectoryHashes, computeFileHash, normalizeLineEndings } from './hasher.js';
|
|
23
23
|
import { isLocalizedLocale } from './locale.js';
|
|
24
24
|
import { getSkillsSourceDir } from './skills-source.js';
|
|
25
25
|
|
|
@@ -504,6 +504,16 @@ export function resolveSkillFiles(skillName, locale = 'en') {
|
|
|
504
504
|
* are different installs, and a hash over contents alone would call a rename
|
|
505
505
|
* "unchanged".
|
|
506
506
|
*
|
|
507
|
+
* Content is line-ending normalized before hashing (GitHub issue #155): the
|
|
508
|
+
* counterpart on the actual-state side, `hashInstalledSkillDir` in
|
|
509
|
+
* `reconciler/actual-state-scanner.js`, hashes files that were checked out
|
|
510
|
+
* into the adopter's own repo and so may have been rewritten to CRLF by
|
|
511
|
+
* `core.autocrlf=true` on Windows. Both sides must apply the same
|
|
512
|
+
* normalization or every skill would report as changed on Windows, same as
|
|
513
|
+
* the `.standards/*` hashes this issue was filed about — the two functions
|
|
514
|
+
* only stay comparable by construction if they agree on this too, which is
|
|
515
|
+
* why the paired tests in `skill-content-hash.test.js` exist.
|
|
516
|
+
*
|
|
507
517
|
* @param {Array<{name: string, content: string}>} files
|
|
508
518
|
* @returns {string|null} `sha256:<hex>`, or null for an empty resolution
|
|
509
519
|
*/
|
|
@@ -513,7 +523,7 @@ export function computeSkillContentHash(files) {
|
|
|
513
523
|
for (const f of files) {
|
|
514
524
|
h.update(f.name);
|
|
515
525
|
h.update('\0');
|
|
516
|
-
h.update(f.content);
|
|
526
|
+
h.update(normalizeLineEndings(f.content));
|
|
517
527
|
h.update('\0');
|
|
518
528
|
}
|
|
519
529
|
return `sha256:${h.digest('hex')}`;
|
|
@@ -779,13 +789,17 @@ export function resolveCommandContent(cmdName, agent, locale = 'en') {
|
|
|
779
789
|
*
|
|
780
790
|
* Same `sha256:` shape as `computeSkillContentHash` so the two sides of the
|
|
781
791
|
* diff can be compared without caring which category an entry came from.
|
|
792
|
+
* Line-ending normalized before hashing for the same reason as
|
|
793
|
+
* `computeSkillContentHash` (GitHub issue #155) — its actual-state
|
|
794
|
+
* counterpart in `reconciler/actual-state-scanner.js` hashes an installed
|
|
795
|
+
* command file that may have been checked out as CRLF on Windows.
|
|
782
796
|
*
|
|
783
797
|
* @param {string|null} content
|
|
784
798
|
* @returns {string|null}
|
|
785
799
|
*/
|
|
786
800
|
export function computeCommandContentHash(content) {
|
|
787
801
|
if (content === null || content === undefined) return null;
|
|
788
|
-
return `sha256:${createHash('sha256').update(content).digest('hex')}`;
|
|
802
|
+
return `sha256:${createHash('sha256').update(normalizeLineEndings(content)).digest('hex')}`;
|
|
789
803
|
}
|
|
790
804
|
|
|
791
805
|
function installSingleCommand(cmdName, targetDir, agent, locale = 'en') {
|
package/src/utils/spec-linter.js
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
* Spec Linter — Stateless analysis functions for spec quality checks
|
|
3
3
|
* @module utils/spec-linter
|
|
4
4
|
* @see specs/superspec-borrowing-phase1-2-spec.md (AC-11, AC-12, AC-13)
|
|
5
|
+
*
|
|
6
|
+
* XSPEC-383 R5 (Option E): `checkACCoverage` / `collectTestFiles` / `scanDir`
|
|
7
|
+
* were removed here. They shipped 2026-04-07, had passing unit tests, and were
|
|
8
|
+
* never wired to any CLI command — `uds lint` did not exist. 2026-08-19,
|
|
9
|
+
* registering `uds lint` and running it against VibeOps's 93 real specs
|
|
10
|
+
* surfaced that the removed check would have reported 0/98 ACs covered on
|
|
11
|
+
* every one of them, for two independent reasons: it derived AC identifiers
|
|
12
|
+
* positionally (`AC-1`, `AC-2`, …) instead of reading the ones a spec
|
|
13
|
+
* declares, and it hardcoded the `@AC-N` tag convention from this repo's own
|
|
14
|
+
* `skills/ac-coverage`, while VibeOps's tests tag coverage as `AC-045-001`
|
|
15
|
+
* without an `@` prefix. Neither project is "wrong" — they never agreed on a
|
|
16
|
+
* convention — but a linter that always reports zero regardless of actual
|
|
17
|
+
* coverage is worse than no linter: it looks like a working gate. Redoing AC
|
|
18
|
+
* coverage requires deciding how identifiers are read and how conventions are
|
|
19
|
+
* negotiated across adopters; that is a new design, not a patch, and is out
|
|
20
|
+
* of scope for R5.
|
|
5
21
|
*/
|
|
6
22
|
|
|
7
23
|
import { readFileSync, readdirSync, existsSync } from 'fs';
|
|
@@ -9,35 +25,6 @@ import { join, basename } from 'path';
|
|
|
9
25
|
import { StandardValidator } from './standard-validator.js';
|
|
10
26
|
import { MicroSpec } from '../vibe/micro-spec.js';
|
|
11
27
|
|
|
12
|
-
/**
|
|
13
|
-
* Check if a spec's ACs are referenced in test files
|
|
14
|
-
* @param {string} specId - e.g. "SPEC-001"
|
|
15
|
-
* @param {string[]} acIds - e.g. ["AC-1", "AC-2", "AC-3"]
|
|
16
|
-
* @param {string} projectPath - Project root directory
|
|
17
|
-
* @returns {{ covered: string[], orphans: string[], coverage: number }}
|
|
18
|
-
*/
|
|
19
|
-
export function checkACCoverage(specId, acIds, projectPath) {
|
|
20
|
-
const covered = [];
|
|
21
|
-
const orphans = [];
|
|
22
|
-
|
|
23
|
-
// Collect all test file contents
|
|
24
|
-
const testContent = collectTestFiles(projectPath);
|
|
25
|
-
|
|
26
|
-
for (const acId of acIds) {
|
|
27
|
-
// Search for @AC-N pattern in test files
|
|
28
|
-
const pattern = new RegExp(`@${acId}\\b`);
|
|
29
|
-
if (testContent.some(content => pattern.test(content))) {
|
|
30
|
-
covered.push(acId);
|
|
31
|
-
} else {
|
|
32
|
-
orphans.push(acId);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const coverage = acIds.length > 0 ? covered.length / acIds.length : 0;
|
|
37
|
-
|
|
38
|
-
return { covered, orphans, coverage };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
28
|
/**
|
|
42
29
|
* Validate depends_on references exist
|
|
43
30
|
* @param {Object[]} specs - Array of specs with { id, dependsOn }
|
|
@@ -75,14 +62,22 @@ export function checkSpecSize(specFilePath, options = {}) {
|
|
|
75
62
|
}
|
|
76
63
|
|
|
77
64
|
/**
|
|
78
|
-
* Run all lint checks on specs in a project
|
|
65
|
+
* Run all lint checks (dependency validity + size) on specs in a project.
|
|
66
|
+
*
|
|
67
|
+
* `specsDirExists` is a three-state signal, not decoration: "no specs
|
|
68
|
+
* directory" and "specs directory with zero problems" must not collapse into
|
|
69
|
+
* the same `{ pass: 0, warn: 0, fail: 0 }` shape, or a caller cannot tell
|
|
70
|
+
* "nothing was checked" from "everything is fine" (XSPEC-383 R4/R5's own
|
|
71
|
+
* standing rule for this repo's gates — see check-module-reachability.mjs and
|
|
72
|
+
* check-command-existence.mjs).
|
|
73
|
+
*
|
|
79
74
|
* @param {string} projectPath - Project root directory
|
|
80
|
-
* @returns {{ results: Object[], summary: { pass: number, warn: number, fail: number } }}
|
|
75
|
+
* @returns {{ results: Object[], summary: { pass: number, warn: number, fail: number }, specsDir: string, specsDirExists: boolean }}
|
|
81
76
|
*/
|
|
82
77
|
export function lintAll(projectPath) {
|
|
83
78
|
const specsDir = join(projectPath, 'specs');
|
|
84
79
|
if (!existsSync(specsDir)) {
|
|
85
|
-
return { results: [], summary: { pass: 0, warn: 0, fail: 0 } };
|
|
80
|
+
return { results: [], summary: { pass: 0, warn: 0, fail: 0 }, specsDir: 'specs', specsDirExists: false };
|
|
86
81
|
}
|
|
87
82
|
|
|
88
83
|
// Load all specs
|
|
@@ -102,10 +97,6 @@ export function lintAll(projectPath) {
|
|
|
102
97
|
const summary = { pass: 0, warn: 0, fail: 0 };
|
|
103
98
|
|
|
104
99
|
for (const spec of allSpecs) {
|
|
105
|
-
// AC coverage
|
|
106
|
-
const acIds = (spec.acceptance || []).map((_, i) => `AC-${i + 1}`);
|
|
107
|
-
const acCoverage = checkACCoverage(spec.id, acIds, projectPath);
|
|
108
|
-
|
|
109
100
|
// Dependencies for this spec
|
|
110
101
|
const specBroken = depResults.broken.filter(b => b.spec === spec.id);
|
|
111
102
|
const specValid = depResults.valid.filter(v => v.spec === spec.id);
|
|
@@ -116,22 +107,18 @@ export function lintAll(projectPath) {
|
|
|
116
107
|
const size = checkSpecSize(specPath);
|
|
117
108
|
|
|
118
109
|
// Determine worst status
|
|
119
|
-
let
|
|
120
|
-
if (specBroken.length > 0 || size.status === 'fail'
|
|
121
|
-
|
|
122
|
-
} else if (size.status === 'warn'
|
|
123
|
-
|
|
110
|
+
let status = 'pass';
|
|
111
|
+
if (specBroken.length > 0 || size.status === 'fail') {
|
|
112
|
+
status = 'fail';
|
|
113
|
+
} else if (size.status === 'warn') {
|
|
114
|
+
status = 'warn';
|
|
124
115
|
}
|
|
125
116
|
|
|
126
|
-
summary[
|
|
117
|
+
summary[status]++;
|
|
127
118
|
|
|
128
119
|
results.push({
|
|
129
120
|
spec: spec.id,
|
|
130
|
-
|
|
131
|
-
covered: acCoverage.covered,
|
|
132
|
-
orphans: acCoverage.orphans,
|
|
133
|
-
coverage: acCoverage.coverage,
|
|
134
|
-
},
|
|
121
|
+
status,
|
|
135
122
|
deps,
|
|
136
123
|
size: {
|
|
137
124
|
effectiveLines: size.effectiveLines,
|
|
@@ -140,33 +127,5 @@ export function lintAll(projectPath) {
|
|
|
140
127
|
});
|
|
141
128
|
}
|
|
142
129
|
|
|
143
|
-
return { results, summary };
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// ─── Internal helpers ───
|
|
147
|
-
|
|
148
|
-
function collectTestFiles(projectPath) {
|
|
149
|
-
const contents = [];
|
|
150
|
-
const testDirs = ['tests', 'test', '__tests__', 'cli/tests'];
|
|
151
|
-
|
|
152
|
-
for (const dir of testDirs) {
|
|
153
|
-
const fullDir = join(projectPath, dir);
|
|
154
|
-
if (existsSync(fullDir)) {
|
|
155
|
-
scanDir(fullDir, contents);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return contents;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function scanDir(dir, contents) {
|
|
163
|
-
const entries = readdirSync(dir, { withFileTypes: true });
|
|
164
|
-
for (const entry of entries) {
|
|
165
|
-
const fullPath = join(dir, entry.name);
|
|
166
|
-
if (entry.isDirectory()) {
|
|
167
|
-
scanDir(fullPath, contents);
|
|
168
|
-
} else if (entry.name.match(/\.(test|spec)\.(js|ts|mjs|cjs)$/)) {
|
|
169
|
-
contents.push(readFileSync(fullPath, 'utf-8'));
|
|
170
|
-
}
|
|
171
|
-
}
|
|
130
|
+
return { results, summary, specsDir: 'specs', specsDirExists: true };
|
|
172
131
|
}
|
package/standards-registry.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.8.0",
|
|
4
4
|
"lastUpdated": "2026-05-13",
|
|
5
5
|
"description": "Standards registry for universal-dev-standards with integrated skills and AI-optimized formats",
|
|
6
6
|
"formats": {
|
|
@@ -58,14 +58,14 @@
|
|
|
58
58
|
"standards": {
|
|
59
59
|
"name": "universal-dev-standards",
|
|
60
60
|
"url": "https://github.com/AsiaOstrich/universal-dev-standards",
|
|
61
|
-
"version": "6.
|
|
61
|
+
"version": "6.8.0"
|
|
62
62
|
},
|
|
63
63
|
"skills": {
|
|
64
64
|
"name": "universal-dev-standards",
|
|
65
65
|
"url": "https://github.com/AsiaOstrich/universal-dev-standards",
|
|
66
66
|
"localPath": "skills",
|
|
67
67
|
"rawUrl": "https://raw.githubusercontent.com/AsiaOstrich/universal-dev-standards/main/skills",
|
|
68
|
-
"version": "6.
|
|
68
|
+
"version": "6.8.0",
|
|
69
69
|
"note": "Skills are now included in the main repository under skills/"
|
|
70
70
|
}
|
|
71
71
|
},
|
|
@@ -2260,7 +2260,7 @@
|
|
|
2260
2260
|
"id": "license-compliance",
|
|
2261
2261
|
"name": "License Compliance Standards",
|
|
2262
2262
|
"nameZh": "授權合規標準",
|
|
2263
|
-
"version": "6.
|
|
2263
|
+
"version": "6.8.0",
|
|
2264
2264
|
"source": {
|
|
2265
2265
|
"human": "core/license-compliance.md",
|
|
2266
2266
|
"ai": "ai/standards/license-compliance.ai.yaml"
|
|
@@ -2272,7 +2272,7 @@
|
|
|
2272
2272
|
"id": "verification-oracle",
|
|
2273
2273
|
"name": "Verification Oracle Standards",
|
|
2274
2274
|
"nameZh": "驗證 Oracle 標準",
|
|
2275
|
-
"version": "6.
|
|
2275
|
+
"version": "6.8.0",
|
|
2276
2276
|
"source": {
|
|
2277
2277
|
"human": "core/verification-oracle.md",
|
|
2278
2278
|
"ai": "ai/standards/verification-oracle.ai.yaml"
|
|
@@ -2284,7 +2284,7 @@
|
|
|
2284
2284
|
"id": "model-provenance",
|
|
2285
2285
|
"name": "Model Provenance Policy Standards",
|
|
2286
2286
|
"nameZh": "模型來源政策標準",
|
|
2287
|
-
"version": "6.
|
|
2287
|
+
"version": "6.8.0",
|
|
2288
2288
|
"source": {
|
|
2289
2289
|
"human": "core/model-provenance.md",
|
|
2290
2290
|
"ai": "ai/standards/model-provenance.ai.yaml"
|
|
@@ -2296,7 +2296,7 @@
|
|
|
2296
2296
|
"id": "resource-cost-boundary",
|
|
2297
2297
|
"name": "Resource / Cost Boundary Declaration Standards",
|
|
2298
2298
|
"nameZh": "資源/成本邊界宣告標準",
|
|
2299
|
-
"version": "6.
|
|
2299
|
+
"version": "6.8.0",
|
|
2300
2300
|
"source": {
|
|
2301
2301
|
"human": "core/resource-cost-boundary.md",
|
|
2302
2302
|
"ai": "ai/standards/resource-cost-boundary.ai.yaml"
|
package/src/commands/sync.js
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Sync Command — Export current work context for session resume
|
|
3
|
-
* Zero AI tokens — pure shell/JS execution
|
|
4
|
-
*
|
|
5
|
-
* @module commands/sync
|
|
6
|
-
* @see specs/superspec-borrowing-phase1-2-spec.md (AC-16, AC-17)
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { execSync } from 'child_process';
|
|
10
|
-
import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync } from 'fs';
|
|
11
|
-
import { join } from 'path';
|
|
12
|
-
|
|
13
|
-
const MAX_LINES = 500;
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Generate context.md from git info and optional workflow state
|
|
17
|
-
* @param {string} projectPath - Project root directory
|
|
18
|
-
* @returns {string} Path to generated context.md
|
|
19
|
-
*/
|
|
20
|
-
export function generateContext(projectPath) {
|
|
21
|
-
const lines = [];
|
|
22
|
-
|
|
23
|
-
lines.push('# Development Context');
|
|
24
|
-
lines.push(`> Auto-generated by \`uds sync\` at ${new Date().toISOString()}`);
|
|
25
|
-
lines.push('> This file helps restore context in a new AI session.');
|
|
26
|
-
lines.push('');
|
|
27
|
-
|
|
28
|
-
// ─── Git Status ───
|
|
29
|
-
lines.push('## Git Status');
|
|
30
|
-
lines.push('');
|
|
31
|
-
|
|
32
|
-
const branch = gitCmd('git rev-parse --abbrev-ref HEAD', projectPath);
|
|
33
|
-
lines.push(`- **Branch**: ${branch}`);
|
|
34
|
-
|
|
35
|
-
const baseBranch = detectBaseBranch(projectPath);
|
|
36
|
-
if (baseBranch && baseBranch !== branch) {
|
|
37
|
-
const commitCount = gitCmd(`git rev-list --count ${baseBranch}..HEAD`, projectPath) || '0';
|
|
38
|
-
lines.push(`- **Base**: ${baseBranch} (+${commitCount} commits)`);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
lines.push('');
|
|
42
|
-
lines.push('### Recent Commits');
|
|
43
|
-
lines.push('');
|
|
44
|
-
|
|
45
|
-
const logOutput = gitCmd('git log --oneline -20', projectPath);
|
|
46
|
-
if (logOutput) {
|
|
47
|
-
lines.push(...logOutput.split('\n').map(l => `- ${l}`));
|
|
48
|
-
} else {
|
|
49
|
-
lines.push('(no commits)');
|
|
50
|
-
}
|
|
51
|
-
lines.push('');
|
|
52
|
-
|
|
53
|
-
lines.push('### Changed Files');
|
|
54
|
-
lines.push('');
|
|
55
|
-
|
|
56
|
-
if (baseBranch && baseBranch !== branch) {
|
|
57
|
-
const diffStat = gitCmd(`git diff --stat ${baseBranch}..HEAD`, projectPath);
|
|
58
|
-
if (diffStat) {
|
|
59
|
-
lines.push('```');
|
|
60
|
-
lines.push(...diffStat.split('\n'));
|
|
61
|
-
lines.push('```');
|
|
62
|
-
}
|
|
63
|
-
} else {
|
|
64
|
-
lines.push('(on base branch, no diff)');
|
|
65
|
-
}
|
|
66
|
-
lines.push('');
|
|
67
|
-
|
|
68
|
-
// ─── Workflow State ───
|
|
69
|
-
const workflowState = readWorkflowState(projectPath);
|
|
70
|
-
if (workflowState) {
|
|
71
|
-
lines.push('## Workflow State');
|
|
72
|
-
lines.push('');
|
|
73
|
-
lines.push(...workflowState);
|
|
74
|
-
lines.push('');
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Truncate to MAX_LINES
|
|
78
|
-
const truncated = lines.slice(0, MAX_LINES);
|
|
79
|
-
|
|
80
|
-
// Write to .workflow-state/context.md
|
|
81
|
-
const outputDir = join(projectPath, '.workflow-state');
|
|
82
|
-
mkdirSync(outputDir, { recursive: true });
|
|
83
|
-
const outputPath = join(outputDir, 'context.md');
|
|
84
|
-
writeFileSync(outputPath, truncated.join('\n'), 'utf-8');
|
|
85
|
-
|
|
86
|
-
return outputPath;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// ─── Helpers ───
|
|
90
|
-
|
|
91
|
-
function gitCmd(cmd, cwd) {
|
|
92
|
-
try {
|
|
93
|
-
return execSync(cmd, { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
|
|
94
|
-
} catch {
|
|
95
|
-
return '';
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function detectBaseBranch(projectPath) {
|
|
100
|
-
// Check for common base branches
|
|
101
|
-
for (const name of ['main', 'master']) {
|
|
102
|
-
const exists = gitCmd(`git rev-parse --verify ${name}`, projectPath);
|
|
103
|
-
if (exists) return name;
|
|
104
|
-
}
|
|
105
|
-
return null;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function readWorkflowState(projectPath) {
|
|
109
|
-
const stateDir = join(projectPath, '.workflow-state');
|
|
110
|
-
if (!existsSync(stateDir)) return null;
|
|
111
|
-
|
|
112
|
-
const yamlFiles = readdirSync(stateDir)
|
|
113
|
-
.filter(f => f.endsWith('.yaml') && !f.endsWith('.log.yaml'))
|
|
114
|
-
.sort();
|
|
115
|
-
|
|
116
|
-
if (yamlFiles.length === 0) return null;
|
|
117
|
-
|
|
118
|
-
// Read the most recent state file
|
|
119
|
-
const latestFile = yamlFiles[yamlFiles.length - 1];
|
|
120
|
-
const content = readFileSync(join(stateDir, latestFile), 'utf-8');
|
|
121
|
-
|
|
122
|
-
const lines = [];
|
|
123
|
-
// Extract key fields
|
|
124
|
-
const specMatch = content.match(/spec_id:\s*(.+)/);
|
|
125
|
-
const phaseMatch = content.match(/current_phase:\s*(.+)/);
|
|
126
|
-
const statusMatch = content.match(/status:\s*(.+)/);
|
|
127
|
-
|
|
128
|
-
if (specMatch) lines.push(`- **Spec**: ${specMatch[1].trim()}`);
|
|
129
|
-
if (phaseMatch) lines.push(`- **Phase**: ${phaseMatch[1].trim()}`);
|
|
130
|
-
if (statusMatch) lines.push(`- **Status**: ${statusMatch[1].trim()}`);
|
|
131
|
-
|
|
132
|
-
return lines.length > 0 ? lines : null;
|
|
133
|
-
}
|