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.
Files changed (74) hide show
  1. package/CHANGELOG.md +101 -1
  2. package/README.md +61 -3
  3. package/architecture.md +7 -6
  4. package/design.md +39 -32
  5. package/dist/agent-skills.d.ts +5 -6
  6. package/dist/agent-skills.d.ts.map +1 -1
  7. package/dist/agent-skills.js +2 -10
  8. package/dist/agent-skills.js.map +1 -1
  9. package/dist/catalog.d.ts +27 -1
  10. package/dist/catalog.d.ts.map +1 -1
  11. package/dist/catalog.js +62 -16
  12. package/dist/catalog.js.map +1 -1
  13. package/dist/cli-help.d.ts +8 -8
  14. package/dist/cli-help.d.ts.map +1 -1
  15. package/dist/cli-help.js +19 -4
  16. package/dist/cli-help.js.map +1 -1
  17. package/dist/command-invocation.d.ts +4 -0
  18. package/dist/command-invocation.d.ts.map +1 -0
  19. package/dist/command-invocation.js +14 -0
  20. package/dist/command-invocation.js.map +1 -0
  21. package/dist/commands/inspect.d.ts +5 -0
  22. package/dist/commands/inspect.d.ts.map +1 -1
  23. package/dist/commands/inspect.js +185 -33
  24. package/dist/commands/inspect.js.map +1 -1
  25. package/dist/commands/scaffold.js +30 -9
  26. package/dist/commands/scaffold.js.map +1 -1
  27. package/dist/commands/suggest-metadata.d.ts +6 -2
  28. package/dist/commands/suggest-metadata.d.ts.map +1 -1
  29. package/dist/commands/suggest-metadata.js +516 -72
  30. package/dist/commands/suggest-metadata.js.map +1 -1
  31. package/dist/context-lens.d.ts +1 -0
  32. package/dist/context-lens.d.ts.map +1 -1
  33. package/dist/context-lens.js +19 -1
  34. package/dist/context-lens.js.map +1 -1
  35. package/dist/diagnostic-ids.d.ts +1 -0
  36. package/dist/diagnostic-ids.d.ts.map +1 -1
  37. package/dist/diagnostic-ids.js +1 -0
  38. package/dist/diagnostic-ids.js.map +1 -1
  39. package/dist/discovery.d.ts +33 -1
  40. package/dist/discovery.d.ts.map +1 -1
  41. package/dist/discovery.js +361 -51
  42. package/dist/discovery.js.map +1 -1
  43. package/dist/metadata.d.ts +15 -1
  44. package/dist/metadata.d.ts.map +1 -1
  45. package/dist/metadata.js +235 -0
  46. package/dist/metadata.js.map +1 -1
  47. package/dist/model.d.ts +5 -10
  48. package/dist/model.d.ts.map +1 -1
  49. package/dist/rules.js +69 -23
  50. package/dist/rules.js.map +1 -1
  51. package/dist/scanner.d.ts.map +1 -1
  52. package/dist/scanner.js +66 -2
  53. package/dist/scanner.js.map +1 -1
  54. package/dist/skill-migration.d.ts.map +1 -1
  55. package/dist/skill-migration.js +6 -1
  56. package/dist/skill-migration.js.map +1 -1
  57. package/dist/types.d.ts +72 -0
  58. package/dist/types.d.ts.map +1 -1
  59. package/dist/types.js +50 -1
  60. package/dist/types.js.map +1 -1
  61. package/docs/README.md +2 -1
  62. package/docs/agent-skills-compatibility.md +8 -1
  63. package/docs/authoring-guide.md +100 -13
  64. package/docs/context-lens.md +319 -153
  65. package/docs/diagnostics.md +385 -3
  66. package/docs/metadata-budget.md +32 -0
  67. package/docs/quality-profile.md +17 -5
  68. package/docs/user-manual.md +136 -14
  69. package/examples/context-lens/README.md +8 -3
  70. package/examples/context-lens/contexts/testing/boundary-value-analysis.md +6 -2
  71. package/examples/context-lens/lenses/testing/spec-review-boundary-values.md +27 -5
  72. package/examples/context-lens/lenses/testing/test-design-boundary-values.md +27 -5
  73. package/examples/context-lens/skills/testing/spec-review/SKILL.md +33 -4
  74. 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 { classifyAbsoluteSkillEntrypointPath, classifyRepositorySkillEntrypointPath, } from "../discovery.js";
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 entrypoint = classifySuggestionSkillEntrypointPath(outputPath);
32
- const initialKind = classifyPath(outputPath, entrypoint);
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
- const kind = initialKind === "context" && metadata.type === "context_lens"
44
- ? "context_lens"
45
- : initialKind;
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: metadataRetrofit
63
- ? "agent-skills-metadata-retrofit"
64
- : "agent-skills-migration",
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, outputPath);
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
- if (existingOwner && explicitOwner && existingOwner !== explicitOwner) {
122
- blockedMetadata.push({
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
- "# Renma Task: Safely Retrofit Renma Metadata",
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
- "Update this existing Renma asset metadata safely.",
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
- "Candidate Metadata:",
154
- ...metadataLines(suggestion.candidateMetadata),
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
- "- Run `renma scan .`.",
161
- "- Run `renma ownership .`.",
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
- "Return a small reviewed patch. Do not rewrite the asset body.",
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.blockedMetadata.length > 0
416
+ const promptState = suggestion.decisionStatus === "blocked"
187
417
  ? "blocked"
188
- : noMigrationProposed || !migration.canonicalFrontmatter
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
- migration.canonicalFrontmatter
230
- ? migration.entrypointMigration === "none"
231
- ? "Return a small reviewed frontmatter patch. Do not rewrite the Skill body."
232
- : "Return one small reviewed patch containing both the entrypoint path migration and frontmatter migration. Do not rewrite the Skill body."
233
- : migration.blocked.length > 0
234
- ? "Do not return or apply a frontmatter patch while migration is blocked. Preserve the existing source until every blocked item is resolved with human review."
235
- : "No frontmatter patch is proposed. Do not return or apply a frontmatter patch; preserve the existing source.",
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 classifyPath(filePath, entrypoint) {
317
- if (entrypoint)
318
- return "skill";
319
- const parts = pathParts(filePath);
320
- const basename = parts.at(-1) ?? "";
321
- if (parts.includes("lenses"))
322
- return "context_lens";
323
- if (parts.includes("contexts") || parts.includes("context"))
324
- return "context";
325
- if (parts.includes("profiles"))
326
- return "profile";
327
- if (parts.includes("references"))
328
- return "reference";
329
- if (parts.includes("examples"))
330
- return "example";
331
- if (basename === "AGENTS.md" || parts.includes(".agents"))
332
- return "agent";
333
- if (basename === "renma.config.json" || basename === ".renma.json") {
334
- return "config";
335
- }
336
- return "unknown";
337
- }
338
- function classifySuggestionSkillEntrypointPath(filePath) {
339
- return isAbsoluteLike(filePath)
340
- ? classifyAbsoluteSkillEntrypointPath(filePath)
341
- : classifyRepositorySkillEntrypointPath(filePath);
342
- }
343
- async function pathMigrationCollision(entrypoint, sourceAbsolutePath) {
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 &&