hive-lite 0.1.9 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -54,7 +54,7 @@ function isCreateCapablePattern(pattern) {
54
54
  return !looksLikeFilePattern(pattern);
55
55
  }
56
56
 
57
- const IDENTITY_ALIASES = {
57
+ const DEFAULT_IDENTITY_ALIASES = {
58
58
  openai: ['openai', 'chatgpt', 'gpt'],
59
59
  google: ['google', 'gemini'],
60
60
  qwen: ['qwen', 'tongyi'],
@@ -64,37 +64,101 @@ const IDENTITY_ALIASES = {
64
64
  azure: ['azure'],
65
65
  };
66
66
 
67
- function canonicalIdentity(value) {
67
+ function identityAliasList(config) {
68
+ if (Array.isArray(config)) return arrayFrom(config);
69
+ if (config && typeof config === 'object') {
70
+ return [
71
+ ...arrayFrom(config.aliases || config.alias),
72
+ ...arrayFrom(config.tokens),
73
+ ];
74
+ }
75
+ return arrayFrom(config);
76
+ }
77
+
78
+ function identityDisplayName(identity, area) {
79
+ const raw = area && (area.identity_aliases || area.identityAliases) ? area.identity_aliases || area.identityAliases : null;
80
+ if (raw) {
81
+ const entries = Array.isArray(raw)
82
+ ? raw.map((item) => [item && (item.id || item.name || item.canonical), item])
83
+ : Object.entries(raw);
84
+ for (const [id, config] of entries) {
85
+ const candidate = (config && typeof config === 'object' ? config.canonical || config.id || config.name : null) || id;
86
+ if (normalize(candidate) === normalize(identity)) return String(candidate);
87
+ }
88
+ }
89
+ return normalize(identity).split(/[^a-z0-9]+/).filter(Boolean)
90
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
91
+ .join('') || String(identity || '');
92
+ }
93
+
94
+ function aliasRegistryForArea(area) {
95
+ const registry = Object.fromEntries(Object.entries(DEFAULT_IDENTITY_ALIASES)
96
+ .map(([canonical, aliases]) => [canonical, unique([canonical, ...aliases.map(normalize)])]));
97
+ const raw = area && (area.identity_aliases || area.identityAliases) ? area.identity_aliases || area.identityAliases : null;
98
+ if (!raw) return registry;
99
+
100
+ const entries = Array.isArray(raw)
101
+ ? raw.map((item) => [item && (item.id || item.name || item.canonical), item])
102
+ : Object.entries(raw);
103
+ for (const [id, config] of entries) {
104
+ if (!config) continue;
105
+ const canonical = normalize(
106
+ (typeof config === 'object' ? config.canonical || config.id || config.name : null)
107
+ || id
108
+ );
109
+ if (!canonical) continue;
110
+ const aliases = identityAliasList(config);
111
+ registry[canonical] = unique([
112
+ ...(registry[canonical] || []),
113
+ canonical,
114
+ normalize(id),
115
+ ...aliases.map(normalize),
116
+ ].filter(Boolean));
117
+ }
118
+ return registry;
119
+ }
120
+
121
+ function isKnownIdentity(value, aliases = DEFAULT_IDENTITY_ALIASES) {
122
+ return Object.prototype.hasOwnProperty.call(aliases, value);
123
+ }
124
+
125
+ function canonicalIdentity(value, aliases = DEFAULT_IDENTITY_ALIASES) {
68
126
  const text = normalize(value);
69
127
  if (!text) return null;
70
- for (const [canonical, aliases] of Object.entries(IDENTITY_ALIASES)) {
71
- if (aliases.includes(text)) return canonical;
128
+ for (const [canonical, values] of Object.entries(aliases)) {
129
+ if (canonical === text || values.includes(text)) return canonical;
72
130
  }
73
131
  return text;
74
132
  }
75
133
 
76
- function targetIdentityForIntent(intent) {
134
+ function targetIdentityForIntent(intent, aliases) {
77
135
  const tokens = tokenize(intent);
78
136
  for (const token of tokens) {
79
- const identity = canonicalIdentity(token);
80
- if (identity && Object.prototype.hasOwnProperty.call(IDENTITY_ALIASES, identity)) return identity;
137
+ const identity = canonicalIdentity(token, aliases);
138
+ if (identity && isKnownIdentity(identity, aliases)) return identity;
81
139
  }
82
140
  return null;
83
141
  }
84
142
 
85
- function peerIdentityFromPath(pattern) {
143
+ function targetIdentitiesForIntent(intent, aliases) {
144
+ return unique(tokenize(intent)
145
+ .map((token) => canonicalIdentity(token, aliases))
146
+ .filter((identity) => identity && isKnownIdentity(identity, aliases)));
147
+ }
148
+
149
+ function peerIdentityFromPath(pattern, aliases) {
86
150
  if (!looksLikeFilePattern(pattern)) return null;
87
151
  const tokens = basenameTokens(pattern);
88
152
  for (const token of tokens) {
89
- const identity = canonicalIdentity(token);
90
- if (identity && Object.prototype.hasOwnProperty.call(IDENTITY_ALIASES, identity)) return identity;
153
+ const identity = canonicalIdentity(token, aliases);
154
+ if (identity && isKnownIdentity(identity, aliases)) return identity;
91
155
  }
92
156
  return null;
93
157
  }
94
158
 
95
- function identityMatches(left, right) {
159
+ function identityMatches(left, right, aliases) {
96
160
  if (!left || !right) return false;
97
- return canonicalIdentity(left) === canonicalIdentity(right);
161
+ return canonicalIdentity(left, aliases) === canonicalIdentity(right, aliases);
98
162
  }
99
163
 
100
164
  function intentTargetsAllPeers(intent) {
@@ -141,6 +205,50 @@ function configuredArtifactFamilies(area) {
141
205
  }));
142
206
  }
143
207
 
208
+ function configuredPeerFileTemplates(area) {
209
+ const raw = area && (area.peer_file_templates || area.peerFileTemplates) ? area.peer_file_templates || area.peerFileTemplates : null;
210
+ if (!raw) return [];
211
+ const groups = Array.isArray(raw)
212
+ ? raw.map((item) => [item && (item.id || item.name || item.artifact_family || item.artifactFamily), item])
213
+ : Object.entries(raw);
214
+ const templates = [];
215
+ for (const [id, value] of groups) {
216
+ if (!value) continue;
217
+ const group = typeof value === 'object' && !Array.isArray(value) ? value : {};
218
+ const artifacts = group.artifacts || group.files || group.templates || (typeof value === 'object' && !Array.isArray(value) ? value : {});
219
+ for (const [artifactId, artifactValue] of Object.entries(artifacts || {})) {
220
+ const template = typeof artifactValue === 'string'
221
+ ? artifactValue
222
+ : artifactValue && (artifactValue.path || artifactValue.template || artifactValue.pattern);
223
+ if (!template) continue;
224
+ templates.push({
225
+ id: id || group.id || group.name || '',
226
+ artifact: typeof artifactValue === 'object' && artifactValue.artifact ? artifactValue.artifact : artifactId,
227
+ artifactFamily: group.artifact_family || group.artifactFamily || id || '',
228
+ targetSlot: group.target_slot || group.targetSlot || null,
229
+ template: String(template),
230
+ intentKinds: arrayFrom(group.intent_kinds || group.intentKinds),
231
+ reason: (typeof artifactValue === 'object' && artifactValue.reason) || group.reason || '',
232
+ });
233
+ }
234
+ }
235
+ return templates;
236
+ }
237
+
238
+ function peerTemplateFamilyMatches(template, hypothesis) {
239
+ const templateFamily = normalize(template.artifactFamily).replace(/-/g, '_');
240
+ const artifactFamily = normalize(hypothesis.artifactFamily).replace(/-/g, '_');
241
+ if (!templateFamily || !artifactFamily || artifactFamily === 'unknown') return true;
242
+ return templateFamily === artifactFamily || templateFamily.endsWith(artifactFamily);
243
+ }
244
+
245
+ function peerTemplateApplies(template, hypothesis) {
246
+ if (template.intentKinds.length > 0 && !template.intentKinds.includes(hypothesis.intentKind)) return false;
247
+ if (!peerTemplateFamilyMatches(template, hypothesis)) return false;
248
+ if (hypothesis.createRequires.length > 0 && !hypothesis.createRequires.includes(template.artifact)) return false;
249
+ return true;
250
+ }
251
+
144
252
  function configuredArtifactFamilyForIntent(area, intent) {
145
253
  const text = normalize(intent);
146
254
  for (const family of configuredArtifactFamilies(area)) {
@@ -189,10 +297,15 @@ function classifyArtifactFamily(intent, area) {
189
297
  }
190
298
 
191
299
  function classifyIntentWrite(intent, area) {
300
+ const identityAliases = aliasRegistryForArea(area);
192
301
  const text = normalize(intent);
193
- const createTerm = [
194
- /\b(add|create|new|introduce|support|implement)\b/,
195
- /新增|新建|创建|添加|接入|支持/,
302
+ const strongCreateTerm = [
303
+ /\b(create|new|introduce|implement)\b/,
304
+ /新增|新建|创建|接入/,
305
+ ].some((pattern) => pattern.test(text));
306
+ const createTerm = strongCreateTerm || [
307
+ /\b(add|support)\b/,
308
+ /添加|支持/,
196
309
  ].some((pattern) => pattern.test(text));
197
310
  const deleteTerm = [
198
311
  /\b(delete|remove)\b/,
@@ -205,7 +318,8 @@ function classifyIntentWrite(intent, area) {
205
318
  const targetAll = intentTargetsAllPeers(intent);
206
319
  const artifactFamily = classifyArtifactFamily(intent, area);
207
320
  const familyConfig = configuredArtifactFamilies(area).find((item) => item.id === artifactFamily) || null;
208
- const targetIdentity = targetIdentityForIntent(intent);
321
+ const targetIdentity = targetIdentityForIntent(intent, identityAliases);
322
+ const targetIdentities = targetIdentitiesForIntent(intent, identityAliases);
209
323
  let action = 'modify';
210
324
  if (deleteTerm) action = 'delete';
211
325
  else if (renameTerm) action = 'rename';
@@ -214,6 +328,7 @@ function classifyIntentWrite(intent, area) {
214
328
  action,
215
329
  artifactFamily,
216
330
  targetIdentity,
331
+ targetIdentities,
217
332
  operation: action === 'create' && artifactFamily === 'provider_proxy' ? 'create_file' : 'modify_existing_file',
218
333
  required: true,
219
334
  targetAll,
@@ -221,10 +336,11 @@ function classifyIntentWrite(intent, area) {
221
336
  createRequires: familyConfig ? familyConfig.createRequires : [],
222
337
  hooks: familyConfig ? familyConfig.hooks : [],
223
338
  confidence: artifactFamily === 'unknown' ? 'low' : 'medium',
339
+ strongCreateIntent: strongCreateTerm,
224
340
  };
225
341
  }
226
342
 
227
- function directCapabilities(scope) {
343
+ function directCapabilities(scope, identityAliases) {
228
344
  const explicitItems = [
229
345
  ...(scope.writableDirectItems || []),
230
346
  ];
@@ -248,12 +364,14 @@ function directCapabilities(scope) {
248
364
  breadth: createCapable ? 'narrow_pattern' : 'exact',
249
365
  reviewGated: false,
250
366
  source: item.source || 'writable_direct',
367
+ operationSpecified: configuredOperations.length > 0,
251
368
  artifact: item.artifact || null,
252
369
  artifactFamily: item.artifactFamily || item.artifact_family || null,
253
370
  intentKinds: arrayFrom(item.intentKinds || item.intent_kinds),
254
371
  requiredWhen: arrayFrom(item.requiredWhen || item.required_when),
255
372
  targetSlot: item.targetSlot || item.target_slot || null,
256
- peerIdentity: canonicalIdentity(item.targetIdentity || item.target_identity || item.peerIdentity || item.peer_identity) || peerIdentityFromPath(pattern),
373
+ peerIdentity: canonicalIdentity(item.targetIdentity || item.target_identity || item.peerIdentity || item.peer_identity, identityAliases)
374
+ || peerIdentityFromPath(pattern, identityAliases),
257
375
  };
258
376
  }).filter(Boolean);
259
377
  }
@@ -288,34 +406,155 @@ function capabilityMatchesIntent(capability, hypothesis) {
288
406
  return capability.intentKinds.includes(hypothesis.intentKind);
289
407
  }
290
408
 
409
+ function expandPeerTemplate(template, hypothesis, area) {
410
+ if (!hypothesis.targetIdentity) return template;
411
+ const label = identityDisplayName(hypothesis.targetIdentity, area);
412
+ const lower = normalize(hypothesis.targetIdentity);
413
+ return template
414
+ .replace(/\{Provider\}|\{Identity\}|\{Target\}/g, label)
415
+ .replace(/\{provider\}|\{identity\}|\{target\}/g, lower)
416
+ .replace(/\{PROVIDER\}|\{IDENTITY\}|\{TARGET\}/g, lower.toUpperCase());
417
+ }
418
+
419
+ function peerFileCandidatesFor(area, hypothesis) {
420
+ return configuredPeerFileTemplates(area)
421
+ .filter((item) => peerTemplateApplies(item, hypothesis))
422
+ .map((item) => ({
423
+ artifact: item.artifact,
424
+ artifactFamily: item.artifactFamily,
425
+ targetSlot: item.targetSlot,
426
+ targetIdentity: hypothesis.targetIdentity || null,
427
+ template: item.template,
428
+ candidatePath: expandPeerTemplate(item.template, hypothesis, area),
429
+ writable: false,
430
+ reason: item.reason || 'peer_file_templates are read-only hints; use writable_create_patterns to grant create permission',
431
+ }));
432
+ }
433
+
434
+ function selectedCapabilitiesFor(capabilities, selected) {
435
+ const selectedSet = new Set(selected);
436
+ return capabilities.filter((item) => selectedSet.has(item.scope));
437
+ }
438
+
439
+ function directReferenceConflicts(references, capabilities) {
440
+ const conflicts = [];
441
+ for (const reference of references) {
442
+ const referencePath = reference.path || reference.pattern;
443
+ if (!referencePath) continue;
444
+ const capability = capabilities.find((item) => (
445
+ item.source !== 'writable_create_patterns'
446
+ && item.operations.includes('update_existing')
447
+ && matchesPattern(referencePath, item.scope)
448
+ ));
449
+ if (capability) {
450
+ conflicts.push({
451
+ path: referencePath,
452
+ directScope: capability.scope,
453
+ });
454
+ }
455
+ }
456
+ return conflicts;
457
+ }
458
+
459
+ function unspecifiedOperationScopes(capabilities, hypothesis) {
460
+ return capabilities
461
+ .filter((item) => (
462
+ item.source === 'writable_direct'
463
+ && item.operationSpecified === false
464
+ && (hypothesis.operation === 'create_file' || item.operations.includes('create_file'))
465
+ ))
466
+ .map((item) => item.scope);
467
+ }
468
+
469
+ function certificateOperation(capability, hypothesis) {
470
+ if (hypothesis.operation === 'create_file' && capability.operations.includes('create_file')) return 'create_file';
471
+ if (capability.operations.includes('update_existing')) return 'update_existing';
472
+ return capability.operations[0] || hypothesis.operation || 'update_existing';
473
+ }
474
+
475
+ function certificateMapEntry(area, capability) {
476
+ const areaId = area && area.id ? area.id : 'unknown_area';
477
+ const source = capability.source || 'writable_direct';
478
+ const label = capability.artifact || capability.artifactFamily || null;
479
+ return label ? `${areaId}.scope.${source}.${label}` : `${areaId}.scope.${source}`;
480
+ }
481
+
482
+ function buildCapabilityCertificate(area, capability, hypothesis) {
483
+ const operation = certificateOperation(capability, hypothesis);
484
+ return {
485
+ path: capability.scope,
486
+ scope: capability.scope,
487
+ operation,
488
+ artifact: capability.artifact || null,
489
+ artifactFamily: capability.artifactFamily || hypothesis.artifactFamily || null,
490
+ targetIdentity: capability.peerIdentity || hypothesis.targetIdentity || null,
491
+ status: 'certified',
492
+ certifiedBy: {
493
+ mapEntry: certificateMapEntry(area, capability),
494
+ source: capability.source || 'writable_direct',
495
+ pattern: capability.scope,
496
+ intentKind: hypothesis.intentKind,
497
+ },
498
+ };
499
+ }
500
+
501
+ function certificatesForSelected(area, capabilities, selected, hypothesis) {
502
+ return selectedCapabilitiesFor(capabilities, selected).map((capability) => (
503
+ buildCapabilityCertificate(area, capability, hypothesis)
504
+ ));
505
+ }
506
+
507
+ /*
508
+ * Write permission is certified here, not by relevance alone:
509
+ * relevant files, entrypoints, peer examples, and broad fallback scopes may help
510
+ * an agent understand the repo, but they do not become Direct Writable unless
511
+ * they cover the inferred required write operation narrowly for this intent.
512
+ */
291
513
  function buildWritePlan(scope, intent, area) {
514
+ const identityAliases = aliasRegistryForArea(area);
292
515
  const hypothesis = classifyIntentWrite(intent, area);
293
- const capabilities = directCapabilities(scope);
516
+ const capabilities = directCapabilities(scope, identityAliases);
294
517
  const warnings = [];
295
518
  const references = (scope.readableReference || []).map((item) => buildReference(
296
519
  {
297
520
  path: item.path,
298
521
  role: item.role || 'reference',
299
522
  artifact: item.artifact || null,
300
- peerIdentity: canonicalIdentity(item.peerIdentity) || peerIdentityFromPath(item.path),
523
+ peerIdentity: canonicalIdentity(item.peerIdentity, identityAliases) || peerIdentityFromPath(item.path, identityAliases),
301
524
  },
302
525
  item.reason || 'readable reference from Project Map'
303
526
  ));
527
+ const peerFileCandidates = peerFileCandidatesFor(area, hypothesis);
304
528
  let selected = capabilities.map((item) => item.scope);
305
529
 
530
+ if (hypothesis.targetIdentities && hypothesis.targetIdentities.length > 1 && !hypothesis.targetAll) {
531
+ warnings.push(warning(
532
+ 'AMBIGUOUS_TARGET_IDENTITY',
533
+ `Intent mentions multiple target identities (${hypothesis.targetIdentities.join(', ')}); Direct Writable must be selected for one unambiguous target.`,
534
+ { targetIdentities: hypothesis.targetIdentities }
535
+ ));
536
+ }
537
+ if (hypothesis.confidence === 'low' && hypothesis.action !== 'modify' && (hypothesis.strongCreateIntent || hypothesis.action !== 'create')) {
538
+ warnings.push(warning(
539
+ 'LOW_CONFIDENCE_INTENT_WRITE_PLAN',
540
+ 'Intent appears to require writes, but Hive Lite could not infer a known artifact family for the required write plan.',
541
+ { action: hypothesis.action, artifactFamily: hypothesis.artifactFamily }
542
+ ));
543
+ }
544
+
306
545
  if (hypothesis.operation === 'create_file' && hypothesis.artifactFamily === 'provider_proxy') {
307
546
  const createCapabilities = capabilities.filter((item) => (
308
547
  item.operations.includes('create_file') && capabilityMatchesIntent(item, hypothesis)
309
548
  ));
310
549
  const genericExistingCapabilities = capabilities.filter((item) => (
311
- !item.peerIdentity
550
+ item.source === 'writable_existing'
312
551
  && !item.operations.includes('create_file')
313
- && item.source === 'writable_existing'
552
+ && (!item.peerIdentity || identityMatches(item.peerIdentity, hypothesis.targetIdentity, identityAliases))
314
553
  ));
315
554
  const referencePeers = capabilities.filter((item) => (
316
555
  item.peerIdentity
317
556
  && hypothesis.targetIdentity
318
- && !identityMatches(item.peerIdentity, hypothesis.targetIdentity)
557
+ && !identityMatches(item.peerIdentity, hypothesis.targetIdentity, identityAliases)
319
558
  ));
320
559
  const selectedCreateCapabilities = hypothesis.createRequires.length
321
560
  ? createCapabilities.filter((item) => hypothesis.createRequires.some((artifact) => artifactMatches(item, artifact)))
@@ -376,13 +615,13 @@ function buildWritePlan(scope, intent, area) {
376
615
  && !hypothesis.targetAll
377
616
  ) {
378
617
  const targetMatches = capabilities.filter((item) => (
379
- item.peerIdentity && identityMatches(item.peerIdentity, hypothesis.targetIdentity)
618
+ item.peerIdentity && identityMatches(item.peerIdentity, hypothesis.targetIdentity, identityAliases)
380
619
  ));
381
620
  const genericCapabilities = capabilities.filter((item) => (
382
621
  !item.peerIdentity && !item.operations.includes('create_file')
383
622
  ));
384
623
  const referencePeers = capabilities.filter((item) => (
385
- item.peerIdentity && !identityMatches(item.peerIdentity, hypothesis.targetIdentity)
624
+ item.peerIdentity && !identityMatches(item.peerIdentity, hypothesis.targetIdentity, identityAliases)
386
625
  ));
387
626
  selected = unique([...targetMatches, ...genericCapabilities].map((item) => item.scope));
388
627
  for (const capability of referencePeers) {
@@ -400,11 +639,31 @@ function buildWritePlan(scope, intent, area) {
400
639
  }
401
640
 
402
641
  selected = unique(selected);
642
+ const selectedCapabilities = selectedCapabilitiesFor(capabilities, selected);
643
+ const selectedWritableCertificates = certificatesForSelected(area, capabilities, selected, hypothesis);
644
+ const referenceConflicts = directReferenceConflicts(references, selectedCapabilities);
645
+ if (referenceConflicts.length > 0) {
646
+ warnings.push(warning(
647
+ 'REFERENCE_DIRECT_ROLE_CONFLICT',
648
+ 'One or more reference files also match selected Direct Writable update scope; keep peer examples reference-only or move new-file permission to writable_create_patterns.',
649
+ { blocking: false, conflicts: referenceConflicts }
650
+ ));
651
+ }
652
+ const unspecifiedScopes = unspecifiedOperationScopes(selectedCapabilities, hypothesis);
653
+ if (unspecifiedScopes.length > 0) {
654
+ warnings.push(warning(
655
+ 'DIRECT_SCOPE_OPERATION_UNSPECIFIED',
656
+ 'Selected Direct Writable scope relies on inferred operations; declare operations under writable_existing or writable_create_patterns for safer write certification.',
657
+ { blocking: false, scopes: unspecifiedScopes }
658
+ ));
659
+ }
403
660
  return {
404
661
  hypotheses: [hypothesis],
405
662
  capabilities,
406
663
  selectedWritableDirect: selected,
664
+ selectedWritableCertificates,
407
665
  referenceFiles: references,
666
+ peerFileCandidates,
408
667
  blockingWarnings: warnings.filter((item) => item.blocking !== false),
409
668
  warnings,
410
669
  };
@@ -780,8 +1039,8 @@ function candidatePhaseSeeds(scored, intent) {
780
1039
  phaseHint: area.name || area.id,
781
1040
  findIntent: `${intent} [focus: ${area.id}]`,
782
1041
  roles,
783
- evidenceClasses: evidenceClassesForRoles(roles),
784
- validationProfiles: ((area.validation || {}).profiles || []).filter(Boolean),
1042
+ evidenceClasses: unique(evidenceClassesForRoles(roles)),
1043
+ validationProfiles: unique(((area.validation || {}).profiles || []).filter(Boolean)),
785
1044
  risk: riskLevelFor(area),
786
1045
  reason: item.signals.slice(0, 4).join(', '),
787
1046
  };
@@ -826,14 +1085,14 @@ function buildSplitMarkdown(note) {
826
1085
  '',
827
1086
  `Area: ${phase.primaryAreaId}`,
828
1087
  '',
829
- 'Run with this Hive Lite CLI invocation:',
1088
+ 'Copy one command block below to run this phase. Do not append the notes after it.',
830
1089
  '',
831
1090
  '```bash',
832
1091
  findCommandForPhase(currentArgv, note, phase),
833
1092
  '```',
834
1093
  '',
835
1094
  ...(!sameArgv(currentArgv, packageAliasArgv) ? [
836
- 'If the package alias is installed:',
1095
+ 'Or, if the package alias is installed:',
837
1096
  '',
838
1097
  '```bash',
839
1098
  findCommandForPhase(packageAliasArgv, note, phase),
@@ -895,10 +1154,10 @@ function createSplitNote(root, packet, phaseSeeds, decomposition, options = {})
895
1154
  title: phase.phaseHint,
896
1155
  findIntent: phase.findIntent,
897
1156
  primaryAreaId: phase.areaId,
898
- expectedEvidence: [
1157
+ expectedEvidence: unique([
899
1158
  ...(phase.validationProfiles || []).map((profile) => `validation:${profile}`),
900
1159
  ...(phase.evidenceClasses || []).map((item) => `evidence:${item}`),
901
- ],
1160
+ ]),
902
1161
  nonGoals: phaseSeeds
903
1162
  .filter((other) => other.areaId !== phase.areaId)
904
1163
  .map((other) => `Do not change ${other.areaId} in this phase.`),
@@ -1113,6 +1372,8 @@ function hasBlockingMapGapWarning(warnings = []) {
1113
1372
  'TARGET_ENTITY_MISMATCH',
1114
1373
  'MISSING_ARTIFACT_FAMILY_SCOPE',
1115
1374
  'MISSING_REQUIRED_HOOK_SCOPE',
1375
+ 'AMBIGUOUS_TARGET_IDENTITY',
1376
+ 'LOW_CONFIDENCE_INTENT_WRITE_PLAN',
1116
1377
  ].includes(warning.code));
1117
1378
  }
1118
1379
 
@@ -1241,11 +1502,21 @@ function buildContextMarkdown(packet) {
1241
1502
  : ['- Coverage: direct writable scope covers the inferred write operation.']),
1242
1503
  '',
1243
1504
  ] : []),
1505
+ ...(packet.writePlan && packet.writePlan.selectedWritableCertificates && packet.writePlan.selectedWritableCertificates.length ? [
1506
+ '## Certified Direct Writable',
1507
+ ...packet.writePlan.selectedWritableCertificates.map((item) => `- ${item.path}: ${item.operation}${item.artifact ? ` ${item.artifact}` : ''}${item.targetIdentity ? ` for ${item.targetIdentity}` : ''} (certified by ${item.certifiedBy && item.certifiedBy.mapEntry ? item.certifiedBy.mapEntry : 'unknown map entry'})`),
1508
+ '',
1509
+ ] : []),
1244
1510
  ...(packet.writePlan && packet.writePlan.referenceFiles.length ? [
1245
1511
  '## Reference Files',
1246
1512
  ...packet.writePlan.referenceFiles.map((file) => `- ${file.path}: ${file.reason}`),
1247
1513
  '',
1248
1514
  ] : []),
1515
+ ...(packet.writePlan && packet.writePlan.peerFileCandidates && packet.writePlan.peerFileCandidates.length ? [
1516
+ '## Peer File Template Candidates',
1517
+ ...packet.writePlan.peerFileCandidates.map((file) => `- ${file.artifact}: ${file.candidatePath || file.template} (read-only hint; not Direct Writable)`),
1518
+ '',
1519
+ ] : []),
1249
1520
  '## Relevant Files',
1250
1521
  ...packet.relevantFiles.map((file) => `- ${file.path} (${file.role || file.source}): ${file.reason}`),
1251
1522
  '',
@@ -1337,7 +1608,9 @@ function createContextPacket(root, intent, options = {}) {
1337
1608
  hypotheses: [],
1338
1609
  capabilities: [],
1339
1610
  selectedWritableDirect: [],
1611
+ selectedWritableCertificates: [],
1340
1612
  referenceFiles: [],
1613
+ peerFileCandidates: [],
1341
1614
  blockingWarnings: [],
1342
1615
  warnings: [],
1343
1616
  };
package/src/lib/git.js CHANGED
@@ -123,6 +123,7 @@ module.exports = {
123
123
  repoRoot,
124
124
  requireGitRepo,
125
125
  runGit,
126
+ runGitRaw,
126
127
  statusLines,
127
128
  tryGit,
128
129
  };