@event4u/agent-config 8.11.0 → 8.12.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +49 -279
- package/README.md +2 -2
- package/dist/agent-src/contexts/execution/roadmap-ci-steps-mechanics.md +117 -0
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +106 -94
- package/dist/agent-src/rules/architecture.md +6 -51
- package/dist/agent-src/rules/code-comment-discipline.md +10 -94
- package/dist/agent-src/rules/decision-revisit-gate.md +9 -77
- package/dist/agent-src/rules/improve-before-implement.md +7 -66
- package/dist/agent-src/rules/no-roadmap-references.md +13 -77
- package/dist/agent-src/rules/persona-governance.md +5 -47
- package/dist/agent-src/rules/provider-lifecycle-discipline.md +6 -32
- package/dist/agent-src/rules/roadmap-ci-steps-policy.md +21 -104
- package/dist/agent-src/rules/untrusted-input-defense.md +3 -75
- package/dist/agent-src/skills/agent-docs-writing/SKILL.md +83 -0
- package/dist/agent-src/skills/decision-review/SKILL.md +86 -0
- package/dist/agent-src/skills/module-detect-on-the-fly/SKILL.md +58 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/contexts/knowledge-card.md +12 -0
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +14 -14
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/mcp/registry-manifest.json +1 -1
- package/dist/router.json +1 -1
- package/docs/CLAIMS.md +30 -2
- package/docs/architecture.md +1 -1
- package/docs/archive/CHANGELOG-pre-8.12.0.md +304 -0
- package/docs/benchmark.md +39 -0
- package/docs/contracts/ai-council-config.md +9 -4
- package/docs/contracts/persona-schema.md +49 -0
- package/docs/contracts/provider-lifecycle.md +29 -4
- package/docs/decisions/ADR-120-council-chairman-mode.md +70 -0
- package/docs/decisions/ADR-121-knowledge-sensitivity-classes.md +90 -0
- package/docs/decisions/INDEX.md +2 -0
- package/docs/design/council-vs-solo-baseline.md +92 -0
- package/docs/design/utilization-window-criteria.md +51 -0
- package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +68 -0
- package/docs/guidelines/agent-infra/rule-body-migration-inventory.md +241 -0
- package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +80 -3
- package/docs/guidelines/code-clarity.md +86 -1
- package/docs/maintainers/branch-protection.md +87 -0
- package/docs/maintainers/system-map.md +85 -0
- package/docs/proof.md +14 -3
- package/package.json +1 -1
- package/src/config/agent-settings.template.yml +9 -0
- package/src/scripts/_cli/cmd_doctor.ts +95 -0
- package/src/scripts/_lib/knowledge_global.ts +44 -0
- package/src/scripts/_lib/knowledge_global_promote.ts +150 -0
- package/src/scripts/ai_council/orchestrator.ts +188 -1
- package/src/scripts/bench_persona_placebo.ts +308 -0
- package/src/scripts/bench_quality_rerun.ts +357 -0
- package/src/scripts/build_proof.ts +9 -0
- package/src/scripts/check_artefact_count_messaging.ts +56 -0
- package/src/scripts/check_knowledge_cards.ts +33 -4
- package/src/scripts/check_portability.ts +1 -0
- package/src/scripts/complexity_report.ts +776 -0
- package/src/scripts/consumer_matrix.ts +251 -12
- package/src/scripts/council_cli.ts +56 -1
- package/src/scripts/explain_run.ts +593 -0
- package/src/scripts/knowledge_global_cli.ts +234 -45
- package/src/scripts/rule_backlinks.ts +0 -0
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
*/
|
|
35
35
|
import { spawnSync } from 'node:child_process';
|
|
36
36
|
import * as fs from 'node:fs';
|
|
37
|
+
import * as os from 'node:os';
|
|
37
38
|
import * as path from 'node:path';
|
|
38
39
|
import process from 'node:process';
|
|
39
40
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
@@ -105,10 +106,13 @@ function _disabled_notice(): number {
|
|
|
105
106
|
// Subcommands
|
|
106
107
|
// ---------------------------------------------------------------------------
|
|
107
108
|
|
|
108
|
-
function cmd_list(args: { json: boolean }): number {
|
|
109
|
+
function cmd_list(args: { json: boolean; revoked: boolean }): number {
|
|
109
110
|
if (!kg.is_enabled()) {
|
|
110
111
|
return _disabled_notice();
|
|
111
112
|
}
|
|
113
|
+
if (args.revoked) {
|
|
114
|
+
return _list_revoked(args.json);
|
|
115
|
+
}
|
|
112
116
|
const cfg = kg.load_global_sharing_config();
|
|
113
117
|
const cards = _list_cards();
|
|
114
118
|
const rows: Record<string, string>[] = [];
|
|
@@ -142,6 +146,24 @@ function cmd_list(args: { json: boolean }): number {
|
|
|
142
146
|
return 0;
|
|
143
147
|
}
|
|
144
148
|
|
|
149
|
+
/** Render the revocation ledger (`list --revoked`) — table or `--json`. */
|
|
150
|
+
function _list_revoked(json: boolean): number {
|
|
151
|
+
const trail = kgp.load_tombstones();
|
|
152
|
+
if (json) {
|
|
153
|
+
process.stdout.write(kg.pyJsonDumps(trail, 2, true) + '\n');
|
|
154
|
+
return 0;
|
|
155
|
+
}
|
|
156
|
+
if (trail.length === 0) {
|
|
157
|
+
process.stdout.write(`No revocations recorded in ${kg.global_store_dir()}.\n`);
|
|
158
|
+
return 0;
|
|
159
|
+
}
|
|
160
|
+
process.stdout.write(`${_ljust('REVOKED-AT', 12)} ${_ljust('CARD', 28)} REASON\n`);
|
|
161
|
+
for (const t of trail) {
|
|
162
|
+
process.stdout.write(`${_ljust(t.revoked_at, 12)} ${_ljust(t.card_id, 28)} ${t.reason}\n`);
|
|
163
|
+
}
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
145
167
|
function cmd_show(args: { card: string }): number {
|
|
146
168
|
if (!kg.is_enabled()) {
|
|
147
169
|
return _disabled_notice();
|
|
@@ -185,37 +207,45 @@ function cmd_trace(args: { card: string; json: boolean }): number {
|
|
|
185
207
|
return 0;
|
|
186
208
|
}
|
|
187
209
|
|
|
188
|
-
|
|
210
|
+
/**
|
|
211
|
+
* Remove one global card + its usage entry. Writes an append-only tombstone
|
|
212
|
+
* to the revocation ledger BEFORE deleting anything — no silent deletes.
|
|
213
|
+
*/
|
|
214
|
+
function _forget_one(card: string, reason: string): boolean {
|
|
189
215
|
const p = _card_path(card);
|
|
190
|
-
let removed = false;
|
|
191
|
-
if (fs.existsSync(p)) {
|
|
192
|
-
fs.unlinkSync(p);
|
|
193
|
-
removed = true;
|
|
194
|
-
}
|
|
195
216
|
const cid = kgp.card_id_from({ card_name: card });
|
|
196
217
|
const usage = kgp.load_usage();
|
|
197
218
|
const cards = usage['cards'] as SettingsDict;
|
|
198
|
-
|
|
219
|
+
const has_card = fs.existsSync(p);
|
|
220
|
+
const has_usage = cid in cards;
|
|
221
|
+
if (!has_card && !has_usage) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
kgp.append_tombstone(cid, reason, { today: _today() });
|
|
225
|
+
if (has_card) {
|
|
226
|
+
fs.unlinkSync(p);
|
|
227
|
+
}
|
|
228
|
+
if (has_usage) {
|
|
199
229
|
delete cards[cid];
|
|
200
230
|
write_atomic(
|
|
201
231
|
path.join(kg.global_store_dir(null, { create: true }), kgp.USAGE_FILENAME),
|
|
202
232
|
kg.pyJsonDumps(usage, 2, true) + '\n',
|
|
203
233
|
);
|
|
204
|
-
removed = true;
|
|
205
234
|
}
|
|
206
|
-
return
|
|
235
|
+
return true;
|
|
207
236
|
}
|
|
208
237
|
|
|
209
|
-
function cmd_forget(args: { card: string; tier: string }): number {
|
|
238
|
+
function cmd_forget(args: { card: string; tier: string; reason: string }): number {
|
|
210
239
|
if (!kg.is_enabled()) {
|
|
211
240
|
return _disabled_notice();
|
|
212
241
|
}
|
|
213
242
|
if (args.tier) {
|
|
243
|
+
const reason = args.reason || `bulk forget (tier=${args.tier})`;
|
|
214
244
|
let removed = 0;
|
|
215
245
|
for (const p of _list_cards()) {
|
|
216
246
|
const prov = kg.parse_provenance_footer(_readReplace(p));
|
|
217
247
|
if (prov['tier'] === args.tier) {
|
|
218
|
-
if (_forget_one(_stem(p))) {
|
|
248
|
+
if (_forget_one(_stem(p), reason)) {
|
|
219
249
|
removed += 1;
|
|
220
250
|
}
|
|
221
251
|
}
|
|
@@ -228,7 +258,7 @@ function cmd_forget(args: { card: string; tier: string }): number {
|
|
|
228
258
|
process.exitCode = 1;
|
|
229
259
|
return 1;
|
|
230
260
|
}
|
|
231
|
-
if (_forget_one(args.card)) {
|
|
261
|
+
if (_forget_one(args.card, args.reason || 'manual forget')) {
|
|
232
262
|
process.stdout.write(`Forgot global card '${args.card}'.\n`);
|
|
233
263
|
return 0;
|
|
234
264
|
}
|
|
@@ -237,7 +267,16 @@ function cmd_forget(args: { card: string; tier: string }): number {
|
|
|
237
267
|
return 1;
|
|
238
268
|
}
|
|
239
269
|
|
|
240
|
-
function cmd_promote(args: {
|
|
270
|
+
function cmd_promote(args: {
|
|
271
|
+
path: string;
|
|
272
|
+
source: string;
|
|
273
|
+
tier: string;
|
|
274
|
+
manual: boolean;
|
|
275
|
+
sensitivity: string;
|
|
276
|
+
reason: string;
|
|
277
|
+
owner: string;
|
|
278
|
+
review_after: string;
|
|
279
|
+
}): number {
|
|
241
280
|
if (!kg.is_enabled()) {
|
|
242
281
|
return _disabled_notice();
|
|
243
282
|
}
|
|
@@ -258,13 +297,20 @@ function cmd_promote(args: { path: string; source: string; tier: string; manual:
|
|
|
258
297
|
const allowed = [...kg.allowed_tiers()];
|
|
259
298
|
|
|
260
299
|
const redaction = _isPlainObject(cfg['redaction']) ? (cfg['redaction'] as SettingsDict) : {};
|
|
300
|
+
const redaction_enabled = _pyBool(redaction['enabled'], true);
|
|
301
|
+
const halt_on_trigger = _pyBool(redaction['halt_on_trigger'], true);
|
|
302
|
+
// Computed once — feeds BOTH the existing tier gate below AND the new
|
|
303
|
+
// sensitivity auto-derivation, independent of `halt_on_trigger` (a
|
|
304
|
+
// `prohibited` override never depends on that unrelated config flag).
|
|
305
|
+
const violations = redaction_enabled ? kgr.redaction_scan(text) : [];
|
|
306
|
+
|
|
261
307
|
const result = kgr.gate_card_for_global(text, {
|
|
262
308
|
tier,
|
|
263
309
|
source: args.source,
|
|
264
310
|
card_name: path.basename(src_path),
|
|
265
311
|
allowed_tiers: allowed,
|
|
266
|
-
redaction_enabled
|
|
267
|
-
halt_on_trigger
|
|
312
|
+
redaction_enabled,
|
|
313
|
+
halt_on_trigger,
|
|
268
314
|
});
|
|
269
315
|
if (!result.eligible) {
|
|
270
316
|
if (result.manual_only && !args.manual) {
|
|
@@ -282,8 +328,7 @@ function cmd_promote(args: { path: string; source: string; tier: string; manual:
|
|
|
282
328
|
return 2;
|
|
283
329
|
}
|
|
284
330
|
// manual override of proprietary: still run redaction, never skip it.
|
|
285
|
-
|
|
286
|
-
if (violations.length > 0 && _pyBool(redaction['halt_on_trigger'], true)) {
|
|
331
|
+
if (violations.length > 0 && halt_on_trigger) {
|
|
287
332
|
process.stderr.write(
|
|
288
333
|
'global-share BLOCKED (manual proprietary): redaction halt — ' +
|
|
289
334
|
violations.map((v) => `${v.category}: ${_pyRepr(v.snippet)}`).join('; ') +
|
|
@@ -294,6 +339,19 @@ function cmd_promote(args: { path: string; source: string; tier: string; manual:
|
|
|
294
339
|
}
|
|
295
340
|
}
|
|
296
341
|
|
|
342
|
+
// Sensitivity gate — layered ON TOP of the tier/redaction gate above.
|
|
343
|
+
const declared_sensitivity =
|
|
344
|
+
args.sensitivity || _frontmatter_sensitivity(text) || kg.DEFAULT_SENSITIVITY;
|
|
345
|
+
const sens = kgp.gate_sensitivity_for_promotion(declared_sensitivity, {
|
|
346
|
+
violations_present: violations.length > 0,
|
|
347
|
+
promotion_reason: args.reason,
|
|
348
|
+
});
|
|
349
|
+
if (!sens.eligible) {
|
|
350
|
+
process.stderr.write(`global-share BLOCKED: ${sens.reason}\n`);
|
|
351
|
+
process.exitCode = 2;
|
|
352
|
+
return 2;
|
|
353
|
+
}
|
|
354
|
+
|
|
297
355
|
// Build provenance + usage.
|
|
298
356
|
const cid = kgp.card_id_from({ card_name: path.basename(src_path), source: args.source });
|
|
299
357
|
const slug = kgp.repo_slug();
|
|
@@ -306,6 +364,11 @@ function cmd_promote(args: { path: string; source: string; tier: string; manual:
|
|
|
306
364
|
? (entry['first_seen'] as SettingsDict)
|
|
307
365
|
: { repo: slug, date: today };
|
|
308
366
|
|
|
367
|
+
const fresh = _isPlainObject(cfg['freshness']) ? (cfg['freshness'] as SettingsDict) : {};
|
|
368
|
+
const review_after =
|
|
369
|
+
args.review_after || _addDaysIso(today, _pyInt(fresh['stale_after_days'], 180));
|
|
370
|
+
const owner = args.owner || _defaultOwner();
|
|
371
|
+
|
|
309
372
|
const footer = kg.render_provenance_footer({
|
|
310
373
|
first_seen_repo: (first['repo'] as string) ?? slug,
|
|
311
374
|
first_seen_date: (first['date'] as string) ?? today,
|
|
@@ -313,8 +376,13 @@ function cmd_promote(args: { path: string; source: string; tier: string; manual:
|
|
|
313
376
|
last_verified: today,
|
|
314
377
|
tier,
|
|
315
378
|
seen_in,
|
|
379
|
+
source_repo: slug,
|
|
380
|
+
owner,
|
|
381
|
+
review_after,
|
|
382
|
+
promotion_reason: args.reason,
|
|
316
383
|
});
|
|
317
|
-
let out_text =
|
|
384
|
+
let out_text = _ensure_frontmatter_field(text, 'tier', tier);
|
|
385
|
+
out_text = _ensure_frontmatter_field(out_text, 'sensitivity', sens.sensitivity);
|
|
318
386
|
out_text = _rstrip(kg.strip_provenance_footer(out_text)) + '\n\n' + footer;
|
|
319
387
|
const dest = _card_path(cid);
|
|
320
388
|
write_atomic(path.join(kg.global_store_dir(null, { create: true }), path.basename(dest)), out_text);
|
|
@@ -329,11 +397,11 @@ function cmd_promote(args: { path: string; source: string; tier: string; manual:
|
|
|
329
397
|
kg.pyJsonDumps(usage, 2, true) + '\n',
|
|
330
398
|
);
|
|
331
399
|
}
|
|
332
|
-
process.stdout.write(`Promoted '${cid}' (tier=${tier}) → ${dest}\n`);
|
|
400
|
+
process.stdout.write(`Promoted '${cid}' (tier=${tier}, sensitivity=${sens.sensitivity}) → ${dest}\n`);
|
|
333
401
|
return 0;
|
|
334
402
|
}
|
|
335
403
|
|
|
336
|
-
function cmd_purge(args: { confirm: boolean }): number {
|
|
404
|
+
function cmd_purge(args: { confirm: boolean; reason: string }): number {
|
|
337
405
|
// Remove the global store and strip provenance footers from project cards.
|
|
338
406
|
if (!args.confirm) {
|
|
339
407
|
process.stderr.write(
|
|
@@ -345,9 +413,30 @@ function cmd_purge(args: { confirm: boolean }): number {
|
|
|
345
413
|
}
|
|
346
414
|
|
|
347
415
|
const store = kg.global_store_dir();
|
|
416
|
+
const reason = args.reason || 'full store purge';
|
|
417
|
+
if (fs.existsSync(store)) {
|
|
418
|
+
// Tombstone every surviving card_id BEFORE deleting anything — no
|
|
419
|
+
// silent deletes, even on the biggest deletion event this CLI has.
|
|
420
|
+
const usage = kgp.load_usage();
|
|
421
|
+
const cards = usage['cards'] as SettingsDict;
|
|
422
|
+
const seen_ids = new Set<string>();
|
|
423
|
+
for (const p of _list_cards()) {
|
|
424
|
+
const cid = _stem(p);
|
|
425
|
+
seen_ids.add(cid);
|
|
426
|
+
kgp.append_tombstone(cid, reason, { today: _today() });
|
|
427
|
+
}
|
|
428
|
+
for (const cid of Object.keys(cards)) {
|
|
429
|
+
if (!seen_ids.has(cid)) {
|
|
430
|
+
kgp.append_tombstone(cid, reason, { today: _today() });
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
348
434
|
let removed = 0;
|
|
349
435
|
if (fs.existsSync(store)) {
|
|
350
436
|
for (const p of _globAllSorted(store)) {
|
|
437
|
+
if (path.basename(p) === kgp.REVOCATIONS_FILENAME) {
|
|
438
|
+
continue; // the ledger survives its own purge entry — the audit trail persists
|
|
439
|
+
}
|
|
351
440
|
if (_isFile(p)) {
|
|
352
441
|
fs.unlinkSync(p);
|
|
353
442
|
removed += 1;
|
|
@@ -356,7 +445,7 @@ function cmd_purge(args: { confirm: boolean }): number {
|
|
|
356
445
|
try {
|
|
357
446
|
fs.rmdirSync(store);
|
|
358
447
|
} catch {
|
|
359
|
-
// non-empty (subdirs) — leave it
|
|
448
|
+
// non-empty (the ledger, or subdirs) — leave it
|
|
360
449
|
}
|
|
361
450
|
}
|
|
362
451
|
process.stdout.write(`Purged global store (${removed} file(s)) at ${store}.\n`);
|
|
@@ -511,32 +600,43 @@ function cmd_lead_check(args: { report: string; strict: boolean }): number {
|
|
|
511
600
|
// ---------------------------------------------------------------------------
|
|
512
601
|
|
|
513
602
|
function _frontmatter_tier(text: string): string {
|
|
603
|
+
const val = _frontmatter_scalar(text, 'tier');
|
|
604
|
+
return kg.TIERS.includes(val) ? val : '';
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function _frontmatter_sensitivity(text: string): string {
|
|
608
|
+
const val = _frontmatter_scalar(text, 'sensitivity');
|
|
609
|
+
return kg.SENSITIVITIES.includes(val) ? val : '';
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/** Read a top-level scalar frontmatter field's raw (quote-stripped) value. */
|
|
613
|
+
function _frontmatter_scalar(text: string, field: string): string {
|
|
514
614
|
const m = _FRONTMATTER_RE.exec(text);
|
|
515
615
|
if (!m) {
|
|
516
616
|
return '';
|
|
517
617
|
}
|
|
518
618
|
for (const line of _splitlines(m[1] as string)) {
|
|
519
619
|
const s = _strip(line);
|
|
520
|
-
if (s.startsWith(
|
|
521
|
-
|
|
522
|
-
return kg.TIERS.includes(val) ? val : '';
|
|
620
|
+
if (s.startsWith(`${field}:`)) {
|
|
621
|
+
return _stripQuotes(_strip(s.slice(field.length + 1)));
|
|
523
622
|
}
|
|
524
623
|
}
|
|
525
624
|
return '';
|
|
526
625
|
}
|
|
527
626
|
|
|
528
|
-
|
|
529
|
-
|
|
627
|
+
/** Ensure the card frontmatter carries an accurate `<field>: <value>` line. */
|
|
628
|
+
function _ensure_frontmatter_field(text: string, field: string, value: string): string {
|
|
530
629
|
const m = _FRONTMATTER_RE.exec(text);
|
|
531
630
|
if (!m) {
|
|
532
631
|
return text;
|
|
533
632
|
}
|
|
534
633
|
const block = m[1] as string;
|
|
634
|
+
const line_re = new RegExp(`^\\s*${field}:.*$`, 'm');
|
|
535
635
|
let new_block: string;
|
|
536
|
-
if (
|
|
537
|
-
new_block = _reSubFirst(block,
|
|
636
|
+
if (line_re.test(block)) {
|
|
637
|
+
new_block = _reSubFirst(block, line_re, `${field}: ${value}`);
|
|
538
638
|
} else {
|
|
539
|
-
new_block =
|
|
639
|
+
new_block = `${field}: ${value}\n${block}`;
|
|
540
640
|
}
|
|
541
641
|
// m.index of group 1: in Python `m.start(1)` / `m.end(1)`. The full match
|
|
542
642
|
// starts at 0 (anchored `^`); group 1 begins after the leading `---\n`.
|
|
@@ -612,20 +712,23 @@ function _wrap<T>(cmd: string, rest: string[], parse: (rest: string[]) => T | nu
|
|
|
612
712
|
|
|
613
713
|
// Per-subcommand argparse-faithful parsers.
|
|
614
714
|
|
|
615
|
-
function _parseList(rest: string[]): { json: boolean } | number {
|
|
715
|
+
function _parseList(rest: string[]): { json: boolean; revoked: boolean } | number {
|
|
616
716
|
let json = false;
|
|
617
|
-
|
|
717
|
+
let revoked = false;
|
|
718
|
+
const usage = `${_PROG} list [-h] [--json] [--revoked]`;
|
|
618
719
|
for (const a of rest) {
|
|
619
720
|
if (a === '-h' || a === '--help') {
|
|
620
721
|
return _subHelp(usage);
|
|
621
722
|
}
|
|
622
723
|
if (a === '--json') {
|
|
623
724
|
json = true;
|
|
725
|
+
} else if (a === '--revoked') {
|
|
726
|
+
revoked = true;
|
|
624
727
|
} else {
|
|
625
728
|
return _subError('list', `unrecognized arguments: ${a}`, usage);
|
|
626
729
|
}
|
|
627
730
|
}
|
|
628
|
-
return { json };
|
|
731
|
+
return { json, revoked };
|
|
629
732
|
}
|
|
630
733
|
|
|
631
734
|
function _parseShow(rest: string[]): { card: string } | number {
|
|
@@ -674,10 +777,11 @@ function _parseTrace(rest: string[]): { card: string; json: boolean } | number {
|
|
|
674
777
|
return { card, json };
|
|
675
778
|
}
|
|
676
779
|
|
|
677
|
-
function _parseForget(rest: string[]): { card: string; tier: string } | number {
|
|
678
|
-
const usage = `${_PROG} forget [-h] [--tier {public,vendor,proprietary}] [card]`;
|
|
780
|
+
function _parseForget(rest: string[]): { card: string; tier: string; reason: string } | number {
|
|
781
|
+
const usage = `${_PROG} forget [-h] [--tier {public,vendor,proprietary}] [--reason REASON] [card]`;
|
|
679
782
|
let card = '';
|
|
680
783
|
let tier = '';
|
|
784
|
+
let reason = '';
|
|
681
785
|
let cardSet = false;
|
|
682
786
|
for (let i = 0; i < rest.length; i += 1) {
|
|
683
787
|
const a = rest[i] as string;
|
|
@@ -689,6 +793,10 @@ function _parseForget(rest: string[]): { card: string; tier: string } | number {
|
|
|
689
793
|
tier = choice;
|
|
690
794
|
} else if (a.startsWith('--tier=')) {
|
|
691
795
|
tier = _choiceOrExit(a.slice('--tier='.length), ['public', 'vendor', 'proprietary'], '--tier', 'forget', usage);
|
|
796
|
+
} else if (a === '--reason') {
|
|
797
|
+
reason = (rest[++i] ?? '') as string;
|
|
798
|
+
} else if (a.startsWith('--reason=')) {
|
|
799
|
+
reason = a.slice('--reason='.length);
|
|
692
800
|
} else if (a.startsWith('-')) {
|
|
693
801
|
return _subError('forget', `unrecognized arguments: ${a}`, usage);
|
|
694
802
|
} else if (!cardSet) {
|
|
@@ -698,15 +806,35 @@ function _parseForget(rest: string[]): { card: string; tier: string } | number {
|
|
|
698
806
|
return _subError('forget', `unrecognized arguments: ${a}`, usage);
|
|
699
807
|
}
|
|
700
808
|
}
|
|
701
|
-
return { card, tier };
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
function _parsePromote(
|
|
705
|
-
|
|
809
|
+
return { card, tier, reason };
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function _parsePromote(
|
|
813
|
+
rest: string[],
|
|
814
|
+
):
|
|
815
|
+
| {
|
|
816
|
+
path: string;
|
|
817
|
+
source: string;
|
|
818
|
+
tier: string;
|
|
819
|
+
manual: boolean;
|
|
820
|
+
sensitivity: string;
|
|
821
|
+
reason: string;
|
|
822
|
+
owner: string;
|
|
823
|
+
review_after: string;
|
|
824
|
+
}
|
|
825
|
+
| number {
|
|
826
|
+
const usage =
|
|
827
|
+
`${_PROG} promote [-h] [--source SOURCE] [--tier {public,vendor,proprietary}] ` +
|
|
828
|
+
'[--manual] [--sensitivity {prohibited,project,shareable}] [--reason REASON] ' +
|
|
829
|
+
'[--owner OWNER] [--review-after DATE] path';
|
|
706
830
|
let p: string | null = null;
|
|
707
831
|
let source = '';
|
|
708
832
|
let tier = '';
|
|
709
833
|
let manual = false;
|
|
834
|
+
let sensitivity = '';
|
|
835
|
+
let reason = '';
|
|
836
|
+
let owner = '';
|
|
837
|
+
let review_after = '';
|
|
710
838
|
for (let i = 0; i < rest.length; i += 1) {
|
|
711
839
|
const a = rest[i] as string;
|
|
712
840
|
if (a === '-h' || a === '--help') {
|
|
@@ -722,6 +850,34 @@ function _parsePromote(rest: string[]): { path: string; source: string; tier: st
|
|
|
722
850
|
tier = _choiceOrExit(a.slice('--tier='.length), ['public', 'vendor', 'proprietary'], '--tier', 'promote', usage);
|
|
723
851
|
} else if (a === '--manual') {
|
|
724
852
|
manual = true;
|
|
853
|
+
} else if (a === '--sensitivity') {
|
|
854
|
+
sensitivity = _choiceOrExit(
|
|
855
|
+
rest[++i],
|
|
856
|
+
['prohibited', 'project', 'shareable'],
|
|
857
|
+
'--sensitivity',
|
|
858
|
+
'promote',
|
|
859
|
+
usage,
|
|
860
|
+
);
|
|
861
|
+
} else if (a.startsWith('--sensitivity=')) {
|
|
862
|
+
sensitivity = _choiceOrExit(
|
|
863
|
+
a.slice('--sensitivity='.length),
|
|
864
|
+
['prohibited', 'project', 'shareable'],
|
|
865
|
+
'--sensitivity',
|
|
866
|
+
'promote',
|
|
867
|
+
usage,
|
|
868
|
+
);
|
|
869
|
+
} else if (a === '--reason') {
|
|
870
|
+
reason = (rest[++i] ?? '') as string;
|
|
871
|
+
} else if (a.startsWith('--reason=')) {
|
|
872
|
+
reason = a.slice('--reason='.length);
|
|
873
|
+
} else if (a === '--owner') {
|
|
874
|
+
owner = (rest[++i] ?? '') as string;
|
|
875
|
+
} else if (a.startsWith('--owner=')) {
|
|
876
|
+
owner = a.slice('--owner='.length);
|
|
877
|
+
} else if (a === '--review-after') {
|
|
878
|
+
review_after = (rest[++i] ?? '') as string;
|
|
879
|
+
} else if (a.startsWith('--review-after=')) {
|
|
880
|
+
review_after = a.slice('--review-after='.length);
|
|
725
881
|
} else if (a.startsWith('-')) {
|
|
726
882
|
return _subError('promote', `unrecognized arguments: ${a}`, usage);
|
|
727
883
|
} else if (p === null) {
|
|
@@ -733,7 +889,7 @@ function _parsePromote(rest: string[]): { path: string; source: string; tier: st
|
|
|
733
889
|
if (p === null) {
|
|
734
890
|
return _subError('promote', 'the following arguments are required: path', usage);
|
|
735
891
|
}
|
|
736
|
-
return { path: p, source, tier, manual };
|
|
892
|
+
return { path: p, source, tier, manual, sensitivity, reason, owner, review_after };
|
|
737
893
|
}
|
|
738
894
|
|
|
739
895
|
function _parseValidate(rest: string[]): { check_urls: boolean } | number {
|
|
@@ -774,20 +930,26 @@ function _parseLeadCheck(rest: string[]): { report: string; strict: boolean } |
|
|
|
774
930
|
return { report, strict };
|
|
775
931
|
}
|
|
776
932
|
|
|
777
|
-
function _parsePurge(rest: string[]): { confirm: boolean } | number {
|
|
778
|
-
const usage = `${_PROG} purge [-h] [--confirm]`;
|
|
933
|
+
function _parsePurge(rest: string[]): { confirm: boolean; reason: string } | number {
|
|
934
|
+
const usage = `${_PROG} purge [-h] [--confirm] [--reason REASON]`;
|
|
779
935
|
let confirm = false;
|
|
780
|
-
|
|
936
|
+
let reason = '';
|
|
937
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
938
|
+
const a = rest[i] as string;
|
|
781
939
|
if (a === '-h' || a === '--help') {
|
|
782
940
|
return _subHelp(usage);
|
|
783
941
|
}
|
|
784
942
|
if (a === '--confirm') {
|
|
785
943
|
confirm = true;
|
|
944
|
+
} else if (a === '--reason') {
|
|
945
|
+
reason = (rest[++i] ?? '') as string;
|
|
946
|
+
} else if (a.startsWith('--reason=')) {
|
|
947
|
+
reason = a.slice('--reason='.length);
|
|
786
948
|
} else {
|
|
787
949
|
return _subError('purge', `unrecognized arguments: ${a}`, usage);
|
|
788
950
|
}
|
|
789
951
|
}
|
|
790
|
-
return { confirm };
|
|
952
|
+
return { confirm, reason };
|
|
791
953
|
}
|
|
792
954
|
|
|
793
955
|
function _subHelp(usage: string): number {
|
|
@@ -1077,6 +1239,33 @@ function _dateDiffDays(a: YMD, b: YMD): number {
|
|
|
1077
1239
|
return Math.round((ta - tb) / 86_400_000);
|
|
1078
1240
|
}
|
|
1079
1241
|
|
|
1242
|
+
/** `YYYY-MM-DD` + N days → `YYYY-MM-DD` (UTC). Invalid input → the input unchanged. */
|
|
1243
|
+
function _addDaysIso(iso: string, days: number): string {
|
|
1244
|
+
const parsed = _strptime(iso);
|
|
1245
|
+
if (parsed === null) {
|
|
1246
|
+
return iso;
|
|
1247
|
+
}
|
|
1248
|
+
const ms = Date.UTC(parsed.y, parsed.m - 1, parsed.d) + days * 86_400_000;
|
|
1249
|
+
const d = new Date(ms);
|
|
1250
|
+
const y = d.getUTCFullYear();
|
|
1251
|
+
const mo = String(d.getUTCMonth() + 1).padStart(2, '0');
|
|
1252
|
+
const dd = String(d.getUTCDate()).padStart(2, '0');
|
|
1253
|
+
return `${y}-${mo}-${dd}`;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
/** Default `owner` for a promoted card when `--owner` is not given. */
|
|
1257
|
+
function _defaultOwner(): string {
|
|
1258
|
+
try {
|
|
1259
|
+
const name = os.userInfo().username;
|
|
1260
|
+
if (name) {
|
|
1261
|
+
return name;
|
|
1262
|
+
}
|
|
1263
|
+
} catch {
|
|
1264
|
+
// platform without a resolvable user (rare) — fall through
|
|
1265
|
+
}
|
|
1266
|
+
return process.env['USER'] ?? process.env['USERNAME'] ?? 'unknown';
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1080
1269
|
/** Mirror Python `OSError`'s `str(exc)` for the read-failure message. */
|
|
1081
1270
|
function _osErrorStr(exc: unknown): string {
|
|
1082
1271
|
if (exc && typeof exc === 'object') {
|
|
Binary file
|