project-tiny-context-harness 0.2.49 → 0.2.51
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/LICENSE +21 -21
- package/README.md +247 -216
- package/assets/README.md +295 -258
- package/assets/agents/.gitkeep +1 -1
- package/assets/agents/AGENTS_CORE.md +56 -56
- package/assets/context_templates/architecture.md +31 -31
- package/assets/context_templates/area.md +24 -24
- package/assets/context_templates/context.toml +27 -27
- package/assets/context_templates/deployment.md +35 -35
- package/assets/context_templates/global.md +53 -53
- package/assets/context_templates/verification.md +28 -28
- package/assets/github/.gitkeep +1 -1
- package/assets/github/harness.yml +25 -25
- package/assets/make/.gitkeep +1 -1
- package/assets/make/sdlc-harness.mk +33 -33
- package/assets/skills/context_development_engineer/SKILL.md +64 -64
- package/assets/skills/context_full_project_export/SKILL.md +13 -13
- package/assets/skills/context_product_plan/SKILL.md +69 -69
- package/assets/skills/context_uiux_design/SKILL.md +110 -110
- package/assets/tools/validate_context.py +276 -276
- package/dist/commands/export-context.js +5 -5
- package/dist/commands/index.js +12 -11
- package/dist/commands/package-source.js +2 -2
- package/dist/commands/upgrade.d.ts +1 -1
- package/dist/commands/upgrade.js +56 -1
- package/dist/lib/migrations.d.ts +31 -3
- package/dist/lib/migrations.js +260 -11
- package/dist/lib/sync-engine.d.ts +4 -1
- package/dist/lib/sync-engine.js +9 -1
- package/dist/lib/upgrade.js +14 -4
- package/migrations/README.md +3 -3
- package/package.json +68 -68
- package/source-mappings.yaml +17 -17
package/dist/commands/upgrade.js
CHANGED
|
@@ -1,7 +1,62 @@
|
|
|
1
|
+
import { createUpgradePlan, formatUpgradePlan, hasUpgradePlanWork, updateModeForPlan } from "../lib/migrations.js";
|
|
1
2
|
import { runUpgrade } from "../lib/upgrade.js";
|
|
2
|
-
export async function upgrade() {
|
|
3
|
+
export async function upgrade(args = []) {
|
|
4
|
+
const options = parseArgs(args);
|
|
5
|
+
if (options.help) {
|
|
6
|
+
printHelp();
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
if (options.check) {
|
|
10
|
+
const plan = await createUpgradePlan(process.cwd());
|
|
11
|
+
if (options.json) {
|
|
12
|
+
console.log(JSON.stringify({ mode: updateModeForPlan(plan), ...plan }, null, 2));
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
for (const line of formatUpgradePlan(plan)) {
|
|
16
|
+
console.log(line);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (hasUpgradePlanWork(plan)) {
|
|
20
|
+
process.exitCode = 1;
|
|
21
|
+
}
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
3
24
|
const report = await runUpgrade(process.cwd());
|
|
25
|
+
if (options.json) {
|
|
26
|
+
console.log(JSON.stringify({ lines: report }, null, 2));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
4
29
|
for (const line of report) {
|
|
5
30
|
console.log(line);
|
|
6
31
|
}
|
|
7
32
|
}
|
|
33
|
+
function parseArgs(args) {
|
|
34
|
+
const options = { check: false, json: false, help: false };
|
|
35
|
+
for (const arg of args) {
|
|
36
|
+
if (arg === "--check") {
|
|
37
|
+
options.check = true;
|
|
38
|
+
}
|
|
39
|
+
else if (arg === "--json") {
|
|
40
|
+
options.json = true;
|
|
41
|
+
}
|
|
42
|
+
else if (arg === "--help" || arg === "-h") {
|
|
43
|
+
options.help = true;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
throw new Error(`unknown upgrade argument: ${arg}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return options;
|
|
50
|
+
}
|
|
51
|
+
function printHelp() {
|
|
52
|
+
console.log(`sdlc-harness upgrade:
|
|
53
|
+
upgrade Run safe migrations, sync managed assets and doctor
|
|
54
|
+
upgrade --check Print the upgrade plan without writing files
|
|
55
|
+
upgrade --check --json
|
|
56
|
+
Print the upgrade plan as JSON
|
|
57
|
+
|
|
58
|
+
Update modes:
|
|
59
|
+
sync-only No migrations are pending
|
|
60
|
+
upgrade-required Safe migrations are pending
|
|
61
|
+
manual-required Manual review or blockers are present`);
|
|
62
|
+
}
|
package/dist/lib/migrations.d.ts
CHANGED
|
@@ -1,11 +1,39 @@
|
|
|
1
|
+
export type UpgradePlanItemStatus = "safe_pending" | "manual_required" | "blocked";
|
|
2
|
+
export interface UpgradePlanItem {
|
|
3
|
+
id: string;
|
|
4
|
+
introducedIn: string;
|
|
5
|
+
description: string;
|
|
6
|
+
scope: string;
|
|
7
|
+
status: UpgradePlanItemStatus;
|
|
8
|
+
message: string;
|
|
9
|
+
path?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface UpgradePlan {
|
|
12
|
+
safe_pending: UpgradePlanItem[];
|
|
13
|
+
manual_required: UpgradePlanItem[];
|
|
14
|
+
blocked: UpgradePlanItem[];
|
|
15
|
+
}
|
|
16
|
+
export type ReleaseUpdateMode = "sync-only" | "upgrade-required" | "manual-required";
|
|
1
17
|
export interface Migration {
|
|
2
|
-
|
|
3
|
-
|
|
18
|
+
id: string;
|
|
19
|
+
introducedIn: string;
|
|
4
20
|
description: string;
|
|
21
|
+
scope: string;
|
|
22
|
+
risk: "safe" | "manual";
|
|
23
|
+
manualMessage: string;
|
|
24
|
+
detect(projectRoot: string, root: string, migrationId: string): Promise<UpgradePlanItem[]>;
|
|
25
|
+
apply?(projectRoot: string, root: string, report: MigrationReport): Promise<void>;
|
|
26
|
+
verify(projectRoot: string, root: string): Promise<void>;
|
|
5
27
|
}
|
|
6
|
-
export declare const migrations: Migration[];
|
|
7
28
|
export interface MigrationReport {
|
|
8
29
|
changed: string[];
|
|
9
30
|
skipped: string[];
|
|
31
|
+
manualRequired: UpgradePlanItem[];
|
|
32
|
+
blocked: UpgradePlanItem[];
|
|
10
33
|
}
|
|
34
|
+
export declare const migrations: Migration[];
|
|
35
|
+
export declare function createUpgradePlan(projectRoot: string): Promise<UpgradePlan>;
|
|
36
|
+
export declare function hasUpgradePlanWork(plan: UpgradePlan): boolean;
|
|
37
|
+
export declare function updateModeForPlan(plan: UpgradePlan): ReleaseUpdateMode;
|
|
38
|
+
export declare function formatUpgradePlan(plan: UpgradePlan): string[];
|
|
11
39
|
export declare function runMigrations(projectRoot: string): Promise<MigrationReport>;
|
package/dist/lib/migrations.js
CHANGED
|
@@ -2,21 +2,135 @@ import path from "node:path";
|
|
|
2
2
|
import { promises as fs } from "node:fs";
|
|
3
3
|
import { CONTEXT_MANIFEST_PATH, contextManifestFromExistingAreas } from "./context-manifest.js";
|
|
4
4
|
import { architectureContextTemplate, areaContextTemplate, globalContextTemplate, verificationContextTemplate } from "./context-templates.js";
|
|
5
|
+
import { CURRENT_SCHEMA_VERSION } from "./constants.js";
|
|
5
6
|
import { defaultConfig, readConfig } from "./config.js";
|
|
6
7
|
import { createDesignMdIfMissing, DESIGN_MD_PATH } from "./design-md.js";
|
|
7
8
|
import { ensureDir, listFiles, pathExists, readText, writeTextIfChanged } from "./fs.js";
|
|
8
9
|
import { harnessConfigPath, harnessRoot } from "./harness-root.js";
|
|
9
10
|
import { stringifyYaml } from "./yaml.js";
|
|
10
|
-
|
|
11
|
+
async function verifyNoop() {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
export const migrations = [
|
|
15
|
+
{
|
|
16
|
+
id: "schema-v4-config-refresh",
|
|
17
|
+
introducedIn: "0.2.0",
|
|
18
|
+
description: "Refresh Harness config core metadata and managed-file list for Schema v4.",
|
|
19
|
+
scope: "<harnessRoot>/config.yaml",
|
|
20
|
+
risk: "safe",
|
|
21
|
+
manualMessage: "Review Harness config manually if custom managed-file paths still drift after upgrade.",
|
|
22
|
+
detect: detectConfigRefresh,
|
|
23
|
+
apply: migrateConfig,
|
|
24
|
+
verify: verifyNoop
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: "legacy-modules-to-areas",
|
|
28
|
+
introducedIn: "0.2.0",
|
|
29
|
+
description: "Move legacy project_context/modules/**/*.md files to project_context/areas/**/*.md.",
|
|
30
|
+
scope: "project_context/modules/**",
|
|
31
|
+
risk: "safe",
|
|
32
|
+
manualMessage: "Resolve target conflicts manually; the Harness will not overwrite existing area Context files.",
|
|
33
|
+
detect: detectLegacyModulesToAreas,
|
|
34
|
+
apply: migrateLegacyModulesToAreas,
|
|
35
|
+
verify: verifyNoop
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "context-manifest-baseline",
|
|
39
|
+
introducedIn: "0.2.0",
|
|
40
|
+
description: "Create the Schema v4 project_context/context.toml manifest when missing.",
|
|
41
|
+
scope: "project_context/context.toml and project_context/areas/**",
|
|
42
|
+
risk: "safe",
|
|
43
|
+
manualMessage: "Review deep area files without manifest roles and assign explicit context roles when needed.",
|
|
44
|
+
detect: detectContextManifestBaseline,
|
|
45
|
+
apply: migrateContextManifest,
|
|
46
|
+
verify: verifyNoop
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: "global-context-v4-sections",
|
|
50
|
+
introducedIn: "0.2.0",
|
|
51
|
+
description: "Add missing Schema v4 global Context sections and rewrite legacy module links.",
|
|
52
|
+
scope: "project_context/global.md",
|
|
53
|
+
risk: "safe",
|
|
54
|
+
manualMessage: "Review global Context manually if project-specific long-term facts need refinement.",
|
|
55
|
+
detect: detectGlobalContextSections,
|
|
56
|
+
apply: migrateGlobalContextSections,
|
|
57
|
+
verify: verifyNoop
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "design-md-baseline",
|
|
61
|
+
introducedIn: "0.2.0",
|
|
62
|
+
description: "Create DESIGN.md for existing Harness projects when missing.",
|
|
63
|
+
scope: "DESIGN.md",
|
|
64
|
+
risk: "safe",
|
|
65
|
+
manualMessage: "Review the starter design baseline and replace it with project-specific visual facts when available.",
|
|
66
|
+
detect: detectDesignMdBaseline,
|
|
67
|
+
apply: migrateDesignMd,
|
|
68
|
+
verify: verifyNoop
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: "deprecated-skill-overrides",
|
|
72
|
+
introducedIn: "0.2.0",
|
|
73
|
+
description: "Report deprecated managed skill overrides that must move to standalone project-local Skills.",
|
|
74
|
+
scope: "<harnessRoot>/pjsdlc_managed/override_skills/**",
|
|
75
|
+
risk: "manual",
|
|
76
|
+
manualMessage: "Move override files into standalone project-local Skills before running sync.",
|
|
77
|
+
detect: detectDeprecatedSkillOverrides,
|
|
78
|
+
verify: verifyNoop
|
|
79
|
+
}
|
|
80
|
+
];
|
|
81
|
+
export async function createUpgradePlan(projectRoot) {
|
|
82
|
+
const root = await harnessRoot(projectRoot);
|
|
83
|
+
const plan = { safe_pending: [], manual_required: [], blocked: [] };
|
|
84
|
+
for (const migration of migrations) {
|
|
85
|
+
for (const item of await migration.detect(projectRoot, root, migration.id)) {
|
|
86
|
+
plan[item.status].push(item);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return plan;
|
|
90
|
+
}
|
|
91
|
+
export function hasUpgradePlanWork(plan) {
|
|
92
|
+
return plan.safe_pending.length > 0 || plan.manual_required.length > 0 || plan.blocked.length > 0;
|
|
93
|
+
}
|
|
94
|
+
export function updateModeForPlan(plan) {
|
|
95
|
+
if (plan.manual_required.length > 0 || plan.blocked.length > 0) {
|
|
96
|
+
return "manual-required";
|
|
97
|
+
}
|
|
98
|
+
if (plan.safe_pending.length > 0) {
|
|
99
|
+
return "upgrade-required";
|
|
100
|
+
}
|
|
101
|
+
return "sync-only";
|
|
102
|
+
}
|
|
103
|
+
export function formatUpgradePlan(plan) {
|
|
104
|
+
const lines = [
|
|
105
|
+
`upgrade plan mode=${updateModeForPlan(plan)} safe_pending=${plan.safe_pending.length} manual_required=${plan.manual_required.length} blocked=${plan.blocked.length}`
|
|
106
|
+
];
|
|
107
|
+
for (const status of ["safe_pending", "manual_required", "blocked"]) {
|
|
108
|
+
for (const item of plan[status]) {
|
|
109
|
+
const location = item.path ? ` ${item.path}` : "";
|
|
110
|
+
lines.push(`${status}: ${item.id}${location} - ${item.message}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return lines;
|
|
114
|
+
}
|
|
11
115
|
export async function runMigrations(projectRoot) {
|
|
12
|
-
const report = { changed: [], skipped: [] };
|
|
116
|
+
const report = { changed: [], skipped: [], manualRequired: [], blocked: [] };
|
|
13
117
|
const root = await harnessRoot(projectRoot);
|
|
14
|
-
await
|
|
15
|
-
|
|
118
|
+
const plan = await createUpgradePlan(projectRoot);
|
|
119
|
+
report.manualRequired.push(...plan.manual_required);
|
|
120
|
+
report.blocked.push(...plan.blocked);
|
|
121
|
+
for (const migration of migrations) {
|
|
122
|
+
if (!migration.apply) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (!plan.safe_pending.some((item) => item.id === migration.id)) {
|
|
126
|
+
report.skipped.push(migration.id);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
await migration.apply(projectRoot, root, report);
|
|
130
|
+
await migration.verify(projectRoot, root);
|
|
131
|
+
}
|
|
16
132
|
await migrateBaseProjectContext(projectRoot, report);
|
|
17
|
-
await migrateContextManifest(projectRoot, report);
|
|
18
133
|
await migrateManifestModulePaths(projectRoot, report);
|
|
19
|
-
await migrateDesignMd(projectRoot, report);
|
|
20
134
|
return report;
|
|
21
135
|
}
|
|
22
136
|
async function migrateBaseProjectContext(projectRoot, report) {
|
|
@@ -40,12 +154,30 @@ async function migrateBaseProjectContext(projectRoot, report) {
|
|
|
40
154
|
report.skipped.push(relative);
|
|
41
155
|
}
|
|
42
156
|
}
|
|
43
|
-
await migrateGlobalContextSections(projectRoot, report);
|
|
44
157
|
}
|
|
45
|
-
async function
|
|
158
|
+
async function detectGlobalContextSections(projectRoot, _root, migration) {
|
|
46
159
|
const relative = "project_context/global.md";
|
|
47
160
|
const target = path.join(projectRoot, ...relative.split("/"));
|
|
48
161
|
if (!(await pathExists(target))) {
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
const original = await readText(target);
|
|
165
|
+
const rewritten = rewriteLegacyModuleReferences(original);
|
|
166
|
+
const needsSections = !hasHeading(rewritten, "Architecture Context") ||
|
|
167
|
+
!hasHeading(rewritten, "Context Graph") ||
|
|
168
|
+
!hasHeading(rewritten, "Context Index");
|
|
169
|
+
if (!needsSections && rewritten === original) {
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
return [
|
|
173
|
+
item(migration, "safe_pending", relative, "Global Context needs Schema v4 sections or legacy module path rewrites.")
|
|
174
|
+
];
|
|
175
|
+
}
|
|
176
|
+
async function migrateGlobalContextSections(projectRoot, _root, report) {
|
|
177
|
+
const relative = "project_context/global.md";
|
|
178
|
+
const target = path.join(projectRoot, ...relative.split("/"));
|
|
179
|
+
if (!(await pathExists(target))) {
|
|
180
|
+
report.skipped.push(relative);
|
|
49
181
|
return;
|
|
50
182
|
}
|
|
51
183
|
const original = await readText(target);
|
|
@@ -61,14 +193,31 @@ async function migrateGlobalContextSections(projectRoot, report) {
|
|
|
61
193
|
additions.push("## Context Index", "", "- See `project_context/context.toml` for the current area and context node list.", "");
|
|
62
194
|
}
|
|
63
195
|
if (additions.length === 0 && rewritten === original) {
|
|
196
|
+
report.skipped.push(`${relative}#schema-v4-sections`);
|
|
64
197
|
return;
|
|
65
198
|
}
|
|
66
199
|
const next = additions.length === 0 ? rewritten : `${rewritten.replace(/\s*$/, "\n\n")}${additions.join("\n")}`;
|
|
67
200
|
if (await writeTextIfChanged(target, next)) {
|
|
68
201
|
report.changed.push(`${relative}#schema-v4-sections`);
|
|
69
202
|
}
|
|
203
|
+
else {
|
|
204
|
+
report.skipped.push(`${relative}#schema-v4-sections`);
|
|
205
|
+
}
|
|
70
206
|
}
|
|
71
|
-
async function
|
|
207
|
+
async function detectContextManifestBaseline(projectRoot, _root, migration) {
|
|
208
|
+
const manifestPath = path.join(projectRoot, CONTEXT_MANIFEST_PATH);
|
|
209
|
+
if (await pathExists(manifestPath)) {
|
|
210
|
+
return [];
|
|
211
|
+
}
|
|
212
|
+
const items = [
|
|
213
|
+
item(migration, "safe_pending", CONTEXT_MANIFEST_PATH, "Context graph manifest is missing and can be created from existing area files.")
|
|
214
|
+
];
|
|
215
|
+
for (const ambiguous of await ambiguousAreaContextFiles(projectRoot)) {
|
|
216
|
+
items.push(item(migration, "manual_required", ambiguous, "Deep area Context cannot be safely role-classified by path alone; review context.toml after upgrade."));
|
|
217
|
+
}
|
|
218
|
+
return items;
|
|
219
|
+
}
|
|
220
|
+
async function migrateContextManifest(projectRoot, _root, report) {
|
|
72
221
|
const manifestPath = path.join(projectRoot, CONTEXT_MANIFEST_PATH);
|
|
73
222
|
if (await pathExists(manifestPath)) {
|
|
74
223
|
report.skipped.push(CONTEXT_MANIFEST_PATH);
|
|
@@ -81,7 +230,26 @@ async function migrateContextManifest(projectRoot, report) {
|
|
|
81
230
|
report.skipped.push(CONTEXT_MANIFEST_PATH);
|
|
82
231
|
}
|
|
83
232
|
}
|
|
84
|
-
async function
|
|
233
|
+
async function detectLegacyModulesToAreas(projectRoot, _root, migration) {
|
|
234
|
+
const modulesRoot = path.join(projectRoot, "project_context", "modules");
|
|
235
|
+
const areasRoot = path.join(projectRoot, "project_context", "areas");
|
|
236
|
+
const moduleFiles = (await listFiles(modulesRoot)).filter((file) => file.endsWith(".md")).sort();
|
|
237
|
+
const items = [];
|
|
238
|
+
for (const source of moduleFiles) {
|
|
239
|
+
const relativeToModules = path.relative(modulesRoot, source);
|
|
240
|
+
const sourceRelative = `project_context/modules/${relativeToModules.split(path.sep).join("/")}`;
|
|
241
|
+
const targetRelative = `project_context/areas/${relativeToModules.split(path.sep).join("/")}`;
|
|
242
|
+
const target = path.join(areasRoot, relativeToModules);
|
|
243
|
+
if (await pathExists(target)) {
|
|
244
|
+
items.push(item(migration, "blocked", sourceRelative, `Cannot move to ${targetRelative} because the target already exists.`));
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
items.push(item(migration, "safe_pending", sourceRelative, `Move to ${targetRelative}.`));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return items;
|
|
251
|
+
}
|
|
252
|
+
async function migrateLegacyModulesToAreas(projectRoot, _root, report) {
|
|
85
253
|
const modulesRoot = path.join(projectRoot, "project_context", "modules");
|
|
86
254
|
const areasRoot = path.join(projectRoot, "project_context", "areas");
|
|
87
255
|
const moduleFiles = (await listFiles(modulesRoot)).filter((file) => file.endsWith(".md")).sort();
|
|
@@ -120,7 +288,12 @@ async function migrateManifestModulePaths(projectRoot, report) {
|
|
|
120
288
|
report.changed.push(`${CONTEXT_MANIFEST_PATH}#areas-paths`);
|
|
121
289
|
}
|
|
122
290
|
}
|
|
123
|
-
async function
|
|
291
|
+
async function detectDesignMdBaseline(projectRoot, _root, migration) {
|
|
292
|
+
return (await pathExists(path.join(projectRoot, DESIGN_MD_PATH)))
|
|
293
|
+
? []
|
|
294
|
+
: [item(migration, "safe_pending", DESIGN_MD_PATH, "DESIGN.md is missing and can be created with the package baseline.")];
|
|
295
|
+
}
|
|
296
|
+
async function migrateDesignMd(projectRoot, _root, report) {
|
|
124
297
|
if (await createDesignMdIfMissing(projectRoot)) {
|
|
125
298
|
report.changed.push(DESIGN_MD_PATH);
|
|
126
299
|
}
|
|
@@ -128,6 +301,26 @@ async function migrateDesignMd(projectRoot, report) {
|
|
|
128
301
|
report.skipped.push(DESIGN_MD_PATH);
|
|
129
302
|
}
|
|
130
303
|
}
|
|
304
|
+
async function detectConfigRefresh(projectRoot, root, migration) {
|
|
305
|
+
const relativeConfigPath = await harnessConfigPath(projectRoot);
|
|
306
|
+
const configPath = path.join(projectRoot, relativeConfigPath);
|
|
307
|
+
if (!(await pathExists(configPath))) {
|
|
308
|
+
return [];
|
|
309
|
+
}
|
|
310
|
+
const config = await readConfig(projectRoot);
|
|
311
|
+
const current = defaultConfig(root);
|
|
312
|
+
const managedMatches = JSON.stringify(config.managed_files) === JSON.stringify(current.managed_files);
|
|
313
|
+
const neverOverwriteHasDefaults = current.never_overwrite.every((entry) => config.never_overwrite.includes(entry));
|
|
314
|
+
if (config.core.package === current.core.package &&
|
|
315
|
+
config.core.schema_version === CURRENT_SCHEMA_VERSION &&
|
|
316
|
+
managedMatches &&
|
|
317
|
+
neverOverwriteHasDefaults) {
|
|
318
|
+
return [];
|
|
319
|
+
}
|
|
320
|
+
return [
|
|
321
|
+
item(migration, "safe_pending", relativeConfigPath, "Harness config needs current package metadata, schema version or managed-file defaults.")
|
|
322
|
+
];
|
|
323
|
+
}
|
|
131
324
|
async function migrateConfig(projectRoot, root, report) {
|
|
132
325
|
const relativeConfigPath = await harnessConfigPath(projectRoot);
|
|
133
326
|
const configPath = path.join(projectRoot, relativeConfigPath);
|
|
@@ -147,6 +340,52 @@ async function migrateConfig(projectRoot, root, report) {
|
|
|
147
340
|
report.skipped.push(relativeConfigPath);
|
|
148
341
|
}
|
|
149
342
|
}
|
|
343
|
+
async function detectDeprecatedSkillOverrides(projectRoot, root, migration) {
|
|
344
|
+
const overrideRoot = path.join(projectRoot, root, "pjsdlc_managed", "override_skills");
|
|
345
|
+
if (!(await pathExists(overrideRoot))) {
|
|
346
|
+
return [];
|
|
347
|
+
}
|
|
348
|
+
const deprecatedFiles = (await listFiles(overrideRoot))
|
|
349
|
+
.filter((file) => path.basename(file) !== ".gitkeep")
|
|
350
|
+
.map((file) => path.relative(projectRoot, file).split(path.sep).join("/"))
|
|
351
|
+
.sort();
|
|
352
|
+
return deprecatedFiles.map((file) => item(migration, "manual_required", file, "Skill overrides are no longer supported; move rules into a standalone project-local Skill before relying on sync."));
|
|
353
|
+
}
|
|
354
|
+
async function ambiguousAreaContextFiles(projectRoot) {
|
|
355
|
+
const areasRoot = path.join(projectRoot, "project_context", "areas");
|
|
356
|
+
const areaFiles = (await listFiles(areasRoot)).filter((file) => file.endsWith(".md")).sort();
|
|
357
|
+
const ambiguous = [];
|
|
358
|
+
for (const file of areaFiles) {
|
|
359
|
+
const relativeToAreas = path.relative(areasRoot, file).split(path.sep).join("/");
|
|
360
|
+
if (!relativeToAreas.includes("/")) {
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
if (inferredRoleContext(relativeToAreas)) {
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
const base = path.basename(relativeToAreas, ".md").toLowerCase();
|
|
367
|
+
if (base === "readme" || base === "index") {
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
ambiguous.push(`project_context/areas/${relativeToAreas}`);
|
|
371
|
+
}
|
|
372
|
+
return ambiguous;
|
|
373
|
+
}
|
|
374
|
+
function item(migrationId, status, pathLabel, message) {
|
|
375
|
+
const migration = migrations.find((entry) => entry.id === migrationId);
|
|
376
|
+
if (!migration) {
|
|
377
|
+
throw new Error(`Unknown migration id: ${migrationId}`);
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
id: migration.id,
|
|
381
|
+
introducedIn: migration.introducedIn,
|
|
382
|
+
description: migration.description,
|
|
383
|
+
scope: migration.scope,
|
|
384
|
+
status,
|
|
385
|
+
path: pathLabel,
|
|
386
|
+
message
|
|
387
|
+
};
|
|
388
|
+
}
|
|
150
389
|
function hasHeading(content, heading) {
|
|
151
390
|
const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
152
391
|
return new RegExp(`^##\\s+${escaped}\\s*$`, "im").test(content);
|
|
@@ -156,6 +395,16 @@ function rewriteLegacyModuleReferences(content) {
|
|
|
156
395
|
.replace(/project_context\/modules\//g, "project_context/areas/")
|
|
157
396
|
.replace(/\(modules\//g, "(areas/");
|
|
158
397
|
}
|
|
398
|
+
function inferredRoleContext(relativeToAreas) {
|
|
399
|
+
const normalized = relativeToAreas.toLowerCase();
|
|
400
|
+
if (normalized.endsWith("/verification.md") || normalized === "verification.md") {
|
|
401
|
+
return "verification";
|
|
402
|
+
}
|
|
403
|
+
if (normalized.endsWith("/deployment.md") || normalized === "deployment.md") {
|
|
404
|
+
return "deployment";
|
|
405
|
+
}
|
|
406
|
+
return undefined;
|
|
407
|
+
}
|
|
159
408
|
function ensureManifestDefaultArea(content) {
|
|
160
409
|
if (/^\s*default\s*=\s*true\s*$/im.test(content)) {
|
|
161
410
|
return content;
|
|
@@ -3,5 +3,8 @@ export interface SyncReport {
|
|
|
3
3
|
skipped: string[];
|
|
4
4
|
blocked: string[];
|
|
5
5
|
}
|
|
6
|
+
export interface SyncOptions {
|
|
7
|
+
allowPendingMigrations?: boolean;
|
|
8
|
+
}
|
|
6
9
|
export declare function emptySyncReport(): SyncReport;
|
|
7
|
-
export declare function runSync(projectRoot: string): Promise<SyncReport>;
|
|
10
|
+
export declare function runSync(projectRoot: string, options?: SyncOptions): Promise<SyncReport>;
|
package/dist/lib/sync-engine.js
CHANGED
|
@@ -6,6 +6,7 @@ import { copyTree, listFiles, pathExists, readText, writeTextIfChanged } from ".
|
|
|
6
6
|
import { AGENTS_BLOCK_MARKERS, GITHUB_WORKFLOW_BLOCK_END, GITHUB_WORKFLOW_BLOCK_START, MAKEFILE_BLOCK_END, MAKEFILE_BLOCK_MARKERS, MAKEFILE_BLOCK_START, MANAGED_BLOCK_END, MANAGED_BLOCK_START } from "./managed-file.js";
|
|
7
7
|
import { packageAssetPath } from "./paths.js";
|
|
8
8
|
import { assertSupportedSchema } from "./schema-guard.js";
|
|
9
|
+
import { createUpgradePlan, formatUpgradePlan, hasUpgradePlanWork } from "./migrations.js";
|
|
9
10
|
export function emptySyncReport() {
|
|
10
11
|
return {
|
|
11
12
|
changed: [],
|
|
@@ -13,11 +14,18 @@ export function emptySyncReport() {
|
|
|
13
14
|
blocked: []
|
|
14
15
|
};
|
|
15
16
|
}
|
|
16
|
-
export async function runSync(projectRoot) {
|
|
17
|
+
export async function runSync(projectRoot, options = {}) {
|
|
17
18
|
await assertSupportedSchema(projectRoot, "sync");
|
|
18
19
|
const root = await harnessRoot(projectRoot);
|
|
19
20
|
const config = await readConfig(projectRoot);
|
|
20
21
|
const report = emptySyncReport();
|
|
22
|
+
if (!options.allowPendingMigrations) {
|
|
23
|
+
const upgradePlan = await createUpgradePlan(projectRoot);
|
|
24
|
+
if (hasUpgradePlanWork(upgradePlan)) {
|
|
25
|
+
report.blocked.push(`upgrade required before sync: ${formatUpgradePlan(upgradePlan).join(" | ")}`);
|
|
26
|
+
return report;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
21
29
|
await blockDeprecatedSkillOverrides(projectRoot, root, report);
|
|
22
30
|
if (report.blocked.length > 0) {
|
|
23
31
|
return report;
|
package/dist/lib/upgrade.js
CHANGED
|
@@ -1,20 +1,30 @@
|
|
|
1
1
|
import { runDoctor } from "./doctor.js";
|
|
2
|
-
import { runMigrations } from "./migrations.js";
|
|
2
|
+
import { formatUpgradePlan, runMigrations } from "./migrations.js";
|
|
3
3
|
import { assertSupportedSchema } from "./schema-guard.js";
|
|
4
4
|
import { runSync } from "./sync-engine.js";
|
|
5
5
|
export async function runUpgrade(projectRoot) {
|
|
6
6
|
const lines = [];
|
|
7
7
|
await assertSupportedSchema(projectRoot, "upgrade");
|
|
8
8
|
const migrationReport = await runMigrations(projectRoot);
|
|
9
|
-
lines.push(`migrations changed=${migrationReport.changed.length} skipped=${migrationReport.skipped.length}`);
|
|
10
|
-
|
|
9
|
+
lines.push(`migrations changed=${migrationReport.changed.length} skipped=${migrationReport.skipped.length} manual_required=${migrationReport.manualRequired.length} blocked=${migrationReport.blocked.length}`);
|
|
10
|
+
if (migrationReport.manualRequired.length > 0 || migrationReport.blocked.length > 0) {
|
|
11
|
+
lines.push(...formatUpgradePlan({
|
|
12
|
+
safe_pending: [],
|
|
13
|
+
manual_required: migrationReport.manualRequired,
|
|
14
|
+
blocked: migrationReport.blocked
|
|
15
|
+
}).slice(1));
|
|
16
|
+
}
|
|
17
|
+
const syncReport = await runSync(projectRoot, { allowPendingMigrations: true });
|
|
11
18
|
lines.push(`sync changed=${syncReport.changed.length} skipped=${syncReport.skipped.length} blocked=${syncReport.blocked.length}`);
|
|
12
19
|
for (const skipped of syncReport.skipped.filter((line) => line.includes("customized"))) {
|
|
13
20
|
lines.push(`sync skipped: ${skipped}`);
|
|
14
21
|
}
|
|
15
22
|
const doctor = await runDoctor(projectRoot);
|
|
16
23
|
lines.push(`doctor warnings=${doctor.warnings.length} errors=${doctor.errors.length}`);
|
|
17
|
-
if (
|
|
24
|
+
if (migrationReport.manualRequired.length > 0 ||
|
|
25
|
+
migrationReport.blocked.length > 0 ||
|
|
26
|
+
syncReport.blocked.length > 0 ||
|
|
27
|
+
doctor.errors.length > 0) {
|
|
18
28
|
throw new Error("upgrade completed with blockers");
|
|
19
29
|
}
|
|
20
30
|
return lines;
|
package/migrations/README.md
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
# Migrations
|
|
2
|
-
|
|
3
|
-
Schema migrations for `.harness/config.yaml` and managed file layout belong here.
|
|
1
|
+
# Migrations
|
|
2
|
+
|
|
3
|
+
Schema migrations for `.harness/config.yaml` and managed file layout belong here.
|
package/package.json
CHANGED
|
@@ -1,68 +1,68 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "project-tiny-context-harness",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Minimal project memory and validation harness for AI coding agents.",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"author": "Seven128",
|
|
7
|
-
"homepage": "https://github.com/Seven128/project-tiny-context-harness#readme",
|
|
8
|
-
"repository": {
|
|
9
|
-
"type": "git",
|
|
10
|
-
"url": "git+https://github.com/Seven128/project-tiny-context-harness.git",
|
|
11
|
-
"directory": "packages/sdlc-harness"
|
|
12
|
-
},
|
|
13
|
-
"bugs": {
|
|
14
|
-
"url": "https://github.com/Seven128/project-tiny-context-harness/issues"
|
|
15
|
-
},
|
|
16
|
-
"keywords": [
|
|
17
|
-
"ai-agents",
|
|
18
|
-
"coding-agent",
|
|
19
|
-
"codex",
|
|
20
|
-
"claude-code",
|
|
21
|
-
"cursor",
|
|
22
|
-
"gemini-cli",
|
|
23
|
-
"opencode",
|
|
24
|
-
"agent-context",
|
|
25
|
-
"context-engineering",
|
|
26
|
-
"context-management",
|
|
27
|
-
"agents-md",
|
|
28
|
-
"project-memory",
|
|
29
|
-
"agent-memory",
|
|
30
|
-
"ai-coding",
|
|
31
|
-
"multi-agent",
|
|
32
|
-
"llm",
|
|
33
|
-
"developer-tools",
|
|
34
|
-
"developer-productivity",
|
|
35
|
-
"cli",
|
|
36
|
-
"sdlc",
|
|
37
|
-
"workflow"
|
|
38
|
-
],
|
|
39
|
-
"type": "module",
|
|
40
|
-
"bin": {
|
|
41
|
-
"sdlc-harness": "dist/cli.js"
|
|
42
|
-
},
|
|
43
|
-
"files": [
|
|
44
|
-
"README.md",
|
|
45
|
-
"dist",
|
|
46
|
-
"assets",
|
|
47
|
-
"migrations",
|
|
48
|
-
"source-mappings.yaml"
|
|
49
|
-
],
|
|
50
|
-
"scripts": {
|
|
51
|
-
"build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json",
|
|
52
|
-
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
53
|
-
"test": "npm run build && node --test ../../tests/sdlc-harness/*.test.mjs",
|
|
54
|
-
"prepack": "npm run build"
|
|
55
|
-
},
|
|
56
|
-
"engines": {
|
|
57
|
-
"node": ">=20"
|
|
58
|
-
},
|
|
59
|
-
"dependencies": {
|
|
60
|
-
"@google/design.md": "^0.2.0",
|
|
61
|
-
"impeccable": "^2.3.2",
|
|
62
|
-
"yaml": "^2.9.0"
|
|
63
|
-
},
|
|
64
|
-
"devDependencies": {
|
|
65
|
-
"@types/node": "^24.0.0",
|
|
66
|
-
"typescript": "^5.5.0"
|
|
67
|
-
}
|
|
68
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "project-tiny-context-harness",
|
|
3
|
+
"version": "0.2.51",
|
|
4
|
+
"description": "Minimal project memory and validation harness for AI coding agents.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Seven128",
|
|
7
|
+
"homepage": "https://github.com/Seven128/project-tiny-context-harness#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/Seven128/project-tiny-context-harness.git",
|
|
11
|
+
"directory": "packages/sdlc-harness"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/Seven128/project-tiny-context-harness/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"ai-agents",
|
|
18
|
+
"coding-agent",
|
|
19
|
+
"codex",
|
|
20
|
+
"claude-code",
|
|
21
|
+
"cursor",
|
|
22
|
+
"gemini-cli",
|
|
23
|
+
"opencode",
|
|
24
|
+
"agent-context",
|
|
25
|
+
"context-engineering",
|
|
26
|
+
"context-management",
|
|
27
|
+
"agents-md",
|
|
28
|
+
"project-memory",
|
|
29
|
+
"agent-memory",
|
|
30
|
+
"ai-coding",
|
|
31
|
+
"multi-agent",
|
|
32
|
+
"llm",
|
|
33
|
+
"developer-tools",
|
|
34
|
+
"developer-productivity",
|
|
35
|
+
"cli",
|
|
36
|
+
"sdlc",
|
|
37
|
+
"workflow"
|
|
38
|
+
],
|
|
39
|
+
"type": "module",
|
|
40
|
+
"bin": {
|
|
41
|
+
"sdlc-harness": "dist/cli.js"
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"README.md",
|
|
45
|
+
"dist",
|
|
46
|
+
"assets",
|
|
47
|
+
"migrations",
|
|
48
|
+
"source-mappings.yaml"
|
|
49
|
+
],
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json",
|
|
52
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
53
|
+
"test": "npm run build && node --test ../../tests/sdlc-harness/*.test.mjs",
|
|
54
|
+
"prepack": "npm run build"
|
|
55
|
+
},
|
|
56
|
+
"engines": {
|
|
57
|
+
"node": ">=20"
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"@google/design.md": "^0.2.0",
|
|
61
|
+
"impeccable": "^2.3.2",
|
|
62
|
+
"yaml": "^2.9.0"
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"@types/node": "^24.0.0",
|
|
66
|
+
"typescript": "^5.5.0"
|
|
67
|
+
}
|
|
68
|
+
}
|