renma 0.18.0 → 0.18.2
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 +101 -1
- package/README.md +61 -3
- package/architecture.md +7 -6
- package/design.md +39 -32
- package/dist/agent-skills.d.ts +5 -6
- package/dist/agent-skills.d.ts.map +1 -1
- package/dist/agent-skills.js +2 -10
- package/dist/agent-skills.js.map +1 -1
- package/dist/catalog.d.ts +27 -1
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +62 -16
- package/dist/catalog.js.map +1 -1
- package/dist/cli-help.d.ts +8 -8
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +19 -4
- package/dist/cli-help.js.map +1 -1
- package/dist/command-invocation.d.ts +4 -0
- package/dist/command-invocation.d.ts.map +1 -0
- package/dist/command-invocation.js +14 -0
- package/dist/command-invocation.js.map +1 -0
- package/dist/commands/inspect.d.ts +5 -0
- package/dist/commands/inspect.d.ts.map +1 -1
- package/dist/commands/inspect.js +185 -33
- package/dist/commands/inspect.js.map +1 -1
- package/dist/commands/scaffold.js +30 -9
- package/dist/commands/scaffold.js.map +1 -1
- package/dist/commands/suggest-metadata.d.ts +6 -2
- package/dist/commands/suggest-metadata.d.ts.map +1 -1
- package/dist/commands/suggest-metadata.js +516 -72
- package/dist/commands/suggest-metadata.js.map +1 -1
- package/dist/context-lens.d.ts +1 -0
- package/dist/context-lens.d.ts.map +1 -1
- package/dist/context-lens.js +19 -1
- package/dist/context-lens.js.map +1 -1
- package/dist/diagnostic-ids.d.ts +1 -0
- package/dist/diagnostic-ids.d.ts.map +1 -1
- package/dist/diagnostic-ids.js +1 -0
- package/dist/diagnostic-ids.js.map +1 -1
- package/dist/discovery.d.ts +33 -1
- package/dist/discovery.d.ts.map +1 -1
- package/dist/discovery.js +361 -51
- package/dist/discovery.js.map +1 -1
- package/dist/metadata.d.ts +15 -1
- package/dist/metadata.d.ts.map +1 -1
- package/dist/metadata.js +235 -0
- package/dist/metadata.js.map +1 -1
- package/dist/model.d.ts +5 -10
- package/dist/model.d.ts.map +1 -1
- package/dist/rules.js +69 -23
- package/dist/rules.js.map +1 -1
- package/dist/scanner.d.ts.map +1 -1
- package/dist/scanner.js +66 -2
- package/dist/scanner.js.map +1 -1
- package/dist/skill-migration.d.ts.map +1 -1
- package/dist/skill-migration.js +6 -1
- package/dist/skill-migration.js.map +1 -1
- package/dist/types.d.ts +72 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +50 -1
- package/dist/types.js.map +1 -1
- package/docs/README.md +2 -1
- package/docs/agent-skills-compatibility.md +8 -1
- package/docs/authoring-guide.md +100 -13
- package/docs/context-lens.md +319 -153
- package/docs/diagnostics.md +385 -3
- package/docs/metadata-budget.md +32 -0
- package/docs/quality-profile.md +17 -5
- package/docs/user-manual.md +136 -14
- package/examples/context-lens/README.md +8 -3
- package/examples/context-lens/contexts/testing/boundary-value-analysis.md +6 -2
- package/examples/context-lens/lenses/testing/spec-review-boundary-values.md +27 -5
- package/examples/context-lens/lenses/testing/test-design-boundary-values.md +27 -5
- package/examples/context-lens/skills/testing/spec-review/SKILL.md +33 -4
- package/package.json +1 -1
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { lstat, readFile, realpath, stat } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { buildSkillParentIndex, resolveSkillSupportParent, withResolvedSkillParent, } from "../catalog.js";
|
|
4
|
+
import { renmaCommand } from "../command-invocation.js";
|
|
3
5
|
import { parseDocument } from "../markdown.js";
|
|
4
6
|
import { parseAssetMetadata } from "../metadata.js";
|
|
5
|
-
import {
|
|
7
|
+
import { collectRepositorySnapshot } from "../repository-evidence.js";
|
|
8
|
+
import { collectSecurityPolicyAssetEvidence } from "../security-policy-inventory.js";
|
|
9
|
+
import { classifyAssetPath, classifyRepositorySkillEntrypointPath, repositoryClassificationPath, } from "../discovery.js";
|
|
6
10
|
import { buildAgentSkillMigrationSuggestion, } from "../skill-migration.js";
|
|
7
11
|
export class SuggestMetadataTargetError extends Error {
|
|
8
12
|
constructor(target, cause) {
|
|
@@ -28,10 +32,20 @@ export async function buildMetadataSuggestion(target, options = {}) {
|
|
|
28
32
|
throw new SuggestMetadataTargetError(target, error);
|
|
29
33
|
}
|
|
30
34
|
const outputPath = toPosix(target);
|
|
31
|
-
const
|
|
32
|
-
const
|
|
35
|
+
const classificationContext = repositoryClassificationPath(target);
|
|
36
|
+
const classificationPath = classificationContext.state === "resolved"
|
|
37
|
+
? classificationContext.relativePath
|
|
38
|
+
: "";
|
|
39
|
+
const repositoryRoot = classificationContext.state === "resolved"
|
|
40
|
+
? classificationContext.root
|
|
41
|
+
: undefined;
|
|
42
|
+
const entrypoint = classificationPath
|
|
43
|
+
? classifyRepositorySkillEntrypointPath(classificationPath)
|
|
44
|
+
: undefined;
|
|
45
|
+
const initialClassification = classifyAssetPath(classificationPath);
|
|
46
|
+
const initialKind = initialClassification.kind;
|
|
33
47
|
const document = parseDocument({
|
|
34
|
-
path: outputPath,
|
|
48
|
+
path: classificationPath || outputPath,
|
|
35
49
|
absolutePath,
|
|
36
50
|
kind: initialKind,
|
|
37
51
|
sizeBytes: Buffer.byteLength(content),
|
|
@@ -40,28 +54,35 @@ export async function buildMetadataSuggestion(target, options = {}) {
|
|
|
40
54
|
content,
|
|
41
55
|
});
|
|
42
56
|
const { metadata } = parseAssetMetadata(document);
|
|
43
|
-
|
|
44
|
-
?
|
|
45
|
-
|
|
57
|
+
let classification = classifyAssetPath(classificationPath, {
|
|
58
|
+
...(metadata.type ? { metadataType: metadata.type } : {}),
|
|
59
|
+
});
|
|
60
|
+
const kind = classification.kind;
|
|
46
61
|
const explicitOwner = optionalText(options.owner);
|
|
47
62
|
if (kind === "skill") {
|
|
48
|
-
const collisionBlock = entrypoint
|
|
49
|
-
? await pathMigrationCollision(entrypoint, absolutePath)
|
|
63
|
+
const collisionBlock = entrypoint && repositoryRoot
|
|
64
|
+
? await pathMigrationCollision(entrypoint, absolutePath, repositoryRoot)
|
|
50
65
|
: undefined;
|
|
51
66
|
const agentSkills = buildAgentSkillMigrationSuggestion(document, {
|
|
52
67
|
...(explicitOwner ? { explicitOwner } : {}),
|
|
53
68
|
...(entrypoint ? { entrypoint } : {}),
|
|
54
69
|
...(collisionBlock ? { additionalBlocks: [collisionBlock] } : {}),
|
|
55
70
|
});
|
|
56
|
-
const metadataRetrofit = agentSkills.proposalKind === "canonical-metadata-retrofit";
|
|
57
71
|
const noMigrationProposed = agentSkills.proposalKind === "none" &&
|
|
58
72
|
agentSkills.sourceFormat === "agent-skills";
|
|
73
|
+
const metadataRetrofit = agentSkills.proposalKind === "canonical-metadata-retrofit";
|
|
74
|
+
const decision = skillDecision(agentSkills);
|
|
59
75
|
return {
|
|
60
76
|
path: outputPath,
|
|
61
77
|
kind,
|
|
62
|
-
suggestedMode:
|
|
63
|
-
? "
|
|
64
|
-
:
|
|
78
|
+
suggestedMode: noMigrationProposed
|
|
79
|
+
? "no-proposal"
|
|
80
|
+
: metadataRetrofit
|
|
81
|
+
? "agent-skills-metadata-retrofit"
|
|
82
|
+
: "agent-skills-migration",
|
|
83
|
+
decisionStatus: decision.status,
|
|
84
|
+
decision: decision.decision,
|
|
85
|
+
classification,
|
|
65
86
|
ownerProvided: Boolean(explicitOwner),
|
|
66
87
|
instructions: noMigrationProposed
|
|
67
88
|
? [
|
|
@@ -93,6 +114,7 @@ export async function buildMetadataSuggestion(target, options = {}) {
|
|
|
93
114
|
],
|
|
94
115
|
candidateMetadata: {},
|
|
95
116
|
blockedMetadata: agentSkills.blocked,
|
|
117
|
+
nextActions: suggestionNextActions(outputPath, classification, repositoryRoot),
|
|
96
118
|
agentSkills,
|
|
97
119
|
};
|
|
98
120
|
}
|
|
@@ -100,8 +122,120 @@ export async function buildMetadataSuggestion(target, options = {}) {
|
|
|
100
122
|
const existingTitle = optionalText(metadataValueText(document.metadata.title));
|
|
101
123
|
const existingOwner = optionalText(metadata.owner);
|
|
102
124
|
const candidateMetadata = {};
|
|
103
|
-
const candidateId = inferCandidateId(kind,
|
|
125
|
+
const candidateId = inferCandidateId(kind, classificationPath);
|
|
104
126
|
const candidateTitle = mainHeadingTitle(document.headings);
|
|
127
|
+
if (classification.scope === "skill-local") {
|
|
128
|
+
const localContext = await resolveSkillLocalContext(classification, classificationPath, classificationContext.state === "resolved"
|
|
129
|
+
? classificationContext.root
|
|
130
|
+
: undefined);
|
|
131
|
+
classification = localContext.classification;
|
|
132
|
+
if (localContext.parent.state !== "resolved") {
|
|
133
|
+
const parentState = localContext.parent.state === "not-applicable"
|
|
134
|
+
? "structural-candidate"
|
|
135
|
+
: localContext.parent.state;
|
|
136
|
+
const blockedMetadata = [
|
|
137
|
+
{
|
|
138
|
+
field: "parent_skill",
|
|
139
|
+
reason: `The structural parent Skill is ${parentState}; inheritance is not established. Review the repository layout before adding local metadata.`,
|
|
140
|
+
},
|
|
141
|
+
];
|
|
142
|
+
return {
|
|
143
|
+
path: outputPath,
|
|
144
|
+
kind,
|
|
145
|
+
suggestedMode: "no-proposal",
|
|
146
|
+
decisionStatus: "blocked",
|
|
147
|
+
decision: {
|
|
148
|
+
reasonCode: "skill-local-parent-unresolved",
|
|
149
|
+
summary: "Renma cannot confirm one parent Skill, so it cannot claim inherited governance or safely propose an independent local override.",
|
|
150
|
+
question: "Resolve the missing or ambiguous parent Skill layout, then rerun this command.",
|
|
151
|
+
},
|
|
152
|
+
classification,
|
|
153
|
+
ownerProvided: Boolean(explicitOwner),
|
|
154
|
+
instructions: skillLocalInstructions(classification, false, true, false),
|
|
155
|
+
candidateMetadata: {},
|
|
156
|
+
blockedMetadata,
|
|
157
|
+
nextActions: suggestionNextActions(outputPath, classification, repositoryRoot),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
const blockedMetadata = conflictingOwnerEvidence(existingOwner, explicitOwner);
|
|
161
|
+
if (!existingOwner && explicitOwner)
|
|
162
|
+
candidateMetadata.owner = explicitOwner;
|
|
163
|
+
const hasOverride = Object.keys(candidateMetadata).length > 0;
|
|
164
|
+
const hasLocalGovernance = Boolean(existingOwner) || localContext.hasLocalPolicyMetadata;
|
|
165
|
+
const inherited = localContext.ownershipSource === "inherited";
|
|
166
|
+
return {
|
|
167
|
+
path: outputPath,
|
|
168
|
+
kind,
|
|
169
|
+
suggestedMode: hasOverride ? "metadata-retrofit" : "no-proposal",
|
|
170
|
+
decisionStatus: blockedMetadata.length > 0
|
|
171
|
+
? "blocked"
|
|
172
|
+
: hasOverride
|
|
173
|
+
? "deterministic"
|
|
174
|
+
: "no-change-recommended",
|
|
175
|
+
decision: blockedMetadata.length > 0
|
|
176
|
+
? {
|
|
177
|
+
reasonCode: "conflicting-ownership-evidence",
|
|
178
|
+
summary: "Renma cannot safely construct a local metadata override while declared and provided ownership evidence conflict.",
|
|
179
|
+
}
|
|
180
|
+
: hasOverride
|
|
181
|
+
? {
|
|
182
|
+
reasonCode: "explicit-human-provided-override",
|
|
183
|
+
summary: "The candidate is an explicit human-provided Skill-local metadata override; it is not required for ordinary local support.",
|
|
184
|
+
}
|
|
185
|
+
: hasLocalGovernance
|
|
186
|
+
? {
|
|
187
|
+
reasonCode: "skill-local-existing-metadata-preserved",
|
|
188
|
+
summary: "Existing explicit local governance metadata is preserved; no inherited-governance claim or retrofit is needed.",
|
|
189
|
+
}
|
|
190
|
+
: inherited
|
|
191
|
+
? {
|
|
192
|
+
reasonCode: "skill-local-governance-inherited",
|
|
193
|
+
summary: "One unambiguous parent Skill supplies effective governance, so no independent metadata retrofit is required.",
|
|
194
|
+
}
|
|
195
|
+
: {
|
|
196
|
+
reasonCode: "skill-local-unowned",
|
|
197
|
+
summary: "The parent Skill is resolved, but neither the local file nor its parent declares an owner; missing ownership remains allowed.",
|
|
198
|
+
},
|
|
199
|
+
classification,
|
|
200
|
+
ownerProvided: Boolean(explicitOwner),
|
|
201
|
+
instructions: skillLocalInstructions(classification, hasOverride, false, hasLocalGovernance),
|
|
202
|
+
candidateMetadata,
|
|
203
|
+
blockedMetadata,
|
|
204
|
+
nextActions: suggestionNextActions(outputPath, classification, repositoryRoot),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
if (classification.matchedRule === "repository-tool" ||
|
|
208
|
+
classification.matchedRule === "unknown") {
|
|
209
|
+
const unsafe = classificationContext.state === "unresolved";
|
|
210
|
+
return {
|
|
211
|
+
path: outputPath,
|
|
212
|
+
kind,
|
|
213
|
+
suggestedMode: "no-proposal",
|
|
214
|
+
decisionStatus: unsafe ? "blocked" : "no-change-recommended",
|
|
215
|
+
decision: unsafe
|
|
216
|
+
? {
|
|
217
|
+
reasonCode: classificationContext.reasonCode,
|
|
218
|
+
summary: "Renma could not infer one safe repository-relative boundary for this target path.",
|
|
219
|
+
}
|
|
220
|
+
: {
|
|
221
|
+
reasonCode: classification.matchedRule === "repository-tool"
|
|
222
|
+
? "repository-tool-not-context"
|
|
223
|
+
: "outside-recognized-asset-boundary",
|
|
224
|
+
summary: "No metadata proposal is generated because the target is not an independently governed Renma asset.",
|
|
225
|
+
question: "Is this file intended to have independent ownership and lifecycle under a recognized asset root?",
|
|
226
|
+
},
|
|
227
|
+
classification,
|
|
228
|
+
ownerProvided: Boolean(explicitOwner),
|
|
229
|
+
instructions: [
|
|
230
|
+
"Preserve the existing file.",
|
|
231
|
+
"Do not infer that the target should become a Context Asset from its content or filename.",
|
|
232
|
+
"Do not move the file or add Renma metadata without a human repository-design decision.",
|
|
233
|
+
],
|
|
234
|
+
candidateMetadata: {},
|
|
235
|
+
blockedMetadata: [],
|
|
236
|
+
nextActions: suggestionNextActions(outputPath, classification, repositoryRoot),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
105
239
|
if (!existingId && candidateId) {
|
|
106
240
|
candidateMetadata.id = candidateId;
|
|
107
241
|
}
|
|
@@ -111,56 +245,152 @@ export async function buildMetadataSuggestion(target, options = {}) {
|
|
|
111
245
|
if (!existingOwner && explicitOwner) {
|
|
112
246
|
candidateMetadata.owner = explicitOwner;
|
|
113
247
|
}
|
|
114
|
-
const blockedMetadata =
|
|
248
|
+
const blockedMetadata = conflictingOwnerEvidence(existingOwner, explicitOwner);
|
|
115
249
|
if (!existingOwner && !explicitOwner) {
|
|
116
250
|
blockedMetadata.push({
|
|
117
251
|
field: "owner",
|
|
118
252
|
reason: "No owner was explicitly provided. Missing owner is allowed.",
|
|
119
253
|
});
|
|
120
254
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
field: "owner",
|
|
124
|
-
reason: `Existing owner "${existingOwner}" differs from explicitly provided owner "${explicitOwner}". Do not change ownership without human review.`,
|
|
125
|
-
});
|
|
126
|
-
}
|
|
255
|
+
const hasCandidate = Object.keys(candidateMetadata).length > 0;
|
|
256
|
+
const hasConflict = blockedMetadata.some((item) => item.reason.includes("differs from explicitly provided owner"));
|
|
127
257
|
return {
|
|
128
258
|
path: outputPath,
|
|
129
259
|
kind,
|
|
130
|
-
suggestedMode: "metadata-retrofit",
|
|
260
|
+
suggestedMode: hasConflict || !hasCandidate ? "no-proposal" : "metadata-retrofit",
|
|
261
|
+
decisionStatus: hasConflict
|
|
262
|
+
? "blocked"
|
|
263
|
+
: hasCandidate
|
|
264
|
+
? classification.scope === "independent"
|
|
265
|
+
? "human-confirmation-required"
|
|
266
|
+
: "deterministic"
|
|
267
|
+
: "no-change-recommended",
|
|
268
|
+
decision: hasConflict
|
|
269
|
+
? {
|
|
270
|
+
reasonCode: "conflicting-ownership-evidence",
|
|
271
|
+
summary: "Renma cannot safely construct a metadata proposal.",
|
|
272
|
+
}
|
|
273
|
+
: hasCandidate && classification.scope === "independent"
|
|
274
|
+
? {
|
|
275
|
+
reasonCode: "independent-governance-intent-unconfirmed",
|
|
276
|
+
summary: "Renma constructed only deterministic candidates; the intended owner, lifecycle, and source-of-truth evidence still require human confirmation.",
|
|
277
|
+
question: "Confirm the intended owner, lifecycle, and source-of-truth evidence for this independent asset.",
|
|
278
|
+
}
|
|
279
|
+
: hasCandidate
|
|
280
|
+
? {
|
|
281
|
+
reasonCode: "deterministic-metadata-candidate",
|
|
282
|
+
summary: "The metadata candidate follows the classified repository boundary and explicit user evidence.",
|
|
283
|
+
}
|
|
284
|
+
: {
|
|
285
|
+
reasonCode: "metadata-already-sufficient",
|
|
286
|
+
summary: "Renma found no supported metadata change to propose.",
|
|
287
|
+
},
|
|
288
|
+
classification,
|
|
131
289
|
ownerProvided: Boolean(explicitOwner),
|
|
132
290
|
instructions: buildInstructions({ existingOwner, explicitOwner }),
|
|
133
|
-
candidateMetadata,
|
|
291
|
+
candidateMetadata: hasConflict ? {} : candidateMetadata,
|
|
134
292
|
blockedMetadata,
|
|
293
|
+
nextActions: suggestionNextActions(outputPath, classification, repositoryRoot),
|
|
135
294
|
};
|
|
136
295
|
}
|
|
137
296
|
export function renderMetadataPrompt(suggestion) {
|
|
138
297
|
if (suggestion.agentSkills)
|
|
139
298
|
return renderAgentSkillMigrationPrompt(suggestion);
|
|
299
|
+
const blocked = suggestion.decisionStatus === "blocked";
|
|
300
|
+
const noChange = suggestion.decisionStatus === "no-change-recommended";
|
|
301
|
+
const noProposal = blocked || noChange;
|
|
302
|
+
const skillLocal = suggestion.classification.scope === "skill-local";
|
|
140
303
|
return `${[
|
|
141
|
-
|
|
304
|
+
blocked
|
|
305
|
+
? "# Renma Result: Metadata Proposal Blocked"
|
|
306
|
+
: noProposal
|
|
307
|
+
? "# Renma Result: No Metadata Proposal"
|
|
308
|
+
: "# Renma Task: Safely Retrofit Renma Metadata",
|
|
142
309
|
"",
|
|
143
|
-
|
|
310
|
+
blocked
|
|
311
|
+
? "Renma cannot safely produce an applicable metadata patch."
|
|
312
|
+
: noProposal
|
|
313
|
+
? "No independent metadata retrofit is required."
|
|
314
|
+
: "Review this existing Renma asset metadata candidate safely.",
|
|
144
315
|
"",
|
|
145
316
|
"Asset:",
|
|
146
317
|
`- Path: \`${suggestion.path}\``,
|
|
147
318
|
`- Kind: \`${suggestion.kind}\``,
|
|
148
319
|
`- Mode: \`${suggestion.suggestedMode}\``,
|
|
320
|
+
`- Decision status: \`${suggestion.decisionStatus}\``,
|
|
321
|
+
"",
|
|
322
|
+
"Classification:",
|
|
323
|
+
...classificationPromptLines(suggestion.classification),
|
|
324
|
+
"",
|
|
325
|
+
"Observed repository fact:",
|
|
326
|
+
observedRepositoryFact(suggestion.classification),
|
|
327
|
+
"",
|
|
328
|
+
"Deterministic Renma interpretation:",
|
|
329
|
+
deterministicInterpretation(suggestion.classification),
|
|
330
|
+
"",
|
|
331
|
+
noProposal ? "Recommendation:" : "Decision:",
|
|
332
|
+
suggestion.decision.summary,
|
|
333
|
+
...(suggestion.decision.question
|
|
334
|
+
? ["", "Remaining human decision:", suggestion.decision.question]
|
|
335
|
+
: []),
|
|
336
|
+
...(skillLocal
|
|
337
|
+
? suggestion.classification.parentResolution === "resolved"
|
|
338
|
+
? [
|
|
339
|
+
"",
|
|
340
|
+
"This file is structurally Skill-local support with one resolved parent:",
|
|
341
|
+
suggestion.classification.parentAssetPath ??
|
|
342
|
+
"(resolved parent path unavailable)",
|
|
343
|
+
"",
|
|
344
|
+
...skillLocalGovernancePromptLines(suggestion),
|
|
345
|
+
"Preserve existing metadata if present.",
|
|
346
|
+
"Do not add an owner, move the file, or create a patch solely to manufacture work.",
|
|
347
|
+
]
|
|
348
|
+
: [
|
|
349
|
+
"",
|
|
350
|
+
"This file is structurally Skill-local support, but its parent governance is unresolved.",
|
|
351
|
+
`Parent resolution: ${suggestion.classification.parentResolution ?? "structural-candidate"}.`,
|
|
352
|
+
"Do not claim inheritance or add an independent local owner until the repository layout resolves to one parent Skill.",
|
|
353
|
+
]
|
|
354
|
+
: []),
|
|
149
355
|
"",
|
|
150
356
|
"Rules:",
|
|
151
357
|
...suggestion.instructions.map((instruction) => `- ${instruction}`),
|
|
152
358
|
"",
|
|
153
|
-
|
|
154
|
-
|
|
359
|
+
blocked
|
|
360
|
+
? "Candidate Metadata (not applicable while blocked):"
|
|
361
|
+
: "Candidate Metadata:",
|
|
362
|
+
...(blocked
|
|
363
|
+
? ["- (suppressed because the decision is blocked)"]
|
|
364
|
+
: metadataLines(suggestion.candidateMetadata)),
|
|
155
365
|
"",
|
|
156
366
|
"Blocked Metadata:",
|
|
157
367
|
...blockedMetadataLines(suggestion.blockedMetadata),
|
|
158
368
|
"",
|
|
159
369
|
"Verification:",
|
|
160
|
-
|
|
161
|
-
|
|
370
|
+
...(blocked
|
|
371
|
+
? suggestion.nextActions.length > 0
|
|
372
|
+
? [
|
|
373
|
+
"- Resolve the blocked evidence and rerun `renma suggest-metadata`.",
|
|
374
|
+
"- Run only the structured safe action; do not apply a patch from this result.",
|
|
375
|
+
]
|
|
376
|
+
: [
|
|
377
|
+
"- Establish the repository root with an explicit root or repository marker, then rerun `renma suggest-metadata`.",
|
|
378
|
+
"- No verification command is safe while the repository boundary is unresolved.",
|
|
379
|
+
]
|
|
380
|
+
: noChange
|
|
381
|
+
? ["- No verification change is required when no file change is made."]
|
|
382
|
+
: [
|
|
383
|
+
"- Run `renma scan . --fail-on high --format json`.",
|
|
384
|
+
"- Run `renma ownership .`.",
|
|
385
|
+
]),
|
|
162
386
|
"",
|
|
163
|
-
|
|
387
|
+
blocked
|
|
388
|
+
? "Do not return or apply a patch while the decision is blocked. Preserve the existing source."
|
|
389
|
+
: noChange
|
|
390
|
+
? "Stop without manufacturing work. Preserve the existing source."
|
|
391
|
+
: suggestion.decisionStatus === "human-confirmation-required"
|
|
392
|
+
? "After confirming the stated human decision, return only the reviewed candidate fields. Do not invent unresolved owner, lifecycle, or source-of-truth metadata, and do not rewrite the asset body."
|
|
393
|
+
: "Return a small reviewed patch containing only the deterministic candidate. Do not rewrite the asset body.",
|
|
164
394
|
].join("\n")}\n`;
|
|
165
395
|
}
|
|
166
396
|
function renderAgentSkillMigrationPrompt(suggestion) {
|
|
@@ -170,7 +400,7 @@ function renderAgentSkillMigrationPrompt(suggestion) {
|
|
|
170
400
|
const metadataRetrofit = migration.proposalKind === "canonical-metadata-retrofit";
|
|
171
401
|
const noMigrationProposed = migration.proposalKind === "none" &&
|
|
172
402
|
migration.sourceFormat === "agent-skills";
|
|
173
|
-
const candidate = migration.canonicalFrontmatter
|
|
403
|
+
const candidate = suggestion.decisionStatus !== "blocked" && migration.canonicalFrontmatter
|
|
174
404
|
? [
|
|
175
405
|
"Canonical Frontmatter Candidate:",
|
|
176
406
|
"",
|
|
@@ -183,9 +413,9 @@ function renderAgentSkillMigrationPrompt(suggestion) {
|
|
|
183
413
|
"",
|
|
184
414
|
"(not generated while migration is blocked or unnecessary)",
|
|
185
415
|
];
|
|
186
|
-
const promptState = suggestion.
|
|
416
|
+
const promptState = suggestion.decisionStatus === "blocked"
|
|
187
417
|
? "blocked"
|
|
188
|
-
:
|
|
418
|
+
: suggestion.decisionStatus === "no-change-recommended"
|
|
189
419
|
? "no-proposal"
|
|
190
420
|
: "candidate";
|
|
191
421
|
const verification = renderSkillSuggestionVerification(promptState);
|
|
@@ -198,6 +428,10 @@ function renderAgentSkillMigrationPrompt(suggestion) {
|
|
|
198
428
|
: "# Renma Task: Review One-Way Agent Skills Migration",
|
|
199
429
|
"",
|
|
200
430
|
`Asset: \`${suggestion.path}\``,
|
|
431
|
+
`Decision status: \`${suggestion.decisionStatus}\``,
|
|
432
|
+
`Decision reason: \`${suggestion.decision.reasonCode}\``,
|
|
433
|
+
"Classification:",
|
|
434
|
+
...classificationPromptLines(suggestion.classification),
|
|
201
435
|
`Source format: \`${migration.sourceFormat}\``,
|
|
202
436
|
`Direction: \`${migration.direction}\``,
|
|
203
437
|
`Proposal: \`${migration.proposalKind}\``,
|
|
@@ -226,13 +460,20 @@ function renderAgentSkillMigrationPrompt(suggestion) {
|
|
|
226
460
|
"",
|
|
227
461
|
...nextSteps,
|
|
228
462
|
"",
|
|
229
|
-
|
|
230
|
-
?
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
463
|
+
promptState === "blocked"
|
|
464
|
+
? "Do not return or apply a frontmatter patch while the decision is blocked. Preserve the existing source until every blocked item is resolved with human review."
|
|
465
|
+
: promptState === "no-proposal"
|
|
466
|
+
? "No frontmatter patch is proposed. Do not return or apply a frontmatter patch; preserve the existing source."
|
|
467
|
+
: suggestion.decisionStatus === "human-confirmation-required" &&
|
|
468
|
+
migration.canonicalFrontmatter
|
|
469
|
+
? migration.entrypointMigration === "none"
|
|
470
|
+
? "After a human confirms the intended Skill semantics, return only the reviewed canonical frontmatter candidate. Do not invent unresolved fields or rewrite the Skill body."
|
|
471
|
+
: "After a human confirms the intended Skill semantics, return one reviewed patch containing only the entrypoint path migration and canonical frontmatter candidate. Do not invent unresolved fields or rewrite the Skill body."
|
|
472
|
+
: migration.canonicalFrontmatter
|
|
473
|
+
? migration.entrypointMigration === "none"
|
|
474
|
+
? "Return a small reviewed frontmatter patch. Do not rewrite the Skill body."
|
|
475
|
+
: "Return one small reviewed patch containing both the entrypoint path migration and frontmatter migration. Do not rewrite the Skill body."
|
|
476
|
+
: "Do not return a patch because no canonical candidate was generated.",
|
|
236
477
|
].join("\n")}\n`;
|
|
237
478
|
}
|
|
238
479
|
function renderSkillSuggestionVerification(state) {
|
|
@@ -313,37 +554,243 @@ function blockedMetadataLines(blockedMetadata) {
|
|
|
313
554
|
}
|
|
314
555
|
return blockedMetadata.map((item) => `- ${item.field}: ${item.reason}`);
|
|
315
556
|
}
|
|
316
|
-
function
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
557
|
+
function skillLocalGovernancePromptLines(suggestion) {
|
|
558
|
+
switch (suggestion.decision.reasonCode) {
|
|
559
|
+
case "skill-local-governance-inherited":
|
|
560
|
+
return [
|
|
561
|
+
"Effective governance is inherited from this one resolved parent Skill.",
|
|
562
|
+
];
|
|
563
|
+
case "skill-local-existing-metadata-preserved":
|
|
564
|
+
return [
|
|
565
|
+
"Existing explicit local governance is preserved and is not relabeled as inherited.",
|
|
566
|
+
];
|
|
567
|
+
case "skill-local-unowned":
|
|
568
|
+
return [
|
|
569
|
+
"The parent is resolved, but no effective owner is declared locally or by the parent.",
|
|
570
|
+
];
|
|
571
|
+
case "explicit-human-provided-override":
|
|
572
|
+
return [
|
|
573
|
+
"The parent is resolved; the local candidate is an explicit human-provided override.",
|
|
574
|
+
];
|
|
575
|
+
default:
|
|
576
|
+
return [
|
|
577
|
+
"Governance provenance remains separate from this structural parent resolution.",
|
|
578
|
+
];
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
function skillDecision(migration) {
|
|
582
|
+
if (migration.blocked.length > 0) {
|
|
583
|
+
return {
|
|
584
|
+
status: "blocked",
|
|
585
|
+
decision: {
|
|
586
|
+
reasonCode: "conflicting-or-incomplete-skill-evidence",
|
|
587
|
+
summary: "Renma cannot safely construct the Agent Skills proposal until the blocked evidence is resolved by a human.",
|
|
588
|
+
},
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
if (migration.proposalKind === "none" &&
|
|
592
|
+
migration.sourceFormat === "agent-skills") {
|
|
593
|
+
return {
|
|
594
|
+
status: "no-change-recommended",
|
|
595
|
+
decision: {
|
|
596
|
+
reasonCode: "canonical-agent-skill-no-change",
|
|
597
|
+
summary: "The target is already a canonical Agent Skill and no metadata or migration change is recommended.",
|
|
598
|
+
},
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
if (migration.proposalKind === "canonical-metadata-retrofit" &&
|
|
602
|
+
migration.canonicalFrontmatter !== undefined &&
|
|
603
|
+
Object.keys(migration.candidateRenmaMetadata).length > 0) {
|
|
604
|
+
return {
|
|
605
|
+
status: "deterministic",
|
|
606
|
+
decision: {
|
|
607
|
+
reasonCode: "explicit-human-provided-override",
|
|
608
|
+
summary: "The metadata candidate uses the owner explicitly supplied by the human; Renma inferred no owner.",
|
|
609
|
+
},
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
return {
|
|
613
|
+
status: "human-confirmation-required",
|
|
614
|
+
decision: {
|
|
615
|
+
reasonCode: "agent-skills-migration-review-required",
|
|
616
|
+
summary: "Renma constructed a deterministic migration candidate, but a human must confirm the intended Skill semantics before applying it.",
|
|
617
|
+
},
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
function conflictingOwnerEvidence(existingOwner, explicitOwner) {
|
|
621
|
+
if (!existingOwner || !explicitOwner || existingOwner === explicitOwner) {
|
|
622
|
+
return [];
|
|
623
|
+
}
|
|
624
|
+
return [
|
|
625
|
+
{
|
|
626
|
+
field: "owner",
|
|
627
|
+
reason: `Existing owner "${existingOwner}" differs from explicitly provided owner "${explicitOwner}". Do not change ownership without human review.`,
|
|
628
|
+
},
|
|
629
|
+
];
|
|
630
|
+
}
|
|
631
|
+
function skillLocalInstructions(classification, hasOverride, parentBlocked, hasLocalGovernance) {
|
|
632
|
+
return [
|
|
633
|
+
"Preserve the existing Skill-local file and any explicitly declared supported metadata.",
|
|
634
|
+
parentBlocked
|
|
635
|
+
? "Do not claim inherited governance until exactly one parent Skill resolves."
|
|
636
|
+
: hasLocalGovernance
|
|
637
|
+
? "Keep existing explicit local governance separate from parent inheritance."
|
|
638
|
+
: `Treat ${classification.parentAssetPath ?? "the resolved parent Skill"} as the default governance source.`,
|
|
639
|
+
"Independent metadata is not required merely because this is a Skill-local file.",
|
|
640
|
+
hasOverride
|
|
641
|
+
? "Apply only the explicit human-provided override; do not describe it as mandatory local metadata."
|
|
642
|
+
: "Do not add metadata, move the file, or create a patch solely to manufacture work.",
|
|
643
|
+
"Never infer owner from the path, prose, Git history, or author.",
|
|
644
|
+
];
|
|
645
|
+
}
|
|
646
|
+
function suggestionNextActions(targetPath, classification, repositoryRoot) {
|
|
647
|
+
if (!repositoryRoot)
|
|
648
|
+
return [];
|
|
649
|
+
const actions = [];
|
|
650
|
+
const scanTarget = repositoryRoot;
|
|
651
|
+
if (classification.parentResolution === "resolved" &&
|
|
652
|
+
classification.parentAssetPath) {
|
|
653
|
+
actions.push({
|
|
654
|
+
kind: "inspect-parent",
|
|
655
|
+
invocation: renmaCommand([
|
|
656
|
+
"inspect",
|
|
657
|
+
path.join(repositoryRoot, classification.parentAssetPath),
|
|
658
|
+
"--format",
|
|
659
|
+
"json",
|
|
660
|
+
]),
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
else if (classification.scope === "skill-local" &&
|
|
664
|
+
classification.parentResolution !== "resolved") {
|
|
665
|
+
actions.push({
|
|
666
|
+
kind: "review-layout",
|
|
667
|
+
invocation: renmaCommand([
|
|
668
|
+
"scan",
|
|
669
|
+
scanTarget,
|
|
670
|
+
"--fail-on",
|
|
671
|
+
"high",
|
|
672
|
+
"--format",
|
|
673
|
+
"json",
|
|
674
|
+
]),
|
|
675
|
+
});
|
|
676
|
+
return actions;
|
|
677
|
+
}
|
|
678
|
+
else {
|
|
679
|
+
actions.push({
|
|
680
|
+
kind: "inspect-target",
|
|
681
|
+
invocation: renmaCommand(["inspect", targetPath, "--format", "json"]),
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
actions.push({
|
|
685
|
+
kind: "verify",
|
|
686
|
+
invocation: renmaCommand([
|
|
687
|
+
"scan",
|
|
688
|
+
scanTarget,
|
|
689
|
+
"--fail-on",
|
|
690
|
+
"high",
|
|
691
|
+
"--format",
|
|
692
|
+
"json",
|
|
693
|
+
]),
|
|
694
|
+
});
|
|
695
|
+
return actions;
|
|
696
|
+
}
|
|
697
|
+
async function resolveSkillLocalContext(classification, relativePath, repositoryRoot) {
|
|
698
|
+
if (!repositoryRoot) {
|
|
699
|
+
return {
|
|
700
|
+
classification,
|
|
701
|
+
parent: { state: "not-applicable" },
|
|
702
|
+
ownershipSource: "unowned",
|
|
703
|
+
hasLocalPolicyMetadata: false,
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
try {
|
|
707
|
+
const snapshot = await collectRepositorySnapshot(repositoryRoot);
|
|
708
|
+
const skillParents = buildSkillParentIndex(snapshot.documents);
|
|
709
|
+
const parent = resolveSkillSupportParent(relativePath, skillParents);
|
|
710
|
+
const resolvedClassification = withResolvedSkillParent(classification, relativePath, skillParents);
|
|
711
|
+
const entry = snapshot.catalog.entries.find((candidate) => candidate.sourcePath === relativePath);
|
|
712
|
+
const policy = collectSecurityPolicyAssetEvidence(snapshot.artifacts, snapshot.config.security).find((candidate) => candidate.path === relativePath);
|
|
713
|
+
return {
|
|
714
|
+
classification: resolvedClassification,
|
|
715
|
+
parent,
|
|
716
|
+
ownershipSource: entry?.ownership.source ??
|
|
717
|
+
(parent.state === "resolved" && parent.parent.owner
|
|
718
|
+
? "inherited"
|
|
719
|
+
: "unowned"),
|
|
720
|
+
hasLocalPolicyMetadata: policy?.hasLocalPolicyMetadata ?? false,
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
catch {
|
|
724
|
+
return {
|
|
725
|
+
classification,
|
|
726
|
+
parent: { state: "not-applicable" },
|
|
727
|
+
ownershipSource: "unowned",
|
|
728
|
+
hasLocalPolicyMetadata: false,
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
function classificationPromptLines(classification) {
|
|
733
|
+
return [
|
|
734
|
+
`- kind: \`${classification.kind}\``,
|
|
735
|
+
`- scope: \`${classification.scope}\``,
|
|
736
|
+
`- matched rule: \`${classification.matchedRule}\``,
|
|
737
|
+
`- reason code: \`${classification.reasonCode}\``,
|
|
738
|
+
...(classification.recognizedRoot
|
|
739
|
+
? [`- recognized root: \`${classification.recognizedRoot}\``]
|
|
740
|
+
: []),
|
|
741
|
+
...(classification.parentAssetPath
|
|
742
|
+
? [`- parent asset: \`${classification.parentAssetPath}\``]
|
|
743
|
+
: []),
|
|
744
|
+
...(classification.parentAssetCandidatePath
|
|
745
|
+
? [
|
|
746
|
+
`- parent candidate: \`${classification.parentAssetCandidatePath}\``,
|
|
747
|
+
`- parent resolution: \`${classification.parentResolution ?? "structural-candidate"}\``,
|
|
748
|
+
]
|
|
749
|
+
: []),
|
|
750
|
+
...(classification.parentAssetCandidates?.length
|
|
751
|
+
? [
|
|
752
|
+
`- parent candidates: ${classification.parentAssetCandidates.map((candidate) => `\`${candidate}\``).join(", ")}`,
|
|
753
|
+
]
|
|
754
|
+
: []),
|
|
755
|
+
...(classification.supportDirectory
|
|
756
|
+
? [`- support directory: \`${classification.supportDirectory}\``]
|
|
757
|
+
: []),
|
|
758
|
+
...(classification.ignoredNestedSegments?.length
|
|
759
|
+
? [
|
|
760
|
+
`- ignored nested segments: ${classification.ignoredNestedSegments.map((segment) => `\`${segment}\``).join(", ")}`,
|
|
761
|
+
]
|
|
762
|
+
: []),
|
|
763
|
+
`- reason: ${classification.reason}`,
|
|
764
|
+
];
|
|
765
|
+
}
|
|
766
|
+
function observedRepositoryFact(classification) {
|
|
767
|
+
if (classification.matchedRule === "skill-local-support") {
|
|
768
|
+
return `The target is under a canonical ${classification.supportDirectory}/ directory within a recognized Skill-root path shape.`;
|
|
769
|
+
}
|
|
770
|
+
if (classification.matchedRule === "context-root" ||
|
|
771
|
+
classification.matchedRule === "context-root-legacy") {
|
|
772
|
+
return `The target is under ${classification.recognizedRoot}/**.`;
|
|
773
|
+
}
|
|
774
|
+
return classification.reason;
|
|
775
|
+
}
|
|
776
|
+
function deterministicInterpretation(classification) {
|
|
777
|
+
if (classification.scope === "skill-local") {
|
|
778
|
+
return classification.parentResolution === "resolved"
|
|
779
|
+
? `The target is structurally Skill-local support and its parent resolves to ${classification.parentAssetPath}; governance provenance is evaluated separately.`
|
|
780
|
+
: "The target is structurally Skill-local support, but inherited governance is unresolved.";
|
|
781
|
+
}
|
|
782
|
+
if (classification.scope === "independent") {
|
|
783
|
+
return `The target is an independently governed ${classification.kind} asset.`;
|
|
784
|
+
}
|
|
785
|
+
if (classification.scope === "repository-support") {
|
|
786
|
+
return "The target is repository support, not an independently governed Context Asset.";
|
|
787
|
+
}
|
|
788
|
+
return "Renma does not classify the target as an independently governed asset.";
|
|
789
|
+
}
|
|
790
|
+
async function pathMigrationCollision(entrypoint, sourceAbsolutePath, repositoryRoot) {
|
|
344
791
|
if (entrypoint.kind === "canonical")
|
|
345
792
|
return undefined;
|
|
346
|
-
const targetAbsolutePath = path.resolve(entrypoint.targetPath);
|
|
793
|
+
const targetAbsolutePath = path.resolve(repositoryRoot, entrypoint.targetPath);
|
|
347
794
|
try {
|
|
348
795
|
await lstat(targetAbsolutePath);
|
|
349
796
|
}
|
|
@@ -378,9 +825,6 @@ async function pathMigrationCollision(entrypoint, sourceAbsolutePath) {
|
|
|
378
825
|
};
|
|
379
826
|
}
|
|
380
827
|
}
|
|
381
|
-
function isAbsoluteLike(filePath) {
|
|
382
|
-
return path.isAbsolute(filePath) || /^[A-Za-z]:\//.test(filePath);
|
|
383
|
-
}
|
|
384
828
|
function isMissingFileError(error) {
|
|
385
829
|
return (typeof error === "object" &&
|
|
386
830
|
error !== null &&
|