renma 0.18.2 → 0.18.3

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 (56) hide show
  1. package/CHANGELOG.md +55 -1
  2. package/architecture.md +4 -0
  3. package/dist/catalog.d.ts +1 -1
  4. package/dist/catalog.d.ts.map +1 -1
  5. package/dist/catalog.js +5 -2
  6. package/dist/catalog.js.map +1 -1
  7. package/dist/commands/inspect.d.ts +2 -66
  8. package/dist/commands/inspect.d.ts.map +1 -1
  9. package/dist/commands/inspect.js +45 -295
  10. package/dist/commands/inspect.js.map +1 -1
  11. package/dist/commands/readiness.d.ts +3 -0
  12. package/dist/commands/readiness.d.ts.map +1 -1
  13. package/dist/commands/readiness.js +9 -6
  14. package/dist/commands/readiness.js.map +1 -1
  15. package/dist/commands/suggest-metadata.d.ts +4 -21
  16. package/dist/commands/suggest-metadata.d.ts.map +1 -1
  17. package/dist/commands/suggest-metadata.js +65 -514
  18. package/dist/commands/suggest-metadata.js.map +1 -1
  19. package/dist/decisions/metadata-suggestion.d.ts +60 -0
  20. package/dist/decisions/metadata-suggestion.d.ts.map +1 -0
  21. package/dist/decisions/metadata-suggestion.js +176 -0
  22. package/dist/decisions/metadata-suggestion.js.map +1 -0
  23. package/dist/discovery.d.ts.map +1 -1
  24. package/dist/discovery.js +4 -0
  25. package/dist/discovery.js.map +1 -1
  26. package/dist/evidence/classification.d.ts +4 -0
  27. package/dist/evidence/classification.d.ts.map +1 -0
  28. package/dist/evidence/classification.js +15 -0
  29. package/dist/evidence/classification.js.map +1 -0
  30. package/dist/evidence/inspect.d.ts +68 -0
  31. package/dist/evidence/inspect.d.ts.map +1 -0
  32. package/dist/evidence/inspect.js +2 -0
  33. package/dist/evidence/inspect.js.map +1 -0
  34. package/dist/evidence/target.d.ts +51 -0
  35. package/dist/evidence/target.d.ts.map +1 -0
  36. package/dist/evidence/target.js +161 -0
  37. package/dist/evidence/target.js.map +1 -0
  38. package/dist/renderers/inspect.d.ts +3 -0
  39. package/dist/renderers/inspect.d.ts.map +1 -0
  40. package/dist/renderers/inspect.js +159 -0
  41. package/dist/renderers/inspect.js.map +1 -0
  42. package/dist/renderers/metadata-suggestion.d.ts +3 -0
  43. package/dist/renderers/metadata-suggestion.d.ts.map +1 -0
  44. package/dist/renderers/metadata-suggestion.js +320 -0
  45. package/dist/renderers/metadata-suggestion.js.map +1 -0
  46. package/dist/repository-evidence.d.ts +7 -1
  47. package/dist/repository-evidence.d.ts.map +1 -1
  48. package/dist/repository-evidence.js +10 -2
  49. package/dist/repository-evidence.js.map +1 -1
  50. package/dist/scanner.d.ts.map +1 -1
  51. package/dist/scanner.js +3 -13
  52. package/dist/scanner.js.map +1 -1
  53. package/docs/README.md +2 -0
  54. package/docs/internal-architecture.md +296 -0
  55. package/package.json +1 -1
  56. package/scripts/verify-package.mjs +154 -9
@@ -1,13 +1,11 @@
1
- import { lstat, readFile, realpath, stat } from "node:fs/promises";
1
+ import { lstat, realpath, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { buildSkillParentIndex, resolveSkillSupportParent, withResolvedSkillParent, } from "../catalog.js";
4
3
  import { renmaCommand } from "../command-invocation.js";
5
- import { parseDocument } from "../markdown.js";
6
- import { parseAssetMetadata } from "../metadata.js";
7
- import { collectRepositorySnapshot } from "../repository-evidence.js";
8
- import { collectSecurityPolicyAssetEvidence } from "../security-policy-inventory.js";
9
- import { classifyAssetPath, classifyRepositorySkillEntrypointPath, repositoryClassificationPath, } from "../discovery.js";
10
- import { buildAgentSkillMigrationSuggestion, } from "../skill-migration.js";
4
+ import { buildMetadataCandidateSuggestionDecision, buildSkillLocalParentUnresolvedDecision, buildSkillLocalSuggestionDecision, buildSkillSuggestionDecision, buildUnsupportedTargetSuggestionDecision, evaluateOwnerConflict, } from "../decisions/metadata-suggestion.js";
5
+ import { collectTargetDocumentEvidence, collectTargetRepositoryEvidence, } from "../evidence/target.js";
6
+ import { buildAgentSkillMigrationSuggestion } from "../skill-migration.js";
7
+ import { renderMetadataPrompt } from "../renderers/metadata-suggestion.js";
8
+ export { renderMetadataPrompt };
11
9
  export class SuggestMetadataTargetError extends Error {
12
10
  constructor(target, cause) {
13
11
  super(`Could not read metadata target ${target}: ${readErrorReason(cause)}`);
@@ -23,40 +21,17 @@ export async function runSuggestMetadataCommand(target, options = {}) {
23
21
  return 0;
24
22
  }
25
23
  export async function buildMetadataSuggestion(target, options = {}) {
26
- const absolutePath = path.resolve(target);
27
- let content;
24
+ let targetEvidence;
28
25
  try {
29
- content = await readFile(absolutePath, "utf8");
26
+ targetEvidence = await collectTargetDocumentEvidence(target, {
27
+ unresolvedArtifactPath: "input",
28
+ });
30
29
  }
31
30
  catch (error) {
32
31
  throw new SuggestMetadataTargetError(target, error);
33
32
  }
34
- const outputPath = toPosix(target);
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;
47
- const document = parseDocument({
48
- path: classificationPath || outputPath,
49
- absolutePath,
50
- kind: initialKind,
51
- sizeBytes: Buffer.byteLength(content),
52
- contentClassification: "text",
53
- markdownParserEligible: /\.mdx?$/i.test(outputPath),
54
- content,
55
- });
56
- const { metadata } = parseAssetMetadata(document);
57
- let classification = classifyAssetPath(classificationPath, {
58
- ...(metadata.type ? { metadataType: metadata.type } : {}),
59
- });
33
+ const { absolutePath, outputPath, repositoryBoundary: classificationContext, repositoryRelativePath: classificationPath, repositoryRoot, entrypoint, document, metadata, } = targetEvidence;
34
+ let { classification } = targetEvidence;
60
35
  const kind = classification.kind;
61
36
  const explicitOwner = optionalText(options.owner);
62
37
  if (kind === "skill") {
@@ -71,7 +46,7 @@ export async function buildMetadataSuggestion(target, options = {}) {
71
46
  const noMigrationProposed = agentSkills.proposalKind === "none" &&
72
47
  agentSkills.sourceFormat === "agent-skills";
73
48
  const metadataRetrofit = agentSkills.proposalKind === "canonical-metadata-retrofit";
74
- const decision = skillDecision(agentSkills);
49
+ const decision = buildSkillSuggestionDecision(agentSkills);
75
50
  return {
76
51
  path: outputPath,
77
52
  kind,
@@ -125,9 +100,7 @@ export async function buildMetadataSuggestion(target, options = {}) {
125
100
  const candidateId = inferCandidateId(kind, classificationPath);
126
101
  const candidateTitle = mainHeadingTitle(document.headings);
127
102
  if (classification.scope === "skill-local") {
128
- const localContext = await resolveSkillLocalContext(classification, classificationPath, classificationContext.state === "resolved"
129
- ? classificationContext.root
130
- : undefined);
103
+ const localContext = resolveSkillLocalContext(await collectTargetRepositoryEvidence(targetEvidence));
131
104
  classification = localContext.classification;
132
105
  if (localContext.parent.state !== "resolved") {
133
106
  const parentState = localContext.parent.state === "not-applicable"
@@ -139,16 +112,13 @@ export async function buildMetadataSuggestion(target, options = {}) {
139
112
  reason: `The structural parent Skill is ${parentState}; inheritance is not established. Review the repository layout before adding local metadata.`,
140
113
  },
141
114
  ];
115
+ const decision = buildSkillLocalParentUnresolvedDecision();
142
116
  return {
143
117
  path: outputPath,
144
118
  kind,
145
119
  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
- },
120
+ decisionStatus: decision.status,
121
+ decision: decision.decision,
152
122
  classification,
153
123
  ownerProvided: Boolean(explicitOwner),
154
124
  instructions: skillLocalInstructions(classification, false, true, false),
@@ -157,45 +127,25 @@ export async function buildMetadataSuggestion(target, options = {}) {
157
127
  nextActions: suggestionNextActions(outputPath, classification, repositoryRoot),
158
128
  };
159
129
  }
160
- const blockedMetadata = conflictingOwnerEvidence(existingOwner, explicitOwner);
130
+ const ownerConflict = evaluateOwnerConflict(existingOwner, explicitOwner);
131
+ const blockedMetadata = ownerConflict.blockedMetadata;
161
132
  if (!existingOwner && explicitOwner)
162
133
  candidateMetadata.owner = explicitOwner;
163
134
  const hasOverride = Object.keys(candidateMetadata).length > 0;
164
135
  const hasLocalGovernance = Boolean(existingOwner) || localContext.hasLocalPolicyMetadata;
165
136
  const inherited = localContext.ownershipSource === "inherited";
137
+ const decision = buildSkillLocalSuggestionDecision({
138
+ hasOwnerConflict: ownerConflict.hasConflict,
139
+ hasOverride,
140
+ hasLocalGovernance,
141
+ inheritsGovernance: inherited,
142
+ });
166
143
  return {
167
144
  path: outputPath,
168
145
  kind,
169
146
  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
- },
147
+ decisionStatus: decision.status,
148
+ decision: decision.decision,
199
149
  classification,
200
150
  ownerProvided: Boolean(explicitOwner),
201
151
  instructions: skillLocalInstructions(classification, hasOverride, false, hasLocalGovernance),
@@ -206,24 +156,18 @@ export async function buildMetadataSuggestion(target, options = {}) {
206
156
  }
207
157
  if (classification.matchedRule === "repository-tool" ||
208
158
  classification.matchedRule === "unknown") {
209
- const unsafe = classificationContext.state === "unresolved";
159
+ const decision = buildUnsupportedTargetSuggestionDecision({
160
+ matchedRule: classification.matchedRule,
161
+ ...(classificationContext.state === "unresolved"
162
+ ? { boundaryReasonCode: classificationContext.reasonCode }
163
+ : {}),
164
+ });
210
165
  return {
211
166
  path: outputPath,
212
167
  kind,
213
168
  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
- },
169
+ decisionStatus: decision.status,
170
+ decision: decision.decision,
227
171
  classification,
228
172
  ownerProvided: Boolean(explicitOwner),
229
173
  instructions: [
@@ -245,7 +189,8 @@ export async function buildMetadataSuggestion(target, options = {}) {
245
189
  if (!existingOwner && explicitOwner) {
246
190
  candidateMetadata.owner = explicitOwner;
247
191
  }
248
- const blockedMetadata = conflictingOwnerEvidence(existingOwner, explicitOwner);
192
+ const ownerConflict = evaluateOwnerConflict(existingOwner, explicitOwner);
193
+ const blockedMetadata = ownerConflict.blockedMetadata;
249
194
  if (!existingOwner && !explicitOwner) {
250
195
  blockedMetadata.push({
251
196
  field: "owner",
@@ -253,38 +198,18 @@ export async function buildMetadataSuggestion(target, options = {}) {
253
198
  });
254
199
  }
255
200
  const hasCandidate = Object.keys(candidateMetadata).length > 0;
256
- const hasConflict = blockedMetadata.some((item) => item.reason.includes("differs from explicitly provided owner"));
201
+ const hasConflict = ownerConflict.hasConflict;
202
+ const decision = buildMetadataCandidateSuggestionDecision({
203
+ hasOwnerConflict: hasConflict,
204
+ hasCandidate,
205
+ scope: classification.scope,
206
+ });
257
207
  return {
258
208
  path: outputPath,
259
209
  kind,
260
210
  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
- },
211
+ decisionStatus: decision.status,
212
+ decision: decision.decision,
288
213
  classification,
289
214
  ownerProvided: Boolean(explicitOwner),
290
215
  instructions: buildInstructions({ existingOwner, explicitOwner }),
@@ -293,227 +218,6 @@ export async function buildMetadataSuggestion(target, options = {}) {
293
218
  nextActions: suggestionNextActions(outputPath, classification, repositoryRoot),
294
219
  };
295
220
  }
296
- export function renderMetadataPrompt(suggestion) {
297
- if (suggestion.agentSkills)
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";
303
- return `${[
304
- blocked
305
- ? "# Renma Result: Metadata Proposal Blocked"
306
- : noProposal
307
- ? "# Renma Result: No Metadata Proposal"
308
- : "# Renma Task: Safely Retrofit Renma Metadata",
309
- "",
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.",
315
- "",
316
- "Asset:",
317
- `- Path: \`${suggestion.path}\``,
318
- `- Kind: \`${suggestion.kind}\``,
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
- : []),
355
- "",
356
- "Rules:",
357
- ...suggestion.instructions.map((instruction) => `- ${instruction}`),
358
- "",
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)),
365
- "",
366
- "Blocked Metadata:",
367
- ...blockedMetadataLines(suggestion.blockedMetadata),
368
- "",
369
- "Verification:",
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
- ]),
386
- "",
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.",
394
- ].join("\n")}\n`;
395
- }
396
- function renderAgentSkillMigrationPrompt(suggestion) {
397
- const migration = suggestion.agentSkills;
398
- if (!migration)
399
- return "";
400
- const metadataRetrofit = migration.proposalKind === "canonical-metadata-retrofit";
401
- const noMigrationProposed = migration.proposalKind === "none" &&
402
- migration.sourceFormat === "agent-skills";
403
- const candidate = suggestion.decisionStatus !== "blocked" && migration.canonicalFrontmatter
404
- ? [
405
- "Canonical Frontmatter Candidate:",
406
- "",
407
- "```yaml",
408
- migration.canonicalFrontmatter,
409
- "```",
410
- ]
411
- : [
412
- "Canonical Frontmatter Candidate:",
413
- "",
414
- "(not generated while migration is blocked or unnecessary)",
415
- ];
416
- const promptState = suggestion.decisionStatus === "blocked"
417
- ? "blocked"
418
- : suggestion.decisionStatus === "no-change-recommended"
419
- ? "no-proposal"
420
- : "candidate";
421
- const verification = renderSkillSuggestionVerification(promptState);
422
- const nextSteps = renderSkillSuggestionNextSteps(promptState);
423
- return `${[
424
- noMigrationProposed
425
- ? "# Renma Task: Inspect Canonical Agent Skill (No Migration Proposed)"
426
- : metadataRetrofit
427
- ? "# Renma Task: Review Canonical Agent Skills Metadata Retrofit"
428
- : "# Renma Task: Review One-Way Agent Skills Migration",
429
- "",
430
- `Asset: \`${suggestion.path}\``,
431
- `Decision status: \`${suggestion.decisionStatus}\``,
432
- `Decision reason: \`${suggestion.decision.reasonCode}\``,
433
- "Classification:",
434
- ...classificationPromptLines(suggestion.classification),
435
- `Source format: \`${migration.sourceFormat}\``,
436
- `Direction: \`${migration.direction}\``,
437
- `Proposal: \`${migration.proposalKind}\``,
438
- `Source path: \`${migration.sourcePath}\``,
439
- `Target path: \`${migration.targetPath}\``,
440
- `Entrypoint migration: \`${migration.entrypointMigration}\``,
441
- "",
442
- "Rules:",
443
- ...suggestion.instructions.map((instruction) => `- ${instruction}`),
444
- "",
445
- "Candidate Agent Skills Fields:",
446
- ...metadataLines(migration.candidateAgentSkillsFields),
447
- "",
448
- "Candidate Renma Metadata:",
449
- ...metadataLines(migration.candidateRenmaMetadata),
450
- "",
451
- ...candidate,
452
- "",
453
- "Blocked Migration Evidence:",
454
- ...blockedMetadataLines(suggestion.blockedMetadata),
455
- "",
456
- "Human Review:",
457
- migration.reviewPrompt,
458
- "",
459
- ...verification,
460
- "",
461
- ...nextSteps,
462
- "",
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.",
477
- ].join("\n")}\n`;
478
- }
479
- function renderSkillSuggestionVerification(state) {
480
- if (state === "no-proposal") {
481
- return [
482
- "Verification:",
483
- "- No verification change is required when no separate authoring change is made.",
484
- "- Only if a separate, intentional authoring change is made: run `renma scan . --fail-on high`, fix relevant diagnostics, and rerun the scan.",
485
- ];
486
- }
487
- return ["Verification:", "- Run `renma scan . --fail-on high`."];
488
- }
489
- function renderSkillSuggestionNextSteps(state) {
490
- if (state === "blocked") {
491
- return [
492
- "Next steps:",
493
- "1. Review the conflicts or invalid evidence and confirm the Skill's intent using your platform's standard Skill authoring guidance.",
494
- "2. Do not apply a candidate while Renma cannot generate it safely.",
495
- "3. Correct the source evidence, then rerun `renma suggest-metadata <SKILL.md>`.",
496
- "4. After intended corrections, run `renma scan . --fail-on high`, fix relevant diagnostics, and rerun the scan.",
497
- ];
498
- }
499
- if (state === "no-proposal") {
500
- return [
501
- "Next steps:",
502
- "1. Review the Skill's trigger description, instructions, workflow, constraints, and completion criteria using your platform's standard Skill authoring guidance.",
503
- "2. No metadata or migration change is proposed; preserve the existing source.",
504
- "3. If a separate, intentionally reviewed authoring change is made, run `renma scan . --fail-on high`, fix relevant diagnostics, and rerun the scan.",
505
- "4. If no separate change is made, stop without manufacturing work.",
506
- ];
507
- }
508
- return [
509
- "Next steps:",
510
- "1. Review the suggestion; Renma does not edit the Skill automatically.",
511
- "2. Review the Skill's trigger description, instructions, workflow, constraints, and completion criteria using your platform's standard Skill authoring guidance.",
512
- "3. Apply only the intended metadata or migration changes.",
513
- "4. Run `renma scan . --fail-on high`.",
514
- "5. Fix relevant diagnostics and rerun the scan.",
515
- ];
516
- }
517
221
  function buildInstructions(input) {
518
222
  return [
519
223
  "Inspect the existing asset before editing.",
@@ -541,93 +245,6 @@ function ownerInstruction(input) {
541
245
  }
542
246
  return "Do not add owner unless the existing asset already declares one or a maintainer provides one.";
543
247
  }
544
- function metadataLines(candidateMetadata) {
545
- const entries = Object.entries(candidateMetadata);
546
- if (entries.length === 0) {
547
- return ["- (none)"];
548
- }
549
- return entries.map(([field, value]) => `- ${field}: \`${value}\``);
550
- }
551
- function blockedMetadataLines(blockedMetadata) {
552
- if (blockedMetadata.length === 0) {
553
- return ["- (none)"];
554
- }
555
- return blockedMetadata.map((item) => `- ${item.field}: ${item.reason}`);
556
- }
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
248
  function skillLocalInstructions(classification, hasOverride, parentBlocked, hasLocalGovernance) {
632
249
  return [
633
250
  "Preserve the existing Skill-local file and any explicitly declared supported metadata.",
@@ -644,9 +261,13 @@ function skillLocalInstructions(classification, hasOverride, parentBlocked, hasL
644
261
  ];
645
262
  }
646
263
  function suggestionNextActions(targetPath, classification, repositoryRoot) {
264
+ // Do not manufacture an action relative to cwd when the repository root is
265
+ // unresolved; a parent workspace may contain unrelated repositories.
647
266
  if (!repositoryRoot)
648
267
  return [];
649
268
  const actions = [];
269
+ // Verification is anchored to the resolved root, never to `scan .` from the
270
+ // caller's workspace or to a guessed parent directory.
650
271
  const scanTarget = repositoryRoot;
651
272
  if (classification.parentResolution === "resolved" &&
652
273
  classification.parentAssetPath) {
@@ -694,102 +315,32 @@ function suggestionNextActions(targetPath, classification, repositoryRoot) {
694
315
  });
695
316
  return actions;
696
317
  }
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 {
318
+ function resolveSkillLocalContext(repository) {
319
+ if (repository.state === "unavailable") {
320
+ // Snapshot failure is unresolved evidence. Keep the command fail-closed
321
+ // instead of treating structural placement as inherited governance.
724
322
  return {
725
- classification,
323
+ classification: repository.classification,
726
324
  parent: { state: "not-applicable" },
727
325
  ownershipSource: "unowned",
728
326
  hasLocalPolicyMetadata: false,
729
327
  };
730
328
  }
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.";
329
+ return {
330
+ classification: repository.classification,
331
+ parent: repository.parent,
332
+ ownershipSource: repository.governance?.ownership.source ??
333
+ (repository.parent.state === "resolved" && repository.parent.parent.owner
334
+ ? "inherited"
335
+ : "unowned"),
336
+ hasLocalPolicyMetadata: repository.policy?.hasLocalPolicyMetadata ?? false,
337
+ };
789
338
  }
790
339
  async function pathMigrationCollision(entrypoint, sourceAbsolutePath, repositoryRoot) {
791
340
  if (entrypoint.kind === "canonical")
792
341
  return undefined;
342
+ // Public migration paths remain repository-relative. Collision I/O must be
343
+ // rebased against the resolved repository root, not process.cwd().
793
344
  const targetAbsolutePath = path.resolve(repositoryRoot, entrypoint.targetPath);
794
345
  try {
795
346
  await lstat(targetAbsolutePath);