feat(persona): 時區改為+08:00等值轉換、補機密遮蔽,並接上shared共用規範

依 todo.md 執行的規範治理專案:
- persona-lib.mjs 的 iso()/nowIso() 改為輸出 +08:00 offset(等值瞬間轉換,
  非加8小時),新增 nowDisplay()(人讀顯示格式)與 todayTaipei()(台北曆日
  日期鍵),修正 11 處原本用 UTC 字串切片產生日期鍵的錯誤用法;新增
  redactSecrets() 並套用到 persona-gitea.mjs 的 Gitea 錯誤路徑,避免機密
  外洩。
- 新增一次性遷移腳本 scripts/migrate-tz.mjs(備份/dry-run/冪等/衝突偵測
  中止),供之後有真實人格資料需要轉換時使用。
- persona 13 個 skill 與 AGENTS.md 接上 /jsc-shared 共用規範(spec-output、
  spec-script-path、spec-subagent、spec-gitea、spec-git-safety、
  spec-skill-invocation 等),不再各自重抄,且完全未變動人格行為本身的規範
  (情緒模型、記憶衰減、劇場模式、鎖與隔離機制等逐字保留)。
- selftest 全數通過(788/788,含本次新增的時區邊界/冪等性測試)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-11 06:06:00 +00:00
co-authored by Claude Sonnet 5
parent 5fb2a43189
commit ac2045e52f
25 changed files with 927 additions and 181 deletions
+7 -7
View File
@@ -479,7 +479,7 @@ commands.release = async ({ flags }) => {
if (!flags["no-gitea"] && !gt.giteaProblem() && pl.personaExists(slug) && gt.personaCode(slug)) {
for (const area of gt.AREA_KEYS) {
try {
const res = await gt.pushArea(slug, area, { message: `release: 對話結束 ${pl.nowIso()}` });
const res = await gt.pushArea(slug, area, { message: `release: 對話結束 ${pl.nowDisplay()}` });
if (res.ok && res.changed) say(` ↑ ${gt.AREAS[area].label}已推上 Gitea。`);
else if (!res.ok && !res.skipped) say(` ⚠ ${gt.AREAS[area].label}推送失敗:${String(res.reason).slice(0, 120)}`);
} catch (err) {
@@ -594,7 +594,7 @@ commands.sleep = async ({ flags }) => {
} else {
for (const area of gt.AREA_KEYS) {
try {
const res = await gt.pushArea(slug, area, { message: `sleep: 收尾 ${pl.nowIso()}` });
const res = await gt.pushArea(slug, area, { message: `sleep: 收尾 ${pl.nowDisplay()}` });
sync[area] = res.ok ? (res.changed ? "pushed" : "no-change") : `failed: ${String(res.reason).slice(0, 80)}`;
} catch (err) {
sync[area] = `failed: ${String(err.message).slice(0, 80)}`;
@@ -1896,7 +1896,7 @@ commands.mindmap = ({ flags, positional }) => {
if (!fs.existsSync(file) || flags.force) {
pl.writeText(file, [
`%% 思維導圖(短期):${topic}`,
`%% created: ${pl.nowIso()} ttl: short-term(固化後請併入 semantic.mmd 並刪除)`,
`%% created: ${pl.nowDisplay()} ttl: short-term(固化後請併入 semantic.mmd 並刪除)`,
"graph LR",
` trigger["觸發:${topic}"] --> obs["觀察"]`,
' obs --> infer["推論"]',
@@ -2019,7 +2019,7 @@ commands.relation = ({ flags, positional }) => {
if (!open.length) die(`跟 \`${node.name || key}\` 之間沒有還沒和好的事。`);
const target = about ? open.filter((r) => String(r.about).includes(about)) : open;
if (!target.length) die(`找不到「${about}」這一件(還沒和好的有:${open.map((r) => r.about).join("/")})。`);
for (const r of target) r.healed_at = pl.nowIso().slice(0, 10);
for (const r of target) r.healed_at = pl.todayTaipei();
node.updated_at = pl.nowIso();
pl.writeJson(pl.relationsJson(slug), data);
pl.renderRelations(slug);
@@ -2038,7 +2038,7 @@ commands.relation = ({ flags, positional }) => {
return;
}
node.rifts.push({
at: str(flags.at) || pl.nowIso().slice(0, 10),
at: str(flags.at) || pl.todayTaipei(),
about,
// 帶上長期記憶的名字,人格要細節自己 recall——不必等使用者提起那件事。
...(str(flags.memory) ? { memory: str(flags.memory) } : {}),
@@ -2079,7 +2079,7 @@ commands.relation = ({ flags, positional }) => {
node.style[facet] = {
value,
except: str(flags.except) || null, // 形如 anger>=40:情緒成立時這條規則暫停
since: str(flags.since) || pl.nowIso().slice(0, 10),
since: str(flags.since) || pl.todayTaipei(),
};
ok(`\`${node.name || node.id}\`:${facet}→「${value}」${str(flags.except) ? `(例外 ${str(flags.except)})` : ""} 已記下。`);
}
@@ -2999,7 +2999,7 @@ commands.icon = async ({ flags, positional }) => {
? {
url: str(flags["source-url"]),
note: str(flags["source-note"]) || null,
date: str(flags["source-date"]) || pl.nowIso().slice(0, 10),
date: str(flags["source-date"]) || pl.todayTaipei(),
}
: null;
const style = str(flags.style) || null;