@walwal-harness/cli 7.1.25 → 7.1.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/HR-Resource/cdo/SKILL.md +4 -0
- package/HR-Resource/ceo/SKILL.md +9 -0
- package/HR-Resource/coo/SKILL.md +20 -4
- package/HR-Resource/cqo/SKILL.md +4 -0
- package/HR-Resource/cto/SKILL.md +4 -0
- package/HR-Resource/hiring/SKILL.md +5 -1
- package/HR-Resource/index.json +5 -1
- package/HR-Resource/ops/SKILL.md +4 -0
- package/HR-Resource/resource-manager/SKILL.md +4 -1
- package/HR-Resource/support-support-mcp-registry-steward/SKILL.md +77 -0
- package/README.md +11 -3
- package/apps/harness-dashboard/app/globals.css +35 -3
- package/apps/harness-dashboard/components/Drawer.tsx +7 -7
- package/apps/harness-dashboard/components/MissionTimeline.tsx +4 -4
- package/apps/harness-dashboard/components/OrgTree.tsx +7 -7
- package/apps/harness-dashboard/components/Scene.tsx +134 -3
- package/apps/harness-dashboard/lib/harness-state.ts +91 -0
- package/apps/harness-dashboard/lib/types.ts +27 -0
- package/assets/templates/AGENTS.md.template +9 -4
- package/assets/templates/HARNESS.md +6 -1
- package/assets/templates/config.json +10 -8
- package/assets/templates/todos-state.json.template +12 -0
- package/bin/init.js +184 -2
- package/conventions/README.md +3 -1
- package/conventions/coo.md +2 -1
- package/gotchas/README.md +3 -1
- package/package.json +1 -1
- package/scripts/harness-gotcha-register.sh +2 -0
- package/scripts/harness-user-prompt-submit.sh +42 -0
- package/scripts/lib/harness-structured-log.sh +59 -0
package/bin/init.js
CHANGED
|
@@ -527,7 +527,6 @@ function scaffoldHarness() {
|
|
|
527
527
|
copyFile(srcPath, destPath);
|
|
528
528
|
}
|
|
529
529
|
}
|
|
530
|
-
|
|
531
530
|
// Copy conventions — mirror gotchas preservation: never overwrite files with
|
|
532
531
|
// accumulated `### [C-NNN]` entries.
|
|
533
532
|
const conventionsSrc = path.join(PKG_ROOT, 'conventions');
|
|
@@ -550,7 +549,6 @@ function scaffoldHarness() {
|
|
|
550
549
|
copyFile(srcPath, destPath);
|
|
551
550
|
}
|
|
552
551
|
}
|
|
553
|
-
|
|
554
552
|
// First-install migration: extract Convention/Gotcha-shaped sections from
|
|
555
553
|
// existing CLAUDE.md / AGENTS.md and copy into the hierarchical stores.
|
|
556
554
|
if (isFirstInstall) {
|
|
@@ -564,6 +562,7 @@ function scaffoldHarness() {
|
|
|
564
562
|
// Copy templates as initial files
|
|
565
563
|
const templateMap = {
|
|
566
564
|
'progress.json.template': path.join(HARNESS_DIR, 'progress.json'),
|
|
565
|
+
'todos-state.json.template': path.join(HARNESS_DIR, 'todos', 'state.json'),
|
|
567
566
|
};
|
|
568
567
|
|
|
569
568
|
const templatesDir = path.join(PKG_ROOT, 'assets', 'templates');
|
|
@@ -571,13 +570,22 @@ function scaffoldHarness() {
|
|
|
571
570
|
for (const [template, dest] of Object.entries(templateMap)) {
|
|
572
571
|
const src = path.join(templatesDir, template);
|
|
573
572
|
if (fs.existsSync(src) && (!fileExists(dest) || isForce)) {
|
|
573
|
+
ensureDir(path.dirname(dest));
|
|
574
574
|
let content = fs.readFileSync(src, 'utf8');
|
|
575
575
|
content = content.replace(/\{\{DATE\}\}/g, new Date().toISOString().split('T')[0]);
|
|
576
|
+
content = content.replace(/\{\{DATE_ISO\}\}/g, new Date().toISOString());
|
|
576
577
|
fs.writeFileSync(dest, content);
|
|
577
578
|
}
|
|
578
579
|
}
|
|
579
580
|
}
|
|
580
581
|
|
|
582
|
+
ensureDir(path.join(HARNESS_DIR, 'todos'));
|
|
583
|
+
for (const rel of ['events.jsonl', path.join('todos', 'events.jsonl')]) {
|
|
584
|
+
const p = path.join(HARNESS_DIR, rel);
|
|
585
|
+
if (!fileExists(p) || isForce) fs.writeFileSync(p, '');
|
|
586
|
+
}
|
|
587
|
+
ensureStructuredRuntimeFiles();
|
|
588
|
+
|
|
581
589
|
// Migrate progress.json v1 → v2 (add mode + team_state fields)
|
|
582
590
|
const progressPath = path.join(HARNESS_DIR, 'progress.json');
|
|
583
591
|
if (fileExists(progressPath)) {
|
|
@@ -713,6 +721,38 @@ function ensureHarnessEnv() {
|
|
|
713
721
|
log('.env: HARNESS_BASE_PORT placeholder added — set value with Owner approval before starting services');
|
|
714
722
|
}
|
|
715
723
|
|
|
724
|
+
function ensureStructuredRuntimeFiles({ dryRun = false } = {}) {
|
|
725
|
+
const todosDir = path.join(HARNESS_DIR, 'todos');
|
|
726
|
+
const statePath = path.join(todosDir, 'state.json');
|
|
727
|
+
const eventsPath = path.join(HARNESS_DIR, 'events.jsonl');
|
|
728
|
+
const todoEventsPath = path.join(todosDir, 'events.jsonl');
|
|
729
|
+
const todoTplPath = path.join(PKG_ROOT, 'assets', 'templates', 'todos-state.json.template');
|
|
730
|
+
|
|
731
|
+
const missing = [eventsPath, statePath, todoEventsPath].filter((p) => !fileExists(p));
|
|
732
|
+
if (!missing.length) return false;
|
|
733
|
+
|
|
734
|
+
log(` structured runtime: ${missing.map((p) => path.relative(HARNESS_DIR, p)).join(', ')} 생성`);
|
|
735
|
+
if (dryRun) return true;
|
|
736
|
+
|
|
737
|
+
ensureDir(todosDir);
|
|
738
|
+
if (!fileExists(eventsPath)) fs.writeFileSync(eventsPath, '');
|
|
739
|
+
if (!fileExists(todoEventsPath)) fs.writeFileSync(todoEventsPath, '');
|
|
740
|
+
if (!fileExists(statePath)) {
|
|
741
|
+
let content;
|
|
742
|
+
if (fileExists(todoTplPath)) {
|
|
743
|
+
content = fs.readFileSync(todoTplPath, 'utf8').replace(/\{\{DATE_ISO\}\}/g, new Date().toISOString());
|
|
744
|
+
} else {
|
|
745
|
+
content = JSON.stringify({
|
|
746
|
+
version: 1,
|
|
747
|
+
updated_at: new Date().toISOString(),
|
|
748
|
+
owners: { ceo: [], coo: [], cdo: [], cto: [], cqo: [], ops: [] },
|
|
749
|
+
}, null, 2) + '\n';
|
|
750
|
+
}
|
|
751
|
+
fs.writeFileSync(statePath, content);
|
|
752
|
+
}
|
|
753
|
+
return true;
|
|
754
|
+
}
|
|
755
|
+
|
|
716
756
|
// ─────────────────────────────────────────
|
|
717
757
|
// 2. Skills → .claude/skills/
|
|
718
758
|
// ─────────────────────────────────────────
|
|
@@ -1265,6 +1305,7 @@ function detectMigrationNeeded() {
|
|
|
1265
1305
|
memoryMissingSystemEntries: [],
|
|
1266
1306
|
gotchaMissingEntries: {}, // { "<filename>": [G-IDs...] }
|
|
1267
1307
|
conventionMissingEntries: {}, // { "<filename>": [C-IDs...] }
|
|
1308
|
+
ruleRegistryLinks: [],
|
|
1268
1309
|
bundleVersionStale: null, // { current, installed }
|
|
1269
1310
|
};
|
|
1270
1311
|
|
|
@@ -1418,6 +1459,7 @@ function detectMigrationNeeded() {
|
|
|
1418
1459
|
flags.memoryMissingSystemEntries = tplEntryIds.filter((id) => !userEntryIds.has(id));
|
|
1419
1460
|
} catch {}
|
|
1420
1461
|
}
|
|
1462
|
+
flags.ruleRegistryLinks = detectRuleRegistryLinks();
|
|
1421
1463
|
return flags;
|
|
1422
1464
|
}
|
|
1423
1465
|
|
|
@@ -1479,6 +1521,136 @@ function extractConventionEntryBlock(md, id) {
|
|
|
1479
1521
|
return m ? m[1].trimEnd() : null;
|
|
1480
1522
|
}
|
|
1481
1523
|
|
|
1524
|
+
const CANONICAL_RULE_SCOPES = new Set([
|
|
1525
|
+
'README.md',
|
|
1526
|
+
'shared.md',
|
|
1527
|
+
'ceo.md',
|
|
1528
|
+
'coo.md',
|
|
1529
|
+
'cdo.md',
|
|
1530
|
+
'cto.md',
|
|
1531
|
+
'cqo.md',
|
|
1532
|
+
'ops.md',
|
|
1533
|
+
'hiring.md',
|
|
1534
|
+
'resource-manager.md',
|
|
1535
|
+
'brick-office.md',
|
|
1536
|
+
]);
|
|
1537
|
+
|
|
1538
|
+
const CXX_RULE_LINK_MARKER = '<!-- walwal-harness:related-rule-links -->';
|
|
1539
|
+
|
|
1540
|
+
function collectKnownWorkerNames() {
|
|
1541
|
+
const names = new Set();
|
|
1542
|
+
const hrRoot = path.join(HARNESS_DIR, 'shared', 'HR-Resource');
|
|
1543
|
+
if (fs.existsSync(hrRoot)) {
|
|
1544
|
+
for (const entry of fs.readdirSync(hrRoot, { withFileTypes: true })) {
|
|
1545
|
+
if (entry.isDirectory()) names.add(entry.name);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
const rosterPath = path.join(HARNESS_DIR, 'shared', 'hr-roster.json');
|
|
1549
|
+
if (fs.existsSync(rosterPath)) {
|
|
1550
|
+
try {
|
|
1551
|
+
const roster = JSON.parse(fs.readFileSync(rosterPath, 'utf8'));
|
|
1552
|
+
for (const entry of Array.isArray(roster.hired) ? roster.hired : []) {
|
|
1553
|
+
if (entry?.worker) names.add(entry.worker);
|
|
1554
|
+
}
|
|
1555
|
+
} catch {}
|
|
1556
|
+
}
|
|
1557
|
+
return names;
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
function classifyRuleRegistryFile(file, kind, knownWorkers, body = '') {
|
|
1561
|
+
const base = path.basename(file, '.md');
|
|
1562
|
+
const ext = path.extname(file);
|
|
1563
|
+
if (ext !== '.md') return null;
|
|
1564
|
+
if (LEGACY_V7_FILE_OWNER[file]) {
|
|
1565
|
+
return { targets: [path.basename(LEGACY_V7_FILE_OWNER[file], '.md')], reason: 'legacy-role' };
|
|
1566
|
+
}
|
|
1567
|
+
const haystack = `${base}\n${body}`.toLowerCase();
|
|
1568
|
+
const targets = new Set();
|
|
1569
|
+
|
|
1570
|
+
if (/\b(i18n|intl|locale|localization|translation|translate|multilingual|language|aria|rtl|l10n)\b/.test(haystack)) {
|
|
1571
|
+
targets.add('cto');
|
|
1572
|
+
targets.add('cqo');
|
|
1573
|
+
}
|
|
1574
|
+
if (/\b(ui|ux|design|brand|visual|layout|accessibility|a11y|responsive|color|typography)\b/.test(haystack)) {
|
|
1575
|
+
targets.add('cdo');
|
|
1576
|
+
targets.add('cqo');
|
|
1577
|
+
}
|
|
1578
|
+
if (/\b(api|backend|frontend|database|schema|auth|flutter|react|next|build|deploy|port|integration|migration)\b/.test(haystack)) {
|
|
1579
|
+
targets.add('cto');
|
|
1580
|
+
}
|
|
1581
|
+
if (/\b(test|qa|quality|regression|e2e|playwright|bug|hotfix|incident|failure|risk)\b/.test(haystack)) {
|
|
1582
|
+
targets.add('cqo');
|
|
1583
|
+
}
|
|
1584
|
+
if (/\b(ops|runtime|server|log|monitor|health|production|docker|port)\b/.test(haystack)) {
|
|
1585
|
+
targets.add('ops');
|
|
1586
|
+
}
|
|
1587
|
+
if (/\b(plan|research|market|hypothesis|requirement|product|scope)\b/.test(haystack)) {
|
|
1588
|
+
targets.add('coo');
|
|
1589
|
+
}
|
|
1590
|
+
if (knownWorkers.has(base)) {
|
|
1591
|
+
return { targets: ['coo', 'cto', 'cqo', 'ops'].filter(Boolean), reason: 'worker-topic' };
|
|
1592
|
+
}
|
|
1593
|
+
if (!targets.size) targets.add('shared');
|
|
1594
|
+
return { targets: [...targets], reason: targets.has('shared') ? 'unclassified-topic' : 'topic-keywords' };
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
function detectRuleRegistryLinks() {
|
|
1598
|
+
const links = [];
|
|
1599
|
+
const knownWorkers = collectKnownWorkerNames();
|
|
1600
|
+
for (const kind of ['conventions', 'gotchas']) {
|
|
1601
|
+
const root = path.join(HARNESS_DIR, kind);
|
|
1602
|
+
if (!fs.existsSync(root)) continue;
|
|
1603
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
1604
|
+
if (entry.isDirectory()) continue;
|
|
1605
|
+
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
|
1606
|
+
if (CANONICAL_RULE_SCOPES.has(entry.name)) continue;
|
|
1607
|
+
const sourcePath = path.join(root, entry.name);
|
|
1608
|
+
const body = fs.existsSync(sourcePath) ? fs.readFileSync(sourcePath, 'utf8') : '';
|
|
1609
|
+
const classification = classifyRuleRegistryFile(entry.name, kind, knownWorkers, body);
|
|
1610
|
+
if (!classification) continue;
|
|
1611
|
+
links.push({
|
|
1612
|
+
kind,
|
|
1613
|
+
sourceRel: entry.name,
|
|
1614
|
+
targets: classification.targets,
|
|
1615
|
+
reason: classification.reason,
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
return links.filter((item) => item.targets.some((target) => !ruleRegistryLinkExists(item.kind, target, item.sourceRel)));
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
function ruleRegistryLinkExists(kind, target, sourceRel) {
|
|
1623
|
+
const targetPath = path.join(HARNESS_DIR, kind, `${target}.md`);
|
|
1624
|
+
if (!fs.existsSync(targetPath)) return false;
|
|
1625
|
+
return fs.readFileSync(targetPath, 'utf8').includes(`](${sourceRel})`);
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
function updateRuleRegistryLinks(links, { dryRun, backupDir }) {
|
|
1629
|
+
if (!links.length) return;
|
|
1630
|
+
for (const item of links) {
|
|
1631
|
+
const root = path.join(HARNESS_DIR, item.kind);
|
|
1632
|
+
const sourcePath = path.join(root, item.sourceRel);
|
|
1633
|
+
if (!fs.existsSync(sourcePath)) continue;
|
|
1634
|
+
for (const target of item.targets) {
|
|
1635
|
+
const targetPath = path.join(root, `${target}.md`);
|
|
1636
|
+
if (path.resolve(sourcePath) === path.resolve(targetPath) || ruleRegistryLinkExists(item.kind, target, item.sourceRel)) continue;
|
|
1637
|
+
log(` ${item.kind}/${target}.md: link ${item.sourceRel} (${item.reason})`);
|
|
1638
|
+
if (dryRun) continue;
|
|
1639
|
+
ensureDir(path.dirname(targetPath));
|
|
1640
|
+
const existing = fs.existsSync(targetPath) ? fs.readFileSync(targetPath, 'utf8').trimEnd() : `# ${target.toUpperCase()} ${item.kind === 'gotchas' ? 'Gotchas' : 'Conventions'}`;
|
|
1641
|
+
if (fs.existsSync(targetPath)) {
|
|
1642
|
+
const backupTarget = path.join(backupDir, `${item.kind}-${target}.md`);
|
|
1643
|
+
fs.copyFileSync(targetPath, backupTarget);
|
|
1644
|
+
}
|
|
1645
|
+
const header = existing.includes(CXX_RULE_LINK_MARKER)
|
|
1646
|
+
? ''
|
|
1647
|
+
: `\n\n## Related ${item.kind === 'gotchas' ? 'Gotcha' : 'Convention'} Links\n${CXX_RULE_LINK_MARKER}\n`;
|
|
1648
|
+
const line = `- [${item.sourceRel}](${item.sourceRel}) — ${item.reason}`;
|
|
1649
|
+
fs.writeFileSync(targetPath, `${existing}${header}${header ? '' : '\n'}${line}\n`);
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1482
1654
|
function showMigrationProposal(flags) {
|
|
1483
1655
|
console.log('');
|
|
1484
1656
|
console.log('╔══════════════════════════════════════════════════════════╗');
|
|
@@ -1544,6 +1716,12 @@ function showMigrationProposal(flags) {
|
|
|
1544
1716
|
console.log(` ${file}: [${ids.join(', ')}]`);
|
|
1545
1717
|
}
|
|
1546
1718
|
}
|
|
1719
|
+
if (flags.ruleRegistryLinks && flags.ruleRegistryLinks.length) {
|
|
1720
|
+
console.log(' • conventions/gotchas: topic 파일을 CXX index 에 link 하여 lazy-load 가능하게 보강');
|
|
1721
|
+
for (const item of flags.ruleRegistryLinks) {
|
|
1722
|
+
console.log(` ${item.kind}/${item.sourceRel} → ${item.targets.map((t) => `${item.kind}/${t}.md`).join(', ')} (${item.reason})`);
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1547
1725
|
if (flags.bundleVersionStale) {
|
|
1548
1726
|
const { current, installed } = flags.bundleVersionStale;
|
|
1549
1727
|
console.log(` • bundle: ${installed ?? '(없음)'} → ${current} 스탬프 갱신 필요`);
|
|
@@ -1572,6 +1750,7 @@ function runMigrate(opts = {}) {
|
|
|
1572
1750
|
const runtimeScriptsAlwaysRefresh = true;
|
|
1573
1751
|
const gotchaMissingTotal = Object.values(flags.gotchaMissingEntries || {}).reduce((n, a) => n + a.length, 0);
|
|
1574
1752
|
const conventionMissingTotal = Object.values(flags.conventionMissingEntries || {}).reduce((n, a) => n + a.length, 0);
|
|
1753
|
+
const ruleRegistryLinkTotal = (flags.ruleRegistryLinks || []).length;
|
|
1575
1754
|
if (
|
|
1576
1755
|
!flags.progressV3toV4 &&
|
|
1577
1756
|
!flags.progressLegacyRouting &&
|
|
@@ -1588,6 +1767,7 @@ function runMigrate(opts = {}) {
|
|
|
1588
1767
|
(!flags.memoryMissingSystemEntries || flags.memoryMissingSystemEntries.length === 0) &&
|
|
1589
1768
|
gotchaMissingTotal === 0 &&
|
|
1590
1769
|
conventionMissingTotal === 0 &&
|
|
1770
|
+
ruleRegistryLinkTotal === 0 &&
|
|
1591
1771
|
!flags.bundleVersionStale &&
|
|
1592
1772
|
!runtimeScriptsAlwaysRefresh
|
|
1593
1773
|
) {
|
|
@@ -1604,10 +1784,12 @@ function runMigrate(opts = {}) {
|
|
|
1604
1784
|
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1605
1785
|
const backupDir = path.join(HARNESS_DIR, 'archive', `migration-${ts}`);
|
|
1606
1786
|
if (!dryRun) ensureDir(backupDir);
|
|
1787
|
+
ensureStructuredRuntimeFiles({ dryRun });
|
|
1607
1788
|
|
|
1608
1789
|
// Runtime scripts are package-owned. Migrate must refresh them so existing
|
|
1609
1790
|
// projects receive wake/meeting/OPS fixes without requiring a full init.
|
|
1610
1791
|
refreshScriptsForMigrate({ dryRun, backupDir });
|
|
1792
|
+
updateRuleRegistryLinks(flags.ruleRegistryLinks || [], { dryRun, backupDir });
|
|
1611
1793
|
|
|
1612
1794
|
// 1. progress.json
|
|
1613
1795
|
const progressPath = path.join(HARNESS_DIR, 'progress.json');
|
package/conventions/README.md
CHANGED
|
@@ -15,4 +15,6 @@ Only company-level roles are provided by default:
|
|
|
15
15
|
- `resource-manager.md`
|
|
16
16
|
- `brick-office.md`
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
Topic-specific convention files may use descriptive names such as `i18n-locale.md`.
|
|
19
|
+
|
|
20
|
+
CXX files such as `cto.md` and `cqo.md` act as lazy-loading indexes. Add links there when a topic file applies to that CXX.
|
package/conventions/coo.md
CHANGED
|
@@ -2,5 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
- COO owns planning, research, service direction, reference models, and hypothesis validation.
|
|
4
4
|
- COO records decisions in `.harness/documents/{mission_name}/coo.md`.
|
|
5
|
-
- COO hires researchers, product planners, junior experiment developers, and documentation workers before delegating specialist work.
|
|
5
|
+
- COO hires researchers, product planners, MCP registry stewards/capability scanners, junior experiment developers, and documentation workers before delegating specialist work.
|
|
6
|
+
- COO planning must include an MCP Capability Inventory: available/applicable MCPs, required setup, read/write risk, recommended mission use, or explicit `None` when discovery is unavailable or no MCP applies.
|
|
6
7
|
- COO reports mission fit, evidence, rejected options, and recommended next CXX to CEO.
|
package/gotchas/README.md
CHANGED
|
@@ -15,4 +15,6 @@ Only company-level roles are provided by default:
|
|
|
15
15
|
- `resource-manager.md`
|
|
16
16
|
- `brick-office.md`
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
Topic-specific gotcha files may use descriptive names such as `i18n-locale-hotfix.md`.
|
|
19
|
+
|
|
20
|
+
CXX files such as `cto.md` and `cqo.md` act as lazy-loading indexes. Add links there when a topic file applies to that CXX.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@walwal-harness/cli",
|
|
3
|
-
"version": "7.1.
|
|
3
|
+
"version": "7.1.27",
|
|
4
4
|
"description": "Company-style AI agent harness for Claude and Codex. Installs commands, CXX agents, skills, HR-Resource hiring pool, and project-local .harness runtime state.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"walwal-harness": "bin/init.js"
|
|
@@ -70,6 +70,7 @@ TODAY="$(date +%Y-%m-%d)"
|
|
|
70
70
|
register_one() {
|
|
71
71
|
local target="$1" rule_id="$2" title="$3" wrong="$4" right="$5" why="$6" scope="$7" source="$8"
|
|
72
72
|
local file="$GOTCHAS_DIR/${target}.md"
|
|
73
|
+
mkdir -p "$(dirname "$file")"
|
|
73
74
|
|
|
74
75
|
# Ensure file exists with header
|
|
75
76
|
if [ ! -f "$file" ]; then
|
|
@@ -184,6 +185,7 @@ register_convention_one() {
|
|
|
184
185
|
local scope="$1" rule_id="$2" title="$3" rule="$4" why="$5" source="$6"
|
|
185
186
|
mkdir -p "$CONVENTIONS_DIR"
|
|
186
187
|
local file="$CONVENTIONS_DIR/${scope}.md"
|
|
188
|
+
mkdir -p "$(dirname "$file")"
|
|
187
189
|
|
|
188
190
|
if [ ! -f "$file" ]; then
|
|
189
191
|
cat > "$file" <<EOF
|
|
@@ -10,6 +10,8 @@ if [ -z "$CWD" ]; then CWD="$PWD"; fi
|
|
|
10
10
|
# 조건 1: 하네스 초기화 확인
|
|
11
11
|
if [ ! -f "$CWD/.harness/config.json" ]; then exit 0; fi
|
|
12
12
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
13
|
+
STRUCTURED_LIB="$SCRIPT_DIR/lib/harness-structured-log.sh"
|
|
14
|
+
[ -f "$STRUCTURED_LIB" ] && source "$STRUCTURED_LIB"
|
|
13
15
|
|
|
14
16
|
# 조건 2: opt-out 플래그 확인
|
|
15
17
|
AUTO_ROUTE="true"
|
|
@@ -71,7 +73,47 @@ if [ -n "$PROMPT" ] && [ -d "$CWD/.harness" ]; then
|
|
|
71
73
|
COMMAND_TYPE="hot-fix"
|
|
72
74
|
fi
|
|
73
75
|
echo "$(date +"%Y-%m-%d %H:%M") | user-prompt | ${COMMAND_TYPE} | ${PROMPT_SHORT}" >> "$PROGRESS_LOG"
|
|
76
|
+
if declare -f harness_emit_event >/dev/null 2>&1; then
|
|
77
|
+
NOW_ISO_EVENT=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
78
|
+
EVENT_JSON=$(jq -nc \
|
|
79
|
+
--arg ts "$NOW_ISO_EVENT" \
|
|
80
|
+
--arg command "$COMMAND_TYPE" \
|
|
81
|
+
--arg summary "$PROMPT_SHORT" \
|
|
82
|
+
'{ts:$ts,type:"user_prompt",command:$command,summary:$summary,source:"UserPromptSubmit"}')
|
|
83
|
+
harness_emit_event "$CWD" "$EVENT_JSON"
|
|
84
|
+
fi
|
|
74
85
|
if [ "$COMMAND_TYPE" = "goal" ] || [ "$COMMAND_TYPE" = "submission" ] || [ "$COMMAND_TYPE" = "hot-fix" ]; then
|
|
86
|
+
if declare -f harness_todo_upsert >/dev/null 2>&1; then
|
|
87
|
+
NOW_ISO_TODO=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
88
|
+
PRIORITY=50
|
|
89
|
+
KIND="$COMMAND_TYPE"
|
|
90
|
+
[ "$COMMAND_TYPE" = "hot-fix" ] && PRIORITY=100 && KIND="hotfix"
|
|
91
|
+
TODO_ID="ceo-${COMMAND_TYPE}-$(date -u +%Y%m%dT%H%M%SZ)"
|
|
92
|
+
TODO_JSON=$(jq -nc \
|
|
93
|
+
--arg id "$TODO_ID" \
|
|
94
|
+
--arg owner "ceo" \
|
|
95
|
+
--arg kind "$KIND" \
|
|
96
|
+
--arg title "$PROMPT_SHORT" \
|
|
97
|
+
--arg status "active" \
|
|
98
|
+
--arg now "$NOW_ISO_TODO" \
|
|
99
|
+
--argjson priority "$PRIORITY" \
|
|
100
|
+
'{
|
|
101
|
+
id:$id,
|
|
102
|
+
owner:$owner,
|
|
103
|
+
kind:$kind,
|
|
104
|
+
title:$title,
|
|
105
|
+
status:$status,
|
|
106
|
+
priority:$priority,
|
|
107
|
+
command:$kind,
|
|
108
|
+
mission_path:null,
|
|
109
|
+
required_artifacts:[],
|
|
110
|
+
created_at:$now,
|
|
111
|
+
updated_at:$now,
|
|
112
|
+
last_heartbeat_at:$now,
|
|
113
|
+
blocked_reason:null
|
|
114
|
+
}')
|
|
115
|
+
harness_todo_upsert "$CWD" "$TODO_JSON"
|
|
116
|
+
fi
|
|
75
117
|
if [ -f "$CWD/.harness/progress.json" ] && command -v jq >/dev/null 2>&1; then
|
|
76
118
|
TMP_PROGRESS="${CWD}/.harness/progress.json.tmp.$$"
|
|
77
119
|
NOW_ISO=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Structured runtime helpers for events.jsonl and todos/state.json.
|
|
3
|
+
|
|
4
|
+
harness_json_escape() {
|
|
5
|
+
jq -Rs . 2>/dev/null
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
harness_ensure_structured_runtime() {
|
|
9
|
+
local project_root="${1:-.}"
|
|
10
|
+
local harness_dir="$project_root/.harness"
|
|
11
|
+
[ -d "$harness_dir" ] || return 0
|
|
12
|
+
mkdir -p "$harness_dir/todos"
|
|
13
|
+
[ -f "$harness_dir/events.jsonl" ] || : > "$harness_dir/events.jsonl"
|
|
14
|
+
[ -f "$harness_dir/todos/events.jsonl" ] || : > "$harness_dir/todos/events.jsonl"
|
|
15
|
+
if [ ! -f "$harness_dir/todos/state.json" ]; then
|
|
16
|
+
jq -n --arg now "$(date -u +%Y-%m-%dT%H:%M:%SZ)" '{
|
|
17
|
+
version: 1,
|
|
18
|
+
updated_at: $now,
|
|
19
|
+
owners: {ceo: [], coo: [], cdo: [], cto: [], cqo: [], ops: []}
|
|
20
|
+
}' > "$harness_dir/todos/state.json"
|
|
21
|
+
fi
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
harness_emit_event() {
|
|
25
|
+
local project_root="$1"
|
|
26
|
+
local event_json="$2"
|
|
27
|
+
harness_ensure_structured_runtime "$project_root"
|
|
28
|
+
local path="$project_root/.harness/events.jsonl"
|
|
29
|
+
jq -c --arg now "$(date -u +%Y-%m-%dT%H:%M:%SZ)" '
|
|
30
|
+
. + {ts: (.ts // $now)}
|
|
31
|
+
' <<<"$event_json" >> "$path" 2>/dev/null || true
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
harness_todo_upsert() {
|
|
35
|
+
local project_root="$1"
|
|
36
|
+
local todo_json="$2"
|
|
37
|
+
harness_ensure_structured_runtime "$project_root"
|
|
38
|
+
local state="$project_root/.harness/todos/state.json"
|
|
39
|
+
local events="$project_root/.harness/todos/events.jsonl"
|
|
40
|
+
local tmp="$state.tmp.$$"
|
|
41
|
+
local now
|
|
42
|
+
now="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
43
|
+
|
|
44
|
+
jq -c --arg now "$now" '. + {ts: (.ts // $now), type: (.type // "todo_upsert")}' <<<"$todo_json" >> "$events" 2>/dev/null || true
|
|
45
|
+
if jq --argjson todo "$todo_json" --arg now "$now" '
|
|
46
|
+
.version = (.version // 1) |
|
|
47
|
+
.updated_at = $now |
|
|
48
|
+
.owners = (.owners // {ceo: [], coo: [], cdo: [], cto: [], cqo: [], ops: []}) |
|
|
49
|
+
($todo.owner // "ceo") as $owner |
|
|
50
|
+
.owners[$owner] = (.owners[$owner] // []) |
|
|
51
|
+
.owners[$owner] = ((.owners[$owner] | map(select(.id != $todo.id))) + [
|
|
52
|
+
($todo + {updated_at: ($todo.updated_at // $now), created_at: ($todo.created_at // $now)})
|
|
53
|
+
] | sort_by(-(.priority // 0), .created_at))
|
|
54
|
+
' "$state" > "$tmp" 2>/dev/null; then
|
|
55
|
+
mv "$tmp" "$state"
|
|
56
|
+
else
|
|
57
|
+
rm -f "$tmp"
|
|
58
|
+
fi
|
|
59
|
+
}
|